1157 lines
58 KiB
HTML
1157 lines
58 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Agent Builder — Nexus One AI</title>
|
||
<link rel="stylesheet" href="style.css?v=4">
|
||
<style>
|
||
/* ── Layout ── */
|
||
.ab-layout { display:grid; grid-template-columns:260px 1fr; min-height:calc(100vh - 64px); }
|
||
@media(max-width:900px){ .ab-layout { grid-template-columns:1fr; } }
|
||
|
||
/* ── Sidebar ── */
|
||
.ab-sidebar { border-right:1px solid var(--bdr); background:var(--navy2); display:flex; flex-direction:column; }
|
||
.ab-sidebar-header { padding:16px 14px 10px; border-bottom:1px solid var(--bdr); }
|
||
.ab-sidebar-header h3 { font-size:12px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin:0 0 10px; }
|
||
.ab-new-btn { display:flex; align-items:center; gap:8px; width:100%; padding:9px 12px; border-radius:8px; border:1.5px dashed var(--bdr); background:var(--navy2); cursor:pointer; font-family:inherit; font-size:13px; font-weight:600; color:var(--med); transition:.15s; }
|
||
.ab-new-btn:hover { border-color:var(--teal); color:var(--teal); }
|
||
.ab-agent-list { flex:1; overflow-y:auto; padding:8px; }
|
||
.ab-agent-item { padding:10px 12px; border-radius:8px; cursor:pointer; transition:.1s; border:1px solid transparent; margin-bottom:4px; }
|
||
.ab-agent-item:hover { background:rgba(255,255,255,.03); }
|
||
.ab-agent-item.active { background:rgba(13,148,136,.12); border-color:rgba(13,148,136,.4); }
|
||
.ab-agent-name { font-size:13px; font-weight:600; color:var(--ink); }
|
||
.ab-agent-desc { font-size:11px; color:var(--lt); margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||
.ab-agent-meta { font-size:10px; color:var(--lt); margin-top:4px; }
|
||
|
||
/* ── Main area ── */
|
||
.ab-main { background:rgba(255,255,255,.03); padding:28px; overflow-y:auto; }
|
||
|
||
/* ── Empty state ── */
|
||
.ab-empty { text-align:center; padding:80px 20px; color:var(--lt); }
|
||
.ab-empty-icon { font-size:56px; margin-bottom:16px; }
|
||
.ab-empty-title { font-size:20px; font-weight:700; color:var(--ink); margin-bottom:8px; }
|
||
.ab-empty-sub { font-size:14px; max-width:400px; margin:0 auto; line-height:1.6; }
|
||
|
||
/* ── Builder card ── */
|
||
.ab-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:24px 28px; margin-bottom:20px; }
|
||
.ab-card-title { font-size:15px; font-weight:700; color:var(--ink); margin-bottom:18px; display:flex; align-items:center; gap:10px; }
|
||
.ab-meta-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:0; }
|
||
.ab-field { display:flex; flex-direction:column; gap:5px; margin-bottom:14px; }
|
||
.ab-field label { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.4px; }
|
||
.ab-field input, .ab-field textarea, .ab-field select {
|
||
padding:9px 12px; border:1.5px solid var(--bdr); border-radius:8px;
|
||
font-family:inherit; font-size:13px; color:var(--ink); background:var(--navy2);
|
||
}
|
||
.ab-field input:focus, .ab-field textarea:focus, .ab-field select:focus { outline:none; border-color:var(--teal); }
|
||
.ab-field textarea { resize:vertical; min-height:60px; }
|
||
|
||
/* ── Steps ── */
|
||
.ab-steps-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; }
|
||
.ab-steps-title { font-size:14px; font-weight:700; color:var(--ink); }
|
||
.ab-steps-list { display:flex; flex-direction:column; gap:12px; margin-bottom:14px; }
|
||
|
||
.ab-step {
|
||
border:1.5px solid var(--bdr); border-radius:12px; background:rgba(255,255,255,.03);
|
||
overflow:hidden; transition:.15s;
|
||
}
|
||
.ab-step:hover { border-color:var(--teal); }
|
||
.ab-step-header {
|
||
display:flex; align-items:center; gap:10px; padding:12px 14px;
|
||
cursor:pointer; user-select:none; background:var(--navy2);
|
||
border-bottom:1px solid transparent;
|
||
}
|
||
.ab-step.open .ab-step-header { border-bottom-color:var(--bdr); }
|
||
.ab-step-num { width:24px; height:24px; border-radius:50%; background:var(--teal); color:var(--ink); font-size:11px; font-weight:700; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||
.ab-step-name-text { flex:1; font-size:13px; font-weight:600; color:var(--ink); }
|
||
.ab-step-type-badge { font-size:11px; background:rgba(255,255,255,.03); border:1px solid var(--bdr); border-radius:20px; padding:2px 9px; color:var(--med); }
|
||
.ab-step-actions { display:flex; gap:6px; }
|
||
.ab-step-btn { background:none; border:none; cursor:pointer; font-size:14px; padding:2px 6px; color:var(--lt); border-radius:4px; }
|
||
.ab-step-btn:hover { background:rgba(255,255,255,.03); color:var(--ink); }
|
||
.ab-step-body { padding:14px; display:none; }
|
||
.ab-step.open .ab-step-body { display:block; }
|
||
.ab-step-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||
@media(max-width:640px){ .ab-step-grid { grid-template-columns:1fr; } }
|
||
|
||
.ab-step-type-icons { prompt:'💬', summarise:'📝', extract:'🔍', classify:'🏷️', rag:'📚', format:'✏️' }
|
||
|
||
/* ── Add step dropdown ── */
|
||
.ab-add-step { position:relative; }
|
||
.ab-add-step-btn { display:flex; align-items:center; gap:8px; width:100%; padding:10px 14px; border-radius:10px; border:1.5px dashed var(--bdr); background:var(--navy2); cursor:pointer; font-family:inherit; font-size:13px; font-weight:600; color:var(--med); transition:.15s; justify-content:center; }
|
||
.ab-add-step-btn:hover { border-color:var(--teal); color:var(--teal); }
|
||
.ab-step-types { display:none; position:absolute; top:calc(100% + 6px); left:0; right:0; background:var(--navy2); border:1.5px solid var(--bdr); border-radius:10px; z-index:50; box-shadow:0 8px 24px rgba(0,0,0,.1); overflow:hidden; }
|
||
.ab-step-types.open { display:block; }
|
||
.ab-step-type-opt { display:flex; align-items:center; gap:10px; padding:11px 14px; cursor:pointer; font-size:13px; color:var(--ink); transition:.1s; }
|
||
.ab-step-type-opt:hover { background:rgba(255,255,255,.03); }
|
||
.ab-step-type-opt .icon { font-size:18px; }
|
||
.ab-step-type-opt .label { font-weight:600; }
|
||
.ab-step-type-opt .sublabel { font-size:11px; color:var(--lt); margin-top:1px; }
|
||
|
||
/* ── Autonomy tier selector ── */
|
||
.ab-autonomy-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:10px; margin-top:4px; }
|
||
@media(max-width:700px){ .ab-autonomy-grid { grid-template-columns:repeat(2,1fr); } }
|
||
.ab-tier-opt { border:1.5px solid var(--bdr); border-radius:12px; padding:12px 14px; cursor:pointer; transition:.15s; background:var(--navy2); text-align:left; }
|
||
.ab-tier-opt:hover { border-color:var(--purple); }
|
||
.ab-tier-opt.selected { border-color:var(--purple); background:rgba(124,58,237,.06); }
|
||
.ab-tier-opt.selected.observe { border-color:#6B7280; background:rgba(107,114,128,.06); }
|
||
.ab-tier-opt.selected.advise { border-color:#2563EB; background:rgba(37,99,235,.06); }
|
||
.ab-tier-opt.selected.approval { border-color:#D97706; background:rgba(217,119,6,.06); }
|
||
.ab-tier-opt.selected.auto { border-color:#16A34A; background:rgba(22,163,74,.06); }
|
||
.ab-tier-icon { font-size:22px; margin-bottom:6px; }
|
||
.ab-tier-name { font-size:12px; font-weight:700; color:var(--ink); margin-bottom:2px; }
|
||
.ab-tier-desc { font-size:11px; color:var(--lt); line-height:1.4; }
|
||
|
||
/* ── Tier badge (sidebar) ── */
|
||
.tier-badge { display:inline-block; font-size:10px; font-weight:700; border-radius:20px; padding:2px 8px; text-transform:uppercase; letter-spacing:.4px; }
|
||
.tier-badge.observe { background:rgba(107,114,128,.1); color:#6B7280; }
|
||
.tier-badge.advise { background:rgba(37,99,235,.1); color:#2563EB; }
|
||
.tier-badge.approval { background:rgba(217,119,6,.1); color:#D97706; }
|
||
.tier-badge.auto { background:rgba(22,163,74,.1); color:#16A34A; }
|
||
|
||
/* ── Permissions card ── */
|
||
.ab-perm-grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
|
||
@media(max-width:640px){ .ab-perm-grid { grid-template-columns:1fr; } }
|
||
.ab-toggle-row { display:flex; align-items:center; gap:10px; padding:10px 14px; border:1px solid var(--bdr); border-radius:8px; background:var(--bg); cursor:pointer; transition:.1s; }
|
||
.ab-toggle-row:hover { border-color:var(--purple); }
|
||
.ab-toggle-row input[type=checkbox] { accent-color:var(--purple); width:15px; height:15px; flex-shrink:0; }
|
||
.ab-toggle-label { font-size:13px; color:var(--ink); font-weight:500; flex:1; }
|
||
.ab-toggle-sub { font-size:11px; color:var(--lt); }
|
||
|
||
/* ── Approval queue ── */
|
||
.ab-queue-banner { background:#FFFBEB; border:1.5px solid #D97706; border-radius:14px; padding:16px 20px; margin-bottom:20px; display:none; }
|
||
.ab-queue-banner.has-items { display:block; }
|
||
.ab-queue-title { font-size:13px; font-weight:700; color:#92400E; margin-bottom:12px; display:flex; align-items:center; gap:8px; }
|
||
.ab-queue-item { background:#FFFFFF; border:1px solid #FDE68A; border-radius:10px; padding:14px; margin-bottom:10px; }
|
||
.ab-queue-item:last-child { margin-bottom:0; }
|
||
.ab-queue-meta { font-size:11px; color:#92400E; margin-bottom:8px; display:flex; gap:12px; }
|
||
.ab-queue-preview { font-size:12px; color:var(--ink); background:var(--bg); border-radius:6px; padding:8px 10px; margin-bottom:10px; line-height:1.5; max-height:80px; overflow:hidden; }
|
||
.ab-queue-actions { display:flex; gap:8px; }
|
||
.ab-approve-btn { padding:6px 16px; border-radius:20px; border:none; font-family:inherit; font-size:12px; font-weight:700; cursor:pointer; background:#16A34A; color:white; transition:.15s; }
|
||
.ab-approve-btn:hover { background:#15803D; }
|
||
.ab-reject-btn { padding:6px 16px; border-radius:20px; border:1px solid #FCA5A5; font-family:inherit; font-size:12px; font-weight:700; cursor:pointer; background:#FEF2F2; color:#B91C1C; transition:.15s; }
|
||
.ab-reject-btn:hover { background:#FEE2E2; }
|
||
|
||
/* ── Approval waiting state ── */
|
||
.ab-awaiting-approval { background:#FFFBEB; border:1.5px solid #D97706; border-radius:12px; padding:20px 24px; text-align:center; margin-top:16px; }
|
||
.ab-awaiting-icon { font-size:32px; margin-bottom:8px; }
|
||
.ab-awaiting-title { font-size:15px; font-weight:700; color:#92400E; margin-bottom:6px; }
|
||
.ab-awaiting-sub { font-size:13px; color:#A16207; }
|
||
|
||
/* ── Action bar ── */
|
||
.ab-actions { display:flex; gap:10px; flex-wrap:wrap; }
|
||
|
||
/* ── Run panel ── */
|
||
.ab-run-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; overflow:hidden; }
|
||
.ab-run-header { padding:16px 20px; border-bottom:1px solid var(--bdr); display:flex; align-items:center; gap:12px; }
|
||
.ab-run-title { font-size:14px; font-weight:700; color:var(--ink); flex:1; }
|
||
.ab-run-body { padding:20px; }
|
||
.ab-run-input textarea { width:100%; box-sizing:border-box; padding:12px 14px; border:1.5px solid var(--bdr); border-radius:10px; font-family:inherit; font-size:13px; color:var(--ink); resize:vertical; min-height:80px; }
|
||
.ab-run-input textarea:focus { outline:none; border-color:var(--teal); }
|
||
#run-btn { margin-top:12px; padding:11px 24px; background:var(--teal); color:var(--ink); border:none; border-radius:8px; font-family:inherit; font-size:14px; font-weight:700; cursor:pointer; transition:.15s; }
|
||
#run-btn:hover { filter:brightness(1.08); }
|
||
#run-btn:disabled { opacity:.5; cursor:not-allowed; }
|
||
|
||
/* ── Step results ── */
|
||
.ab-step-results { margin-top:20px; display:flex; flex-direction:column; gap:12px; }
|
||
.ab-step-result { border:1px solid var(--bdr); border-radius:10px; overflow:hidden; }
|
||
.ab-step-result-header { padding:10px 14px; background:rgba(255,255,255,.03); display:flex; align-items:center; gap:10px; border-bottom:1px solid var(--bdr); }
|
||
.ab-step-result-label { font-size:12px; font-weight:700; color:var(--ink); flex:1; }
|
||
.ab-step-result-body { padding:14px; font-size:13px; color:var(--ink); line-height:1.65; white-space:pre-wrap; max-height:300px; overflow-y:auto; }
|
||
.ab-step-result.ok .ab-step-result-header { background:rgba(13,148,136,.12); border-bottom-color:#99F6E4; }
|
||
.ab-step-result.error .ab-step-result-header { background:rgba(185,28,28,.08); border-bottom-color:#FECACA; }
|
||
.ab-spinner { display:inline-block; animation:spin 1s linear infinite; }
|
||
@keyframes spin { to { transform:rotate(360deg); } }
|
||
|
||
/* ── Runs history ── */
|
||
.ab-hist-table-wrap { border:1px solid var(--bdr); border-radius:10px; overflow:hidden; }
|
||
table.ab-hist { width:100%; border-collapse:collapse; }
|
||
.ab-hist th { background:rgba(255,255,255,.03); padding:9px 14px; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.4px; color:var(--lt); border-bottom:1px solid var(--bdr); text-align:left; }
|
||
.ab-hist td { padding:10px 14px; border-bottom:1px solid var(--bdr); font-size:13px; color:var(--ink); }
|
||
.ab-hist tr:last-child td { border-bottom:none; }
|
||
.ab-hist tr:hover td { background:rgba(255,255,255,.03); cursor:pointer; }
|
||
.ab-status { display:inline-block; padding:2px 9px; border-radius:20px; font-size:11px; font-weight:700; }
|
||
.ab-status.done { background:rgba(34,197,94,.15); color:#15803D; }
|
||
.ab-status.running { background:#DBEAFE; color:#1D4ED8; }
|
||
.ab-status.error { background:#FEE2E2; color:#B91C1C; }
|
||
.ab-status.pending { background:rgba(234,179,8,.15); color:#A16207; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="topnav">
|
||
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
|
||
<nav>
|
||
<a href="index.html">Home</a>
|
||
<a href="quickstart.html">Quick Start</a>
|
||
<a href="prompts.html">Prompt Library</a>
|
||
<a href="usecases.html">Use Cases</a>
|
||
<span class="nav-sep"></span>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn">Help ▾</button>
|
||
<div class="nav-drop-menu">
|
||
<span class="nav-drop-cat">LEARN /</span>
|
||
<a href="quickstart.html">Quick Start</a>
|
||
<a href="models.html">Models</a>
|
||
<span class="nav-drop-cat">SUPPORT /</span>
|
||
<a href="troubleshooting.html">Troubleshoot</a>
|
||
<a href="faq.html">FAQ</a>
|
||
<span class="nav-drop-cat">MORE /</span>
|
||
<a href="glossary.html">Glossary</a>
|
||
<a href="whats-new.html">What's New</a>
|
||
</div>
|
||
</div>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn">Admin ▾</button>
|
||
<div class="nav-drop-menu nav-drop-menu-wide">
|
||
<span class="nav-drop-cat">DOCS /</span>
|
||
<a href="security.html">Security & Privacy</a>
|
||
<a href="admin.html">Admin Guide</a>
|
||
<span class="nav-drop-cat">MONITOR /</span>
|
||
<a href="dashboard.html">Dashboard</a>
|
||
<a href="analytics.html">Usage Analytics</a>
|
||
<a href="audit.html">Audit Log</a>
|
||
<a href="feedback.html">Feedback & Ratings</a>
|
||
<span class="nav-drop-cat">MANAGE /</span>
|
||
<a href="users.html">Users</a>
|
||
<a href="teams.html">Teams</a>
|
||
<a href="models-admin.html">Model Manager</a>
|
||
<a href="training.html">Training</a>
|
||
<a href="knowledge.html">Knowledge Base</a>
|
||
<span class="nav-drop-cat">TOOLS /</span>
|
||
<a href="apikeys.html">API Keys</a>
|
||
<a href="benchmark.html">Benchmarking</a>
|
||
<a href="model-compare.html">Model Compare</a>
|
||
<a href="api-playground.html">API Playground</a>
|
||
<a href="guardrails.html">Guardrails</a>
|
||
<a href="rag-quality.html">RAG Quality</a>
|
||
<a href="router.html">Model Router</a>
|
||
<a href="connectors.html">Connectors</a>
|
||
<span class="nav-drop-cat">SYSTEM /</span>
|
||
<a href="console.html">Console</a>
|
||
<a href="settings.html">Settings</a>
|
||
</div>
|
||
</div>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn active">AI Tools ▾</button>
|
||
<div class="nav-drop-menu">
|
||
<span class="nav-drop-cat">INTELLIGENCE /</span>
|
||
<a href="documents.html">Document Intelligence</a>
|
||
<a href="chat-multi.html">Multimodal Chat</a>
|
||
<a href="prompt-studio.html">Prompt Studio</a>
|
||
<a href="meeting.html">Meeting Assistant</a>
|
||
<span class="nav-drop-cat">AUTOMATION /</span>
|
||
<a href="agents.html">Agent Builder</a>
|
||
<a href="schedules.html">Scheduled Jobs</a>
|
||
<a href="workflows.html">Workflow Automation</a>
|
||
<span class="nav-drop-cat">QUALITY /</span>
|
||
<a href="evals.html">AI Eval Suite</a>
|
||
<a href="chatrooms.html">Chat Rooms</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
<a href="notifications.html" style="position:relative">🔔</a>
|
||
<span class="badge" data-brand="tier">Basic Tier</span>
|
||
<div id="nav-org-logo" class="nav-org-logo"></div>
|
||
</header>
|
||
|
||
<div class="ab-layout">
|
||
|
||
<!-- Sidebar -->
|
||
<aside class="ab-sidebar">
|
||
<div class="ab-sidebar-header">
|
||
<h3>My Agents</h3>
|
||
<button class="ab-new-btn" onclick="newAgent()">+ New Agent</button>
|
||
</div>
|
||
<div class="ab-agent-list" id="agent-list">
|
||
<div style="padding:16px;font-size:12px;color:var(--lt);text-align:center">No agents yet</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main -->
|
||
<main class="ab-main" id="ab-main">
|
||
|
||
<!-- Empty state -->
|
||
<div class="ab-empty" id="ab-empty">
|
||
<div class="ab-empty-icon">🤖</div>
|
||
<div class="ab-empty-title">Agent Builder</div>
|
||
<div class="ab-empty-sub">
|
||
Chain prompts and AI steps into automated pipelines. Each step's output feeds the next — build multi-step workflows that run with one click.
|
||
</div>
|
||
<div style="margin-top:24px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
|
||
<button class="btn btn-primary" onclick="loadTemplate('report')">📄 Report Generator</button>
|
||
<button class="btn btn-ghost" onclick="loadTemplate('classify')">🏷️ Classify & Summarise</button>
|
||
<button class="btn btn-ghost" onclick="loadTemplate('qa')">❓ Q&A Extractor</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Approval Queue (shown when pending approvals exist) -->
|
||
<div class="ab-queue-banner" id="approval-queue"></div>
|
||
|
||
<!-- Builder (hidden until agent selected/created) -->
|
||
<div id="ab-builder" style="display:none">
|
||
|
||
<!-- Agent meta -->
|
||
<div class="ab-card">
|
||
<div class="ab-card-title">🤖 Agent Details</div>
|
||
<div class="ab-meta-row">
|
||
<div class="ab-field">
|
||
<label>Agent Name</label>
|
||
<input type="text" id="agent-name" placeholder="e.g. Technical Report Summariser">
|
||
</div>
|
||
<div class="ab-field">
|
||
<label>Description</label>
|
||
<input type="text" id="agent-desc" placeholder="What does this agent do?">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Governance card -->
|
||
<div class="ab-card">
|
||
<div class="ab-card-title">🛡️ Governance & Approval
|
||
<span style="font-size:11px;font-weight:500;color:var(--lt);margin-left:auto">Controls how autonomously this agent can act</span>
|
||
</div>
|
||
|
||
<!-- Autonomy tier -->
|
||
<div class="ab-field" style="margin-bottom:18px">
|
||
<label>Autonomy Level</label>
|
||
<div class="ab-autonomy-grid" id="autonomy-grid">
|
||
<button class="ab-tier-opt observe" data-tier="observe" onclick="setTier('observe')">
|
||
<div class="ab-tier-icon">🔍</div>
|
||
<div class="ab-tier-name">Observe</div>
|
||
<div class="ab-tier-desc">Runs in background, logs only — no output surfaced to users</div>
|
||
</button>
|
||
<button class="ab-tier-opt advise" data-tier="advise" onclick="setTier('advise')">
|
||
<div class="ab-tier-icon">💡</div>
|
||
<div class="ab-tier-name">Advise</div>
|
||
<div class="ab-tier-desc">Shows recommendations — human decides whether to act</div>
|
||
</button>
|
||
<button class="ab-tier-opt approval selected" data-tier="act_approval" onclick="setTier('act_approval')">
|
||
<div class="ab-tier-icon">✋</div>
|
||
<div class="ab-tier-name">Act with Approval</div>
|
||
<div class="ab-tier-desc">Runs pipeline, waits for human sign-off before finalising</div>
|
||
</button>
|
||
<button class="ab-tier-opt auto" data-tier="act_auto" onclick="setTier('act_auto')">
|
||
<div class="ab-tier-icon">⚡</div>
|
||
<div class="ab-tier-name">Act Automatically</div>
|
||
<div class="ab-tier-desc">Runs and applies output without confirmation — use with care</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Approval settings (shown for act_approval tier) -->
|
||
<div id="approval-settings">
|
||
<div class="ab-perm-grid" style="margin-bottom:16px">
|
||
<div class="ab-field">
|
||
<label>Approvers</label>
|
||
<select id="gov-approvers">
|
||
<option value="admin">Admins only</option>
|
||
<option value="any">Any logged-in user</option>
|
||
<option value="owner">Agent owner only</option>
|
||
</select>
|
||
</div>
|
||
<div class="ab-field">
|
||
<label>Approval Timeout</label>
|
||
<select id="gov-timeout">
|
||
<option value="1">1 hour — then auto-reject</option>
|
||
<option value="4">4 hours</option>
|
||
<option value="24" selected>24 hours</option>
|
||
<option value="72">3 days</option>
|
||
<option value="0">No timeout</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Auto-run limit (shown for act_auto tier) -->
|
||
<div id="auto-settings" style="display:none">
|
||
<div class="ab-perm-grid" style="margin-bottom:16px">
|
||
<div class="ab-field">
|
||
<label>Max Auto-Runs Per Day</label>
|
||
<input type="number" id="gov-max-runs" value="50" min="1" max="500">
|
||
</div>
|
||
<div class="ab-field">
|
||
<label>Cool-down Between Runs</label>
|
||
<select id="gov-cooldown">
|
||
<option value="0">None</option>
|
||
<option value="60">1 minute</option>
|
||
<option value="300" selected>5 minutes</option>
|
||
<option value="3600">1 hour</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Permissions -->
|
||
<div class="ab-field" style="margin-bottom:8px">
|
||
<label>Permissions — Tools This Agent May Use</label>
|
||
<div class="ab-perm-grid" style="margin-top:6px">
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="perm-read-kb" checked>
|
||
<div>
|
||
<div class="ab-toggle-label">📚 Read Knowledge Base</div>
|
||
<div class="ab-toggle-sub">RAG queries against allowed collections</div>
|
||
</div>
|
||
</label>
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="perm-read-docs">
|
||
<div>
|
||
<div class="ab-toggle-label">📄 Read Uploaded Documents</div>
|
||
<div class="ab-toggle-sub">Access files uploaded by users</div>
|
||
</div>
|
||
</label>
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="perm-export">
|
||
<div>
|
||
<div class="ab-toggle-label">📤 Export / Save Output</div>
|
||
<div class="ab-toggle-sub">Write results to files or exports</div>
|
||
</div>
|
||
</label>
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="perm-notify">
|
||
<div>
|
||
<div class="ab-toggle-label">🔔 Send Notifications</div>
|
||
<div class="ab-toggle-sub">In-portal alerts and email notices</div>
|
||
</div>
|
||
</label>
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="perm-api">
|
||
<div>
|
||
<div class="ab-toggle-label">🌐 Call External APIs</div>
|
||
<div class="ab-toggle-sub">HTTP requests to configured connectors</div>
|
||
</div>
|
||
</label>
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="perm-schedule">
|
||
<div>
|
||
<div class="ab-toggle-label">⏱️ Trigger Scheduled Jobs</div>
|
||
<div class="ab-toggle-sub">Chain into the scheduler</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Safety toggles -->
|
||
<div class="ab-perm-grid" style="margin-top:12px">
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="gov-log-io" checked>
|
||
<div>
|
||
<div class="ab-toggle-label">📋 Log All Inputs & Outputs</div>
|
||
<div class="ab-toggle-sub">Every run recorded in audit log</div>
|
||
</div>
|
||
</label>
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="gov-pii" checked>
|
||
<div>
|
||
<div class="ab-toggle-label">🔒 PII Detection</div>
|
||
<div class="ab-toggle-sub">Warn before processing personal data</div>
|
||
</div>
|
||
</label>
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="gov-guardrails" checked>
|
||
<div>
|
||
<div class="ab-toggle-label">🛡️ Apply Guardrails Policy</div>
|
||
<div class="ab-toggle-sub">Content filter rules from Guardrails page</div>
|
||
</div>
|
||
</label>
|
||
<label class="ab-toggle-row">
|
||
<input type="checkbox" id="gov-sensitive">
|
||
<div>
|
||
<div class="ab-toggle-label">⚠️ Mark as Sensitive</div>
|
||
<div class="ab-toggle-sub">Outputs redacted from non-admin audit views</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Allowed knowledge base collections -->
|
||
<div class="ab-field" style="margin-top:16px">
|
||
<label>Allowed Knowledge Base Collections <span style="color:var(--lt);font-weight:400">(comma-separated, or * for all)</span></label>
|
||
<input type="text" id="gov-collections" value="*" placeholder="e.g. hr-policy, finance-docs, *">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Steps builder -->
|
||
<div class="ab-card">
|
||
<div class="ab-steps-header">
|
||
<div class="ab-steps-title">⚡ Pipeline Steps</div>
|
||
<span style="font-size:12px;color:var(--lt)">Each step receives the previous step's output as <code>{{prev}}</code></span>
|
||
</div>
|
||
<div class="ab-steps-list" id="steps-list"></div>
|
||
|
||
<!-- Add step -->
|
||
<div class="ab-add-step" id="add-step-wrap">
|
||
<button class="ab-add-step-btn" onclick="toggleStepMenu()">+ Add Step</button>
|
||
<div class="ab-step-types" id="step-types-menu">
|
||
<div class="ab-step-type-opt" onclick="addStep('prompt')">
|
||
<span class="icon">💬</span>
|
||
<div><div class="label">Prompt</div><div class="sublabel">Send a custom prompt to the LLM</div></div>
|
||
</div>
|
||
<div class="ab-step-type-opt" onclick="addStep('summarise')">
|
||
<span class="icon">📝</span>
|
||
<div><div class="label">Summarise</div><div class="sublabel">Shorten the previous output</div></div>
|
||
</div>
|
||
<div class="ab-step-type-opt" onclick="addStep('extract')">
|
||
<span class="icon">🔍</span>
|
||
<div><div class="label">Extract</div><div class="sublabel">Pull out specific fields as JSON</div></div>
|
||
</div>
|
||
<div class="ab-step-type-opt" onclick="addStep('classify')">
|
||
<span class="icon">🏷️</span>
|
||
<div><div class="label">Classify</div><div class="sublabel">Assign a category label</div></div>
|
||
</div>
|
||
<div class="ab-step-type-opt" onclick="addStep('rag')">
|
||
<span class="icon">📚</span>
|
||
<div><div class="label">RAG Query</div><div class="sublabel">Search knowledge base and inject context</div></div>
|
||
</div>
|
||
<div class="ab-step-type-opt" onclick="addStep('format')">
|
||
<span class="icon">✏️</span>
|
||
<div><div class="label">Format</div><div class="sublabel">Reformat output using a template</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Action bar -->
|
||
<div class="ab-actions" style="margin-top:20px">
|
||
<button class="btn btn-primary" onclick="saveAgent()">💾 Save Agent</button>
|
||
<button class="btn btn-ghost" onclick="deleteCurrentAgent()" id="delete-btn" style="display:none">🗑 Delete</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Run panel -->
|
||
<div class="ab-run-card">
|
||
<div class="ab-run-header">
|
||
<span style="font-size:20px">▶️</span>
|
||
<div class="ab-run-title">Run Agent</div>
|
||
<span style="font-size:12px;color:var(--lt)" id="run-status-badge"></span>
|
||
</div>
|
||
<div class="ab-run-body">
|
||
<div class="ab-run-input">
|
||
<label style="font-size:11px;font-weight:700;color:var(--lt);text-transform:uppercase;letter-spacing:.4px;display:block;margin-bottom:6px">Input (available as {{input}} in all steps)</label>
|
||
<textarea id="run-input" placeholder="Paste your input text here — a document, question, data, or anything the pipeline should process…"></textarea>
|
||
</div>
|
||
<button id="run-btn" onclick="runAgent()">▶ Run Pipeline</button>
|
||
|
||
<div class="ab-step-results" id="step-results" style="display:none"></div>
|
||
|
||
<!-- Approval waiting state (shown for act_approval agents after run) -->
|
||
<div class="ab-awaiting-approval" id="approval-waiting" style="display:none">
|
||
<div class="ab-awaiting-icon">✋</div>
|
||
<div class="ab-awaiting-title">Waiting for Approval</div>
|
||
<div class="ab-awaiting-sub">This agent requires human sign-off before the output is finalised. A pending approval has been added to the queue above.</div>
|
||
<div style="margin-top:14px;display:flex;gap:10px;justify-content:center">
|
||
<button class="ab-approve-btn" onclick="approveCurrentRun()">✓ Approve & Finalise</button>
|
||
<button class="ab-reject-btn" onclick="rejectCurrentRun()">✕ Reject</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Recent runs -->
|
||
<div class="ab-card" style="margin-top:20px" id="runs-card">
|
||
<div class="ab-card-title">🕐 Recent Runs</div>
|
||
<div id="runs-table"><div style="color:var(--lt);font-size:13px">No runs yet</div></div>
|
||
</div>
|
||
|
||
</div><!-- /#ab-builder -->
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
const _API = '/api';
|
||
let agents = [];
|
||
let currentAgentId = null;
|
||
let steps = [];
|
||
let stepCounter = 0;
|
||
let models = [];
|
||
let pollTimer = null;
|
||
let currentRunId = null;
|
||
let pendingApprovals = JSON.parse(localStorage.getItem('cezen_approvals') || '[]');
|
||
|
||
const STEP_ICONS = { prompt:'💬', summarise:'📝', extract:'🔍', classify:'🏷️', rag:'📚', format:'✏️' };
|
||
const STEP_LABELS = { prompt:'Prompt', summarise:'Summarise', extract:'Extract', classify:'Classify', rag:'RAG Query', format:'Format' };
|
||
|
||
const TIER_META = {
|
||
observe: { icon:'🔍', label:'Observe', cls:'observe' },
|
||
advise: { icon:'💡', label:'Advise', cls:'advise' },
|
||
act_approval: { icon:'✋', label:'Act w/ Approval', cls:'approval' },
|
||
act_auto: { icon:'⚡', label:'Act Automatically', cls:'auto' },
|
||
};
|
||
|
||
// ── Governance ────────────────────────────────────────────────────────────────
|
||
let currentTier = 'act_approval';
|
||
|
||
function setTier(tier) {
|
||
currentTier = tier;
|
||
document.querySelectorAll('.ab-tier-opt').forEach(el => {
|
||
const t = el.dataset.tier;
|
||
el.classList.toggle('selected', t === tier);
|
||
if (t === tier) {
|
||
el.classList.add(TIER_META[tier]?.cls || '');
|
||
} else {
|
||
['observe','advise','approval','auto'].forEach(c => el.classList.remove(c));
|
||
}
|
||
});
|
||
document.getElementById('approval-settings').style.display = tier === 'act_approval' ? '' : 'none';
|
||
document.getElementById('auto-settings').style.display = tier === 'act_auto' ? '' : 'none';
|
||
}
|
||
|
||
function getGovernance() {
|
||
return {
|
||
autonomy: currentTier,
|
||
approvers: document.getElementById('gov-approvers')?.value || 'admin',
|
||
timeout_h: parseInt(document.getElementById('gov-timeout')?.value || '24'),
|
||
max_runs_day: parseInt(document.getElementById('gov-max-runs')?.value || '50'),
|
||
cooldown_s: parseInt(document.getElementById('gov-cooldown')?.value || '300'),
|
||
perm_read_kb: document.getElementById('perm-read-kb')?.checked ?? true,
|
||
perm_read_docs:document.getElementById('perm-read-docs')?.checked ?? false,
|
||
perm_export: document.getElementById('perm-export')?.checked ?? false,
|
||
perm_notify: document.getElementById('perm-notify')?.checked ?? false,
|
||
perm_api: document.getElementById('perm-api')?.checked ?? false,
|
||
perm_schedule:document.getElementById('perm-schedule')?.checked ?? false,
|
||
log_io: document.getElementById('gov-log-io')?.checked ?? true,
|
||
pii_detect: document.getElementById('gov-pii')?.checked ?? true,
|
||
guardrails: document.getElementById('gov-guardrails')?.checked ?? true,
|
||
sensitive: document.getElementById('gov-sensitive')?.checked ?? false,
|
||
collections: document.getElementById('gov-collections')?.value || '*',
|
||
};
|
||
}
|
||
|
||
function applyGovernance(gov) {
|
||
if (!gov) return;
|
||
setTier(gov.autonomy || 'act_approval');
|
||
const set = (id, val) => { const el = document.getElementById(id); if (el) el.value = val; };
|
||
const chk = (id, val) => { const el = document.getElementById(id); if (el) el.checked = !!val; };
|
||
set('gov-approvers', gov.approvers || 'admin');
|
||
set('gov-timeout', gov.timeout_h || '24');
|
||
set('gov-max-runs', gov.max_runs_day || '50');
|
||
set('gov-cooldown', gov.cooldown_s || '300');
|
||
set('gov-collections', gov.collections || '*');
|
||
chk('perm-read-kb', gov.perm_read_kb ?? true);
|
||
chk('perm-read-docs', gov.perm_read_docs ?? false);
|
||
chk('perm-export', gov.perm_export ?? false);
|
||
chk('perm-notify', gov.perm_notify ?? false);
|
||
chk('perm-api', gov.perm_api ?? false);
|
||
chk('perm-schedule', gov.perm_schedule ?? false);
|
||
chk('gov-log-io', gov.log_io ?? true);
|
||
chk('gov-pii', gov.pii_detect ?? true);
|
||
chk('gov-guardrails', gov.guardrails ?? true);
|
||
chk('gov-sensitive', gov.sensitive ?? false);
|
||
}
|
||
|
||
// ── Approval queue ─────────────────────────────────────────────────────────────
|
||
function saveApprovals() {
|
||
localStorage.setItem('cezen_approvals', JSON.stringify(pendingApprovals));
|
||
}
|
||
|
||
function renderApprovalQueue() {
|
||
const banner = document.getElementById('approval-queue');
|
||
if (!pendingApprovals.length) { banner.classList.remove('has-items'); return; }
|
||
banner.classList.add('has-items');
|
||
banner.innerHTML = `
|
||
<div class="ab-queue-title">✋ ${pendingApprovals.length} Pending Approval${pendingApprovals.length>1?'s':''} — Review before output is finalised</div>
|
||
${pendingApprovals.map((a,i) => `
|
||
<div class="ab-queue-item">
|
||
<div class="ab-queue-meta">
|
||
<span>🤖 <strong>${esc(a.agentName)}</strong></span>
|
||
<span>👤 ${esc(a.user)}</span>
|
||
<span>🕐 ${new Date(a.ts).toLocaleString()}</span>
|
||
${a.timeoutH ? `<span style="color:#D97706">⏱ Expires in ${a.timeoutH}h</span>` : ''}
|
||
</div>
|
||
<div style="font-size:11px;font-weight:700;color:var(--lt);margin-bottom:4px;text-transform:uppercase;letter-spacing:.4px">Input</div>
|
||
<div class="ab-queue-preview">${esc(a.input.slice(0,200))}${a.input.length>200?'…':''}</div>
|
||
<div style="font-size:11px;font-weight:700;color:var(--lt);margin-bottom:4px;text-transform:uppercase;letter-spacing:.4px">Agent Output</div>
|
||
<div class="ab-queue-preview">${esc(a.output.slice(0,200))}${a.output.length>200?'…':''}</div>
|
||
<div class="ab-queue-actions">
|
||
<button class="ab-approve-btn" onclick="approveQueued(${i})">✓ Approve & Finalise</button>
|
||
<button class="ab-reject-btn" onclick="rejectQueued(${i})">✕ Reject</button>
|
||
</div>
|
||
</div>`).join('')}`;
|
||
}
|
||
|
||
function approveQueued(i) {
|
||
const a = pendingApprovals[i];
|
||
pendingApprovals.splice(i, 1);
|
||
saveApprovals();
|
||
renderApprovalQueue();
|
||
showToast(`✓ Approved: ${a.agentName}`);
|
||
}
|
||
|
||
function rejectQueued(i) {
|
||
const a = pendingApprovals[i];
|
||
pendingApprovals.splice(i, 1);
|
||
saveApprovals();
|
||
renderApprovalQueue();
|
||
showToast(`Rejected: ${a.agentName}`, true);
|
||
}
|
||
|
||
function approveCurrentRun() {
|
||
document.getElementById('approval-waiting').style.display = 'none';
|
||
showToast('✓ Output approved and finalised');
|
||
loadRuns(currentAgentId);
|
||
}
|
||
|
||
function rejectCurrentRun() {
|
||
document.getElementById('approval-waiting').style.display = 'none';
|
||
showToast('Run rejected — output discarded', true);
|
||
// Remove from pending queue too
|
||
pendingApprovals = pendingApprovals.filter(a => a.runId !== currentRunId);
|
||
saveApprovals();
|
||
renderApprovalQueue();
|
||
}
|
||
|
||
// ── Templates ─────────────────────────────────────────────────────────────────
|
||
const TEMPLATES = {
|
||
report: {
|
||
name: 'Technical Report Summariser',
|
||
description: 'Extracts key points then writes an executive summary',
|
||
steps: [
|
||
{ type:'prompt', name:'Extract Key Points', model:'llama3',
|
||
prompt:'Read the following document and extract the top 10 key points as a numbered list:\n\n{{input}}' },
|
||
{ type:'summarise', name:'Condense Points', model:'llama3', target_words:'100' },
|
||
{ type:'format', name:'Write Executive Summary', model:'llama3',
|
||
prompt:'Write a professional executive summary based on these points. Use formal language suitable for a PSU context:\n\n{{prev}}' }
|
||
]
|
||
},
|
||
classify: {
|
||
name: 'Classify & Summarise',
|
||
description: 'Classifies the input then provides a category-specific summary',
|
||
steps: [
|
||
{ type:'classify', name:'Classify Document', model:'llama3',
|
||
categories:'Technical Report, Policy Document, Research Paper, Meeting Minutes, Other' },
|
||
{ type:'prompt', name:'Category-Aware Summary', model:'llama3',
|
||
prompt:'The document has been classified as: {{prev}}\n\nNow provide a concise summary of the following document, highlighting aspects most relevant to a {{prev}}:\n\n{{input}}' }
|
||
]
|
||
},
|
||
qa: {
|
||
name: 'Q&A Extractor',
|
||
description: 'Generates questions and answers from a document',
|
||
steps: [
|
||
{ type:'prompt', name:'Generate Questions', model:'llama3',
|
||
prompt:'Read the following document and generate 8 important questions a reader might ask:\n\n{{input}}' },
|
||
{ type:'prompt', name:'Answer Questions', model:'llama3',
|
||
prompt:'Using the original document below, answer each of these questions:\n\nQuestions:\n{{prev}}\n\nOriginal Document:\n{{input}}' },
|
||
{ type:'format', name:'Format as Q&A', model:'llama3',
|
||
prompt:'Format the following as a clean Q&A document with bold questions and clear answers:\n\n{{prev}}' }
|
||
]
|
||
}
|
||
};
|
||
|
||
function loadTemplate(key) {
|
||
const t = TEMPLATES[key];
|
||
if (!t) return;
|
||
currentAgentId = null;
|
||
document.getElementById('agent-name').value = t.name;
|
||
document.getElementById('agent-desc').value = t.description;
|
||
steps = t.steps.map((s, i) => ({ ...s, _id: ++stepCounter }));
|
||
showBuilder();
|
||
renderSteps();
|
||
}
|
||
|
||
// ── New agent ─────────────────────────────────────────────────────────────────
|
||
function newAgent() {
|
||
currentAgentId = null;
|
||
document.getElementById('agent-name').value = '';
|
||
document.getElementById('agent-desc').value = '';
|
||
steps = [];
|
||
stepCounter = 0;
|
||
showBuilder();
|
||
renderSteps();
|
||
document.getElementById('delete-btn').style.display = 'none';
|
||
document.getElementById('step-results').style.display = 'none';
|
||
document.getElementById('approval-waiting').style.display = 'none';
|
||
document.getElementById('runs-table').innerHTML = '<div style="color:var(--lt);font-size:13px">No runs yet</div>';
|
||
document.querySelectorAll('.ab-agent-item').forEach(i => i.classList.remove('active'));
|
||
setTier('act_approval');
|
||
}
|
||
|
||
function showBuilder() {
|
||
document.getElementById('ab-empty').style.display = 'none';
|
||
document.getElementById('ab-builder').style.display = '';
|
||
}
|
||
|
||
// ── Steps rendering ───────────────────────────────────────────────────────────
|
||
function renderSteps() {
|
||
const list = document.getElementById('steps-list');
|
||
if (!steps.length) {
|
||
list.innerHTML = '<div style="text-align:center;padding:24px 0;color:var(--lt);font-size:13px">No steps yet — add a step below to build your pipeline</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = steps.map((s, i) => renderStep(s, i)).join('');
|
||
}
|
||
|
||
function renderStep(s, idx) {
|
||
const icon = STEP_ICONS[s.type] || '⚙️';
|
||
const label = STEP_LABELS[s.type] || s.type;
|
||
|
||
// Build type-specific config fields
|
||
let fields = '';
|
||
const modelOpts = models.map(m => `<option value="${esc(m)}" ${s.model===m?'selected':''}>${esc(m)}</option>`).join('');
|
||
const modelSel = `<div class="ab-field"><label>Model</label><select onchange="updateStep(${s._id},'model',this.value)">${modelOpts || `<option value="llama3">llama3</option>`}</select></div>`;
|
||
|
||
if (s.type === 'prompt' || s.type === 'format') {
|
||
fields = `
|
||
<div class="ab-field"><label>Step Name</label><input type="text" value="${esc(s.name||'')}" onchange="updateStep(${s._id},'name',this.value)" placeholder="e.g. Write Summary"></div>
|
||
${modelSel}
|
||
<div class="ab-field" style="grid-column:1/-1"><label>Prompt Template <span style="color:var(--lt);font-weight:400">(use {{input}} for pipeline input, {{prev}} for previous step output)</span></label>
|
||
<textarea rows="3" onchange="updateStep(${s._id},'prompt',this.value)">${esc(s.prompt||'')}</textarea></div>`;
|
||
} else if (s.type === 'summarise') {
|
||
fields = `
|
||
<div class="ab-field"><label>Step Name</label><input type="text" value="${esc(s.name||'')}" onchange="updateStep(${s._id},'name',this.value)" placeholder="Summarise"></div>
|
||
${modelSel}
|
||
<div class="ab-field"><label>Target Words</label><input type="number" value="${esc(s.target_words||'150')}" min="30" max="1000" onchange="updateStep(${s._id},'target_words',this.value)"></div>`;
|
||
} else if (s.type === 'extract') {
|
||
fields = `
|
||
<div class="ab-field"><label>Step Name</label><input type="text" value="${esc(s.name||'')}" onchange="updateStep(${s._id},'name',this.value)" placeholder="Extract Fields"></div>
|
||
${modelSel}
|
||
<div class="ab-field" style="grid-column:1/-1"><label>Fields to Extract <span style="color:var(--lt);font-weight:400">(comma-separated)</span></label>
|
||
<input type="text" value="${esc(s.fields||'')}" onchange="updateStep(${s._id},'fields',this.value)" placeholder="title, date, author, summary, recommendations"></div>`;
|
||
} else if (s.type === 'classify') {
|
||
fields = `
|
||
<div class="ab-field"><label>Step Name</label><input type="text" value="${esc(s.name||'')}" onchange="updateStep(${s._id},'name',this.value)" placeholder="Classify"></div>
|
||
${modelSel}
|
||
<div class="ab-field" style="grid-column:1/-1"><label>Categories <span style="color:var(--lt);font-weight:400">(comma-separated)</span></label>
|
||
<input type="text" value="${esc(s.categories||'')}" onchange="updateStep(${s._id},'categories',this.value)" placeholder="Technical, Financial, Legal, HR, Other"></div>`;
|
||
} else if (s.type === 'rag') {
|
||
fields = `
|
||
<div class="ab-field"><label>Step Name</label><input type="text" value="${esc(s.name||'')}" onchange="updateStep(${s._id},'name',this.value)" placeholder="RAG Query"></div>
|
||
${modelSel}
|
||
<div class="ab-field"><label>Knowledge Base Collection</label>
|
||
<input type="text" value="${esc(s.collection||'')}" onchange="updateStep(${s._id},'collection',this.value)" placeholder="collection name"></div>
|
||
<div class="ab-field"><label>Results to Fetch</label>
|
||
<input type="number" value="${esc(s.n_results||'3')}" min="1" max="10" onchange="updateStep(${s._id},'n_results',this.value)"></div>`;
|
||
}
|
||
|
||
return `
|
||
<div class="ab-step" id="step-${s._id}">
|
||
<div class="ab-step-header" onclick="toggleStep(${s._id})">
|
||
<div class="ab-step-num">${idx+1}</div>
|
||
<span style="font-size:16px">${icon}</span>
|
||
<div class="ab-step-name-text">${esc(s.name || label)}</div>
|
||
<span class="ab-step-type-badge">${label}</span>
|
||
<div class="ab-step-actions" onclick="event.stopPropagation()">
|
||
${idx > 0 ? `<button class="ab-step-btn" title="Move up" onclick="moveStep(${s._id},-1)">↑</button>` : ''}
|
||
${idx < steps.length-1 ? `<button class="ab-step-btn" title="Move down" onclick="moveStep(${s._id},1)">↓</button>` : ''}
|
||
<button class="ab-step-btn" title="Remove" onclick="removeStep(${s._id})">✕</button>
|
||
</div>
|
||
</div>
|
||
<div class="ab-step-body">
|
||
<div class="ab-step-grid">${fields}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function toggleStep(id) {
|
||
document.getElementById(`step-${id}`).classList.toggle('open');
|
||
}
|
||
|
||
function updateStep(id, key, value) {
|
||
const s = steps.find(s => s._id === id);
|
||
if (s) s[key] = value;
|
||
}
|
||
|
||
function addStep(type) {
|
||
const defaultNames = { prompt:'Prompt Step', summarise:'Summarise', extract:'Extract Fields', classify:'Classify', rag:'RAG Query', format:'Format Output' };
|
||
steps.push({ type, name: defaultNames[type]||type, model: models[0]||'llama3', _id: ++stepCounter });
|
||
toggleStepMenu();
|
||
renderSteps();
|
||
// Auto-open the new step
|
||
setTimeout(() => {
|
||
const el = document.getElementById(`step-${stepCounter}`);
|
||
if (el) { el.classList.add('open'); el.scrollIntoView({ behavior:'smooth', block:'nearest' }); }
|
||
}, 50);
|
||
}
|
||
|
||
function removeStep(id) {
|
||
steps = steps.filter(s => s._id !== id);
|
||
renderSteps();
|
||
}
|
||
|
||
function moveStep(id, dir) {
|
||
const idx = steps.findIndex(s => s._id === id);
|
||
if (idx < 0) return;
|
||
const newIdx = idx + dir;
|
||
if (newIdx < 0 || newIdx >= steps.length) return;
|
||
[steps[idx], steps[newIdx]] = [steps[newIdx], steps[idx]];
|
||
renderSteps();
|
||
}
|
||
|
||
function toggleStepMenu() {
|
||
document.getElementById('step-types-menu').classList.toggle('open');
|
||
}
|
||
document.addEventListener('click', e => {
|
||
if (!e.target.closest('.ab-add-step')) {
|
||
document.getElementById('step-types-menu').classList.remove('open');
|
||
}
|
||
});
|
||
|
||
// ── Save agent ────────────────────────────────────────────────────────────────
|
||
async function saveAgent() {
|
||
const name = document.getElementById('agent-name').value.trim();
|
||
if (!name) { alert('Please give the agent a name.'); return; }
|
||
if (!steps.length) { alert('Add at least one step.'); return; }
|
||
|
||
const cleanSteps = steps.map(({ _id, ...rest }) => rest);
|
||
const payload = {
|
||
name,
|
||
description: document.getElementById('agent-desc').value.trim(),
|
||
steps: cleanSteps,
|
||
governance: getGovernance(),
|
||
};
|
||
|
||
try {
|
||
let res;
|
||
if (currentAgentId) {
|
||
res = await fetch(`${_API}/agents/${currentAgentId}`, {
|
||
method:'PUT', credentials:'include',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
} else {
|
||
res = await fetch(`${_API}/agents`, {
|
||
method:'POST', credentials:'include',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
if (res.ok) {
|
||
const d = await res.json();
|
||
currentAgentId = d.id;
|
||
}
|
||
}
|
||
if (!res.ok) throw new Error((await res.json()).detail || 'Save failed');
|
||
document.getElementById('delete-btn').style.display = '';
|
||
await loadAgentList();
|
||
showToast('Agent saved');
|
||
} catch(e) { alert('Save failed: ' + e.message); }
|
||
}
|
||
|
||
function showToast(msg, isError = false) {
|
||
const t = document.createElement('div');
|
||
t.textContent = msg;
|
||
t.style.cssText = isError
|
||
? 'position:fixed;bottom:24px;right:24px;background:#FEF2F2;color:#B91C1C;border:1px solid #FECACA;border-radius:10px;padding:11px 18px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,.1);'
|
||
: 'position:fixed;bottom:24px;right:24px;background:#F0FDFA;color:#0F766E;border:1px solid #99F6E4;border-radius:10px;padding:11px 18px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,.1);';
|
||
document.body.appendChild(t);
|
||
setTimeout(() => t.remove(), 2800);
|
||
}
|
||
|
||
// ── Delete agent ──────────────────────────────────────────────────────────────
|
||
async function deleteCurrentAgent() {
|
||
if (!currentAgentId || !confirm('Delete this agent?')) return;
|
||
await fetch(`${_API}/agents/${currentAgentId}`, { method:'DELETE', credentials:'include' });
|
||
currentAgentId = null;
|
||
document.getElementById('ab-empty').style.display = '';
|
||
document.getElementById('ab-builder').style.display = 'none';
|
||
loadAgentList();
|
||
}
|
||
|
||
// ── Load agent list ───────────────────────────────────────────────────────────
|
||
async function loadAgentList() {
|
||
try {
|
||
const res = await fetch(`${_API}/agents`, { credentials:'include' });
|
||
agents = await res.json();
|
||
renderAgentList();
|
||
} catch(e) {}
|
||
}
|
||
|
||
function renderAgentList() {
|
||
const el = document.getElementById('agent-list');
|
||
if (!agents.length) {
|
||
el.innerHTML = '<div style="padding:16px;font-size:12px;color:var(--lt);text-align:center">No agents yet</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = agents.map(a => {
|
||
const tier = a.governance?.autonomy || 'act_approval';
|
||
const tm = TIER_META[tier] || TIER_META.act_approval;
|
||
return `
|
||
<div class="ab-agent-item ${a.id === currentAgentId ? 'active' : ''}" onclick="openAgent(${a.id})">
|
||
<div class="ab-agent-name" style="display:flex;align-items:center;gap:6px">
|
||
<span>${tm.icon}</span> ${esc(a.name)}
|
||
</div>
|
||
${a.description ? `<div class="ab-agent-desc">${esc(a.description)}</div>` : ''}
|
||
<div class="ab-agent-meta" style="display:flex;align-items:center;gap:6px;margin-top:5px">
|
||
<span class="tier-badge ${tm.cls}">${tm.label}</span>
|
||
<span>${a.username} · ${new Date(a.updated_at).toLocaleDateString()}</span>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
async function openAgent(id) {
|
||
try {
|
||
const res = await fetch(`${_API}/agents/${id}`, { credentials:'include' });
|
||
const agent = await res.json();
|
||
currentAgentId = id;
|
||
document.getElementById('agent-name').value = agent.name;
|
||
document.getElementById('agent-desc').value = agent.description || '';
|
||
stepCounter = 0;
|
||
steps = agent.steps.map(s => ({ ...s, _id: ++stepCounter }));
|
||
applyGovernance(agent.governance);
|
||
showBuilder();
|
||
renderSteps();
|
||
document.getElementById('delete-btn').style.display = '';
|
||
document.getElementById('step-results').style.display = 'none';
|
||
document.getElementById('approval-waiting').style.display = 'none';
|
||
renderAgentList();
|
||
loadRuns(id);
|
||
} catch(e) {}
|
||
}
|
||
|
||
// ── Run agent ─────────────────────────────────────────────────────────────────
|
||
async function runAgent() {
|
||
if (!currentAgentId) { alert('Save the agent first.'); return; }
|
||
const input = document.getElementById('run-input').value.trim();
|
||
if (!input) { alert('Enter some input to process.'); return; }
|
||
|
||
document.getElementById('run-btn').disabled = true;
|
||
const resultsEl = document.getElementById('step-results');
|
||
resultsEl.style.display = '';
|
||
resultsEl.innerHTML = steps.map((s, i) => `
|
||
<div class="ab-step-result pending" id="run-step-${i+1}">
|
||
<div class="ab-step-result-header">
|
||
<span>${STEP_ICONS[s.type]||'⚙️'}</span>
|
||
<div class="ab-step-result-label">Step ${i+1}: ${esc(s.name||s.type)}</div>
|
||
<span class="ab-spinner" id="spin-${i+1}" style="display:none">⚙️</span>
|
||
</div>
|
||
<div class="ab-step-result-body" id="run-body-${i+1}" style="color:var(--lt)">Waiting…</div>
|
||
</div>`).join('');
|
||
|
||
// Show first spinner
|
||
if (steps.length) document.getElementById('spin-1').style.display = '';
|
||
|
||
document.getElementById('approval-waiting').style.display = 'none';
|
||
|
||
try {
|
||
const res = await fetch(`${_API}/agents/${currentAgentId}/run`, {
|
||
method:'POST', credentials:'include',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({ input })
|
||
});
|
||
if (!res.ok) throw new Error((await res.json()).detail || 'Run failed');
|
||
const { run_id } = await res.json();
|
||
currentRunId = run_id;
|
||
pollRun(run_id);
|
||
} catch(e) {
|
||
document.getElementById('run-btn').disabled = false;
|
||
resultsEl.innerHTML = `<div style="color:#B91C1C;font-size:13px">Run failed: ${esc(e.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
function pollRun(runId) {
|
||
if (pollTimer) clearInterval(pollTimer);
|
||
let lastDone = 0;
|
||
pollTimer = setInterval(async () => {
|
||
try {
|
||
const res = await fetch(`${_API}/agents/runs/${runId}`, { credentials:'include' });
|
||
const run = await res.json();
|
||
|
||
// Update completed steps
|
||
(run.steps_log || []).forEach((sl, i) => {
|
||
const bodyEl = document.getElementById(`run-body-${sl.step}`);
|
||
const cardEl = document.getElementById(`run-step-${sl.step}`);
|
||
const spinEl = document.getElementById(`spin-${sl.step}`);
|
||
if (bodyEl && cardEl) {
|
||
if (sl.status === 'ok') {
|
||
bodyEl.textContent = sl.output;
|
||
bodyEl.style.color = '';
|
||
cardEl.className = 'ab-step-result ok';
|
||
if (spinEl) spinEl.style.display = 'none';
|
||
// Show spinner for next step
|
||
const nextSpin = document.getElementById(`spin-${sl.step+1}`);
|
||
if (nextSpin && run.status === 'running') nextSpin.style.display = '';
|
||
} else if (sl.status === 'error') {
|
||
bodyEl.textContent = 'Error: ' + sl.error;
|
||
bodyEl.style.color = '#B91C1C';
|
||
cardEl.className = 'ab-step-result error';
|
||
if (spinEl) spinEl.style.display = 'none';
|
||
}
|
||
}
|
||
});
|
||
|
||
if (run.status === 'done' || run.status === 'error') {
|
||
clearInterval(pollTimer);
|
||
document.getElementById('run-btn').disabled = false;
|
||
|
||
if (run.status === 'done' && currentTier === 'act_approval') {
|
||
// Queue for approval
|
||
const lastStep = (run.steps_log || []).slice(-1)[0];
|
||
const outputPreview = lastStep?.output || '(no output)';
|
||
const approval = {
|
||
runId: runId,
|
||
agentId: currentAgentId,
|
||
agentName: document.getElementById('agent-name').value,
|
||
input: document.getElementById('run-input').value,
|
||
output: outputPreview,
|
||
user: window.cezenUser?.username || 'unknown',
|
||
ts: new Date().toISOString(),
|
||
timeoutH: parseInt(document.getElementById('gov-timeout')?.value || '24'),
|
||
};
|
||
pendingApprovals.unshift(approval);
|
||
saveApprovals();
|
||
renderApprovalQueue();
|
||
document.getElementById('approval-waiting').style.display = '';
|
||
document.getElementById('run-status-badge').textContent = '⏳ Awaiting approval';
|
||
} else if (run.status === 'done' && currentTier === 'observe') {
|
||
document.getElementById('run-status-badge').textContent = '🔍 Logged (Observe mode)';
|
||
} else {
|
||
document.getElementById('run-status-badge').textContent =
|
||
run.status === 'done' ? '✓ Complete' : '✗ Failed';
|
||
}
|
||
loadRuns(currentAgentId);
|
||
}
|
||
} catch(e) {}
|
||
}, 2000);
|
||
}
|
||
|
||
// ── Runs table ────────────────────────────────────────────────────────────────
|
||
async function loadRuns(agentId) {
|
||
try {
|
||
const res = await fetch(`${_API}/agents/runs/list`, { credentials:'include' });
|
||
const runs = await res.json();
|
||
const myRuns = runs.filter(r => r.agent_id === agentId);
|
||
const el = document.getElementById('runs-table');
|
||
if (!myRuns.length) {
|
||
el.innerHTML = '<div style="color:var(--lt);font-size:13px">No runs yet</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = `<div class="ab-hist-table-wrap"><table class="ab-hist">
|
||
<tr><th>When</th><th>Input</th><th>Status</th><th>Duration</th></tr>
|
||
${myRuns.slice(0,20).map(r => {
|
||
const dur = r.finished_at
|
||
? Math.round((new Date(r.finished_at)-new Date(r.created_at))/1000) + 's'
|
||
: '—';
|
||
return `<tr>
|
||
<td style="color:var(--lt);font-size:12px">${new Date(r.created_at).toLocaleString()}</td>
|
||
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(r.input.slice(0,60))}${r.input.length>60?'…':''}</td>
|
||
<td><span class="ab-status ${r.status}">${r.status}</span></td>
|
||
<td style="color:var(--lt)">${dur}</td>
|
||
</tr>`;
|
||
}).join('')}
|
||
</table></div>`;
|
||
} catch(e) {}
|
||
}
|
||
|
||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||
function esc(s) {
|
||
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
async function loadModels() {
|
||
try {
|
||
const res = await fetch(`${_API}/models/list`, { credentials:'include' });
|
||
const data = await res.json();
|
||
models = (data.models || []).map(m => m.name);
|
||
} catch(e) { models = ['llama3']; }
|
||
}
|
||
|
||
// ── Boot ──────────────────────────────────────────────────────────────────────
|
||
(async () => {
|
||
await loadModels();
|
||
await loadAgentList();
|
||
setTier('act_approval'); // default governance tier
|
||
renderApprovalQueue(); // show any stored pending approvals
|
||
})();
|
||
</script>
|
||
|
||
<script src="auth.js"></script>
|
||
<script src="branding.js"></script>
|
||
</body>
|
||
</html>
|