bio-experiment / dashboard.html
Ev3Dev's picture
Upload folder using huggingface_hub
db03c40 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bio-Experiment Agent Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #0c0e14;
--surface: #151822;
--surface2: #1c2030;
--border: #2a2f42;
--text: #e2e4ea;
--text-dim: #8b90a5;
--accent: #5ce0d8;
--accent2: #7c6cf0;
--green: #4ade80;
--red: #f87171;
--amber: #fbbf24;
--blue: #60a5fa;
--pink: #f472b6;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--bg); color: var(--text); font-family: 'DM Sans', system-ui, sans-serif; line-height: 1.5; min-height: 100vh; }
.mono { font-family: 'JetBrains Mono', monospace; }
.header { display: flex; align-items: center; justify-content: space-between; padding: 14px 28px; border-bottom: 1px solid var(--border); background: var(--surface); }
.header h1 { font-size: 18px; font-weight: 700; letter-spacing: -.3px; }
.header h1 span { color: var(--accent); }
.header-right { display: flex; align-items: center; gap: 10px; }
.status-pill { font-size: 12px; padding: 4px 14px; border-radius: 20px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; }
.status-pill.live { background: rgba(76,222,128,.15); color: var(--green); }
.status-pill.done { background: rgba(248,113,113,.15); color: var(--red); }
.status-pill.waiting { background: rgba(139,144,165,.15); color: var(--text-dim); }
.btn { padding: 6px 16px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface2); color: var(--text); font-size: 12px; font-weight: 600; cursor: pointer; transition: all .15s; }
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn.primary { background: rgba(92,224,216,.12); border-color: var(--accent); color: var(--accent); }
.btn.primary:hover { background: rgba(92,224,216,.25); }
.btn.danger { border-color: var(--red); color: var(--red); }
.btn.danger:hover { background: rgba(248,113,113,.12); }
.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; padding: 20px 28px; max-width: 1600px; }
@media (max-width: 1100px) { .grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 18px 20px; overflow: hidden; }
.card h2 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); margin-bottom: 12px; }
.card.span2 { grid-column: span 2; }
.card.span3 { grid-column: span 3; }
@media (max-width: 700px) { .card.span2, .card.span3 { grid-column: span 1; } }
.gauge-row { display: flex; gap: 14px; flex-wrap: wrap; }
.gauge { flex: 1; min-width: 130px; background: var(--surface2); border-radius: 10px; padding: 14px; }
.gauge-label { font-size: 11px; color: var(--text-dim); margin-bottom: 6px; text-transform: uppercase; letter-spacing: .5px; }
.gauge-value { font-size: 22px; font-weight: 700; }
.gauge-bar { height: 5px; border-radius: 3px; background: var(--border); margin-top: 8px; overflow: hidden; }
.gauge-bar-fill { height: 100%; border-radius: 3px; transition: width .6s ease; }
.timeline { position: relative; padding-left: 20px; }
.timeline::before { content: ''; position: absolute; left: 6px; top: 0; bottom: 0; width: 2px; background: var(--border); }
.timeline-item { position: relative; margin-bottom: 14px; padding-left: 18px; }
.timeline-item::before { content: ''; position: absolute; left: -18px; top: 6px; width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--accent); background: var(--bg); }
.timeline-item.fail::before { border-color: var(--red); }
.tl-action { font-weight: 600; font-size: 14px; }
.tl-meta { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
.mini-table { width: 100%; font-size: 13px; border-collapse: collapse; }
.mini-table td { padding: 5px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
.mini-table td:first-child { color: var(--text-dim); white-space: nowrap; width: 40%; }
.tag-list { display: flex; flex-wrap: wrap; gap: 6px; }
.tag { font-size: 12px; padding: 3px 10px; border-radius: 6px; background: var(--surface2); border: 1px solid var(--border); font-family: 'JetBrains Mono', monospace; }
.tag.green { border-color: rgba(76,222,128,.3); color: var(--green); }
.tag.pink { border-color: rgba(244,114,182,.3); color: var(--pink); }
.tag.amber { border-color: rgba(251,191,36,.3); color: var(--amber); }
.tag.red { border-color: rgba(248,113,113,.3); color: var(--red); }
.tag.match { background: rgba(76,222,128,.15); }
.tag.miss { background: rgba(248,113,113,.08); }
.code-block { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; font-family: 'JetBrains Mono', monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; max-height: 220px; overflow-y: auto; color: var(--text-dim); line-height: 1.6; }
.progress-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 6px; }
.progress-item { display: flex; align-items: center; gap: 6px; font-size: 12px; }
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--border); }
.dot.done { background: var(--green); }
.pop-bar-container { margin-bottom: 10px; }
.pop-bar-label { font-size: 12px; margin-bottom: 3px; display: flex; justify-content: space-between; }
.pop-bar { height: 14px; border-radius: 4px; background: var(--surface2); overflow: hidden; }
.pop-bar-fill { height: 100%; border-radius: 4px; }
#reward-chart { width: 100%; height: 120px; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.conclusion-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; margin-bottom: 12px; }
.conclusion-card .cc-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.cc-type { font-size: 11px; padding: 2px 10px; border-radius: 4px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; }
.cc-type.causal { background: rgba(244,114,182,.15); color: var(--pink); }
.cc-type.correlative { background: rgba(96,165,250,.15); color: var(--blue); }
.cc-type.descriptive { background: rgba(139,144,165,.15); color: var(--text-dim); }
.cc-conf { font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 600; }
.cc-claim { font-size: 14px; margin-bottom: 8px; line-height: 1.5; }
.cc-section-label { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 3px; margin-top: 8px; }
/* ── control panel ────────────────────────────── */
.control-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; margin: 20px 28px 0; padding: 18px 20px; }
.control-panel summary { cursor: pointer; font-size: 13px; font-weight: 600; color: var(--accent); }
.control-panel[open] summary { margin-bottom: 14px; }
.form-row { display: flex; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; align-items: end; }
.form-field { display: flex; flex-direction: column; gap: 4px; }
.form-field label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .5px; }
.form-field input, .form-field textarea, .form-field select {
background: var(--surface2); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 7px 10px; font-size: 13px; font-family: inherit; outline: none;
}
.form-field input:focus, .form-field textarea:focus, .form-field select:focus { border-color: var(--accent); }
.form-field textarea { min-height: 60px; resize: vertical; }
/* ── final report ─────────────────────────────── */
.report-overlay { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(12,14,20,.85); backdrop-filter: blur(6px); overflow-y: auto; padding: 40px 20px; }
.report-overlay.visible { display: flex; justify-content: center; align-items: flex-start; }
.report-card { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 32px 36px; max-width: 900px; width: 100%; }
.report-card h2 { font-size: 22px; font-weight: 700; margin-bottom: 4px; color: var(--text); text-transform: none; letter-spacing: normal; }
.report-card .subtitle { font-size: 13px; color: var(--text-dim); margin-bottom: 20px; }
.report-section { margin-bottom: 20px; }
.report-section h3 { font-size: 12px; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
.comparison-row { display: flex; gap: 20px; margin-bottom: 16px; }
.comparison-col { flex: 1; }
.comparison-col h4 { font-size: 11px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 6px; }
.pulse { animation: pulse 1.5s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .5; } }
</style>
</head>
<body>
<div class="header">
<h1><span>BioExp</span> Agent Dashboard</h1>
<div class="header-right">
<span id="thinking-badge" class="mono" style="font-size:11px;color:var(--accent2);display:none">REASONING ON</span>
<span id="step-label" class="mono" style="font-size:13px;color:var(--text-dim)">Step 0</span>
<span id="status-pill" class="status-pill waiting">Waiting</span>
<button class="btn primary" onclick="doRestart()">Restart</button>
<button class="btn" onclick="showReport()">Report</button>
</div>
</div>
<!-- Control Panel (collapsible) -->
<details class="control-panel" id="control-panel">
<summary>New Task / Custom Ground Truth</summary>
<div class="form-row">
<div class="form-field" style="flex:2">
<label>Scenario (leave blank for random)</label>
<select id="f-scenario"><option value="">— random —</option></select>
</div>
<div class="form-field" style="flex:1">
<label>True Markers (comma-separated)</label>
<input id="f-markers" placeholder="e.g. MYH7, TNNT2, ACTA1" />
</div>
<div class="form-field" style="flex:1">
<label>Causal Mechanisms (comma-separated)</label>
<input id="f-mechanisms" placeholder="e.g. sarcomere dysfunction" />
</div>
</div>
<div class="form-row">
<div class="form-field" style="flex:2">
<label>True Pathways (name:score, comma-sep)</label>
<input id="f-pathways" placeholder="e.g. Wnt_signaling:0.8, MAPK:0.6" />
</div>
<div class="form-field">
<button class="btn primary" onclick="doCustomRun()">Run with Ground Truth</button>
</div>
</div>
</details>
<div class="grid">
<div class="card span2" id="card-task">
<h2>Task</h2>
<div id="task-statement" style="font-size:15px;font-weight:500;margin-bottom:8px;"></div>
<div id="task-meta" style="font-size:13px;color:var(--text-dim)"></div>
</div>
<div class="card">
<h2>Reward</h2>
<div id="reward-value" class="mono" style="font-size:32px;font-weight:700;margin-bottom:6px;">0.000</div>
<canvas id="reward-chart"></canvas>
</div>
<div class="card span3"><h2>Resources</h2><div class="gauge-row" id="gauges"></div></div>
<div class="card span2" style="max-height:460px;overflow-y:auto">
<h2>Pipeline History <span style="color:var(--accent);font-size:10px">OBSERVABLE</span></h2>
<div class="timeline" id="timeline"></div>
</div>
<div class="card">
<h2>Current Action</h2>
<table class="mini-table" id="action-table"><tbody></tbody></table>
<h2 style="margin-top:14px" id="thinking-header" style="display:none">Model Reasoning</h2>
<div class="code-block" id="model-thinking" style="display:none;border-color:rgba(124,108,240,.2);max-height:140px;margin-bottom:10px"></div>
<h2 style="margin-top:10px">Model Raw Output</h2>
<div class="code-block" id="model-response"></div>
</div>
<div class="card">
<h2>Discovered Markers <span style="color:var(--accent);font-size:10px">OBSERVABLE</span></h2>
<div class="tag-list" id="markers-list"><span class="tag" style="color:var(--text-dim)">none yet</span></div>
<h2 style="margin-top:14px">Candidate Mechanisms</h2>
<div class="tag-list" id="mechanisms-list"><span class="tag" style="color:var(--text-dim)">none yet</span></div>
</div>
<div class="card">
<h2>Rule Violations</h2>
<div id="violations" style="font-size:13px;color:var(--text-dim)">None</div>
<h2 style="margin-top:14px">Uncertainty Summary</h2>
<table class="mini-table" id="uncertainty-table"><tbody></tbody></table>
<h2 style="margin-top:14px">Reward Breakdown</h2>
<table class="mini-table" id="reward-breakdown-table"><tbody></tbody></table>
</div>
<div class="card">
<h2>Latest Output</h2>
<table class="mini-table" id="output-table"><tbody></tbody></table>
<div class="code-block" id="output-data" style="margin-top:10px;max-height:140px"></div>
</div>
<div class="card span3" id="card-conclusions" style="display:none;border-color:rgba(76,222,128,.25)">
<h2 style="color:var(--green)">Synthesized Conclusions</h2>
<div id="conclusions-list"></div>
</div>
<!-- Ground Truth Comparison (shown when episode done + has conclusions) -->
<div class="card span3" id="card-gt-comparison" style="display:none;border-color:rgba(251,191,36,.25)">
<h2 style="color:var(--amber)">Ground Truth Comparison</h2>
<div class="comparison-row">
<div class="comparison-col">
<h4>Agent's Markers</h4>
<div class="tag-list" id="gt-agent-markers"></div>
</div>
<div class="comparison-col">
<h4>True Markers</h4>
<div class="tag-list" id="gt-true-markers"></div>
</div>
</div>
<div class="comparison-row">
<div class="comparison-col">
<h4>Agent's Mechanisms</h4>
<div class="tag-list" id="gt-agent-mechs"></div>
</div>
<div class="comparison-col">
<h4>True Mechanisms</h4>
<div class="tag-list" id="gt-true-mechs"></div>
</div>
</div>
<div id="gt-score" style="margin-top:8px;font-size:14px;font-weight:600"></div>
</div>
<div class="card" style="border-color:rgba(124,108,240,.25)">
<h2 style="color:var(--accent2)">Cell Populations <span style="font-size:10px">HIDDEN</span></h2>
<div id="populations"></div>
</div>
<div class="card" style="border-color:rgba(124,108,240,.25)">
<h2 style="color:var(--accent2)">Ground Truth <span style="font-size:10px">HIDDEN</span></h2>
<div style="margin-bottom:8px"><span style="font-size:11px;color:var(--text-dim);text-transform:uppercase">True Markers</span><div class="tag-list" id="true-markers" style="margin-top:4px"></div></div>
<div style="margin-bottom:8px"><span style="font-size:11px;color:var(--text-dim);text-transform:uppercase">Causal Mechanisms</span><div class="tag-list" id="true-mechanisms" style="margin-top:4px"></div></div>
<div><span style="font-size:11px;color:var(--text-dim);text-transform:uppercase">Top Pathways</span><table class="mini-table" id="pathways-table" style="margin-top:4px"><tbody></tbody></table></div>
</div>
<div class="card" style="border-color:rgba(124,108,240,.25)">
<h2 style="color:var(--accent2)">Technical State <span style="font-size:10px">HIDDEN</span></h2>
<table class="mini-table" id="technical-table"><tbody></tbody></table>
<h2 style="margin-top:14px;color:var(--accent2)">Failure Conditions <span style="font-size:10px">HIDDEN</span></h2>
<div class="tag-list" id="failure-conditions"></div>
</div>
<div class="card span3" style="border-color:rgba(124,108,240,.25)">
<h2 style="color:var(--accent2)">Experiment Progress <span style="font-size:10px">HIDDEN</span></h2>
<div class="progress-grid" id="progress-grid"></div>
</div>
</div>
<!-- Final Report Overlay -->
<div class="report-overlay" id="report-overlay" onclick="if(event.target===this)hideReport()">
<div class="report-card" id="report-content"></div>
</div>
<script>
const POLL_MS = 1200;
const POP_COLORS = ['#5ce0d8','#7c6cf0','#f472b6','#60a5fa','#fbbf24','#4ade80','#f87171','#c084fc','#fb923c','#38bdf8'];
let rewardHistory = [];
let lastTimestamp = 0;
let latestState = null;
function $(id) { return document.getElementById(id); }
function setHTML(id, html) { $(id).innerHTML = html; }
function tagsHTML(arr, cls) {
if (!arr || !arr.length) return '<span class="tag" style="color:var(--text-dim)">—</span>';
return arr.map(t => `<span class="tag ${cls||''}">${esc(t)}</span>`).join('');
}
function esc(s) { if (s == null) return '—'; const d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }
function pct(used, total) { if (!total) return 0; return Math.min(100, Math.max(0, (used / total) * 100)); }
function gaugeColor(p) { return p < 50 ? 'var(--green)' : p < 80 ? 'var(--amber)' : 'var(--red)'; }
function fmt(n) { if (n == null) return '0'; return Number(n).toLocaleString('en-US', { maximumFractionDigits: 0 }); }
function uniqueItems(arr) {
const out = [];
const seen = new Set();
(arr || []).forEach(item => {
if (item == null) return;
const text = String(item).trim();
if (!text) return;
const key = text.toUpperCase();
if (seen.has(key)) return;
seen.add(key);
out.push(text);
});
return out;
}
function gauge(label, value, pctVal, inv) {
let bar = '';
if (pctVal != null) { const c = inv ? gaugeColor(100-pctVal) : gaugeColor(pctVal); bar = `<div class="gauge-bar"><div class="gauge-bar-fill" style="width:${pctVal.toFixed(1)}%;background:${c}"></div></div>`; }
return `<div class="gauge"><div class="gauge-label">${label}</div><div class="gauge-value mono">${value}</div>${bar}</div>`;
}
function miniRows(obj) { return Object.entries(obj).map(([k,v]) => `<tr><td>${esc(k)}</td><td>${esc(v)}</td></tr>`).join(''); }
function drawRewardChart(canvas, data) {
const ctx = canvas.getContext('2d'); const W = canvas.width = canvas.offsetWidth * 2; const H = canvas.height = canvas.offsetHeight * 2;
ctx.clearRect(0, 0, W, H); if (data.length < 2) return;
const vals = data.map(d => d.v); const minV = Math.min(0, ...vals); const maxV = Math.max(0.1, ...vals); const range = maxV - minV || 1; const pad = 8;
ctx.strokeStyle = 'rgba(92,224,216,.4)'; ctx.lineWidth = 2; ctx.beginPath();
const yZ = H - pad - ((0 - minV) / range) * (H - 2*pad); ctx.moveTo(pad, yZ); ctx.lineTo(W-pad, yZ); ctx.stroke();
ctx.strokeStyle = '#5ce0d8'; ctx.lineWidth = 3; ctx.beginPath();
data.forEach((d,i) => { const x = pad+(i/(data.length-1))*(W-2*pad); const y = H-pad-((d.v-minV)/range)*(H-2*pad); i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); }); ctx.stroke();
data.forEach((d,i) => { const x = pad+(i/(data.length-1))*(W-2*pad); const y = H-pad-((d.v-minV)/range)*(H-2*pad); ctx.fillStyle = d.v>=0?'#4ade80':'#f87171'; ctx.beginPath(); ctx.arc(x,y,5,0,Math.PI*2); ctx.fill(); });
}
function comparedTags(agentArr, trueArr, cls) {
if (!agentArr || !agentArr.length) return '<span class="tag" style="color:var(--text-dim)">—</span>';
const trueSet = new Set((trueArr||[]).map(t => t.toUpperCase()));
return agentArr.map(t => {
const hit = trueSet.has(t.toUpperCase());
return `<span class="tag ${cls} ${hit?'match':'miss'}">${esc(t)} ${hit?'✓':'✗'}</span>`;
}).join('');
}
// ── API actions ──
async function doRestart() {
rewardHistory = []; lastTimestamp = 0;
await fetch('/api/restart', { method: 'POST' });
}
async function doCustomRun() {
const scenario = $('f-scenario').value || undefined;
const markers = $('f-markers').value.split(',').map(s=>s.trim()).filter(Boolean);
const mechs = $('f-mechanisms').value.split(',').map(s=>s.trim()).filter(Boolean);
const pwRaw = $('f-pathways').value.split(',').map(s=>s.trim()).filter(Boolean);
const pathways = {};
pwRaw.forEach(p => { const [k,v] = p.split(':'); if (k && v) pathways[k.trim()] = parseFloat(v); });
const gt = {};
if (markers.length) gt.true_markers = markers;
if (mechs.length) gt.causal_mechanisms = mechs;
if (Object.keys(pathways).length) gt.true_pathways = pathways;
rewardHistory = []; lastTimestamp = 0;
await fetch('/api/run', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ scenario_name: scenario, ground_truth: Object.keys(gt).length ? gt : undefined }) });
}
function showReport() {
const s = latestState; if (!s) return;
const rc = $('report-content');
const t = s.task || {};
const lat = s.latent || {};
const conc = s.conclusions || [];
const trueM = lat.true_markers || [];
const trueMech = lat.causal_mechanisms || [];
const conclusionMarkers = uniqueItems(conc.flatMap(c => c.top_markers || []));
const conclusionMechanisms = uniqueItems(conc.flatMap(c => c.causal_mechanisms || []));
const agentM = uniqueItems((s.discovered_markers && s.discovered_markers.length) ? s.discovered_markers : conclusionMarkers);
const agentMechanisms = uniqueItems((s.candidate_mechanisms && s.candidate_mechanisms.length) ? s.candidate_mechanisms : conclusionMechanisms);
const markerHits = agentM.filter(m => trueM.some(t => t.toUpperCase() === m.toUpperCase()));
const r = s.resources || {};
let html = `<h2>Experiment Report</h2>
<div class="subtitle">${esc(t.problem_statement)}</div>
<div class="report-section"><h3>Summary</h3>
<table class="mini-table"><tbody>
<tr><td>Status</td><td>${s.episode_done ? 'Completed' : 'In Progress'}</td></tr>
<tr><td>Steps</td><td>${s.step}</td></tr>
<tr><td>Cumulative Reward</td><td style="color:${(s.cumulative_reward||0)>=0?'var(--green)':'var(--red)'}">${((s.cumulative_reward||0)>=0?'+':'')}${(s.cumulative_reward||0).toFixed(3)}</td></tr>
<tr><td>Budget Used</td><td>$${fmt(r.budget_used)} / $${fmt((r.budget_used||0)+(r.budget_remaining||0))}</td></tr>
<tr><td>Time Used</td><td>${(r.time_used_days||0).toFixed(0)}d / ${((r.time_used_days||0)+(r.time_remaining_days||0)).toFixed(0)}d</td></tr>
<tr><td>Markers Found</td><td>${agentM.length} (${markerHits.length} match ground truth)</td></tr>
</tbody></table>
</div>`;
if (conc.length) {
html += `<div class="report-section"><h3>Conclusions</h3>`;
conc.forEach(c => {
html += `<div class="conclusion-card"><div class="cc-header"><span class="cc-type ${(c.claim_type||'').toLowerCase()}">${esc(c.claim_type)}</span><span class="cc-conf" style="color:${c.confidence>=.7?'var(--green)':c.confidence>=.4?'var(--amber)':'var(--red)'}">${((c.confidence||0)*100).toFixed(0)}%</span></div>`;
if (c.claim) html += `<div class="cc-claim">${esc(c.claim)}</div>`;
if (c.top_markers?.length) html += `<div class="cc-section-label">Top Markers</div><div class="tag-list">${c.top_markers.map(m=>`<span class="tag green">${esc(m)}</span>`).join('')}</div>`;
if (c.causal_mechanisms?.length) html += `<div class="cc-section-label">Causal Mechanisms</div><div class="tag-list">${c.causal_mechanisms.map(m=>`<span class="tag pink">${esc(m)}</span>`).join('')}</div>`;
if (c.predicted_pathways && Object.keys(c.predicted_pathways).length) html += `<div class="cc-section-label">Predicted Pathways</div><table class="mini-table"><tbody>${Object.entries(c.predicted_pathways).map(([k,v])=>`<tr><td>${esc(k)}</td><td>${Number(v).toFixed(3)}</td></tr>`).join('')}</tbody></table>`;
html += `</div>`;
});
html += `</div>`;
}
html += `<div class="report-section"><h3>Ground Truth Comparison</h3>
<div class="comparison-row"><div class="comparison-col"><h4>Agent's Markers</h4><div class="tag-list">${comparedTags(agentM, trueM, 'green')}</div></div>
<div class="comparison-col"><h4>True Markers</h4><div class="tag-list">${tagsHTML(trueM,'green')}</div></div></div>
<div class="comparison-row"><div class="comparison-col"><h4>Agent's Mechanisms</h4><div class="tag-list">${comparedTags(agentMechanisms, trueMech, 'pink')}</div></div>
<div class="comparison-col"><h4>True Mechanisms</h4><div class="tag-list">${tagsHTML(trueMech,'pink')}</div></div></div>
</div>`;
const hist = s.pipeline_history || [];
if (hist.length) {
html += `<div class="report-section"><h3>Pipeline Steps</h3><table class="mini-table"><tbody>`;
hist.forEach(h => { html += `<tr><td>${h.success?'✓':'✗'} ${esc(h.action_type)}</td><td>${esc(h.output_summary)} · q=${h.quality_score}</td></tr>`; });
html += `</tbody></table></div>`;
}
html += `<div style="margin-top:20px;text-align:right"><button class="btn" onclick="hideReport()">Close</button> <button class="btn primary" onclick="doRestart();hideReport()">New Run</button></div>`;
rc.innerHTML = html;
$('report-overlay').classList.add('visible');
}
function hideReport() { $('report-overlay').classList.remove('visible'); }
function renderState(s) {
latestState = s;
if (s.error) { $('status-pill').className='status-pill waiting'; $('status-pill').textContent='Waiting'; $('task-statement').textContent=s.error; return; }
const pill = $('status-pill');
if (s.episode_done) { pill.className='status-pill done'; pill.textContent='Done'; } else { pill.className='status-pill live'; pill.textContent='Live'; }
$('step-label').textContent = `Step ${s.step}`;
if (s.thinking_enabled) { $('thinking-badge').style.display = ''; } else { $('thinking-badge').style.display = 'none'; }
const t = s.task || {};
$('task-statement').textContent = t.problem_statement || '—';
$('task-meta').innerHTML = [t.organism, t.tissue, t.modality, t.conditions ? t.conditions.join(' vs ') : null].filter(Boolean).map(v => `<span class="tag">${esc(v)}</span>`).join(' ');
const cum = s.cumulative_reward || 0;
$('reward-value').textContent = (cum >= 0 ? '+' : '') + cum.toFixed(3);
$('reward-value').style.color = cum >= 0 ? 'var(--green)' : 'var(--red)';
if (s.timestamp !== lastTimestamp && s.step > 0) { rewardHistory.push({ step: s.step, v: cum }); lastTimestamp = s.timestamp; }
drawRewardChart($('reward-chart'), rewardHistory);
const r = s.resources || {};
const bT = (r.budget_used||0)+(r.budget_remaining||0), tT = (r.time_used_days||0)+(r.time_remaining_days||0);
const bP = pct(r.budget_used, bT), tP = pct(r.time_used_days, tT);
$('gauges').innerHTML = [gauge('Budget Used',`$${fmt(r.budget_used)}`,bP), gauge('Budget Left',`$${fmt(r.budget_remaining)}`,100-bP,true), gauge('Time Used',`${(r.time_used_days||0).toFixed(0)}d`,tP), gauge('Time Left',`${(r.time_remaining_days||0).toFixed(0)}d`,100-tP,true), gauge('Samples',String(r.samples_consumed||0),null), gauge('Compute',`${(r.compute_hours_used||0).toFixed(1)}h`,null)].join('');
const hist = s.pipeline_history || [];
$('timeline').innerHTML = hist.length ? hist.map(h => `<div class="timeline-item ${!h.success?'fail':''}"><div class="tl-action">${esc(h.action_type)}${h.method?` <span style="color:var(--text-dim);font-weight:400;font-size:12px">${esc(h.method)}</span>`:''}</div><div class="tl-meta">${h.success?'✓':'✗'} ${esc(h.output_summary)} · q=${h.quality_score} · $${fmt(h.resource_cost)} · ${h.time_cost_days}d</div></div>`).join('') : '<div style="color:var(--text-dim);font-size:13px">No steps yet</div>';
const a = s.current_action;
if (a) { $('action-table').querySelector('tbody').innerHTML = miniRows({'Type':a.action_type,'Method':a.method||'—','Confidence':a.confidence?.toFixed(2),'Justification':a.justification||'—','Fallback?':s.used_fallback?'YES':'no'}); }
if (s.model_thinking) { $('model-thinking').style.display=''; $('model-thinking').textContent = s.model_thinking; } else { $('model-thinking').style.display='none'; }
$('model-response').textContent = s.model_response_raw || '—';
setHTML('markers-list', tagsHTML(s.discovered_markers, 'green'));
setHTML('mechanisms-list', tagsHTML(s.candidate_mechanisms, 'pink'));
const v = s.rule_violations || [];
$('violations').innerHTML = v.length ? v.map(x=>`<div class="tag red" style="margin-bottom:4px">${esc(x)}</div>`).join('') : '<span style="color:var(--text-dim)">None</span>';
$('uncertainty-table').querySelector('tbody').innerHTML = miniRows(s.uncertainty_summary || {});
const rb = s.reward_breakdown || {};
$('reward-breakdown-table').querySelector('tbody').innerHTML = miniRows(Object.fromEntries(Object.entries(rb).map(([k,v])=>[k,(v>=0?'+':'')+v.toFixed(4)])));
const lo = s.latest_output;
if (lo) { $('output-table').querySelector('tbody').innerHTML = miniRows({'Summary':lo.summary,'Success':lo.success?'✓':'✗','Quality':lo.quality_score,'Uncertainty':lo.uncertainty,'Warnings':(lo.warnings||[]).join('; ')||'—'}); $('output-data').textContent = lo.data_preview||'—'; }
const conc = s.conclusions || [];
if (conc.length) {
$('card-conclusions').style.display = '';
$('conclusions-list').innerHTML = conc.map(c => {
const confColor = c.confidence>=.7?'var(--green)':c.confidence>=.4?'var(--amber)':'var(--red)';
let h = `<div class="conclusion-card"><div class="cc-header"><span class="cc-type ${(c.claim_type||'').toLowerCase()}">${esc(c.claim_type||'unknown')}</span><span class="cc-conf" style="color:${confColor}">${((c.confidence||0)*100).toFixed(0)}%</span></div>`;
if (c.claim) h += `<div class="cc-claim">${esc(c.claim)}</div>`;
if (c.top_markers?.length) h += `<div class="cc-section-label">Top Markers</div><div class="tag-list">${c.top_markers.map(m=>`<span class="tag green">${esc(m)}</span>`).join('')}</div>`;
if (c.causal_mechanisms?.length) h += `<div class="cc-section-label">Causal Mechanisms</div><div class="tag-list">${c.causal_mechanisms.map(m=>`<span class="tag pink">${esc(m)}</span>`).join('')}</div>`;
if (c.predicted_pathways && Object.keys(c.predicted_pathways).length) h += `<div class="cc-section-label">Predicted Pathways</div><table class="mini-table"><tbody>${Object.entries(c.predicted_pathways).map(([k,v])=>`<tr><td>${esc(k)}</td><td>${Number(v).toFixed(3)}</td></tr>`).join('')}</tbody></table>`;
return h + '</div>';
}).join('');
} else { $('card-conclusions').style.display = 'none'; }
// Ground truth comparison (visible when done or has conclusions)
const lat = s.latent;
if ((s.episode_done || conc.length) && lat) {
const conclusionMarkers = uniqueItems(conc.flatMap(c => c.top_markers || []));
const conclusionMechanisms = uniqueItems(conc.flatMap(c => c.causal_mechanisms || []));
const comparisonMarkers = uniqueItems((s.discovered_markers && s.discovered_markers.length) ? s.discovered_markers : conclusionMarkers);
const comparisonMechanisms = uniqueItems((s.candidate_mechanisms && s.candidate_mechanisms.length) ? s.candidate_mechanisms : conclusionMechanisms);
$('card-gt-comparison').style.display = '';
setHTML('gt-agent-markers', comparedTags(comparisonMarkers, lat.true_markers, 'green'));
setHTML('gt-true-markers', tagsHTML(lat.true_markers, 'green'));
setHTML('gt-agent-mechs', comparedTags(comparisonMechanisms, lat.causal_mechanisms, 'pink'));
setHTML('gt-true-mechs', tagsHTML(lat.causal_mechanisms, 'pink'));
const hits = comparisonMarkers.filter(m => (lat.true_markers||[]).some(t => t.toUpperCase()===m.toUpperCase()));
$('gt-score').innerHTML = `Marker accuracy: <span style="color:var(--accent)">${hits.length}</span> / ${(lat.true_markers||[]).length} true markers recovered`;
} else { $('card-gt-comparison').style.display = 'none'; }
if (!lat) return;
const pops = lat.cell_populations || [];
$('populations').innerHTML = pops.map((p,i) => { const c = POP_COLORS[i%POP_COLORS.length]; const w = (p.proportion*100).toFixed(1); return `<div class="pop-bar-container"><div class="pop-bar-label"><span>${esc(p.name)} <span style="color:var(--text-dim);font-size:11px">${p.state}</span></span><span class="mono" style="font-size:12px">${w}%</span></div><div class="pop-bar"><div class="pop-bar-fill" style="width:${w}%;background:${c}"></div></div><div class="tag-list" style="margin-top:3px">${p.marker_genes.map(g=>`<span class="tag" style="font-size:11px">${esc(g)}</span>`).join('')}</div></div>`; }).join('') || '<span style="color:var(--text-dim)">—</span>';
setHTML('true-markers', tagsHTML(lat.true_markers, 'green'));
setHTML('true-mechanisms', tagsHTML(lat.causal_mechanisms, 'pink'));
const pw = lat.true_pathways || {};
$('pathways-table').querySelector('tbody').innerHTML = miniRows(Object.fromEntries(Object.entries(pw).slice(0,10).map(([k,v])=>[k,v.toFixed(3)])));
$('technical-table').querySelector('tbody').innerHTML = miniRows(lat.technical || {});
setHTML('failure-conditions', tagsHTML(lat.hidden_failure_conditions, 'red'));
const prog = lat.progress || {};
const bK = Object.entries(prog).filter(([,v])=>typeof v==='boolean'), nK = Object.entries(prog).filter(([,v])=>typeof v!=='boolean');
$('progress-grid').innerHTML = bK.map(([k,v])=>`<div class="progress-item"><div class="dot ${v?'done':''}"></div>${k.replace(/_/g,' ')}</div>`).join('') + nK.map(([k,v])=>`<div class="progress-item" style="color:var(--accent)"><span class="mono" style="font-size:11px;margin-right:4px">${v??'—'}</span>${k.replace(/_/g,' ')}</div>`).join('');
if (s.episode_done && !reportShownForTimestamp && s.timestamp) { reportShownForTimestamp = s.timestamp; setTimeout(showReport, 800); }
}
let reportShownForTimestamp = null;
async function loadScenarios() {
try {
const res = await fetch('/api/scenarios');
const data = await res.json();
const sel = $('f-scenario');
(data.scenarios || []).forEach(n => { const o = document.createElement('option'); o.value = n; o.textContent = n; sel.appendChild(o); });
} catch(e) {}
}
async function poll() {
try { const res = await fetch('/api/state',{cache:'no-store'}); const data = await res.json(); renderState(data); } catch(e) {}
setTimeout(poll, POLL_MS);
}
loadScenarios();
poll();
</script>
</body>
</html>