Swap the muddy #f3ae3e palette for the real OLIVER brand pulled from the master
PPT template: yellow #FFCB05 + near-black #1A1A1A + off-white #F6F7F7, Montserrat
font. White-first page with a brand-yellow highlight rectangle behind page titles,
stat tiles with yellow left-strip, and a short yellow accent line under each
card section title — picks up the template's "01" chapter-marker rhythm.
Fixes two production bugs along the way:
- Nav stays pinned at top while page scrolls. The conflicting
`.navbar { position: relative !important }` rule was removed from nav.html
so the `position: fixed` from style.css can take effect.
- Clicking admin tabs no longer scrolls the page. Converted
`<a href="#users">` to `<button data-bs-target="#users">` (Bootstrap 5's
recommended pattern), so the anchor jump can't happen.
Other refinements: table padding loosened, `transform: scale` row hover
removed (jittery on dense rows), modal headers switched to near-black,
Chart.js palette aligned with brand tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
824 lines
44 KiB
HTML
824 lines
44 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{# Mode is "register" (default) or "complete" (filling in a LibreChat-synced agent). #}
|
|
{% set is_complete_mode = (mode is defined) and (mode == "complete") %}
|
|
{% set form_action = (base_path + "/agent-complete/" + agent._id|string) if is_complete_mode else (base_path + "/agent-register") %}
|
|
{% set page_heading = "Complete Agent Registration" if is_complete_mode else "Register AI Agent" %}
|
|
{% set page_subtitle = "Fill in the missing fields to complete this agent's registration." if is_complete_mode else "Add a new AI agent to the AgentHub registry" %}
|
|
{% set submit_label = "Complete Registration" if is_complete_mode else "Register Agent" %}
|
|
{% set a = agent if is_complete_mode else {} %}
|
|
|
|
{% block title %}{{ page_heading }} - AgentHub{% endblock %}
|
|
|
|
{% block style %}
|
|
<style>
|
|
:root {
|
|
--reg-primary: #FFCB05;
|
|
--reg-primary-dark: #F2BE00;
|
|
--reg-primary-light:#FFF5C4;
|
|
--reg-text-dark: #1A1A1A;
|
|
--reg-text-muted: #626262;
|
|
--reg-border: #DEE2E5;
|
|
--reg-bg-light: #F6F7F7;
|
|
}
|
|
|
|
.reg-page { padding: 2rem 0 4rem; }
|
|
.reg-page .main-card {
|
|
border-radius: 20px; border: none; background: white;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,.15);
|
|
}
|
|
.reg-page .agent-icon {
|
|
width: 80px; height: 80px; border-radius: 8px; background: var(--reg-primary);
|
|
display: flex; align-items: center; justify-content: center;
|
|
margin: 0 auto; font-size: 2rem; color: var(--reg-text-dark);
|
|
box-shadow: 0 8px 20px rgba(255,203,5,.3);
|
|
}
|
|
|
|
.reg-page .section-header {
|
|
display: flex; align-items: center; gap: .5rem;
|
|
padding: .65rem 1rem;
|
|
background: var(--reg-primary-light);
|
|
border-left: 4px solid var(--reg-primary);
|
|
border-radius: 0 10px 10px 0;
|
|
font-size: .76rem; font-weight: 700;
|
|
text-transform: uppercase; letter-spacing: .06em;
|
|
color: var(--reg-text-dark);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.reg-page .section-num {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 20px; height: 20px; background: var(--reg-primary);
|
|
color: white; border-radius: 50%; font-size: .65rem; font-weight: 700; flex-shrink: 0;
|
|
}
|
|
.reg-page .section-header .si { color: var(--reg-primary); }
|
|
.reg-page .section-divider { border: none; border-top: 2px dashed var(--reg-border); margin: 2rem 0; opacity: 1; }
|
|
|
|
.reg-page .form-label {
|
|
font-weight: 600; font-size: .875rem; color: var(--reg-text-dark);
|
|
margin-bottom: .4rem; display: flex; align-items: center; flex-wrap: wrap; gap: .3rem;
|
|
}
|
|
.reg-page .form-label .li { color: var(--reg-primary); }
|
|
.reg-page .req { color: #ef4444; font-weight: 700; }
|
|
|
|
.reg-page .form-control,
|
|
.reg-page .form-select {
|
|
border: 1.5px solid var(--reg-border); border-radius: 10px;
|
|
padding: 10px 14px; font-size: .875rem; font-family: 'Inter', sans-serif;
|
|
color: var(--reg-text-dark); transition: border-color .2s, box-shadow .2s; background: white;
|
|
}
|
|
.reg-page .form-control:focus,
|
|
.reg-page .form-select:focus {
|
|
border-color: var(--reg-primary); box-shadow: 0 0 0 3px rgba(243,174,62,.18); outline: none;
|
|
}
|
|
.reg-page textarea.form-control { resize: vertical; min-height: 80px; }
|
|
.reg-page .form-text { font-size: .76rem; color: var(--reg-text-muted); margin-top: .3rem; line-height: 1.5; }
|
|
.reg-page .field-group { margin-bottom: 1.5rem; }
|
|
|
|
.reg-page .autofill-field {
|
|
background: var(--reg-bg-light);
|
|
border: 1.5px dashed var(--reg-border);
|
|
border-radius: 10px; padding: 10px 14px;
|
|
font-size: .875rem; color: var(--reg-text-muted);
|
|
display: flex; align-items: center; gap: .5rem;
|
|
}
|
|
.reg-page .autofill-field i { color: var(--reg-primary); }
|
|
|
|
.reg-page .sel-grid-2 { display: grid; grid-template-columns: repeat(2,1fr); gap: .65rem; }
|
|
.reg-page .sel-grid-3 { display: grid; grid-template-columns: repeat(3,1fr); gap: .65rem; }
|
|
.reg-page .sel-card {
|
|
border: 2px solid var(--reg-border); border-radius: 12px; padding: .9rem;
|
|
cursor: pointer; transition: border-color .2s, background .2s; position: relative;
|
|
}
|
|
.reg-page .sel-card:hover { border-color: var(--reg-primary); background: var(--reg-primary-light); }
|
|
.reg-page .sel-card.selected { border-color: var(--reg-primary); background: var(--reg-primary-light); }
|
|
.reg-page .sel-card input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
|
|
.reg-page .card-emoji { font-size: 1.25rem; margin-bottom: .3rem; }
|
|
.reg-page .card-name { font-weight: 700; font-size: .85rem; color: var(--reg-text-dark); margin-bottom: .2rem; }
|
|
.reg-page .card-desc { font-size: .72rem; color: var(--reg-text-muted); line-height: 1.4; }
|
|
.reg-page .sel-card.selected .card-name { color: var(--reg-primary-dark); }
|
|
|
|
.reg-page .capability-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: .5rem; }
|
|
.reg-page .cap-item {
|
|
display: flex; align-items: center; gap: .6rem;
|
|
padding: .55rem .75rem;
|
|
border: 1.5px solid var(--reg-border); border-radius: 8px;
|
|
cursor: pointer; font-size: .82rem; font-weight: 500;
|
|
color: var(--reg-text-dark); transition: border-color .2s, background .2s;
|
|
user-select: none;
|
|
}
|
|
.reg-page .cap-item:hover { border-color: var(--reg-primary); background: var(--reg-primary-light); }
|
|
.reg-page .cap-item.selected { border-color: var(--reg-primary); background: var(--reg-primary-light); }
|
|
.reg-page .cap-item input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--reg-primary); flex-shrink: 0; }
|
|
.reg-page .cap-icon { color: var(--reg-primary); width: 16px; text-align: center; }
|
|
|
|
.reg-page .confirm-box {
|
|
background: #f0fdf4; border: 1.5px solid #86efac;
|
|
border-radius: 12px; padding: 1rem 1.25rem;
|
|
}
|
|
.reg-page .confirm-box .form-check { margin-bottom: .6rem; }
|
|
.reg-page .confirm-box .form-check:last-child { margin-bottom: 0; }
|
|
.reg-page .form-check-input:checked { background-color: var(--reg-primary) !important; border-color: var(--reg-primary) !important; }
|
|
.reg-page .form-check-input:focus { box-shadow: 0 0 0 3px rgba(243,174,62,.25) !important; border-color: var(--reg-primary) !important; }
|
|
|
|
.reg-page .pii-alert {
|
|
background: #fef2f2; border: 1.5px solid #fca5a5;
|
|
border-radius: 10px; padding: 1rem 1.25rem; font-size: .82rem; color: #991b1b;
|
|
}
|
|
|
|
.reg-page .char-count { font-size: .76rem; color: #94a3b8; }
|
|
|
|
.reg-page .btn-register {
|
|
background: var(--reg-primary); border: none; border-radius: 12px;
|
|
padding: 14px 28px; font-weight: 700; font-size: 1rem;
|
|
color: white; letter-spacing: .02em; width: 100%;
|
|
transition: background .2s, transform .15s, box-shadow .2s;
|
|
cursor: pointer;
|
|
}
|
|
.reg-page .btn-register:hover {
|
|
background: var(--reg-primary-dark); transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(243,174,62,.45); color: white;
|
|
}
|
|
.reg-page .btn-register:disabled { opacity: .7; transform: none; cursor: not-allowed; }
|
|
|
|
@media (max-width: 768px) {
|
|
.reg-page .sel-grid-2,
|
|
.reg-page .sel-grid-3,
|
|
.reg-page .capability-grid { grid-template-columns: 1fr; }
|
|
}
|
|
@media (max-width: 576px) {
|
|
.reg-page .card-body { padding: 1.5rem 1rem !important; }
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="reg-page">
|
|
<div class="container">
|
|
<div class="row justify-content-center">
|
|
<div class="col-12 col-md-11 col-lg-9 col-xl-8">
|
|
<div class="main-card">
|
|
<div class="card-body p-4 p-md-5">
|
|
|
|
<div class="text-center mb-5">
|
|
<div class="agent-icon mb-3"><i class="fas fa-robot"></i></div>
|
|
<h2 class="fw-bold mb-1" style="color:var(--reg-text-dark);font-size:1.75rem;">{{ page_heading }}</h2>
|
|
<p class="text-muted">{{ page_subtitle }}</p>
|
|
</div>
|
|
|
|
{% if is_complete_mode %}
|
|
<div class="alert alert-warning d-flex align-items-start gap-2" role="alert" style="border-radius:10px;">
|
|
<i class="fas fa-info-circle mt-1"></i>
|
|
<div>
|
|
<strong>This agent was synced from LibreChat.</strong>
|
|
Existing fields are pre-filled — please review them and complete the missing
|
|
governance fields (marked <span class="req">*</span>). Saving will mark this
|
|
registration as complete and assign you as the owner.
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="POST" id="agentForm" action="{{ form_action }}" novalidate>
|
|
|
|
<!-- 1 · IDENTITY -->
|
|
<div class="section-header">
|
|
<span class="section-num">1</span>
|
|
<i class="fas fa-id-card si"></i> Identity
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-tag li"></i> Agent Name <span class="req">*</span>
|
|
</label>
|
|
<input type="text" class="form-control form-control-lg" name="agent_name"
|
|
value="{{ a.agent_name|default('', true) }}"
|
|
placeholder="e.g., Content Brief Generator" required>
|
|
<div class="form-text">
|
|
Use a clear, descriptive name that reflects what the agent does —
|
|
e.g. <strong>[Function] + [Output] + [Version]</strong>.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-user-tie li"></i> Author / Contact Person
|
|
</label>
|
|
<div class="autofill-field">
|
|
<i class="fas fa-bolt"></i>
|
|
{% if is_complete_mode and a.agent_contact_person %}{{ a.agent_contact_person }}{% else %}{{ current_user.email }}{% endif %}
|
|
</div>
|
|
<div class="form-text">
|
|
Filled from your login. This person is the primary contact for questions or issues relating to this agent.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-7">
|
|
<label class="form-label">
|
|
<i class="fas fa-building li"></i> Business Entity <span class="req">*</span>
|
|
</label>
|
|
<select class="form-select" name="business_entity" required>
|
|
<option value="">Select entity…</option>
|
|
{% for entity in ['OLIVER', 'DARE', 'Brandtech Group', 'Pencil', 'Jellyfish', 'Adjust', 'Other'] %}
|
|
<option value="{{ entity }}" {% if a.business_entity == entity %}selected{% endif %}>{{ entity }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<div class="form-text">The Group Company this agent sits under.</div>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label">
|
|
<i class="fas fa-circle li"></i> Status
|
|
</label>
|
|
{% set current_status = a.agent_status|default('Development', true) %}
|
|
<select class="form-select" name="agent_status">
|
|
{% for s in ['Development', 'Active', 'Inactive', 'Deprecated'] %}
|
|
<option value="{{ s }}" {% if current_status == s %}selected{% endif %}>{{ s }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-handshake li"></i> Client Scope <span class="req">*</span>
|
|
</label>
|
|
{% set scope_pref = prefilled_client_scope|default('', true) %}
|
|
<select class="form-select" name="client_scope" id="clientScope"
|
|
onchange="handleClientScope()" required>
|
|
<option value="">Select client scope…</option>
|
|
<option value="internal" {% if scope_pref == 'internal' %}selected{% endif %}>Internal only — no client</option>
|
|
<option value="all" {% if scope_pref == 'all' %}selected{% endif %}>Applicable across all clients</option>
|
|
<option value="specific" {% if scope_pref == 'specific' %}selected{% endif %}>Built for a specific client</option>
|
|
</select>
|
|
<div class="form-text">
|
|
Is this an internal tool, a cross-client asset, or built exclusively for one client?
|
|
This also determines IP ownership in Section 4.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-group" id="clientNameSection" style="display:{% if scope_pref == 'specific' %}block{% else %}none{% endif %};">
|
|
<label class="form-label">
|
|
<i class="fas fa-briefcase li"></i> Client Name <span class="req">*</span>
|
|
</label>
|
|
<input type="text" class="form-control" name="client_name" id="clientNameInput"
|
|
value="{{ a.client_name|default('', true) }}"
|
|
placeholder="Search or enter client name…">
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label"><i class="fas fa-film li"></i> Studio / Department</label>
|
|
<input type="text" class="form-control" name="studio_dept"
|
|
value="{{ a.studio_name|default('', true) }}"
|
|
placeholder="e.g., Studio 27, Performance Team">
|
|
<div class="form-text">Stored as the agent's studio name.</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label"><i class="fas fa-map-marker-alt li"></i> Location</label>
|
|
<input type="text" class="form-control" name="agent_location"
|
|
value="{{ a.agent_location|default('', true) }}"
|
|
placeholder="e.g., London, Remote, Global">
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- 2 · CLASSIFICATION & PURPOSE -->
|
|
<div class="section-header">
|
|
<span class="section-num">2</span>
|
|
<i class="fas fa-layer-group si"></i> Classification & Purpose
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-sitemap li"></i> Agent Type <span class="req">*</span>
|
|
</label>
|
|
{% set cls = a.agent_classification|default('', true) %}
|
|
<div class="sel-grid-2" id="classGrid">
|
|
<div class="sel-card {% if cls == 'Utility' %}selected{% endif %}" onclick="selectCard(this,'classGrid','agent_classification')">
|
|
<input type="radio" name="agent_classification" value="Utility" {% if cls == 'Utility' %}checked{% endif %}>
|
|
<div class="card-emoji">🔧</div>
|
|
<div class="card-name">Utility</div>
|
|
<div class="card-desc">A single-purpose tool that does one specific task — e.g. summariser, formatter, tone adjuster.</div>
|
|
</div>
|
|
<div class="sel-card {% if cls == 'Functional' %}selected{% endif %}" onclick="selectCard(this,'classGrid','agent_classification')">
|
|
<input type="radio" name="agent_classification" value="Functional" {% if cls == 'Functional' %}checked{% endif %}>
|
|
<div class="card-emoji">⚙️</div>
|
|
<div class="card-name">Functional</div>
|
|
<div class="card-desc">A multi-step agent completing a defined workflow — e.g. brief writer, campaign planner.</div>
|
|
</div>
|
|
<div class="sel-card {% if cls == 'Supervisory' %}selected{% endif %}" onclick="selectCard(this,'classGrid','agent_classification')">
|
|
<input type="radio" name="agent_classification" value="Supervisory" {% if cls == 'Supervisory' %}checked{% endif %}>
|
|
<div class="card-emoji">🔍</div>
|
|
<div class="card-name">Supervisory</div>
|
|
<div class="card-desc">Orchestrates or oversees other agents — acts as a coordinator, router, or planner.</div>
|
|
</div>
|
|
<div class="sel-card {% if cls == 'Guardian' %}selected{% endif %}" onclick="selectCard(this,'classGrid','agent_classification')">
|
|
<input type="radio" name="agent_classification" value="Guardian" {% if cls == 'Guardian' %}checked{% endif %}>
|
|
<div class="card-emoji">🛡️</div>
|
|
<div class="card-name">Guardian</div>
|
|
<div class="card-desc">Monitors, filters, and enforces safety and compliance guardrails on other agents or outputs.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-tags li"></i> Discipline <span class="req">*</span>
|
|
</label>
|
|
{% set disc = a.discipline|default('', true) %}
|
|
<select class="form-select" name="discipline" required>
|
|
<option value="">Select discipline…</option>
|
|
{% for d in ['Strategy', 'Creative', 'Oversight including delivery', 'Optimisation', 'Back Office including operations', 'Pencil Agents'] %}
|
|
<option value="{{ d }}" {% if disc == d %}selected{% endif %}>{{ d }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label"><i class="fas fa-align-left li"></i> Description</label>
|
|
<textarea class="form-control" name="agent_description" rows="3" maxlength="300"
|
|
id="agentDesc" oninput="updateCount('agentDesc','descCount',300)"
|
|
placeholder="Describe what this agent does and what it produces…">{{ a.agent_description|default('', true) }}</textarea>
|
|
<div class="form-text d-flex justify-content-between">
|
|
<span>A brief summary of the agent's function and intended outcome.</span>
|
|
<span class="char-count" id="descCount">0 / 300</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label"><i class="fas fa-bullseye li"></i> Purpose</label>
|
|
{# §5h: collector defaults purpose=description for 94% of agents.
|
|
Leave purpose blank in that case so the user writes a real one. #}
|
|
{% set purpose_value = '' if (is_complete_mode and a.agent_purpose == a.agent_description) else a.agent_purpose|default('', true) %}
|
|
<input type="text" class="form-control" name="agent_purpose" maxlength="200"
|
|
id="agentPurpose" oninput="updateCount('agentPurpose','purposeCount',200)"
|
|
value="{{ purpose_value }}"
|
|
placeholder="The primary goal or problem this agent solves…">
|
|
<div class="form-text d-flex justify-content-between">
|
|
<span></span>
|
|
<span class="char-count" id="purposeCount">0 / 200</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label"><i class="fas fa-tags li"></i> Tags</label>
|
|
<input type="text" class="form-control" name="agent_tags"
|
|
value="{{ (a.agent_tags|default([], true))|join(', ') }}"
|
|
placeholder="e.g., creative, automation, briefing">
|
|
<div class="form-text">Separate with commas.</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label"><i class="fas fa-users li"></i> Target Userbase</label>
|
|
<input type="text" class="form-control" name="agent_userbase"
|
|
value="{{ (a.agent_userbase|default([], true))|join(', ') }}"
|
|
placeholder="e.g., account managers, creatives">
|
|
<div class="form-text">Who is this agent built for?</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- 3 · AUTONOMY & SAFETY -->
|
|
<div class="section-header">
|
|
<span class="section-num">3</span>
|
|
<i class="fas fa-shield-alt si"></i> Autonomy & Safety
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-sliders-h li"></i> Autonomy Level <span class="req">*</span>
|
|
</label>
|
|
{% set aut = a.autonomy_level|default('', true) %}
|
|
<div class="sel-grid-3" id="autonomyGrid">
|
|
<div class="sel-card {% if aut == 'Human-Led' %}selected{% endif %}" onclick="selectCard(this,'autonomyGrid','autonomy_level')">
|
|
<input type="radio" name="autonomy_level" value="Human-Led" {% if aut == 'Human-Led' %}checked{% endif %}>
|
|
<div class="card-emoji">👤</div>
|
|
<div class="card-name">Human-Led</div>
|
|
<div class="card-desc">A human reviews and approves every action before it happens.</div>
|
|
</div>
|
|
<div class="sel-card {% if aut == 'Hybrid' %}selected{% endif %}" onclick="selectCard(this,'autonomyGrid','autonomy_level')">
|
|
<input type="radio" name="autonomy_level" value="Hybrid" {% if aut == 'Hybrid' %}checked{% endif %}>
|
|
<div class="card-emoji">🔄</div>
|
|
<div class="card-name">Hybrid</div>
|
|
<div class="card-desc">Human sign-off at key decision points, with automation in between.</div>
|
|
</div>
|
|
<div class="sel-card {% if aut == 'Autopilot' %}selected{% endif %}" onclick="selectCard(this,'autonomyGrid','autonomy_level')">
|
|
<input type="radio" name="autonomy_level" value="Autopilot" {% if aut == 'Autopilot' %}checked{% endif %}>
|
|
<div class="card-emoji">🤖</div>
|
|
<div class="card-name">Autopilot</div>
|
|
<div class="card-desc">Fully automated. A human monitors but does not intervene. Senior sign-off required.</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-text mt-2">
|
|
Higher autonomy levels require additional sign-off before deployment.
|
|
If unsure, select <strong>Hybrid</strong> and discuss with your team lead.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-check-double li"></i> Safety Confirmations
|
|
</label>
|
|
<div class="confirm-box">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="offSwitch" name="off_switch_confirmed" value="true" {% if prefilled_off_switch %}checked{% endif %}>
|
|
<label class="form-check-label" for="offSwitch" style="font-size:.875rem;">
|
|
<strong>An emergency stop procedure is in place</strong> — a responsible person can immediately halt this agent's operations if something goes wrong.
|
|
</label>
|
|
</div>
|
|
<div class="form-check mt-2">
|
|
<input class="form-check-input" type="checkbox" id="accessRights" name="access_rights_confirmed" value="true" {% if prefilled_access_rights %}checked{% endif %}>
|
|
<label class="form-check-label" for="accessRights" style="font-size:.875rem;">
|
|
<strong>This agent only accesses data and systems</strong> that the authorising user is already permitted to access — it does not inherit elevated permissions.
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-text mt-2">
|
|
These confirmations are recorded but do not block submission. If you're unsure, please check with your technical lead before going live.
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- 4 · INTELLECTUAL PROPERTY -->
|
|
<div class="section-header">
|
|
<span class="section-num">4</span>
|
|
<i class="fas fa-copyright si"></i> Intellectual Property
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-balance-scale li"></i> IP Ownership <span class="req">*</span>
|
|
</label>
|
|
{% set ip = a.ip_ownership|default('', true) %}
|
|
<div class="sel-grid-3" id="ipGrid">
|
|
<div class="sel-card {% if ip == 'Brandtech IP' %}selected{% endif %}" onclick="selectCard(this,'ipGrid','ip_ownership')">
|
|
<input type="radio" name="ip_ownership" value="Brandtech IP" {% if ip == 'Brandtech IP' %}checked{% endif %}>
|
|
<div class="card-emoji">🏢</div>
|
|
<div class="card-name">Brandtech IP</div>
|
|
<div class="card-desc">An internal tool built on Brandtech's platform. IP belongs to the Group.</div>
|
|
</div>
|
|
<div class="sel-card {% if ip == 'Client IP' %}selected{% endif %}" onclick="selectCard(this,'ipGrid','ip_ownership')">
|
|
<input type="radio" name="ip_ownership" value="Client IP" {% if ip == 'Client IP' %}checked{% endif %}>
|
|
<div class="card-emoji">🤝</div>
|
|
<div class="card-name">Client IP</div>
|
|
<div class="card-desc">Built specifically for a client. The agent's configuration and instructions belong to them.</div>
|
|
</div>
|
|
<div class="sel-card {% if ip == 'Shared/TBD' %}selected{% endif %}" onclick="selectCard(this,'ipGrid','ip_ownership')">
|
|
<input type="radio" name="ip_ownership" value="Shared/TBD" {% if ip == 'Shared/TBD' %}checked{% endif %}>
|
|
<div class="card-emoji">⚖️</div>
|
|
<div class="card-name">Shared / TBD</div>
|
|
<div class="card-desc">Ownership to be agreed. Legal review required before client-facing deployment.</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-text mt-2">
|
|
This should align with the <strong>Client Scope</strong> selected in Section 1.
|
|
If in doubt, choose <strong>Shared / TBD</strong> and flag for review.
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- 5 · TECH STACK -->
|
|
<div class="section-header">
|
|
<span class="section-num">5</span>
|
|
<i class="fas fa-microchip si"></i> Tech Stack
|
|
</div>
|
|
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-6">
|
|
<label class="form-label">
|
|
<i class="fas fa-brain li"></i> Foundation Model / Provider
|
|
</label>
|
|
{% set fm = a.foundation_model|default('', true) %}
|
|
{% set known_models = ['GPT-4o','GPT-4o Mini','GPT-4 Turbo','Claude 3.5 Sonnet','Claude 3 Haiku','Claude 3 Opus','Gemini 1.5 Pro','Gemini 1.5 Flash','Llama 3.1 70B','Mistral Large','Custom / Other'] %}
|
|
<select class="form-select" name="foundation_model">
|
|
<option value="">Select or confirm model…</option>
|
|
{% if fm and fm not in known_models %}
|
|
<option value="{{ fm }}" selected>{{ fm }} (current)</option>
|
|
{% endif %}
|
|
<optgroup label="OpenAI">
|
|
{% for m in ['GPT-4o', 'GPT-4o Mini', 'GPT-4 Turbo'] %}
|
|
<option {% if fm == m %}selected{% endif %}>{{ m }}</option>
|
|
{% endfor %}
|
|
</optgroup>
|
|
<optgroup label="Anthropic">
|
|
{% for m in ['Claude 3.5 Sonnet', 'Claude 3 Haiku', 'Claude 3 Opus'] %}
|
|
<option {% if fm == m %}selected{% endif %}>{{ m }}</option>
|
|
{% endfor %}
|
|
</optgroup>
|
|
<optgroup label="Google">
|
|
{% for m in ['Gemini 1.5 Pro', 'Gemini 1.5 Flash'] %}
|
|
<option {% if fm == m %}selected{% endif %}>{{ m }}</option>
|
|
{% endfor %}
|
|
</optgroup>
|
|
<optgroup label="Open Source / Other">
|
|
{% for m in ['Llama 3.1 70B', 'Mistral Large', 'Custom / Other'] %}
|
|
<option {% if fm == m %}selected{% endif %}>{{ m }}</option>
|
|
{% endfor %}
|
|
</optgroup>
|
|
</select>
|
|
<div class="form-text">
|
|
The primary model at time of registration. Please keep this updated
|
|
if the model changes over time.
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">
|
|
<i class="fas fa-tools li"></i> Platform / Tool <span class="req">*</span>
|
|
</label>
|
|
<input type="text" class="form-control" name="agent_tool"
|
|
value="{{ a.agent_tool|default('', true) }}"
|
|
placeholder="e.g., LibreChat, Copilot, Custom API" required>
|
|
<div class="form-text">
|
|
The platform or environment where this agent is deployed.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-group" style="max-width:200px;">
|
|
<label class="form-label"><i class="fas fa-code-branch li"></i> Version</label>
|
|
<input type="text" class="form-control" name="agent_version"
|
|
value="{{ a.agent_version|default('', true) }}"
|
|
placeholder="e.g., 1.0.0">
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-cogs li"></i> Capabilities & Tooling
|
|
</label>
|
|
{% set checked_caps = prefilled_known_capabilities|default([], true) %}
|
|
{% set cap_options = [
|
|
('RAG', 'fa-database', 'RAG — Retrieval Augmented Generation'),
|
|
('Web', 'fa-globe', 'Web Search / Browsing'),
|
|
('API/MCP', 'fa-plug', 'API / MCP Connections'),
|
|
('Image Gen', 'fa-image', 'Image Generation'),
|
|
('Code Execution', 'fa-code', 'Code Execution'),
|
|
('File Operations', 'fa-file-alt', 'File Read / Write'),
|
|
('Email/Calendar', 'fa-envelope', 'Email / Calendar Integration'),
|
|
('Multi-Agent', 'fa-network-wired', 'Multi-Agent Orchestration'),
|
|
] %}
|
|
<div class="capability-grid">
|
|
{% for value, icon, label in cap_options %}
|
|
<div class="cap-item {% if value in checked_caps %}selected{% endif %}" onclick="toggleCap(this)">
|
|
<input type="checkbox" name="capabilities" value="{{ value }}" {% if value in checked_caps %}checked{% endif %}>
|
|
<i class="fas {{ icon }} cap-icon"></i> {{ label }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="form-text mt-2">
|
|
Select every tool or capability this agent has access to. Declaring these accurately helps with oversight and access reviews.
|
|
</div>
|
|
<div class="mt-3">
|
|
<label class="form-label" style="font-weight:500;font-size:.82rem;">
|
|
Other capabilities
|
|
</label>
|
|
<input type="text" class="form-control" name="capabilities_other"
|
|
value="{{ prefilled_other_capabilities|default('', true) }}"
|
|
placeholder="Add anything not in the list above, comma-separated">
|
|
<div class="form-text">Comma-separated. These will be appended to the capability list.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- 6 · DATA SAFETY -->
|
|
<div class="section-header">
|
|
<span class="section-num">6</span>
|
|
<i class="fas fa-lock si"></i> Data Safety
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-user-shield li"></i>
|
|
Does this agent process personal or confidential client data? <span class="req">*</span>
|
|
</label>
|
|
|
|
{% set pii_yes = prefilled_pii_handles|default(false, true) %}
|
|
<div class="form-check mb-2">
|
|
<input class="form-check-input" type="radio" name="pii_check"
|
|
id="piiNo" value="No" {% if not pii_yes %}checked{% endif %} onchange="togglePii()">
|
|
<label class="form-check-label fw-semibold" for="piiNo" style="font-size:.875rem;">
|
|
No — this agent does not handle personal or confidential data
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="pii_check"
|
|
id="piiYes" value="Yes" {% if pii_yes %}checked{% endif %} onchange="togglePii()">
|
|
<label class="form-check-label fw-semibold" for="piiYes"
|
|
style="font-size:.875rem; color:#991b1b;">
|
|
Yes — this agent handles personal (PII) or confidential client data
|
|
</label>
|
|
</div>
|
|
|
|
<div id="piiDetails" class="pii-alert mt-3" style="display:{% if pii_yes %}block{% else %}none{% endif %};">
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<strong>Written approval is required before this agent can be deployed</strong>
|
|
</div>
|
|
<p style="font-size:.8rem; margin-bottom:.75rem; line-height:1.55;">
|
|
Agents that process personal data or confidential client information require
|
|
explicit written sign-off from the relevant client <em>and</em> the Group Legal team
|
|
before going live. Please obtain and record that approval below.
|
|
</p>
|
|
<div class="mb-3">
|
|
<label class="form-label" style="color:#991b1b; font-size:.82rem; font-weight:600;">
|
|
Legal / DPO Approval Reference
|
|
</label>
|
|
<input type="text" class="form-control" name="pii_legal_ref"
|
|
value="{{ a.pii.legal_ref|default('', true) if a.pii else '' }}"
|
|
placeholder="e.g., Legal ticket number, DPO sign-off email date">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" style="color:#991b1b; font-size:.82rem; font-weight:600;">
|
|
Types of Data Processed
|
|
</label>
|
|
<input type="text" class="form-control" name="pii_data_types"
|
|
value="{{ a.pii.data_types|default('', true) if a.pii else '' }}"
|
|
placeholder="e.g., names, email addresses, campaign performance data">
|
|
</div>
|
|
<div class="form-check mt-1">
|
|
{% set pii_consent_pre = a.pii.consent_recorded|default(false, true) if a.pii else false %}
|
|
<input class="form-check-input" type="checkbox" id="piiConsent" name="pii_consent" value="true" {% if pii_consent_pre %}checked{% endif %}>
|
|
<label class="form-check-label" for="piiConsent"
|
|
style="font-size:.82rem; color:#991b1b; font-weight:600;">
|
|
I confirm explicit written approval has been obtained and is on record.
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-text mt-2">
|
|
If you're unsure whether your agent touches personal data, err on the side of caution and select <strong>Yes</strong>.
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- 7 · PERFORMANCE & TESTING -->
|
|
<div class="section-header">
|
|
<span class="section-num">7</span>
|
|
<i class="fas fa-chart-line si"></i> Performance & Testing
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label class="form-label">
|
|
<i class="fas fa-clipboard-check li"></i> Testing & Validation
|
|
</label>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="font-weight:500; font-size:.82rem;">Validated By</label>
|
|
<input type="text" class="form-control" name="validated_by"
|
|
value="{{ a.validated_by|default('', true) }}"
|
|
placeholder="Name or team who tested this agent">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="font-weight:500; font-size:.82rem;">Date Validated</label>
|
|
<input type="date" class="form-control" name="validation_date"
|
|
value="{{ a.validation_date|default('', true) }}">
|
|
</div>
|
|
</div>
|
|
<label class="form-label" style="font-weight:500; font-size:.82rem;">
|
|
Testing Approach
|
|
</label>
|
|
<textarea class="form-control" name="evals_method" rows="3"
|
|
placeholder="Describe how this agent was tested — e.g. what scenarios were covered, edge cases explored, how quality of outputs was assessed, and any limitations found…">{{ a.evals_method|default('', true) }}</textarea>
|
|
<div class="form-text mt-1">
|
|
Most important for client-facing agents. Documenting your testing approach helps with reviews and builds confidence when sharing the agent more broadly.
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- DECLARATIONS -->
|
|
<div class="section-header">
|
|
<span class="section-num"><i class="fas fa-check-double" style="font-size:.65rem;"></i></span>
|
|
<i class="fas fa-file-signature si"></i> Declarations
|
|
</div>
|
|
|
|
<div class="confirm-box mb-4">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="decl1" name="decl_governance" value="true" required>
|
|
<label class="form-check-label" for="decl1" style="font-size:.875rem;">
|
|
I confirm this agent has been built responsibly and meets our AI governance requirements.
|
|
</label>
|
|
</div>
|
|
<div class="form-check mt-2">
|
|
<input class="form-check-input" type="checkbox" id="decl2" name="decl_accuracy" value="true" required>
|
|
<label class="form-check-label" for="decl2" style="font-size:.875rem;">
|
|
I confirm the information provided in this form is accurate and complete to the best of my knowledge.
|
|
</label>
|
|
</div>
|
|
<div class="form-check mt-2">
|
|
<input class="form-check-input" type="checkbox" id="decl3" name="decl_upkeep" value="true" required>
|
|
<label class="form-check-label" for="decl3" style="font-size:.875rem;">
|
|
I will keep this registration up to date if this agent is significantly changed or retired.
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-grid mb-4">
|
|
<button type="submit" class="btn-register" id="submitBtn">
|
|
<i class="fas fa-robot me-2"></i>{{ submit_label }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="text-center">
|
|
<a href="{{ base_path }}/agent-management" class="text-decoration-none fw-bold" style="color:var(--reg-primary);">
|
|
<i class="fas fa-list me-1"></i>View My Agents
|
|
</a>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function handleClientScope() {
|
|
const v = document.getElementById('clientScope').value;
|
|
const s = document.getElementById('clientNameSection');
|
|
const i = document.getElementById('clientNameInput');
|
|
s.style.display = v === 'specific' ? 'block' : 'none';
|
|
i.required = v === 'specific';
|
|
if (v !== 'specific') i.value = '';
|
|
}
|
|
|
|
function togglePii() {
|
|
const yes = document.getElementById('piiYes').checked;
|
|
document.getElementById('piiDetails').style.display = yes ? 'block' : 'none';
|
|
}
|
|
|
|
function selectCard(el, gridId, radioName) {
|
|
const radio = el.querySelector('input[name="' + radioName + '"]');
|
|
if (!radio) return;
|
|
radio.checked = true;
|
|
document.querySelectorAll('#' + gridId + ' .sel-card').forEach(c => c.classList.remove('selected'));
|
|
el.classList.add('selected');
|
|
}
|
|
|
|
function toggleCap(el) {
|
|
const cb = el.querySelector('input[type="checkbox"]');
|
|
cb.checked = !cb.checked;
|
|
el.classList.toggle('selected', cb.checked);
|
|
}
|
|
document.querySelectorAll('.cap-item input[type="checkbox"]').forEach(cb => {
|
|
cb.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
this.closest('.cap-item').classList.toggle('selected', this.checked);
|
|
});
|
|
});
|
|
|
|
function updateCount(inputId, countId, max) {
|
|
const len = document.getElementById(inputId).value.length;
|
|
const el = document.getElementById(countId);
|
|
el.textContent = len + ' / ' + max;
|
|
el.style.color = len > max * .9 ? '#ef4444' : '#94a3b8';
|
|
}
|
|
|
|
// Initialise character counters from any pre-filled values (completion mode).
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateCount('agentDesc', 'descCount', 300);
|
|
updateCount('agentPurpose', 'purposeCount', 200);
|
|
});
|
|
|
|
let formSubmitted = false;
|
|
document.getElementById('agentForm').addEventListener('submit', function(e) {
|
|
if (formSubmitted) { e.preventDefault(); return; }
|
|
|
|
const fail = (msg) => {
|
|
e.preventDefault();
|
|
alert(msg);
|
|
// base.html disables the submit button on every form submit; re-enable it.
|
|
const btn = document.getElementById('submitBtn');
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-robot me-2"></i>{{ submit_label }}';
|
|
};
|
|
|
|
if (!this.querySelector('[name="agent_name"]').value.trim()) return fail('Agent Name is required.');
|
|
if (!this.querySelector('[name="business_entity"]').value) return fail('Business Entity is required.');
|
|
if (!this.querySelector('[name="agent_tool"]').value.trim()) return fail('Platform / Tool is required.');
|
|
if (!this.querySelector('[name="discipline"]').value) return fail('Discipline is required.');
|
|
|
|
const scope = document.getElementById('clientScope').value;
|
|
if (!scope) return fail('Client Scope is required.');
|
|
if (scope === 'specific' && !document.getElementById('clientNameInput').value.trim()) {
|
|
return fail('Please enter a Client Name for the specific client.');
|
|
}
|
|
|
|
if (!document.querySelector('input[name="agent_classification"]:checked')) return fail('Please select an Agent Type in Section 2.');
|
|
if (!document.querySelector('input[name="autonomy_level"]:checked')) return fail('Please select an Autonomy Level in Section 3.');
|
|
if (!document.querySelector('input[name="ip_ownership"]:checked')) return fail('Please select an IP Ownership status in Section 4.');
|
|
|
|
if (!['decl1','decl2','decl3'].every(id => document.getElementById(id).checked)) {
|
|
return fail('Please agree to all three declarations before submitting.');
|
|
}
|
|
|
|
formSubmitted = true;
|
|
});
|
|
</script>
|
|
{% endblock %}
|