| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Problem Solver — AI Intern Tool</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,300;0,500;0,700;1,300&family=Geist+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> |
| <style> |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| :root { |
| --bg: #f7f6f2; |
| --white: #ffffff; |
| --surface: #f0eeea; |
| --border: #e2dfd8; |
| --border2: #d4d0c8; |
| --ink: #1a1916; |
| --ink2: #3d3b35; |
| --muted: #7a776e; |
| --muted2: #a09d95; |
| --accent: #d4621a; |
| --accent2: #b8501a; |
| --accent-bg: #fdf1ea; |
| --green: #2d7a4f; |
| --green-bg: #eaf4ee; |
| --font-head: 'Fraunces', Georgia, serif; |
| --font-body: 'Plus Jakarta Sans', sans-serif; |
| --font-mono: 'Geist Mono', monospace; |
| } |
| |
| html { scroll-behavior: smooth; } |
| body { |
| font-family: var(--font-body); |
| background: var(--bg); |
| color: var(--ink); |
| min-height: 100vh; |
| font-size: 14px; |
| } |
| |
| |
| .app { |
| display: grid; |
| grid-template-columns: 400px 1fr; |
| grid-template-rows: 56px 1fr; |
| height: 100vh; |
| overflow: hidden; |
| } |
| |
| |
| .topbar { |
| grid-column: 1 / -1; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 0 28px; |
| border-bottom: 1px solid var(--border); |
| background: var(--white); |
| } |
| .topbar-logo { |
| font-family: var(--font-head); |
| font-size: 17px; |
| font-weight: 700; |
| color: var(--ink); |
| letter-spacing: -.02em; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| .topbar-logo .dot { width: 8px; height: 8px; background: var(--accent); border-radius: 50%; } |
| .topbar-meta { font-size: 12px; color: var(--muted); font-family: var(--font-mono); letter-spacing: .02em; } |
| |
| |
| .panel-left { |
| border-right: 1px solid var(--border); |
| background: var(--white); |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| } |
| .panel-inner { flex: 1; overflow-y: auto; padding: 28px 24px; } |
| .panel-inner::-webkit-scrollbar { width: 3px; } |
| .panel-inner::-webkit-scrollbar-thumb { background: var(--border2); } |
| |
| .section-label { |
| font-family: var(--font-mono); |
| font-size: 10px; |
| font-weight: 500; |
| letter-spacing: .12em; |
| text-transform: uppercase; |
| color: var(--muted2); |
| margin-bottom: 12px; |
| } |
| |
| |
| .fields-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 24px; } |
| .field label { display: block; font-size: 11px; font-weight: 600; color: var(--muted); margin-bottom: 5px; letter-spacing: .03em; } |
| input[type="text"] { |
| width: 100%; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| color: var(--ink); |
| font-family: var(--font-body); |
| font-size: 13px; |
| padding: 9px 12px; |
| transition: border-color .15s, box-shadow .15s; |
| outline: none; |
| } |
| input[type="text"]:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(212,98,26,.1); background: var(--white); } |
| input::placeholder { color: var(--muted2); } |
| |
| |
| .tabs { |
| display: flex; gap: 2px; |
| background: var(--surface); |
| border-radius: 10px; padding: 3px; |
| margin-bottom: 14px; |
| } |
| .tab { |
| flex: 1; padding: 8px; text-align: center; |
| font-size: 12px; font-weight: 600; color: var(--muted); |
| border-radius: 8px; cursor: pointer; |
| transition: background .15s, color .15s; user-select: none; |
| } |
| .tab.active { background: var(--white); color: var(--ink); box-shadow: 0 1px 4px rgba(0,0,0,.08); } |
| |
| #pastePane, #uploadPane { display: none; } |
| #pastePane.shown, #uploadPane.shown { display: block; } |
| |
| textarea { |
| width: 100%; |
| background: var(--surface); border: 1px solid var(--border); |
| border-radius: 10px; color: var(--ink); |
| font-family: var(--font-body); font-size: 13px; line-height: 1.7; |
| padding: 14px; min-height: 210px; resize: none; outline: none; |
| transition: border-color .15s, box-shadow .15s; |
| } |
| textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(212,98,26,.1); background: var(--white); } |
| textarea::placeholder { color: var(--muted2); } |
| |
| |
| .upload-zone { |
| border: 1.5px dashed var(--border2); border-radius: 10px; |
| padding: 36px 20px; text-align: center; cursor: pointer; |
| transition: border-color .2s, background .2s; background: var(--surface); |
| } |
| .upload-zone:hover, .upload-zone.drag { border-color: var(--accent); background: var(--accent-bg); } |
| .upload-zone .icon { font-size: 26px; margin-bottom: 10px; } |
| .upload-zone p { font-size: 13px; color: var(--muted); line-height: 1.6; } |
| .upload-zone p strong { color: var(--ink2); } |
| .upload-zone .formats { font-family: var(--font-mono); font-size: 10px; color: var(--muted2); margin-top: 8px; letter-spacing: .06em; } |
| .file-chosen { |
| display: none; align-items: center; gap: 10px; |
| padding: 10px 14px; background: var(--green-bg); |
| border: 1px solid #b6dfca; border-radius: 8px; margin-top: 10px; |
| font-size: 12px; color: var(--green); font-weight: 500; |
| } |
| .file-chosen.show { display: flex; } |
| .file-chosen .fname { flex: 1; font-family: var(--font-mono); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| .file-clear { cursor: pointer; font-size: 15px; color: var(--muted); flex-shrink: 0; } |
| input[type="file"] { display: none; } |
| |
| |
| .panel-footer { padding: 16px 24px; border-top: 1px solid var(--border); background: var(--white); } |
| .btn-run { |
| width: 100%; background: var(--accent); color: #fff; border: none; |
| border-radius: 10px; padding: 13px; font-family: var(--font-body); |
| font-size: 14px; font-weight: 600; cursor: pointer; |
| display: flex; align-items: center; justify-content: center; gap: 8px; |
| transition: background .15s, box-shadow .15s, transform .1s; |
| } |
| .btn-run:hover:not(:disabled) { background: var(--accent2); box-shadow: 0 4px 16px rgba(212,98,26,.28); } |
| .btn-run:active:not(:disabled) { transform: scale(.98); } |
| .btn-run:disabled { opacity: .45; cursor: not-allowed; } |
| |
| .btn-pdf { |
| width: 100%; margin-top: 8px; background: transparent; |
| color: var(--green); border: 1.5px solid #b6dfca; |
| border-radius: 10px; padding: 10px; font-family: var(--font-body); |
| font-size: 13px; font-weight: 600; cursor: pointer; |
| display: flex; align-items: center; justify-content: center; gap: 7px; |
| transition: background .15s; |
| } |
| .btn-pdf:hover:not(:disabled) { background: var(--green-bg); } |
| .btn-pdf:disabled { opacity: .35; cursor: not-allowed; } |
| |
| |
| .panel-right { display: flex; flex-direction: column; overflow: hidden; background: var(--bg); } |
| |
| .status-strip { |
| display: flex; align-items: center; gap: 10px; |
| padding: 0 28px; height: 42px; |
| border-bottom: 1px solid var(--border); background: var(--white); flex-shrink: 0; |
| } |
| .status-pill { display: flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 11px; color: var(--muted); } |
| .status-pill .s-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border2); } |
| .status-pill.running .s-dot { background: var(--accent); animation: pulse .8s infinite; } |
| .status-pill.done .s-dot { background: var(--green); } |
| .status-pill.error .s-dot { background: #c53030; } |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} } |
| |
| .pipe-track { margin-left: auto; display: flex; align-items: center; gap: 4px; } |
| .p-node { |
| font-family: var(--font-mono); font-size: 10px; color: var(--muted2); |
| padding: 3px 8px; border-radius: 4px; |
| background: var(--surface); border: 1px solid var(--border); |
| transition: all .2s; |
| } |
| .p-node.active { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); } |
| .p-node.done { color: var(--green); border-color: #b6dfca; background: var(--green-bg); } |
| .p-sep { color: var(--border2); font-size: 10px; } |
| |
| .output-scroll { flex: 1; overflow-y: auto; padding: 28px 32px; } |
| .output-scroll::-webkit-scrollbar { width: 4px; } |
| .output-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; } |
| |
| |
| .empty { |
| display: flex; flex-direction: column; align-items: center; |
| justify-content: center; height: 100%; text-align: center; |
| color: var(--muted2); gap: 12px; padding-bottom: 60px; |
| } |
| .empty-icon { |
| width: 52px; height: 52px; border-radius: 14px; |
| background: var(--white); border: 1px solid var(--border); |
| display: flex; align-items: center; justify-content: center; font-size: 22px; |
| } |
| .empty h3 { font-family: var(--font-head); font-size: 20px; font-weight: 500; color: var(--ink2); font-style: italic; } |
| .empty p { font-size: 13px; max-width: 280px; line-height: 1.6; } |
| |
| |
| .agent-block { |
| background: var(--white); border: 1px solid var(--border); |
| border-radius: 12px; margin-bottom: 16px; overflow: hidden; |
| opacity: 0; transform: translateY(10px); |
| transition: opacity .35s, transform .35s; |
| } |
| .agent-block.in { opacity: 1; transform: translateY(0); } |
| |
| .agent-head { |
| display: flex; align-items: center; gap: 10px; |
| padding: 12px 18px; border-bottom: 1px solid var(--border); |
| } |
| .agent-num { |
| width: 22px; height: 22px; border-radius: 6px; |
| display: flex; align-items: center; justify-content: center; |
| font-family: var(--font-mono); font-size: 11px; font-weight: 500; flex-shrink: 0; |
| } |
| .agent-name { font-size: 12px; font-weight: 600; letter-spacing: .02em; flex: 1; } |
| .agent-spinner { |
| width: 14px; height: 14px; border: 1.5px solid currentColor; |
| border-top-color: transparent; border-radius: 50%; |
| animation: spin .7s linear infinite; opacity: .6; |
| } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| .agent-check { font-size: 14px; } |
| |
| .ag-1 .agent-head { background: #fdf8f5; } |
| .ag-1 .agent-num { background: #fde8d8; color: #c45518; } |
| .ag-1 .agent-name { color: #c45518; } |
| .ag-1 .agent-spinner, .ag-1 .agent-check { color: #c45518; } |
| |
| .ag-2 .agent-head { background: #f5fbf7; } |
| .ag-2 .agent-num { background: #d6f0e2; color: #2d7a4f; } |
| .ag-2 .agent-name { color: #2d7a4f; } |
| .ag-2 .agent-spinner, .ag-2 .agent-check { color: #2d7a4f; } |
| |
| .ag-3 .agent-head { background: #f6f5fd; } |
| .ag-3 .agent-num { background: #e0ddfb; color: #5b4fcf; } |
| .ag-3 .agent-name { color: #5b4fcf; } |
| .ag-3 .agent-spinner, .ag-3 .agent-check { color: #5b4fcf; } |
| |
| .ag-4 .agent-head { background: #fdf9f2; } |
| .ag-4 .agent-num { background: #fdedc8; color: #b07c10; } |
| .ag-4 .agent-name { color: #b07c10; } |
| .ag-4 .agent-spinner, .ag-4 .agent-check { color: #b07c10; } |
| |
| .ag-5 .agent-head { background: #fdf5f5; } |
| .ag-5 .agent-num { background: #fdd8d8; color: #c53030; } |
| .ag-5 .agent-name { color: #c53030; } |
| .ag-5 .agent-spinner, .ag-5 .agent-check { color: #c53030; } |
| |
| .agent-body { padding: 18px 20px; font-size: 13.5px; line-height: 1.78; color: var(--ink2); } |
| .agent-body h2 { |
| font-family: var(--font-head); font-size: 16px; font-weight: 500; |
| font-style: italic; color: var(--ink); margin: 14px 0 6px; |
| padding-bottom: 6px; border-bottom: 1px solid var(--border); |
| } |
| .agent-body h2:first-child { margin-top: 0; } |
| .agent-body h3 { font-size: 13px; font-weight: 600; color: var(--ink); margin: 10px 0 4px; } |
| .agent-body strong { font-weight: 600; color: var(--ink); } |
| .agent-body ul, .agent-body ol { padding-left: 18px; margin: 4px 0 8px; } |
| .agent-body li { margin-bottom: 3px; } |
| |
| |
| .toast { |
| position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); |
| background: var(--ink); color: #fff; padding: 10px 20px; |
| border-radius: 8px; font-size: 13px; z-index: 100; |
| display: none; white-space: nowrap; box-shadow: 0 4px 16px rgba(0,0,0,.18); |
| } |
| .toast.err { background: #c53030; } |
| |
| @media (max-width: 800px) { |
| .app { grid-template-columns: 1fr; grid-template-rows: 56px auto 1fr; height: auto; } |
| .panel-left { border-right: none; border-bottom: 1px solid var(--border); } |
| .panel-right { min-height: 60vh; } |
| .pipe-track { display: none; } |
| .output-scroll { padding: 20px; } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="app"> |
|
|
| <header class="topbar"> |
| <div class="topbar-logo"><span class="dot"></span>Problem Solver</div> |
| <div class="topbar-meta">5-agent pipeline</div> |
| </header> |
|
|
| |
| <aside class="panel-left"> |
| <div class="panel-inner"> |
|
|
| <div class="section-label">Your details</div> |
| <div class="fields-row"> |
| <div class="field"> |
| <label>Name</label> |
| <input type="text" id="internName" placeholder="Your name" /> |
| </div> |
| <div class="field"> |
| <label>Role</label> |
| <input type="text" id="internRole" value="AI Developer Intern" /> |
| </div> |
| </div> |
|
|
| <div class="section-label">Problem input</div> |
| <div class="tabs"> |
| <div class="tab active" id="tabPaste" onclick="switchTab('paste')">Paste Text</div> |
| <div class="tab" id="tabUpload" onclick="switchTab('upload')">Upload File</div> |
| </div> |
|
|
| <div id="pastePane" class="shown"> |
| <textarea id="pasteText" placeholder="Paste your meeting transcript, problem description, or blocker here…" rows="10"></textarea> |
| </div> |
|
|
| <div id="uploadPane"> |
| <div class="upload-zone" id="dropZone" |
| onclick="document.getElementById('fileInput').click()" |
| ondragover="onDrag(event,true)" |
| ondragleave="onDrag(event,false)" |
| ondrop="onDrop(event)"> |
| <div class="icon">📄</div> |
| <p><strong>Click to upload</strong> or drag & drop</p> |
| <div class="formats">PDF · TXT · MD</div> |
| </div> |
| <div class="file-chosen" id="fileChosen"> |
| <span>📎</span> |
| <span class="fname" id="fileName">—</span> |
| <span class="file-clear" onclick="clearFile()">✕</span> |
| </div> |
| <input type="file" id="fileInput" accept=".pdf,.txt,.md" onchange="onFileSelect(event)" /> |
| </div> |
|
|
| </div> |
|
|
| <div class="panel-footer"> |
| <button class="btn-run" id="btnRun" onclick="runAnalysis()"> |
| <svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path d="M5 3l14 9-14 9V3z"/></svg> |
| Analyze Problem |
| </button> |
| <button class="btn-pdf" id="btnPdf" disabled onclick="downloadPDF()"> |
| <svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M12 15V3m0 12l-4-4m4 4l4-4M3 17v2a2 2 0 002 2h14a2 2 0 002-2v-2"/></svg> |
| Download PDF Report |
| </button> |
| </div> |
| </aside> |
|
|
| |
| <main class="panel-right"> |
| <div class="status-strip"> |
| <div class="status-pill" id="statusPill"> |
| <span class="s-dot"></span> |
| <span id="statusText">Ready</span> |
| </div> |
| <div class="pipe-track"> |
| <div class="p-node" id="pn-analyst">Analyst</div> |
| <div class="p-sep">›</div> |
| <div class="p-node" id="pn-root_cause">Root Cause</div> |
| <div class="p-sep">›</div> |
| <div class="p-node" id="pn-solutions">Solutions</div> |
| <div class="p-sep">›</div> |
| <div class="p-node" id="pn-action_plan">Action Plan</div> |
| <div class="p-sep">›</div> |
| <div class="p-node" id="pn-thinking">Coach</div> |
| </div> |
| </div> |
|
|
| <div class="output-scroll" id="outputScroll"> |
| <div class="empty" id="emptyState"> |
| <div class="empty-icon">✦</div> |
| <h3>Start with a problem.</h3> |
| <p>Paste a transcript, meeting notes, or blocker — five agents will analyze it and hand you a plan.</p> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| <div class="toast" id="toast"></div> |
|
|
| <script> |
| |
| const API_URL = 'https://banao-tech-problem-decoder.hf.space'; |
| |
| const AGENTS = [ |
| { key: 'analyst', label: 'Problem Analyst', cls: 'ag-1', num: '1' }, |
| { key: 'root_cause', label: 'Root Cause Analyst', cls: 'ag-2', num: '2' }, |
| { key: 'solutions', label: 'Solution Brainstorm', cls: 'ag-3', num: '3' }, |
| { key: 'action_plan', label: 'Action Planner', cls: 'ag-4', num: '4' }, |
| { key: 'thinking', label: 'Thinking Coach', cls: 'ag-5', num: '5' }, |
| ]; |
| |
| let running = false, selectedFile = null, lastPayload = null, activeTab = 'paste'; |
| |
| function switchTab(tab) { |
| activeTab = tab; |
| document.getElementById('tabPaste').classList.toggle('active', tab === 'paste'); |
| document.getElementById('tabUpload').classList.toggle('active', tab === 'upload'); |
| document.getElementById('pastePane').classList.toggle('shown', tab === 'paste'); |
| document.getElementById('uploadPane').classList.toggle('shown', tab === 'upload'); |
| } |
| |
| function onDrag(e, over) { |
| e.preventDefault(); |
| document.getElementById('dropZone').classList.toggle('drag', over); |
| } |
| function onDrop(e) { |
| e.preventDefault(); |
| document.getElementById('dropZone').classList.remove('drag'); |
| const f = e.dataTransfer.files[0]; |
| if (f) setFile(f); |
| } |
| function onFileSelect(e) { const f = e.target.files[0]; if (f) setFile(f); } |
| function setFile(f) { |
| selectedFile = f; |
| document.getElementById('fileName').textContent = f.name; |
| document.getElementById('fileChosen').classList.add('show'); |
| } |
| function clearFile() { |
| selectedFile = null; |
| document.getElementById('fileName').textContent = '—'; |
| document.getElementById('fileChosen').classList.remove('show'); |
| document.getElementById('fileInput').value = ''; |
| } |
| |
| async function readFile(file) { |
| if (file.name.endsWith('.pdf')) { |
| return new Promise((res, rej) => { |
| const r = new FileReader(); |
| r.onload = () => res('__PDF_BASE64__' + r.result.split(',')[1]); |
| r.onerror = rej; |
| r.readAsDataURL(file); |
| }); |
| } |
| return new Promise((res, rej) => { |
| const r = new FileReader(); |
| r.onload = () => res(r.result); |
| r.onerror = rej; |
| r.readAsText(file); |
| }); |
| } |
| |
| function toast(msg, isErr = false) { |
| const t = document.getElementById('toast'); |
| t.textContent = msg; |
| t.className = 'toast' + (isErr ? ' err' : ''); |
| t.style.display = 'block'; |
| setTimeout(() => t.style.display = 'none', 4000); |
| } |
| function setStatus(text, state = '') { |
| document.getElementById('statusText').textContent = text; |
| document.getElementById('statusPill').className = 'status-pill ' + state; |
| } |
| function setPipe(key, state) { |
| const el = document.getElementById('pn-' + key); |
| if (el) el.className = 'p-node ' + state; |
| } |
| function resetPipe() { AGENTS.forEach(a => setPipe(a.key, '')); } |
| |
| function renderMd(text) { |
| return text |
| .replace(/^## (.+)$/gm, '<h2>$1</h2>') |
| .replace(/^### (.+)$/gm, '<h3>$1</h3>') |
| .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') |
| .replace(/^[-*] (.+)$/gm, '<li>$1</li>') |
| .replace(/(<li>.*<\/li>\n?)+/g, s => `<ul>${s}</ul>`) |
| .replace(/\n/g, '<br>'); |
| } |
| |
| async function runAnalysis() { |
| if (running) return; |
| |
| const name = document.getElementById('internName').value.trim() || 'Intern'; |
| const role = document.getElementById('internRole').value.trim() || 'AI Developer Intern'; |
| let content = ''; |
| |
| if (activeTab === 'paste') { |
| content = document.getElementById('pasteText').value.trim(); |
| if (!content) { toast('Paste a problem or transcript first.', true); return; } |
| } else { |
| if (!selectedFile) { toast('Select a file first.', true); return; } |
| try { content = await readFile(selectedFile); } |
| catch { toast('Could not read file.', true); return; } |
| } |
| |
| running = true; |
| lastPayload = { content, intern_name: name, intern_role: role }; |
| document.getElementById('btnRun').disabled = true; |
| document.getElementById('btnPdf').disabled = true; |
| resetPipe(); |
| |
| const scroll = document.getElementById('outputScroll'); |
| scroll.innerHTML = ''; |
| setStatus('Starting pipeline…', 'running'); |
| |
| AGENTS.forEach(a => { |
| const div = document.createElement('div'); |
| div.className = `agent-block ${a.cls}`; |
| div.id = `ab-${a.key}`; |
| div.innerHTML = ` |
| <div class="agent-head"> |
| <div class="agent-num">${a.num}</div> |
| <div class="agent-name">${a.label}</div> |
| <div class="agent-spinner" id="spin-${a.key}"></div> |
| </div> |
| <div class="agent-body" id="body-${a.key}"></div>`; |
| scroll.appendChild(div); |
| }); |
| |
| try { |
| const res = await fetch(`${API_URL}/analyze/stream`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(lastPayload), |
| }); |
| if (!res.ok) { |
| const e = await res.json().catch(() => ({ detail: res.statusText })); |
| throw new Error(e.detail || 'Server error'); |
| } |
| |
| const reader = res.body.getReader(); |
| const dec = new TextDecoder(); |
| let buf = '', current = null; |
| |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| buf += dec.decode(value, { stream: true }); |
| const lines = buf.split('\n'); |
| buf = lines.pop(); |
| |
| for (const line of lines) { |
| if (!line.startsWith('data: ')) continue; |
| try { |
| const msg = JSON.parse(line.slice(6)); |
| if (msg.event === 'agent_start') { |
| current = msg.agent; |
| setPipe(current, 'active'); |
| const block = document.getElementById(`ab-${current}`); |
| block.classList.add('in'); |
| setStatus(`Running: ${msg.label}…`, 'running'); |
| block.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); |
| } else if (msg.event === 'token' && msg.agent === current) { |
| const body = document.getElementById(`body-${msg.agent}`); |
| if (body) { |
| body.dataset.raw = (body.dataset.raw || '') + msg.text; |
| body.innerHTML = renderMd(body.dataset.raw); |
| } |
| } else if (msg.event === 'agent_done') { |
| const spin = document.getElementById(`spin-${msg.agent}`); |
| if (spin) spin.outerHTML = `<span class="agent-check">✓</span>`; |
| setPipe(msg.agent, 'done'); |
| } else if (msg.event === 'done') { |
| setStatus('Analysis complete.', 'done'); |
| document.getElementById('btnPdf').disabled = false; |
| } |
| } catch {} |
| } |
| } |
| } catch (err) { |
| setStatus('Error — ' + err.message, 'error'); |
| toast(err.message, true); |
| } |
| |
| running = false; |
| document.getElementById('btnRun').disabled = false; |
| } |
| |
| async function downloadPDF() { |
| if (!lastPayload) return; |
| const btn = document.getElementById('btnPdf'); |
| btn.textContent = 'Generating…'; |
| btn.disabled = true; |
| try { |
| const res = await fetch(`${API_URL}/analyze/pdf`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(lastPayload), |
| }); |
| if (!res.ok) throw new Error('PDF generation failed'); |
| const blob = await res.blob(); |
| const a = document.createElement('a'); |
| a.href = URL.createObjectURL(blob); |
| a.download = `analysis_${Date.now()}.pdf`; |
| a.click(); |
| } catch (e) { toast(e.message, true); } |
| btn.innerHTML = `<svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M12 15V3m0 12l-4-4m4 4l4-4M3 17v2a2 2 0 002 2h14a2 2 0 002-2v-2"/></svg> Download PDF Report`; |
| btn.disabled = false; |
| } |
| </script> |
| </body> |
| </html> |