Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>AutoDataLab++ — The Office</title> | |
| <style> | |
| :root { | |
| --bg:#0b1020; | |
| --bg2:#121a33; | |
| --ink:#e7ecff; | |
| --muted:#9aa4c7; | |
| --accent:#6ea8ff; | |
| --good:#45d98f; | |
| --warn:#ffb347; | |
| --bad:#ff6b8a; | |
| --panel:#151d3a; | |
| --panel2:#1b2550; | |
| --border:rgba(255,255,255,0.08); | |
| --glow:0 0 0 2px rgba(110,168,255,0.35), 0 0 22px rgba(110,168,255,0.35); | |
| } | |
| * { box-sizing:border-box; } | |
| body { | |
| margin:0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif; | |
| background: radial-gradient(1200px 600px at 50% -200px, #1a2356 0%, #0b1020 60%) no-repeat, var(--bg); | |
| color:var(--ink); min-height:100vh; | |
| } | |
| header { | |
| display:flex; align-items:center; gap:14px; padding:14px 22px; | |
| border-bottom:1px solid var(--border); background:rgba(11,16,32,0.6); | |
| position:sticky; top:0; z-index:10; backdrop-filter: blur(8px); | |
| } | |
| header h1 { margin:0; font-size:18px; letter-spacing:0.3px; } | |
| header .tag { color:var(--muted); font-size:12px; } | |
| header { flex-wrap:wrap; } | |
| .task-hint { | |
| flex:1 0 100%; | |
| margin:0; padding:2px 22px 10px 22px; | |
| font-size:12px; color:var(--muted); line-height:1.5; | |
| } | |
| .controls { margin-left:auto; display:flex; gap:10px; align-items:center; flex-wrap:wrap; } | |
| select, button { | |
| background:var(--panel); color:var(--ink); border:1px solid var(--border); | |
| border-radius:8px; padding:8px 12px; font-size:13px; cursor:pointer; | |
| } | |
| button.primary { background:linear-gradient(180deg,#3e6bff,#2b4fd6); border-color:transparent; font-weight:600; } | |
| button.primary:hover { filter:brightness(1.08); } | |
| button:disabled { opacity:0.5; cursor:not-allowed; } | |
| .speed-wrap { display:flex; gap:6px; align-items:center; font-size:12px; color:var(--muted); } | |
| input[type=range] { width:110px; } | |
| /* RAG toggle (checkbox) */ | |
| .rag-wrap { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); user-select:none; } | |
| .rag-wrap input { position:absolute; opacity:0; width:0; height:0; } | |
| .rag-ui { | |
| position:relative; width:42px; height:22px; border-radius:999px; background:var(--panel2); | |
| border:1px solid var(--border); transition: background .2s, border-color .2s; cursor:pointer; | |
| } | |
| .rag-ui::after { | |
| content:""; position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; | |
| background:var(--muted); transition: transform .2s, background .2s; | |
| } | |
| .rag-wrap input:focus-visible + .rag-ui { box-shadow: var(--glow); } | |
| .rag-wrap input:checked + .rag-ui { background:rgba(69,217,143,0.2); border-color:rgba(69,217,143,0.4); } | |
| .rag-wrap input:checked + .rag-ui::after { transform: translateX(20px); background: var(--good); } | |
| .rag-hint { font-size:11px; color:var(--muted); margin-top:6px; line-height:1.4; } | |
| .rag-on .rag-hint { color:var(--good); } | |
| main { | |
| display:grid; | |
| grid-template-columns: minmax(0,1.35fr) minmax(320px,1fr); | |
| gap:18px; padding:18px 22px; max-width:1480px; margin:0 auto; | |
| } | |
| @media (max-width: 980px) { main { grid-template-columns: 1fr; } } | |
| .office { | |
| position:relative; background: | |
| linear-gradient(180deg, rgba(255,255,255,0.02), transparent 40%), | |
| radial-gradient(600px 300px at 50% 0%, rgba(110,168,255,0.18), transparent 60%), | |
| #0f1733; | |
| border:1px solid var(--border); border-radius:18px; | |
| min-height: 560px; | |
| overflow:hidden; | |
| } | |
| .office::before { | |
| content:""; position:absolute; inset:0; | |
| background-image: | |
| linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px); | |
| background-size: 44px 44px; | |
| mask-image: linear-gradient(180deg, transparent, black 20%, black 80%, transparent); | |
| pointer-events:none; | |
| } | |
| .office h2 { margin:14px 18px 4px; font-size:14px; letter-spacing:2px; color:var(--muted); text-transform:uppercase; } | |
| .office p.instruction { margin:0 18px 12px; font-size:13px; color:var(--ink); opacity:.88; } | |
| .floor { position:relative; height:470px; margin: 8px 18px 18px; } | |
| .desk { | |
| position:absolute; width:180px; padding:12px; border-radius:14px; | |
| background: linear-gradient(180deg, var(--panel2), var(--panel)); | |
| border:1px solid var(--border); transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease; | |
| } | |
| .desk .who { display:flex; align-items:center; gap:10px; margin-bottom:6px; } | |
| .desk .avatar { | |
| width:40px; height:40px; border-radius:50%; | |
| background: linear-gradient(135deg, #2b376b, #1a2044); | |
| display:flex; align-items:center; justify-content:center; font-size:22px; | |
| border:1px solid var(--border); | |
| } | |
| .desk .name { font-size:13px; font-weight:600; } | |
| .desk .role { font-size:11px; color:var(--muted); } | |
| .desk .status { font-size:11px; color:var(--muted); margin-top:2px; min-height:14px; } | |
| .desk.active { border-color: var(--accent); box-shadow: var(--glow); transform: translateY(-2px); } | |
| .desk.done { border-color: var(--good); } | |
| .desk .chip { | |
| display:inline-block; font-size:10px; padding:2px 6px; border-radius:999px; | |
| background:rgba(69,217,143,0.12); color:var(--good); border:1px solid rgba(69,217,143,0.35); | |
| margin-right:4px; | |
| } | |
| .desk .chip.warn { background:rgba(255,179,71,0.12); color:var(--warn); border-color:rgba(255,179,71,0.35); } | |
| /* Grid positions */ | |
| .desk.ceo { left:50%; top:2%; transform:translateX(-50%); width:220px; } | |
| .desk.cos { left:50%; top:38%; transform:translateX(-50%); width:220px; } | |
| .desk.analyst { left:2%; top:14%; } | |
| .desk.finance { right:2%; top:14%; } | |
| .desk.strategy { left:2%; bottom:6%; } | |
| .desk.hr { right:2%; bottom:6%; } | |
| .desk.ceo .avatar { background:linear-gradient(135deg,#4a5fff,#243389); } | |
| .desk.cos .avatar { background:linear-gradient(135deg,#6ea8ff,#2b4fd6); } | |
| /* SVG wires */ | |
| svg.wires { position:absolute; inset:0; width:100%; height:100%; pointer-events:none; } | |
| .wire { stroke: rgba(110,168,255,0.22); stroke-width:2; fill:none; } | |
| .wire.active { stroke: var(--accent); stroke-width:3; filter: drop-shadow(0 0 6px rgba(110,168,255,.6)); } | |
| .pulse { | |
| r: 6; fill: var(--accent); filter: drop-shadow(0 0 8px rgba(110,168,255,.9)); | |
| } | |
| /* Message bubble */ | |
| .bubble { | |
| position:absolute; max-width:220px; padding:8px 10px; font-size:12px; | |
| background:rgba(20,28,60,0.92); border:1px solid var(--border); border-radius:10px; | |
| color:var(--ink); opacity:0; transform:translateY(4px); | |
| transition: opacity .25s, transform .25s; pointer-events:none; | |
| } | |
| .bubble.show { opacity:1; transform:translateY(0); } | |
| .bubble .tiny { color:var(--muted); font-size:10px; letter-spacing:1px; text-transform:uppercase; } | |
| /* Right column */ | |
| aside { | |
| display:flex; flex-direction:column; gap:14px; min-width:0; | |
| } | |
| .card { | |
| background: var(--panel); border:1px solid var(--border); border-radius:14px; padding:14px; | |
| } | |
| .card h3 { margin:0 0 10px; font-size:12px; letter-spacing:2px; text-transform:uppercase; color:var(--muted); } | |
| .stat-row { display:flex; gap:10px; flex-wrap:wrap; } | |
| .stat { | |
| flex:1 1 120px; background:var(--panel2); border:1px solid var(--border); | |
| border-radius:10px; padding:10px 12px; min-width:120px; | |
| } | |
| .stat .k { font-size:11px; color:var(--muted); } | |
| .stat .v { font-size:20px; font-weight:700; margin-top:2px; } | |
| .stat .v.good { color:var(--good); } | |
| .stat .v.bad { color:var(--bad); } | |
| .bar { height:8px; background:var(--panel2); border-radius:999px; overflow:hidden; border:1px solid var(--border); } | |
| .bar > span { display:block; height:100%; width:0%; background:linear-gradient(90deg, #6ea8ff, #45d98f); transition: width .4s ease; } | |
| .log { max-height:200px; overflow:auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px; line-height:1.5; } | |
| .log .line { color:var(--ink); opacity:.92; white-space:pre-wrap; word-break:break-word; } | |
| .log .line .ts { color:var(--muted); margin-right:6px; } | |
| .log .line.step { color:#cfe1ff; } | |
| .log .line.reward.pos { color:var(--good); } | |
| .log .line.reward.neg { color:var(--bad); } | |
| .log .line.end { color:var(--accent); font-weight:600; } | |
| .brief-body { font-size:13px; line-height:1.55; } | |
| .brief-body .kv { display:flex; justify-content:space-between; gap:10px; padding:4px 0; border-bottom:1px dashed rgba(255,255,255,0.06); } | |
| .brief-body .kv:last-child { border:none; } | |
| .brief-body .k { color:var(--muted); } | |
| .brief-body ul { margin:6px 0 0 18px; padding:0; } | |
| .brief-body pre { white-space:pre-wrap; background:var(--panel2); padding:8px 10px; border-radius:8px; border:1px solid var(--border); max-height:180px; overflow:auto; } | |
| .policy-badge { | |
| display:inline-flex; align-items:center; gap:6px; padding:3px 8px; border-radius:999px; | |
| background:rgba(110,168,255,0.12); color:var(--accent); border:1px solid rgba(110,168,255,0.35); | |
| font-size:11px; letter-spacing:.5px; | |
| } | |
| .policy-badge .dot { width:6px; height:6px; border-radius:50%; background:var(--accent); box-shadow: 0 0 8px var(--accent); } | |
| footer { text-align:center; color:var(--muted); font-size:11px; padding:20px; } | |
| a { color: var(--accent); } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div> | |
| <h1>AutoDataLab++ <span class="tag">— the office</span></h1> | |
| <div class="tag">CEO → Chief of Staff → 4 specialists · OpenEnv multi-agent env</div> | |
| </div> | |
| <div class="controls"> | |
| <label class="tag" for="task">Task</label> | |
| <select id="task" aria-describedby="taskHint"> | |
| <optgroup label="Core (no strategy required)"> | |
| <option value="easy_brief">easy_brief — analyst + finance + HR</option> | |
| </optgroup> | |
| <optgroup label="Strategist on critical path (5 tasks)"> | |
| <option value="medium_brief">medium_brief — four-expert stack</option> | |
| <option value="hard_brief">hard_brief — full office + tape</option> | |
| <option value="expert_brief" selected>expert_brief — all tools + expert stack</option> | |
| <option value="risk_brief">risk_brief — audit / risk board</option> | |
| <option value="crisis_brief">crisis_brief — mid-quarter shock</option> | |
| </optgroup> | |
| </select> | |
| <label class="tag" for="policy">Policy</label> | |
| <select id="policy"> | |
| <option value="trained" selected>MLP trained CoS</option> | |
| <option value="oracle">oracle</option> | |
| <option value="roundrobin">round-robin</option> | |
| <option value="naive">naive baseline</option> | |
| </select> | |
| <div class="speed-wrap"> | |
| <span>speed</span> | |
| <input type="range" id="speed" min="200" max="2000" step="100" value="850" /> | |
| </div> | |
| <label class="rag-wrap" title="When on, experts retrieve SOPs/policies and the grader adds a small grounding head."> | |
| <input type="checkbox" id="useRag" /> | |
| <span class="rag-ui" id="ragUi" aria-hidden="true"></span> | |
| <span>RAG</span> | |
| </label> | |
| <button id="run" class="primary">Run episode</button> | |
| <button id="reset">Reset</button> | |
| </div> | |
| <p id="taskHint" class="task-hint"></p> | |
| </header> | |
| <main> | |
| <section class="office"> | |
| <h2>Open floor</h2> | |
| <p class="instruction" id="instruction">Pick a task and press Run. The Chief of Staff will route work to specialists in real time.</p> | |
| <div class="floor" id="floor"> | |
| <svg class="wires" id="wires" viewBox="0 0 1000 600" preserveAspectRatio="none"> | |
| <!-- wires are drawn in JS based on desk positions --> | |
| </svg> | |
| <div class="desk ceo" data-id="ceo"> | |
| <div class="who"> | |
| <div class="avatar">🧑💼</div> | |
| <div> | |
| <div class="name">CEO</div> | |
| <div class="role">human — issues the brief</div> | |
| </div> | |
| </div> | |
| <div class="status" id="st-ceo">Waiting for Chief of Staff…</div> | |
| </div> | |
| <div class="desk cos" data-id="cos"> | |
| <div class="who"> | |
| <div class="avatar">🧠</div> | |
| <div> | |
| <div class="name">Chief of Staff</div> | |
| <div class="role">trainable orchestrator</div> | |
| </div> | |
| </div> | |
| <div class="status" id="st-cos">Idle. <span class="policy-badge"><span class="dot"></span><span id="policy-label">MLP trained CoS</span></span></div> | |
| </div> | |
| <div class="desk analyst" data-id="analyst"> | |
| <div class="who"> | |
| <div class="avatar">📊</div> | |
| <div> | |
| <div class="name">Data Analyst</div> | |
| <div class="role">cleans + KPIs</div> | |
| </div> | |
| </div> | |
| <div class="status" id="st-analyst">Waiting for a request…</div> | |
| </div> | |
| <div class="desk finance" data-id="finance"> | |
| <div class="who"> | |
| <div class="avatar">💹</div> | |
| <div> | |
| <div class="name">Finance</div> | |
| <div class="role">forecast + variance</div> | |
| </div> | |
| </div> | |
| <div class="status" id="st-finance">Waiting for a request…</div> | |
| </div> | |
| <div class="desk strategy" data-id="strategy"> | |
| <div class="who"> | |
| <div class="avatar">♟️</div> | |
| <div> | |
| <div class="name">Strategy</div> | |
| <div class="role">recommendations</div> | |
| </div> | |
| </div> | |
| <div class="status" id="st-strategy">Waiting for a request…</div> | |
| </div> | |
| <div class="desk hr" data-id="hr"> | |
| <div class="who"> | |
| <div class="avatar">📨</div> | |
| <div> | |
| <div class="name">HR / Comms</div> | |
| <div class="role">internal memo</div> | |
| </div> | |
| </div> | |
| <div class="status" id="st-hr">Waiting for a request…</div> | |
| </div> | |
| <div class="bubble" id="bubble"></div> | |
| </div> | |
| </section> | |
| <aside> | |
| <div class="card"> | |
| <h3>Episode stats</h3> | |
| <div class="stat-row"> | |
| <div class="stat"><div class="k">Step</div><div class="v" id="stat-step">0</div></div> | |
| <div class="stat"><div class="k">Cumulative reward</div><div class="v" id="stat-cum">0.00</div></div> | |
| <div class="stat"><div class="k">Terminal score</div><div class="v" id="stat-term">—</div></div> | |
| </div> | |
| <div style="margin-top:12px;"> | |
| <div class="tag" style="font-size:11px;color:var(--muted)">progress</div> | |
| <div class="bar"><span id="bar-progress"></span></div> | |
| </div> | |
| <div style="margin-top:10px;"> | |
| <div class="tag" style="font-size:11px;color:var(--muted)">terminal score (0 → 1)</div> | |
| <div class="bar"><span id="bar-term"></span></div> | |
| </div> | |
| </div> | |
| <div class="card" id="taskMapCard"> | |
| <h3>Strategist coverage (6 tasks)</h3> | |
| <p class="tag" style="line-height:1.55;"> | |
| <strong>Five</strong> scenarios put Strategy on the critical path (medium, hard, expert, risk, crisis). <strong>Easy</strong> does not. After each run, the <strong>Strategy ideas</strong> card fills from the final strategist report: watchlist stances, Present/Future, bullets, and tape (bundled long CSV if RAG is off; Stooq + memory if RAG is on). | |
| </p> | |
| <p class="tag" style="margin-top:6px; color:var(--accent);"> | |
| Demo focus (excluding easy + medium): <strong>hard</strong> · <strong>expert</strong> · <strong>risk</strong> · <strong>crisis</strong> — all four show strategist + tape in the panel when the CoS consults strategy. | |
| </p> | |
| </div> | |
| <div class="card" id="ragWikiCard" style="display:none;"> | |
| <h3>Company memory (RAG)</h3> | |
| <p class="rag-hint" id="ragWikiHint">Experts ground answers in bundled SOPs, policies, and exemplar memos. Terminal grading includes a grounding component.</p> | |
| </div> | |
| <div class="card" id="strategyCard" style="display:none;"> | |
| <h3>Strategy ideas (NVDA · AAPL · JPM)</h3> | |
| <div id="strategyBody" class="brief-body"> | |
| <span class="tag">Strategy hasn't been consulted yet.</span> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>Step log</h3> | |
| <div class="log" id="log"></div> | |
| </div> | |
| <div class="card"> | |
| <h3>CEO brief (live)</h3> | |
| <div class="brief-body" id="brief"> | |
| <span class="tag">No brief yet. Watch the office.</span> | |
| </div> | |
| </div> | |
| </aside> | |
| </main> | |
| <footer> | |
| AutoDataLab++ · OpenEnv multi-agent demo · <a href="/health">/health</a> · <a href="/tasks">/tasks</a> | |
| </footer> | |
| <script> | |
| const EXPERT_IDS = ['analyst', 'finance', 'strategy', 'hr']; | |
| const ACTION_EMOJI = { consult: '📩', ask: '❓', summarize: '📝', submit: '✅', noop: '…' }; | |
| /** Tasks where strategy is required for a full grade (everyone except easy_brief). */ | |
| const TASK_REQUIRES_STRATEGY = new Set(['medium_brief', 'hard_brief', 'expert_brief', 'risk_brief', 'crisis_brief']); | |
| /** “Excluding easy + medium” showcase: strategist + tape in the side panel (3+ tasks). */ | |
| const TASK_STRATEGY_DEMO_FOCUS = new Set(['hard_brief', 'expert_brief', 'risk_brief', 'crisis_brief']); | |
| const $ = (id) => document.getElementById(id); | |
| const log = $('log'); | |
| const bubble = $('bubble'); | |
| const wires = $('wires'); | |
| const floor = $('floor'); | |
| let runLock = false; | |
| function setStatus(id, text, cls) { | |
| const el = $('st-' + id); | |
| if (!el) return; | |
| el.innerHTML = text; | |
| const desk = document.querySelector(`.desk[data-id="${id}"]`); | |
| if (!desk) return; | |
| desk.classList.remove('active', 'done'); | |
| if (cls) desk.classList.add(cls); | |
| } | |
| function clearFloor() { | |
| document.querySelectorAll('.desk').forEach(d => d.classList.remove('active','done')); | |
| $('st-ceo').textContent = 'Briefing the Chief of Staff…'; | |
| $('st-cos').innerHTML = 'Planning next move… <span class="policy-badge"><span class="dot"></span><span id="policy-label">' + ($('policy').selectedOptions[0].text) + '</span></span>'; | |
| EXPERT_IDS.forEach(e => setStatus(e, 'Waiting for a request…')); | |
| log.innerHTML = ''; | |
| $('brief').innerHTML = '<span class="tag">No brief yet. Watch the office.</span>'; | |
| $('stat-step').textContent = '0'; | |
| $('stat-cum').textContent = '0.00'; | |
| $('stat-term').textContent = '—'; | |
| $('stat-term').classList.remove('good','bad'); | |
| $('bar-progress').style.width = '0%'; | |
| $('bar-term').style.width = '0%'; | |
| const wiki = $('ragWikiCard'); | |
| if (wiki) wiki.style.display = $('useRag') && $('useRag').checked ? 'block' : 'none'; | |
| document.body.classList.toggle('rag-on', $('useRag') && $('useRag').checked); | |
| const sCard = $('strategyCard'); | |
| const sBody = $('strategyBody'); | |
| if (sCard) sCard.style.display = 'none'; | |
| if (sBody) sBody.innerHTML = '<span class="tag">Strategy hasn\'t been consulted yet.</span>'; | |
| updateTaskHint(); | |
| } | |
| const STANCE_COLOR = { | |
| buy_more: 'var(--good)', buy: 'var(--good)', add: 'var(--good)', | |
| hold: 'var(--muted)', | |
| reduce: 'var(--warn)', trim: 'var(--warn)', | |
| sell: 'var(--bad)', none: 'var(--muted)' | |
| }; | |
| function stancePill(v) { | |
| const t = String(v || '').toLowerCase(); | |
| const color = STANCE_COLOR[t] || 'var(--muted)'; | |
| return `<span class="chip" style="color:${color};border-color:${color}33;background:transparent">${t || '—'}</span>`; | |
| } | |
| function escapeHtml(s) { | |
| if (s == null) return ''; | |
| return String(s) | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"'); | |
| } | |
| function renderStrategyIdeas(report, options) { | |
| const opts = options || {}; | |
| const showTape = opts.showTape !== false; | |
| const card = $('strategyCard'); | |
| const body = $('strategyBody'); | |
| if (!card || !body || !report || !report.metrics) return; | |
| const m = report.metrics; | |
| const tickers = ['nvda', 'aapl', 'jpm']; | |
| const labels = { nvda: 'NVDA · NVIDIA', aapl: 'AAPL · Apple', jpm: 'JPM · JPMorgan' }; | |
| const rows = tickers.map(t => ` | |
| <div class="kv"> | |
| <span class="k">${labels[t]}</span> | |
| <span> | |
| ${stancePill(m[t])} | |
| <span class="tag" style="margin:0 4px;color:var(--muted);font-size:10px">P:</span>${stancePill(m[t + '_present'])} | |
| <span class="tag" style="margin:0 4px;color:var(--muted);font-size:10px">F:</span>${stancePill(m[t + '_future'])} | |
| </span> | |
| </div> | |
| `).join(''); | |
| const bullets = (report.bullet_points || []).slice(0, 3) | |
| .map(b => `<li>${escapeHtml(b)}</li>`).join(''); | |
| const cites = report.memory_citations || []; | |
| const snips = report.memory_snippets || []; | |
| const n = Math.min(cites.length, snips.length, 8); | |
| const tapeRows = []; | |
| for (let i = 0; i < n; i++) { | |
| tapeRows.push( | |
| `<div class="kv" style="align-items:flex-start;"><span class="k" style="max-width:38%;word-break:break-all;">${escapeHtml(cites[i])}</span><span style="font-size:11px;line-height:1.45;">${escapeHtml(snips[i])}</span></div>` | |
| ); | |
| } | |
| const tapeBlock = (showTape && tapeRows.length) | |
| ? `<div style="margin-top:10px;"><div class="tag">Tape & citations (strategist)</div>${tapeRows.join('')}</div>` | |
| : ''; | |
| body.innerHTML = ` | |
| <div style="font-size:12px;color:var(--muted);margin-bottom:6px;">${escapeHtml(report.summary || '')}</div> | |
| ${rows} | |
| ${bullets ? `<div style="margin-top:8px;"><div class="tag">Bullets</div><ul>${bullets}</ul></div>` : ''} | |
| ${tapeBlock} | |
| `; | |
| card.style.display = 'block'; | |
| } | |
| function finalizeStrategyPanel(data) { | |
| const st = data.expert_reports && data.expert_reports.strategy; | |
| const task = data.task; | |
| const body = $('strategyBody'); | |
| const card = $('strategyCard'); | |
| if (!body || !card) return; | |
| if (st) { | |
| renderStrategyIdeas(st, { showTape: true }); | |
| return; | |
| } | |
| if (TASK_REQUIRES_STRATEGY.has(task)) { | |
| body.innerHTML = '<span class="tag" style="color:var(--warn)">This task requires a strategist report for grading, but this policy did not route to strategy. Try <strong>MLP trained CoS</strong>, <strong>oracle</strong>, or <strong>round-robin</strong>.</span>'; | |
| card.style.display = 'block'; | |
| } else { | |
| body.innerHTML = '<span class="tag">Strategist not on the required path for <code>easy_brief</code>; the policy did not consult it this run.</span>'; | |
| card.style.display = 'block'; | |
| } | |
| } | |
| function updateTaskHint() { | |
| const t = $('task') && $('task').value; | |
| const el = $('taskHint'); | |
| if (!el) return; | |
| if (t === 'easy_brief') { | |
| el.textContent = 'Strategist is not required here (analyst → finance → HR). The strategy panel will note that after the run.'; | |
| } else if (TASK_STRATEGY_DEMO_FOCUS.has(t)) { | |
| el.textContent = 'Strategist required: after the run, the side panel shows watchlist stances (P/F), bullets, and full tape row (long CSV or Stooq+RAG).'; | |
| } else if (TASK_REQUIRES_STRATEGY.has(t)) { | |
| el.textContent = 'Strategist is on the critical path; the side panel shows outputs when the CoS consults strategy.'; | |
| } else { | |
| el.textContent = ''; | |
| } | |
| } | |
| function logLine(html, cls='') { | |
| const div = document.createElement('div'); | |
| div.className = 'line ' + cls; | |
| div.innerHTML = html; | |
| log.appendChild(div); | |
| log.scrollTop = log.scrollHeight; | |
| } | |
| function deskCenter(id) { | |
| const desk = document.querySelector(`.desk[data-id="${id}"]`); | |
| const fr = floor.getBoundingClientRect(); | |
| const r = desk.getBoundingClientRect(); | |
| const cx = (r.left + r.right) / 2 - fr.left; | |
| const cy = (r.top + r.bottom) / 2 - fr.top; | |
| // viewBox is 1000x600 so scale to it | |
| return { x: (cx / fr.width) * 1000, y: (cy / fr.height) * 600, raw: { cx, cy } }; | |
| } | |
| function drawStaticWires() { | |
| wires.innerHTML = ''; | |
| const cos = deskCenter('cos'); | |
| ['ceo','analyst','finance','strategy','hr'].forEach(id => { | |
| const p = deskCenter(id); | |
| const path = document.createElementNS('http://www.w3.org/2000/svg','path'); | |
| const c1x = cos.x, c1y = (cos.y + p.y)/2; | |
| path.setAttribute('d', `M ${cos.x} ${cos.y} Q ${c1x} ${c1y} ${p.x} ${p.y}`); | |
| path.setAttribute('class','wire'); | |
| path.setAttribute('data-to', id); | |
| wires.appendChild(path); | |
| }); | |
| } | |
| async function animateMessage(toId, text) { | |
| const paths = wires.querySelectorAll('path.wire'); | |
| paths.forEach(p => p.classList.toggle('active', p.getAttribute('data-to') === toId)); | |
| // moving pulse along a straight line approximation | |
| const cos = deskCenter('cos'); | |
| const tgt = deskCenter(toId); | |
| const dot = document.createElementNS('http://www.w3.org/2000/svg','circle'); | |
| dot.setAttribute('class','pulse'); | |
| dot.setAttribute('cx', cos.x); | |
| dot.setAttribute('cy', cos.y); | |
| wires.appendChild(dot); | |
| // bubble | |
| const fr = floor.getBoundingClientRect(); | |
| const tgtRaw = deskCenter(toId).raw; | |
| bubble.innerHTML = text; | |
| bubble.style.left = Math.max(10, tgtRaw.cx - 110) + 'px'; | |
| bubble.style.top = Math.max(10, tgtRaw.cy - 70) + 'px'; | |
| bubble.classList.add('show'); | |
| const steps = 24; | |
| for (let i=1;i<=steps;i++) { | |
| const t = i/steps; | |
| const x = cos.x + (tgt.x - cos.x)*t; | |
| const y = cos.y + (tgt.y - cos.y)*t; | |
| dot.setAttribute('cx', x); | |
| dot.setAttribute('cy', y); | |
| await sleep(14); | |
| } | |
| dot.remove(); | |
| } | |
| function hideBubble() { | |
| bubble.classList.remove('show'); | |
| wires.querySelectorAll('path.wire').forEach(p => p.classList.remove('active')); | |
| } | |
| function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } | |
| function fmtReward(r) { | |
| const s = (r >= 0 ? '+' : '') + r.toFixed(2); | |
| return s; | |
| } | |
| function describeAction(a) { | |
| if (a.action_type === 'consult' || a.action_type === 'ask') { | |
| return `${ACTION_EMOJI[a.action_type]} ${a.action_type} → ${a.expert_id}`; | |
| } | |
| return `${ACTION_EMOJI[a.action_type] || '•'} ${a.action_type}`; | |
| } | |
| function renderReportChip(report) { | |
| if (!report) return ''; | |
| const pills = []; | |
| if (report.metrics && typeof report.metrics === 'object') { | |
| const keys = Object.keys(report.metrics).slice(0, 2); | |
| for (const k of keys) { | |
| const v = report.metrics[k]; | |
| const s = (typeof v === 'number') ? (Number.isInteger(v) ? v : v.toFixed(2)) : String(v).slice(0, 14); | |
| pills.push(`<span class="chip">${k}=${s}</span>`); | |
| } | |
| } | |
| return pills.join(' '); | |
| } | |
| function renderBrief(data) { | |
| const brief = data.final_brief; | |
| const root = $('brief'); | |
| if (!brief) { root.innerHTML = '<span class="tag">No brief yet.</span>'; return; } | |
| const metrics = brief.metrics || {}; | |
| const mHtml = Object.keys(metrics).slice(0, 6).map(k => { | |
| const v = metrics[k]; | |
| const s = (typeof v === 'number') ? (Number.isInteger(v) ? v : v.toFixed(2)) : String(v).slice(0, 32); | |
| return `<div class="kv"><span class="k">${k}</span><span>${s}</span></div>`; | |
| }).join(''); | |
| const recs = (brief.recommendations || []).map(x => `<li>${x}</li>`).join(''); | |
| const memo = brief.hr_memo ? `<div style="margin-top:8px;"><div class="tag">HR memo</div><pre>${brief.hr_memo}</pre></div>` : ''; | |
| root.innerHTML = ` | |
| <div>${brief.summary || ''}</div> | |
| ${mHtml ? `<div style="margin-top:8px;">${mHtml}</div>` : ''} | |
| ${recs ? `<div style="margin-top:8px;"><div class="tag">Recommendations</div><ul>${recs}</ul></div>` : ''} | |
| ${memo} | |
| <div style="margin-top:10px; font-size:11px; color:var(--muted);">Consulted: ${(brief.consulted_experts || []).join(', ') || '—'}</div> | |
| `; | |
| } | |
| async function runEpisode() { | |
| if (runLock) return; | |
| runLock = true; | |
| $('run').disabled = true; | |
| clearFloor(); | |
| const task = $('task').value; | |
| const policy = $('policy').value; | |
| const speed = parseInt($('speed').value, 10); | |
| let data; | |
| try { | |
| const useRag = !!$('useRag').checked; | |
| const res = await fetch('/visualize/run', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ task, policy, use_rag: useRag }) | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| data = await res.json(); | |
| } catch (e) { | |
| logLine(`<span class="ts">!</span>failed to run: ${e}`, 'end'); | |
| runLock = false; $('run').disabled = false; | |
| return; | |
| } | |
| $('instruction').textContent = data.instruction + ` · policy: ${data.policy_label}`; | |
| setStatus('ceo', 'Briefed Chief of Staff. Watching…'); | |
| const ragOn = data.rag_enabled ? 'on' : 'off'; | |
| const wikiCard = $('ragWikiCard'); | |
| if (wikiCard) wikiCard.style.display = data.rag_enabled ? 'block' : 'none'; | |
| logLine(`<span class="ts">[START]</span> task=${data.task} policy=${data.policy_label} rag=${ragOn} max_steps=${data.max_steps}`, 'step'); | |
| logLine(`<span class="ts">[NOTE]</span> Comparison mode uses the policy's actual route; missing experts are not auto-filled. Expert text can match when two policies consult the same experts.`, 'step'); | |
| if (TASK_REQUIRES_STRATEGY.has(data.task)) { | |
| const sCard = $('strategyCard'); | |
| const sBody = $('strategyBody'); | |
| if (sCard && sBody) { | |
| sCard.style.display = 'block'; | |
| sBody.innerHTML = '<span class="tag">Awaiting strategist consult… (panel will populate when the CoS routes work to strategy)</span>'; | |
| } | |
| } | |
| for (const step of data.steps) { | |
| $('stat-step').textContent = step.step; | |
| $('stat-cum').textContent = step.cumulative_reward.toFixed(2); | |
| $('bar-progress').style.width = Math.min(100, (step.step / data.max_steps) * 100) + '%'; | |
| const a = step.action; | |
| const actTxt = describeAction(a); | |
| const rwdCls = step.reward >= 0 ? 'pos' : 'neg'; | |
| logLine(`<span class="ts">[STEP ${String(step.step).padStart(2,'0')}]</span> ${actTxt} <span class="reward ${rwdCls}">reward ${fmtReward(step.reward)}</span>`, 'step'); | |
| setStatus('cos', `Issuing: <b>${actTxt}</b>`, 'active'); | |
| if ((a.action_type === 'consult' || a.action_type === 'ask') && a.expert_id) { | |
| const exp = a.expert_id; | |
| setStatus(exp, `Receiving ${a.action_type}…`, 'active'); | |
| await animateMessage(exp, `<div class="tiny">CoS → ${exp}</div>${a.action_type} ${a.sub_question_id ? '· '+a.sub_question_id : ''}`); | |
| await sleep(speed * 0.3); | |
| const report = step.latest_report; | |
| if (report) { | |
| const chips = renderReportChip(report); | |
| setStatus(exp, `<b>${report.title || 'Report ready'}</b> ${chips}`, 'done'); | |
| if (exp === 'strategy') renderStrategyIdeas(report, { showTape: true }); | |
| } else { | |
| setStatus(exp, 'Already consulted (redundant).', 'done'); | |
| } | |
| hideBubble(); | |
| } else if (a.action_type === 'summarize') { | |
| setStatus('cos', 'Composing brief from expert reports…', 'active'); | |
| await sleep(speed * 0.6); | |
| } else if (a.action_type === 'submit') { | |
| setStatus('cos', 'Submitting brief to CEO…', 'active'); | |
| await sleep(speed * 0.6); | |
| setStatus('ceo', 'Received brief ✔︎', 'done'); | |
| } else { | |
| await sleep(speed * 0.3); | |
| } | |
| await sleep(speed * 0.45); | |
| } | |
| // terminal | |
| $('stat-term').textContent = data.terminal_score.toFixed(3); | |
| $('stat-term').classList.add(data.success ? 'good' : 'bad'); | |
| $('bar-term').style.width = Math.min(100, data.terminal_score * 100) + '%'; | |
| const verdict = data.success ? 'SUCCESS' : 'BELOW THRESHOLD'; | |
| logLine(`<span class="ts">[END]</span> terminal=${data.terminal_score.toFixed(3)} · ${verdict}`, 'end'); | |
| setStatus('cos', `Done. Terminal score <b>${data.terminal_score.toFixed(3)}</b>`, data.success ? 'done' : 'active'); | |
| renderBrief(data); | |
| finalizeStrategyPanel(data); | |
| runLock = false; | |
| $('run').disabled = false; | |
| } | |
| // wiring | |
| window.addEventListener('load', () => { | |
| drawStaticWires(); | |
| $('run').addEventListener('click', runEpisode); | |
| $('reset').addEventListener('click', clearFloor); | |
| const ragCb = $('useRag'); | |
| if (ragCb) { | |
| ragCb.addEventListener('change', () => { | |
| document.body.classList.toggle('rag-on', ragCb.checked); | |
| const w = $('ragWikiCard'); | |
| if (w) w.style.display = ragCb.checked ? 'block' : 'none'; | |
| }); | |
| } | |
| const taskSel = $('task'); | |
| if (taskSel) { | |
| updateTaskHint(); | |
| taskSel.addEventListener('change', updateTaskHint); | |
| } | |
| window.addEventListener('resize', drawStaticWires); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |