Download src/ui/web/orchestrator.html from techprotrade/annator: direct link, hf CLI and curl.
- Browser
- Download file 50.7 kB
-
https://huggingface.co/techprotrade/annator/resolve/main/src/ui/web/orchestrator.html
- Command line
-
hf download hf://techprotrade/annator/src/ui/web/orchestrator.html
-
curl -L -o orchestrator.html https://huggingface.co/techprotrade/annator/resolve/main/src/ui/web/orchestrator.html
50.7 kB
| <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> | |