annator / src /ui /web /orchestrator.html
techprotrade's picture
Add src directory
734b5b4 verified
Raw
History Blame Contribute Delete
50.7 kB
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>PDF AI Orchestrator</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0d0d0f;
--surf: #131316;
--surf2: #1a1a1f;
--surf3: #222228;
--border: rgba(255,255,255,0.07);
--border2: rgba(255,255,255,0.12);
--text: #e2e2e8;
--muted: #7a7a8c;
--faint: #3a3a4a;
--primary: #5b7cfa;
--primary2: #3d5ce0;
--teal: #2ec9b7;
--green: #3ecf8e;
--orange: #f59e0b;
--red: #f87171;
--purple: #a78bfa;
--font: 'Inter', sans-serif;
--mono: 'JetBrains Mono', monospace;
--r: 10px;
--r2: 14px;
--trans: 160ms cubic-bezier(0.16,1,0.3,1);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
button { font-family: var(--font); cursor: pointer; border: none; background: none; color: inherit; }
input, select, textarea { font-family: var(--font); font-size: 13px; }
/* ── Top bar ────────────────────────────────────────────────────── */
.topbar {
display: flex; align-items: center; gap: 12px;
padding: 0 24px; height: 52px;
background: var(--surf);
border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 100;
}
.logo {
display: flex; align-items: center; gap: 8px;
font-size: 15px; font-weight: 700; color: var(--text);
text-decoration: none;
}
.logo-icon {
width: 28px; height: 28px; border-radius: 8px;
background: linear-gradient(135deg,#5b7cfa,#2ec9b7);
display: flex; align-items: center; justify-content: center;
font-size: 14px;
}
.topbar-center { flex: 1; display: flex; justify-content: center; gap: 6px; }
.tab-btn {
padding: 5px 14px; border-radius: 7px;
font-size: 13px; font-weight: 500; color: var(--muted);
transition: all var(--trans);
}
.tab-btn.active { background: var(--surf3); color: var(--text); }
.tab-btn:hover:not(.active) { color: var(--text); }
.topbar-right { display: flex; align-items: center; gap: 8px; }
.btn-run {
padding: 7px 20px; border-radius: 8px;
background: var(--primary); color: #fff;
font-size: 13px; font-weight: 600;
transition: all var(--trans);
display: flex; align-items: center; gap: 6px;
}
.btn-run:hover { background: var(--primary2); transform: translateY(-1px); box-shadow: 0 4px 16px rgba(91,124,250,0.3); }
.btn-run:active { transform: translateY(0); }
.btn-ghost {
padding: 6px 14px; border-radius: 8px; font-size: 13px;
border: 1px solid var(--border2); color: var(--muted);
transition: all var(--trans);
}
.btn-ghost:hover { color: var(--text); border-color: var(--border2); background: var(--surf2); }
/* ── Layout ─────────────────────────────────────────────────────── */
.layout {
display: flex; flex: 1; overflow: hidden; height: calc(100vh - 52px);
}
/* ── Left panel ─────────────────────────────────────────────────── */
.left-panel {
width: 280px; min-width: 260px;
background: var(--surf);
border-right: 1px solid var(--border);
display: flex; flex-direction: column;
overflow-y: auto;
}
.panel-section { padding: 16px; border-bottom: 1px solid var(--border); }
.panel-label {
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.08em; color: var(--muted); margin-bottom: 10px;
}
.file-drop {
border: 1.5px dashed var(--faint);
border-radius: var(--r);
padding: 20px 12px;
text-align: center;
cursor: pointer;
transition: all var(--trans);
position: relative;
}
.file-drop:hover, .file-drop.drag { border-color: var(--primary); background: rgba(91,124,250,0.05); }
.file-drop input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.file-drop-icon { font-size: 24px; margin-bottom: 6px; }
.file-drop p { font-size: 12px; color: var(--muted); }
.file-drop p b { color: var(--text); }
.file-name {
margin-top: 8px; padding: 7px 10px; border-radius: 7px;
background: var(--surf2); font-size: 12px;
display: flex; align-items: center; gap: 6px;
display: none;
}
.file-name.show { display: flex; }
.form-row { margin-bottom: 10px; }
.form-row label { display: block; font-size: 11px; color: var(--muted); margin-bottom: 4px; }
.form-row input, .form-row select {
width: 100%; padding: 7px 10px; border-radius: 7px;
background: var(--surf2); border: 1px solid var(--border);
color: var(--text); outline: none;
transition: border-color var(--trans);
}
.form-row input:focus, .form-row select:focus { border-color: var(--primary); }
.toggle-row {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 8px;
}
.toggle-row span { font-size: 12px; color: var(--muted); }
.toggle {
width: 36px; height: 20px; border-radius: 999px;
background: var(--faint); cursor: pointer;
position: relative; transition: background var(--trans);
}
.toggle.on { background: var(--primary); }
.toggle::after {
content: ''; position: absolute;
width: 14px; height: 14px; border-radius: 50%;
background: #fff; top: 3px; left: 3px;
transition: transform var(--trans);
}
.toggle.on::after { transform: translateX(16px); }
/* Agent palette */
.agent-palette { display: flex; flex-direction: column; gap: 6px; }
.palette-agent {
display: flex; align-items: center; gap: 10px;
padding: 9px 12px; border-radius: var(--r);
background: var(--surf2); border: 1px solid var(--border);
cursor: grab; transition: all var(--trans);
user-select: none;
}
.palette-agent:hover { border-color: var(--border2); background: var(--surf3); transform: translateX(2px); }
.palette-agent:active { cursor: grabbing; }
.agent-avatar {
width: 32px; height: 32px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 15px; flex-shrink: 0;
}
.agent-info { flex: 1; min-width: 0; }
.agent-info .name { font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-info .role { font-size: 10px; color: var(--muted); }
.drag-hint { font-size: 10px; color: var(--faint); }
/* ── Main canvas ────────────────────────────────────────────────── */
.main-canvas {
flex: 1; overflow-y: auto; overflow-x: hidden;
padding: 24px;
display: flex; flex-direction: column; gap: 16px;
}
.canvas-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 4px;
}
.canvas-title { font-size: 16px; font-weight: 700; }
.canvas-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
/* Pipeline drop zone */
.pipeline-zone {
min-height: 280px;
border: 1.5px dashed var(--faint);
border-radius: var(--r2);
padding: 16px;
display: flex; flex-direction: column; gap: 10px;
transition: border-color var(--trans), background var(--trans);
}
.pipeline-zone.drag-over {
border-color: var(--primary);
background: rgba(91,124,250,0.04);
}
.pipeline-empty {
display: flex; flex-direction: column; align-items: center;
justify-content: center; gap: 8px; padding: 40px;
color: var(--muted); text-align: center;
}
.pipeline-empty-icon { font-size: 36px; opacity: 0.4; }
/* Pipeline card */
.pipeline-card {
background: var(--surf2);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 14px 16px;
display: flex; flex-direction: column; gap: 10px;
position: relative;
transition: all var(--trans);
animation: slideIn 0.18s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.pipeline-card:hover { border-color: var(--border2); }
.pipeline-card.running { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary) inset; }
.pipeline-card.done { border-color: var(--green); }
.pipeline-card.error { border-color: var(--red); }
.pipeline-card.skipped { opacity: 0.5; }
.card-header { display: flex; align-items: center; gap: 10px; }
.card-num {
width: 22px; height: 22px; border-radius: 50%;
background: var(--faint); font-size: 11px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; color: var(--muted);
}
.pipeline-card.done .card-num { background: var(--green); color: #000; }
.pipeline-card.running .card-num { background: var(--primary); color: #fff; animation: pulse 1s infinite; }
.pipeline-card.error .card-num { background: var(--red); color: #fff; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
.view-panel.hidden { display: none; }
.command-studio { display: flex; flex-direction: column; gap: 16px; }
.command-box, .team-summary, .team-card {
background: var(--surf2);
border: 1px solid var(--border);
border-radius: var(--r2);
}
.command-box {
padding: 18px;
display: flex;
flex-direction: column;
gap: 12px;
}
.command-box textarea {
width: 100%;
min-height: 140px;
resize: vertical;
padding: 12px 14px;
border-radius: 10px;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
outline: none;
}
.command-box textarea:focus { border-color: var(--primary); }
.command-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.command-meta { font-size: 12px; color: var(--muted); }
.team-summary {
padding: 16px 18px;
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 16px;
}
.summary-block h4 {
font-size: 12px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 8px;
}
.summary-block p, .summary-block li { font-size: 13px; }
.summary-block ul {
margin-left: 18px;
display: flex;
flex-direction: column;
gap: 6px;
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
}
.team-card {
padding: 14px;
display: flex;
flex-direction: column;
gap: 10px;
}
.team-card-header {
display: flex;
align-items: center;
gap: 10px;
}
.team-avatar {
width: 36px;
height: 36px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.team-card-title { font-size: 13px; font-weight: 700; }
.team-card-sub { font-size: 11px; color: var(--muted); }
.model-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 999px;
font-size: 10px;
background: var(--surf3);
border: 1px solid var(--border);
color: var(--muted);
}
.model-line {
font-size: 11px;
color: var(--muted);
word-break: break-all;
}
.team-insight {
display: flex;
flex-direction: column;
gap: 7px;
font-size: 12px;
}
.team-insight div {
padding: 8px 10px;
border-radius: 10px;
background: var(--bg);
border: 1px solid var(--border);
}
.team-insight strong { color: var(--text); }
.card-title { font-size: 13px; font-weight: 600; flex: 1; }
.card-badge {
font-size: 10px; font-weight: 600; padding: 2px 8px;
border-radius: 999px; text-transform: uppercase; letter-spacing: 0.05em;
}
.badge-blue { background: rgba(91,124,250,0.15); color: var(--primary); }
.badge-teal { background: rgba(46,201,183,0.15); color: var(--teal); }
.badge-green { background: rgba(62,207,142,0.15); color: var(--green); }
.badge-orange { background: rgba(245,158,11,0.15); color: var(--orange); }
.badge-purple { background: rgba(167,139,250,0.15); color: var(--purple); }
.badge-red { background: rgba(248,113,113,0.15); color: var(--red); }
.badge-muted { background: var(--surf3); color: var(--muted); }
.card-actions {
position: absolute; top: 10px; right: 12px;
display: flex; gap: 4px;
}
.card-btn {
width: 24px; height: 24px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 12px; color: var(--muted);
transition: all var(--trans);
}
.card-btn:hover { background: var(--surf3); color: var(--text); }
.card-btn.del:hover { color: var(--red); }
/* Task list inside card */
.task-list { display: flex; flex-direction: column; gap: 6px; }
.task-row {
display: flex; align-items: flex-start; gap: 8px;
padding: 8px 10px; border-radius: 8px;
background: var(--surf3); border: 1px solid var(--border);
transition: all var(--trans);
}
.task-row:hover { border-color: var(--border2); }
.task-type {
font-size: 10px; font-weight: 600; padding: 2px 7px;
border-radius: 999px; background: var(--faint); color: var(--muted);
flex-shrink: 0; margin-top: 1px;
}
.task-fields { flex: 1; display: flex; flex-wrap: wrap; gap: 6px; }
.task-fields input {
flex: 1; min-width: 80px; padding: 4px 8px;
border-radius: 6px; background: var(--surf2);
border: 1px solid var(--border); color: var(--text);
font-size: 12px; outline: none;
transition: border-color var(--trans);
}
.task-fields input:focus { border-color: var(--primary); }
.task-del {
color: var(--faint); font-size: 14px; margin-top: 2px;
transition: color var(--trans); cursor: pointer; flex-shrink: 0;
}
.task-del:hover { color: var(--red); }
.add-task-btn {
padding: 5px 12px; border-radius: 7px; font-size: 11px;
border: 1px dashed var(--faint); color: var(--muted);
display: flex; align-items: center; gap: 4px; width: 100%;
justify-content: center; transition: all var(--trans);
}
.add-task-btn:hover { border-color: var(--primary); color: var(--primary); background: rgba(91,124,250,0.04); }
/* Connector arrow */
.connector {
display: flex; justify-content: center; align-items: center;
height: 20px; color: var(--faint); font-size: 16px;
position: relative;
}
.connector::before, .connector::after {
content: ''; position: absolute; left: 50%;
transform: translateX(-50%);
width: 1px; height: 8px;
background: var(--faint);
}
.connector::before { top: 0; }
.connector::after { bottom: 0; }
/* ── Right panel ────────────────────────────────────────────────── */
.right-panel {
width: 300px; min-width: 280px;
background: var(--surf);
border-left: 1px solid var(--border);
display: flex; flex-direction: column; overflow-y: auto;
}
.log-header {
padding: 14px 16px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
}
.log-title { font-size: 12px; font-weight: 700; color: var(--text); }
.log-clear { font-size: 11px; color: var(--muted); cursor: pointer; transition: color var(--trans); }
.log-clear:hover { color: var(--red); }
.log-body { flex: 1; padding: 12px; display: flex; flex-direction: column; gap: 4px; overflow-y: auto; }
.log-entry {
font-family: var(--mono); font-size: 11px; line-height: 1.6;
padding: 6px 8px; border-radius: 6px; background: var(--surf2);
border-left: 2px solid var(--faint);
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn { from { opacity: 0; transform: translateX(4px); } to { opacity: 1; } }
.log-entry.info { border-color: var(--primary); }
.log-entry.ok { border-color: var(--green); }
.log-entry.warn { border-color: var(--orange); }
.log-entry.error { border-color: var(--red); }
.log-time { color: var(--muted); }
.log-stage { font-weight: 600; }
.log-entry.info .log-stage { color: var(--primary); }
.log-entry.ok .log-stage { color: var(--green); }
.log-entry.warn .log-stage { color: var(--orange); }
.log-entry.error .log-stage { color: var(--red); }
.result-section { padding: 12px 16px; border-top: 1px solid var(--border); }
.result-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 10px; }
.result-row { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.result-label { font-size: 11px; color: var(--muted); flex: 1; }
.result-val { font-family: var(--mono); font-size: 11px; color: var(--teal); }
.result-pill {
font-size: 10px; font-weight: 600; padding: 2px 8px;
border-radius: 999px;
}
.pill-ok { background: rgba(62,207,142,0.15); color: var(--green); }
.pill-err { background: rgba(248,113,113,0.15); color: var(--red); }
.pill-run { background: rgba(91,124,250,0.15); color: var(--primary); animation: pulse 1s infinite; }
.pill-idle { background: var(--surf3); color: var(--muted); }
.download-btn {
width: 100%; padding: 9px; border-radius: 8px;
background: rgba(62,207,142,0.1); border: 1px solid rgba(62,207,142,0.3);
color: var(--green); font-size: 12px; font-weight: 600;
display: flex; align-items: center; justify-content: center; gap: 6px;
margin-top: 10px; cursor: pointer; transition: all var(--trans);
display: none;
}
.download-btn.show { display: flex; }
.download-btn:hover { background: rgba(62,207,142,0.2); }
/* ── Status bar ─────────────────────────────────────────────────── */
.statusbar {
height: 26px; background: var(--surf);
border-top: 1px solid var(--border);
display: flex; align-items: center; gap: 16px;
padding: 0 16px; font-size: 11px; color: var(--muted);
}
.statusbar span { display: flex; align-items: center; gap: 4px; }
.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); }
.dot.busy { background: var(--orange); animation: pulse 1s infinite; }
/* scrollbar */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--faint); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
/* mobile note */
@media (max-width: 900px) {
.left-panel { width: 220px; }
.right-panel { width: 240px; }
.team-summary { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- TOP BAR -->
<header class="topbar">
<a href="#" class="logo">
<div class="logo-icon">βš™</div>
PDF Orchestrator
</a>
<div class="topbar-center">
<button class="tab-btn active" onclick="switchTab(this,'pipeline')">Pipeline</button>
<button class="tab-btn" onclick="switchTab(this,'config')">Config</button>
<button class="tab-btn" onclick="switchTab(this,'team')">AI Team</button>
</div>
<div class="topbar-right">
<button class="btn-ghost" id="clearBtn" onclick="clearPipeline()">Clear</button>
<button class="btn-run" id="runBtn" onclick="runPipeline()">
<span>β–Ά</span> Run Pipeline
</button>
</div>
</header>
<!-- LAYOUT -->
<div class="layout">
<!-- LEFT: file + agent palette -->
<aside class="left-panel">
<div class="panel-section">
<div class="panel-label">Input PDF</div>
<div class="file-drop" id="fileDrop">
<input type="file" accept=".pdf" onchange="handleFile(this)">
<div class="file-drop-icon">πŸ“„</div>
<p><b>Click or drag</b> a PDF here</p>
</div>
<div class="file-name" id="fileName">
<span>πŸ“„</span>
<span id="fileNameText">β€”</span>
</div>
</div>
<div class="panel-section">
<div class="panel-label">Output Folder</div>
<div class="form-row">
<input type="text" id="outputDir" placeholder="C:\Users\...\pdf-output" style="width:100%;padding:7px 10px;border-radius:7px;background:var(--surf2);border:1px solid var(--border);color:var(--text);outline:none;">
</div>
<div class="toggle-row">
<span>Force OCR</span>
<div class="toggle" id="ocrToggle" onclick="this.classList.toggle('on')"></div>
</div>
<div class="form-row">
<label>OCR Language</label>
<input type="text" id="ocrLang" value="eng" placeholder="eng / est / deu">
</div>
</div>
<div class="panel-section" style="flex:1">
<div class="panel-label">Available Agents</div>
<div class="panel-label" style="font-size:10px;color:var(--faint);margin-bottom:8px;margin-top:-6px">Drag into pipeline β†’</div>
<div class="agent-palette" id="agentPalette"></div>
</div>
</aside>
<!-- CENTRE: pipeline canvas -->
<main class="main-canvas" id="mainCanvas">
<section id="pipelineView" class="view-panel">
<div>
<div class="canvas-header">
<div>
<div class="canvas-title">Agent Pipeline</div>
<div class="canvas-sub">Drag agents from the left to build your processing flow. Configure tasks per agent.</div>
</div>
</div>
</div>
<div class="pipeline-zone" id="pipelineZone"
ondragover="onDragOver(event)" ondragleave="onDragLeave(event)" ondrop="onDrop(event)">
<div class="pipeline-empty" id="pipelineEmpty">
<div class="pipeline-empty-icon">⬑</div>
<p style="font-weight:600;color:var(--text)">Pipeline is empty</p>
<p style="font-size:12px">Drag agents from the left panel to start building</p>
</div>
</div>
</section>
<section id="teamView" class="view-panel hidden">
<div class="canvas-header">
<div>
<div class="canvas-title">AI Team Command Router</div>
<div class="canvas-sub">Sisesta ΓΌks kΓ΅rgema taseme kΓ€sk ja sΓΌsteem jagab selle AI rollidele laiali koos mudelite, mΓ΅ttekΓ€igu ja soovitusliku pipeline'iga.</div>
</div>
</div>
<div class="command-studio">
<div class="command-box">
<div class="panel-label" style="margin-bottom:0">Command Brief</div>
<textarea id="teamCommandInput" placeholder="NΓ€ide: VΓ΅ta see konto vΓ€ljavΓ΅tte PDF, tee see otsitavaks, sΓ€ilita tabelid, paranda struktuur ja valmista mulle kontrollitud lΓ΅pp-PDF."></textarea>
<div class="command-actions">
<button class="btn-run" type="button" onclick="routeCommand()">
<span>🧠</span> Build Team Plan
</button>
<button class="btn-ghost" type="button" onclick="loadRecommendedPipeline()">Load Recommended Pipeline</button>
<span class="command-meta" id="teamCommandMeta">AI team is ready to reason about your PDF command.</span>
</div>
</div>
<div class="team-summary" id="teamSummary">
<div class="summary-block">
<h4>Recommended pipeline</h4>
<p id="recommendedPipelineText">No recommendation yet.</p>
</div>
<div class="summary-block">
<h4>Reasoning</h4>
<ul id="teamReasons">
<li>Enter a command to generate a team plan.</li>
</ul>
</div>
</div>
<div class="team-grid" id="teamGrid"></div>
</div>
</section>
</main>
<!-- RIGHT: logs + result -->
<aside class="right-panel">
<div class="log-header">
<span class="log-title">Live Logs</span>
<span class="log-clear" onclick="clearLogs()">Clear</span>
</div>
<div class="log-body" id="logBody"></div>
<div class="result-section">
<div class="result-title">Job Status</div>
<div class="result-row">
<span class="result-label">Status</span>
<span class="result-pill pill-idle" id="statusPill">Idle</span>
</div>
<div class="result-row">
<span class="result-label">Pages</span>
<span class="result-val" id="statPages">β€”</span>
</div>
<div class="result-row">
<span class="result-label">Duration</span>
<span class="result-val" id="statDuration">β€”</span>
</div>
<div class="result-row">
<span class="result-label">Output</span>
<span class="result-val" id="statOutput" style="font-size:10px;word-break:break-all">β€”</span>
</div>
<button class="download-btn" id="downloadBtn" onclick="downloadResult()">
⬇ Download Final PDF
</button>
</div>
</aside>
</div>
<!-- STATUS BAR -->
<footer class="statusbar">
<span><div class="dot" id="statusDot"></div> <span id="statusText">Ready β€” AI team online</span></span>
<span>🧠 AI team · Local-first routing</span>
<span id="agentCountBar">0 agents in pipeline</span>
</footer>
<script>
// ═══════════════════════════════════════════════════════════
// AGENT DEFINITIONS
// ═══════════════════════════════════════════════════════════
const AGENTS = [
{
id: 'inspector', name: 'Structure Inspector', role: 'Analyzes PDF structure & metadata',
icon: 'πŸ”', color: '#5b7cfa', badge: 'badge-blue',
tasks: [
{ type: 'inspect', label: 'Inspect PDF', fields: [] }
],
taskTypes: []
},
{
id: 'ocr', name: 'OCR Agent', role: 'Converts scanned pages to searchable text',
icon: 'πŸ‘', color: '#2ec9b7', badge: 'badge-teal',
tasks: [],
taskTypes: []
},
{
id: 'editor', name: 'Editing Agent', role: 'Text, region and page operations',
icon: '✏️', color: '#a78bfa', badge: 'badge-purple',
tasks: [],
taskTypes: [
{ type: 'insert_text', label: '+ Insert Text', fields: ['page','x','y','text'] },
{ type: 'whiteout_region',label: '⬜ Whiteout Region',fields: ['page','x0','y0','x1','y1','replacement'] },
{ type: 'rotate_page', label: '↻ Rotate Page', fields: ['page','degrees'] },
{ type: 'delete_page', label: 'βœ• Delete Page', fields: ['page'] },
{ type: 'reorder_pages', label: 'β‡… Reorder Pages', fields: ['new_order (e.g. 0,2,1)'] },
]
},
{
id: 'annotator', name: 'Annotation Agent', role: 'Highlights, comments, redactions',
icon: 'πŸ–Š', color: '#f59e0b', badge: 'badge-orange',
tasks: [],
taskTypes: [
{ type: 'highlight', label: '🟑 Highlight', fields: ['page','x0','y0','x1','y1'] },
{ type: 'comment', label: 'πŸ’¬ Comment', fields: ['page','x','y','text'] },
{ type: 'rectangle', label: '⬛ Rectangle', fields: ['page','x0','y0','x1','y1'] },
{ type: 'redact', label: '⬛ Redact', fields: ['page','x0','y0','x1','y1','label'] },
]
},
{
id: 'validator', name: 'Validation Agent', role: 'Checks integrity & rebuilds if needed',
icon: 'βœ…', color: '#3ecf8e', badge: 'badge-green',
tasks: [],
taskTypes: []
},
{
id: 'exporter', name: 'Export Agent', role: 'Saves PDF, manifest & JSONL logs',
icon: 'πŸ“¦', color: '#f87171', badge: 'badge-red',
tasks: [],
taskTypes: []
},
];
// ═══════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════
let pipeline = []; // [{agentId, instanceId, tasks:[]}]
let dragAgentId = null;
let selectedFile = null;
let running = false;
let startTime = null;
let resultPath = null;
let activeTab = 'pipeline';
let teamConfig = null;
let teamPlan = null;
// ═══════════════════════════════════════════════════════════
// INIT β€” render palette
// ═══════════════════════════════════════════════════════════
function init() {
const pal = document.getElementById('agentPalette');
AGENTS.forEach(a => {
const el = document.createElement('div');
el.className = 'palette-agent';
el.draggable = true;
el.dataset.agentId = a.id;
el.innerHTML = `
<div class="agent-avatar" style="background:${a.color}22;font-size:18px">${a.icon}</div>
<div class="agent-info">
<div class="name">${a.name}</div>
<div class="role">${a.role}</div>
</div>
<span class="drag-hint">β Ώ</span>`;
el.addEventListener('dragstart', e => { dragAgentId = a.id; e.dataTransfer.effectAllowed = 'copy'; });
el.addEventListener('dragend', () => { dragAgentId = null; });
pal.appendChild(el);
});
loadTeamConfig();
}
// ═══════════════════════════════════════════════════════════
// FILE HANDLING
// ═══════════════════════════════════════════════════════════
function handleFile(input) {
const f = input.files[0];
if (!f) return;
selectedFile = f;
document.getElementById('fileNameText').textContent = f.name;
document.getElementById('fileName').classList.add('show');
appendLog('info','File','Loaded: ' + f.name);
}
// ═══════════════════════════════════════════════════════════
// DRAG & DROP PIPELINE
// ═══════════════════════════════════════════════════════════
function onDragOver(e) {
e.preventDefault();
e.currentTarget.classList.add('drag-over');
}
function onDragLeave(e) { e.currentTarget.classList.remove('drag-over'); }
function onDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('drag-over');
if (!dragAgentId) return;
addAgentToPipeline(dragAgentId);
}
function addAgentToPipeline(agentId) {
const agentDef = AGENTS.find(a => a.id === agentId);
if (!agentDef) return;
const instanceId = agentId + '_' + Date.now();
const entry = {
agentId,
instanceId,
tasks: agentDef.tasks.map(t => ({...t, values:{}})),
};
pipeline.push(entry);
renderPipeline();
updateStatusBar();
appendLog('info', agentDef.name, 'Added to pipeline');
}
// ═══════════════════════════════════════════════════════════
// RENDER PIPELINE
// ═══════════════════════════════════════════════════════════
function renderPipeline() {
const zone = document.getElementById('pipelineZone');
const empty = document.getElementById('pipelineEmpty');
zone.innerHTML = '';
zone.appendChild(empty);
empty.style.display = pipeline.length === 0 ? 'flex' : 'none';
pipeline.forEach((entry, idx) => {
const agentDef = AGENTS.find(a => a.id === entry.agentId);
if (!agentDef) return;
if (idx > 0) {
const conn = document.createElement('div');
conn.className = 'connector';
conn.innerHTML = '↓';
zone.appendChild(conn);
}
const card = document.createElement('div');
card.className = 'pipeline-card';
card.id = 'card_' + entry.instanceId;
// task rows
const taskHTML = entry.tasks.map((task, ti) => renderTaskRow(entry, task, ti)).join('');
// add-task dropdown options
const taskOpts = agentDef.taskTypes.map(tt =>
`<option value="${tt.type}">${tt.label}</option>`).join('');
const showAddTask = agentDef.taskTypes.length > 0;
card.innerHTML = `
<div class="card-header">
<div class="card-num">${idx+1}</div>
<div class="agent-avatar" style="background:${agentDef.color}22;width:28px;height:28px;font-size:15px;">${agentDef.icon}</div>
<span class="card-title">${agentDef.name}</span>
<span class="card-badge ${agentDef.badge}">${agentDef.role}</span>
<div class="card-actions">
${idx>0 ? `<button class="card-btn" title="Move up" onclick="moveCard('${entry.instanceId}',-1)">↑</button>` : ''}
${idx<pipeline.length-1 ? `<button class="card-btn" title="Move down" onclick="moveCard('${entry.instanceId}',1)">↓</button>` : ''}
<button class="card-btn del" title="Remove" onclick="removeCard('${entry.instanceId}')">βœ•</button>
</div>
</div>
<div class="task-list" id="tasks_${entry.instanceId}">${taskHTML}</div>
${showAddTask ? `
<div style="display:flex;gap:6px;align-items:center">
<select id="sel_${entry.instanceId}" style="flex:1;padding:5px 8px;border-radius:7px;background:var(--surf3);border:1px solid var(--border);color:var(--text);font-size:12px;outline:none">
<option value="">β€” Add a task β€”</option>
${taskOpts}
</select>
<button class="add-task-btn" style="width:auto;padding:5px 12px" onclick="addTask('${entry.instanceId}')">+ Add</button>
</div>` : ''}
`;
zone.appendChild(card);
});
updateStatusBar();
}
function renderTaskRow(entry, task, ti) {
const fieldInputs = (task.fields || []).map(f =>
`<input placeholder="${f}" title="${f}" value="${task.values?.[f]||''}"
onchange="updateTaskField('${entry.instanceId}',${ti},'${f}',this.value)">`
).join('');
return `
<div class="task-row" id="task_${entry.instanceId}_${ti}">
<span class="task-type">${task.type}</span>
<div class="task-fields">${fieldInputs}</div>
<span class="task-del" onclick="removeTask('${entry.instanceId}',${ti})">βœ•</span>
</div>`;
}
function addTask(instanceId) {
const sel = document.getElementById('sel_' + instanceId);
if (!sel || !sel.value) return;
const entry = pipeline.find(e => e.instanceId === instanceId);
const agentDef = AGENTS.find(a => a.id === entry.agentId);
const tt = agentDef.taskTypes.find(t => t.type === sel.value);
if (!tt) return;
entry.tasks.push({ type: tt.type, fields: tt.fields, values: {} });
renderPipeline();
}
function removeTask(instanceId, ti) {
const entry = pipeline.find(e => e.instanceId === instanceId);
entry.tasks.splice(ti, 1);
renderPipeline();
}
function updateTaskField(instanceId, ti, field, val) {
const entry = pipeline.find(e => e.instanceId === instanceId);
if (!entry.tasks[ti].values) entry.tasks[ti].values = {};
entry.tasks[ti].values[field] = val;
}
function removeCard(instanceId) {
pipeline = pipeline.filter(e => e.instanceId !== instanceId);
renderPipeline();
}
function moveCard(instanceId, dir) {
const idx = pipeline.findIndex(e => e.instanceId === instanceId);
const newIdx = idx + dir;
if (newIdx < 0 || newIdx >= pipeline.length) return;
[pipeline[idx], pipeline[newIdx]] = [pipeline[newIdx], pipeline[idx]];
renderPipeline();
}
function clearPipeline() {
pipeline = [];
renderPipeline();
clearLogs();
document.getElementById('statusPill').className = 'result-pill pill-idle';
document.getElementById('statusPill').textContent = 'Idle';
document.getElementById('downloadBtn').classList.remove('show');
appendLog('info','System','Pipeline cleared');
}
function clearTeamPlan() {
teamPlan = null;
document.getElementById('teamCommandInput').value = '';
document.getElementById('teamCommandMeta').textContent = 'AI team is ready to reason about your PDF command.';
document.getElementById('recommendedPipelineText').textContent = 'No recommendation yet.';
document.getElementById('teamReasons').innerHTML = '<li>Enter a command to generate a team plan.</li>';
renderTeamCards(teamConfig?.team || [], []);
appendLog('info','AI Team','Team plan cleared');
}
// ── HTTP bridge to local Python server ──────────────────────────
async function runViaHTTP(config) {
const r = await fetch('/run', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(config)
});
return await r.json();
}
async function fetchTeamConfig() {
const r = await fetch('/team-config');
return await r.json();
}
async function buildTeamPlan(payload) {
const r = await fetch('/team-plan', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
return await r.json();
}
// ═══════════════════════════════════════════════════════════
// PIPELINE RUNNER (simulated + sends to Python bridge)
// ═══════════════════════════════════════════════════════════
async function runPipeline() {
if (running) return;
if (pipeline.length === 0) { appendLog('warn','System','Add at least one agent first'); return; }
running = true;
startTime = Date.now();
document.getElementById('runBtn').innerHTML = '<span style="animation:pulse 1s infinite;display:inline-block">⏳</span> Running…';
document.getElementById('statusPill').className = 'result-pill pill-run';
document.getElementById('statusPill').textContent = 'Running';
document.getElementById('statusDot').classList.add('busy');
document.getElementById('statusText').textContent = 'Pipeline running…';
document.getElementById('downloadBtn').classList.remove('show');
const config = buildConfig();
appendLog('info','Orchestrator','Job started id=' + config.job_id);
// Try to send to Python backend via pywebview / bridge
// Falls back to simulation if bridge not available
let result = null;
try {
try { result = await runViaHTTP(config); } catch(e) { result = await simulatePipeline(config); }
} catch(e) {
result = await simulatePipeline(config);
}
finishRun(result);
}
function buildConfig() {
const forceOcr = document.getElementById('ocrToggle').classList.contains('on');
const lang = document.getElementById('ocrLang').value.trim() || 'eng';
const outDir = document.getElementById('outputDir').value.trim() || (selectedFile ? '' : 'C:\\pdf-output');
const ops = [];
pipeline.forEach(entry => {
entry.tasks.forEach(task => {
if (!task.type) return;
const op = { type: task.type };
(task.fields||[]).forEach(f => {
const v = task.values?.[f];
if (v !== undefined && v !== '') {
const num = Number(v);
op[f] = isNaN(num) ? v : num;
}
});
ops.push(op);
});
});
return {
job_id: Math.random().toString(36).slice(2,10),
input_pdf: selectedFile ? selectedFile.name : '',
output_dir: outDir,
force_ocr: forceOcr,
ocr_language: lang,
pipeline: pipeline.map(e => e.agentId),
operations: ops,
};
}
async function simulatePipeline(config) {
for (let i = 0; i < pipeline.length; i++) {
const entry = pipeline[i];
const agentDef = AGENTS.find(a => a.id === entry.agentId);
const card = document.getElementById('card_' + entry.instanceId);
if (card) card.classList.add('running');
appendLog('info', agentDef.name, 'Starting…');
await sleep(600 + Math.random()*600);
if (card) { card.classList.remove('running'); card.classList.add('done'); }
const numEl = card?.querySelector('.card-num');
if (numEl) numEl.textContent = 'βœ“';
appendLog('ok', agentDef.name, 'Completed βœ“');
}
return {
success: true,
job_id: config.job_id,
final_pdf: (config.output_dir || 'output') + '\\result-final.pdf',
page_count: 4,
};
}
function finishRun(result) {
running = false;
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
document.getElementById('runBtn').innerHTML = '<span>β–Ά</span> Run Pipeline';
document.getElementById('statusDot').classList.remove('busy');
if (result.success) {
document.getElementById('statusPill').className = 'result-pill pill-ok';
document.getElementById('statusPill').textContent = 'Done';
document.getElementById('statusText').textContent = 'Pipeline complete βœ“';
document.getElementById('statPages').textContent = result.page_count || 'β€”';
document.getElementById('statDuration').textContent = elapsed + 's';
document.getElementById('statOutput').textContent = result.final_pdf || 'β€”';
document.getElementById('downloadBtn').classList.add('show');
resultPath = result.final_pdf;
appendLog('ok','Orchestrator','Job finished in ' + elapsed + 's');
} else {
document.getElementById('statusPill').className = 'result-pill pill-err';
document.getElementById('statusPill').textContent = 'Failed';
document.getElementById('statusText').textContent = 'Pipeline failed';
appendLog('error','Orchestrator','Job failed: ' + (result.error || 'unknown error'));
}
}
function downloadResult() {
if (!resultPath) return;
// When running inside Python webview, trigger native save
if (window.pybridge && window.pybridge.reveal_file) {
window.pybridge.reveal_file(resultPath);
} else {
appendLog('info','Export','File path: ' + resultPath);
}
}
// ═══════════════════════════════════════════════════════════
// LOGGING
// ═══════════════════════════════════════════════════════════
function appendLog(level, stage, msg) {
const body = document.getElementById('logBody');
const now = new Date().toTimeString().slice(0,8);
const el = document.createElement('div');
el.className = 'log-entry ' + level;
el.innerHTML = `<span class="log-time">${now}</span> <span class="log-stage">[${stage}]</span> ${msg}`;
body.appendChild(el);
body.scrollTop = body.scrollHeight;
}
function clearLogs() {
document.getElementById('logBody').innerHTML = '';
}
// ═══════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════
function updateStatusBar() {
document.getElementById('agentCountBar').textContent = pipeline.length + ' agent' + (pipeline.length !== 1 ? 's' : '') + ' in pipeline';
}
function switchTab(btn, tab) {
activeTab = tab;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('pipelineView').classList.toggle('hidden', tab !== 'pipeline' && tab !== 'config');
document.getElementById('teamView').classList.toggle('hidden', tab !== 'team');
const clearBtn = document.getElementById('clearBtn');
if (tab === 'team') {
clearBtn.textContent = 'Clear Team';
clearBtn.onclick = clearTeamPlan;
} else {
clearBtn.textContent = 'Clear';
clearBtn.onclick = clearPipeline;
}
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function loadTeamConfig() {
try {
const res = await fetchTeamConfig();
if (!res.success) return;
teamConfig = res;
renderTeamCards(res.team, []);
} catch (error) {
appendLog('warn','AI Team','Could not load team config: ' + error.message);
}
}
function renderTeamCards(team, insights) {
const grid = document.getElementById('teamGrid');
const insightMap = Object.fromEntries((insights || []).map(i => [i.agent_key, i]));
grid.innerHTML = '';
(team || []).forEach(member => {
const insight = insightMap[member.agent_key];
const card = document.createElement('div');
card.className = 'team-card';
card.innerHTML = `
<div class="team-card-header">
<div class="team-avatar" style="background:${member.color}22;color:${member.color}">${member.icon}</div>
<div style="min-width:0">
<div class="team-card-title">${member.name}</div>
<div class="team-card-sub">${member.title}</div>
</div>
</div>
<div class="model-pill">requested=${member.requested_provider || member.provider} Β· active=${member.provider}</div>
<div class="model-line">${member.model || 'β€”'}<br>${member.base_url || 'β€”'}</div>
<div class="team-insight">
<div><strong>Focus:</strong> ${insight?.focus || 'Waiting for a command...'}</div>
<div><strong>Action:</strong> ${insight?.action || 'No action yet.'}</div>
<div><strong>Handoff:</strong> ${insight?.handoff || 'No handoff yet.'}</div>
</div>
`;
grid.appendChild(card);
});
}
function renderRecommendation(recommendation) {
document.getElementById('recommendedPipelineText').textContent =
recommendation ? `${recommendation.pipeline_name} β†’ ${recommendation.recommended_agents.join(' -> ')}` : 'No recommendation yet.';
const reasons = document.getElementById('teamReasons');
reasons.innerHTML = '';
const items = recommendation?.reasons?.length ? recommendation.reasons : ['No reasoning available yet.'];
items.forEach(reason => {
const li = document.createElement('li');
li.textContent = reason;
reasons.appendChild(li);
});
}
function buildCommandPayload() {
return {
brief: document.getElementById('teamCommandInput').value.trim(),
input_pdf: selectedFile ? selectedFile.name : '',
output_dir: document.getElementById('outputDir').value.trim() || '',
};
}
async function routeCommand() {
const payload = buildCommandPayload();
if (!payload.brief) {
appendLog('warn','AI Team','Kirjelda kΓ΅igepealt, mida PDF-iga teha tahad');
return;
}
document.getElementById('teamCommandMeta').textContent = 'Thinking across the team…';
appendLog('info','AI Team','Distributing command across the AI team');
try {
const res = await buildTeamPlan(payload);
if (!res.success) {
appendLog('error','AI Team','Team planning failed: ' + (res.error || 'unknown error'));
document.getElementById('teamCommandMeta').textContent = 'Team planning failed.';
return;
}
teamPlan = res;
renderTeamCards(res.team, res.insights);
renderRecommendation(res.recommendation);
document.getElementById('teamCommandMeta').textContent =
`Team plan ready. Recommended pipeline: ${res.recommendation.pipeline_name}`;
appendLog('ok','AI Team','Team plan is ready');
} catch (error) {
appendLog('error','AI Team','Team planning failed: ' + error.message);
document.getElementById('teamCommandMeta').textContent = 'Team planning failed.';
}
}
function loadRecommendedPipeline() {
const recommendation = teamPlan?.recommendation;
if (!recommendation) {
appendLog('warn','AI Team','No recommended pipeline available yet');
return;
}
pipeline = [];
recommendation.recommended_agents.forEach(agentId => addAgentToPipeline(agentId));
switchTab(document.querySelectorAll('.tab-btn')[0], 'pipeline');
appendLog('ok','AI Team','Loaded recommended pipeline: ' + recommendation.pipeline_name);
}
// ═══════════════════════════════════════════════════════════
// DRAG FILE ONTO DROP ZONE
// ═══════════════════════════════════════════════════════════
const drop = document.getElementById('fileDrop');
drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('drag'); });
drop.addEventListener('dragleave', () => drop.classList.remove('drag'));
drop.addEventListener('drop', e => {
e.preventDefault(); drop.classList.remove('drag');
const f = e.dataTransfer.files[0];
if (f && f.name.endsWith('.pdf')) {
selectedFile = f;
document.getElementById('fileNameText').textContent = f.name;
document.getElementById('fileName').classList.add('show');
appendLog('info','File','Dropped: ' + f.name);
}
});
// ═══════════════════════════════════════════════════════════
// BOOT
// ═══════════════════════════════════════════════════════════
init();
appendLog('info','System','Orchestrator ready β€” AI team enabled');
appendLog('info','System','Drag agents from the left panel or switch to AI Team for command routing');
</script>
</body>
</html>