AdithyaSK's picture
AdithyaSK HF Staff
Harbor run viewer: Phase 0 eval, 15 harnesses x 50 tasks, pass@4
3d20eb8 verified
Raw
History Blame Contribute Delete
37.9 kB
<!doctype html>
<meta charset="utf-8">
<title>Harbor runs</title>
<style>
:root{
--bg:#0d1117;--panel:#161b22;--panel2:#0e1116;--border:#30363d;--text:#e6edf3;
--muted:#8b949e;--gray:#6e7681;--accent:#58a6ff;--pass:#3ec97a;--fail:#e25c5c;--warn:#e6b455;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font:13px/1.5 ui-sans-serif,system-ui,sans-serif}
/* ── top bar ─────────────────────────────────────────────────────────── */
header{display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid var(--border);
background:var(--panel);position:sticky;top:0;z-index:30}
header h1{font-size:13px;margin:0;font-weight:600;letter-spacing:.2px}
select,button{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:5px;
padding:4px 8px;font:inherit;font-size:12px;cursor:pointer}
select:hover,button:hover{border-color:var(--accent)}
.view-btn.on{background:rgba(89,166,255,.14);border-color:var(--accent);color:var(--accent)}
.src{font:10.5px ui-monospace,monospace;color:var(--gray)}
.pill{font-size:10px;padding:1px 6px;border-radius:3px;border:1px solid var(--border);white-space:nowrap}
.pill.ok{background:rgba(62,201,122,.15);color:var(--pass);border-color:transparent}
.pill.exp{background:rgba(230,180,85,.15);color:var(--warn);border-color:transparent}
/* ── layout ──────────────────────────────────────────────────────────── */
main{display:grid;grid-template-columns:184px minmax(0,1fr);height:calc(100vh - 41px)}
.statwrap{border-bottom:1px solid var(--border)}
.statwrap>summary{cursor:pointer;padding:7px 14px;font-size:11.5px;color:var(--muted);list-style:none}
.statwrap>summary::-webkit-details-marker{display:none}
.statwrap>summary::before{content:"▸ ";color:var(--gray)}
.statwrap[open]>summary::before{content:"▾ "}
#burger{display:none}
/* Laptop-first: below 1100px the sidebar narrows, below 820px it becomes an overlay so the grid keeps
the full width — a data-dense table is the thing that must not be squeezed. */
@media(max-width:1100px){
main{grid-template-columns:150px minmax(0,1fr)}
td.cell{width:52px}
table.heatmap th.col-task,table.heatmap td.col-task{min-width:210px;max-width:210px}
}
@media(max-width:820px){
#burger{display:inline-block}
main{grid-template-columns:minmax(0,1fr)}
aside{position:fixed;left:0;top:41px;bottom:0;width:210px;z-index:25;transform:translateX(-100%);
transition:transform .15s;box-shadow:2px 0 12px rgba(0,0,0,.4)}
body.nav-open aside{transform:none}
.matrices{flex-direction:column}
table.heatmap th.col-task,table.heatmap td.col-task{min-width:150px;max-width:150px}
}
aside{border-right:1px solid var(--border);overflow:auto;background:var(--panel2)}
.pgroup{padding:7px 10px 3px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--gray)}
.kid{padding:5px 10px 5px 16px;font-size:12px;color:var(--muted);cursor:pointer;border-left:2px solid transparent}
.kid:hover{background:var(--panel);color:var(--text)}
.kid.sel{background:rgba(89,166,255,.10);border-left-color:var(--accent);color:var(--text);font-weight:600}
section{overflow:auto;padding:0}
/* ── stats ───────────────────────────────────────────────────────────── */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:8px;padding:12px 14px 4px}
.stat{background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:8px 12px}
.stat .num{font-size:17px;font-weight:600}
.stat .lbl{font-size:11px;color:var(--muted);margin-top:2px}
/* ── aggregate matrices ──────────────────────────────────────────────── */
.matrices{display:flex;flex-wrap:wrap;gap:10px;padding:10px 14px}
.matrix-card{background:var(--panel2);border:1px solid var(--border);border-radius:6px;padding:11px 13px}
.matrix-card h4{margin:0 0 9px;font-size:12px;color:var(--muted);font-weight:500;display:flex;gap:8px}
.matrix-card h4 .sub{color:var(--gray);font-size:10.5px}
.matrix-card table{border-collapse:collapse;font-size:12px}
.matrix-card th,.matrix-card td{padding:5px 9px;border:1px solid var(--border);text-align:center;white-space:nowrap}
.matrix-card th{background:var(--panel);color:var(--muted);font-weight:500}
.matrix-card td.lbl{background:var(--panel);text-align:left;font-size:11.5px}
.matrix-card td.cell b{font-size:13px}
.matrix-card td.cell .det{color:rgba(255,255,255,.55);font-size:10px}
.matrix-card td.empty{color:var(--gray)}
/* ── the heatmap ─────────────────────────────────────────────────────── */
.hwrap{overflow:auto;padding:4px 14px 24px}
table.heatmap{border-collapse:separate;border-spacing:0;font-size:12px}
table.heatmap th{background:var(--panel);color:var(--muted);font-weight:500;font-size:11px;
padding:5px 8px;border-bottom:1px solid var(--border);position:sticky;top:0;z-index:6;white-space:nowrap}
table.heatmap td{border-bottom:1px solid var(--border);padding:0}
table.heatmap th.col-task,table.heatmap td.col-task{position:sticky;left:0;z-index:5;background:var(--panel2);
min-width:280px;max-width:280px;padding:5px 9px;text-align:left;border-right:2px solid var(--border)}
table.heatmap thead th.col-task{z-index:8;background:var(--panel)}
td.col-task .tid{font:11px ui-monospace,monospace;color:var(--muted)}
td.col-task .q{color:var(--text);font-size:11.5px;display:-webkit-box;-webkit-line-clamp:2;
-webkit-box-orient:vertical;overflow:hidden}
.diff{font-size:9.5px;text-transform:uppercase;font-weight:600;padding:1px 5px;border-radius:3px;margin-right:5px}
.diff.easy{background:rgba(62,201,122,.15);color:var(--pass)}
.diff.medium{background:rgba(230,180,85,.15);color:var(--warn)}
.diff.hard{background:rgba(226,92,92,.15);color:var(--fail)}
td.cell{width:62px;height:26px;cursor:pointer;text-align:center;font:11px ui-monospace,monospace;
transition:filter .1s;border-left:1px solid var(--border)}
td.cell:hover{filter:brightness(1.45);outline:1px solid var(--accent);outline-offset:-1px}
/* pass@k gradient — darker green = passed on an earlier attempt */
td.cell.pass-1{background:rgba(62,201,122,.55);color:#fff;font-weight:600}
td.cell.pass-2{background:rgba(62,201,122,.40);color:#fff;font-weight:600}
td.cell.pass-3{background:rgba(62,201,122,.28);color:#fff;font-weight:600}
td.cell.pass-4{background:rgba(62,201,122,.18);color:#fff;font-weight:600}
td.cell.fail{background:rgba(226,92,92,.25);color:rgba(255,255,255,.85)}
td.cell.na{background:rgba(230,180,85,.16);color:var(--warn)}
td.cell.empty{background:transparent;color:var(--gray);cursor:default}
td.cell.empty:hover{filter:none;outline:0}
.filters{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:8px 14px 2px;border-bottom:1px solid var(--border)}
.fchip{font-size:11px;padding:2px 7px;border-radius:11px;border:1px solid var(--border);background:var(--panel2);
color:var(--muted);cursor:pointer;white-space:nowrap}
.fchip:hover{border-color:var(--accent);color:var(--text)}
.fchip.on{background:rgba(89,166,255,.16);border-color:var(--accent);color:var(--accent)}
.fkey{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--gray);margin-left:6px}
.kid input{margin-right:6px;vertical-align:-1px}
.ctrls{display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:8px 14px;border-bottom:1px solid var(--border)}
.ctrls label{font-size:11px;color:var(--muted);display:flex;align-items:center;gap:5px}
input[type=range]{width:120px;accent-color:var(--accent)}
.chartgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:8px;padding:8px 14px 24px}
.chartbox{background:var(--panel2);border:1px solid var(--border);border-radius:6px;padding:6px 8px 2px}
.chartbox h5{margin:0 0 2px;font-size:11px;color:var(--muted);font-weight:500;display:flex;justify-content:space-between}
.chartbox h5 span{color:var(--gray);font-size:10px}
.rlegend{display:flex;flex-wrap:wrap;gap:10px;padding:4px 14px;font-size:11px}
.rlegend i{display:inline-block;width:16px;height:3px;border-radius:2px;margin-right:4px;vertical-align:2px}
.taskcard{background:var(--panel2);border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:10px}
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px;margin-top:8px}
.metagrid div{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:11px}
.metagrid em{display:block;font-style:normal;color:var(--gray);font-size:10px}
.legend{display:flex;gap:12px;align-items:center;padding:6px 14px;font-size:10.5px;color:var(--muted)}
.legend i{display:inline-block;width:14px;height:11px;border-radius:2px;margin-right:3px;vertical-align:middle}
/* ── trace dialog ────────────────────────────────────────────────────── */
dialog{background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:9px;
width:min(1020px,95vw);max-height:90vh;padding:0}
dialog::backdrop{background:rgba(0,0,0,.65)}
.dhead{display:flex;align-items:center;gap:9px;padding:10px 14px;border-bottom:1px solid var(--border);
background:var(--panel);position:sticky;top:0}
.dbody{padding:12px 14px;overflow:auto;max-height:calc(90vh - 48px)}
.trial-table{width:100%;border-collapse:collapse;font-size:12px;margin:8px 0}
.trial-table th,.trial-table td{padding:5px 8px;border-bottom:1px solid var(--border);text-align:left}
.trial-table .reward.pass{color:var(--pass);font-weight:600}
.trial-table .reward.fail{color:var(--fail)}
.trial-table .reward.na{color:var(--warn)}
.msg{border:1px solid var(--border);border-radius:6px;margin-bottom:6px;overflow:hidden}
.msg-head{background:var(--panel);padding:3px 9px;font:10.5px ui-monospace,monospace;color:var(--muted)}
.msg-body{margin:0;padding:7px 10px;white-space:pre-wrap;word-break:break-word;
font:11px/1.5 ui-monospace,monospace;max-height:280px;overflow:auto}
.gold{color:var(--pass)}
.empty-msg{color:var(--gray);padding:34px;text-align:center}
svg{background:var(--panel2);border:1px solid var(--border);border-radius:6px}
</style>
<header>
<button id="burger" title="Show/hide runs"></button>
<select id="proj"></select>
<button id="refresh" title="Re-read the bucket / data directory"></button>
<span style="flex:1"></span>
<span class="src">cols</span>
<button id="axH" class="view-btn on">harness</button>
<button id="axM" class="view-btn">model</button>
<span class="src" id="src"></span>
</header>
<main>
<aside id="tree"></aside>
<section id="body"><div class="empty-msg">select a dataset</div></section>
</main>
<dialog id="dlg">
<div class="dhead"><b id="dtitle"></b><span id="dpills"></span><span style="flex:1"></span>
<button onclick="document.getElementById('dlg').close()">esc</button></div>
<div class="dbody" id="dbody"></div>
</dialog>
<script>
const $=s=>document.querySelector(s);
let TREE=[],AXIS="harness",DS=null,SEL=null,KMAX=4;
// Active metadata filters: {key: Set(values)}. Metadata is whatever the suite recorded, so nothing is
// hardcoded — difficulty, package_tier and reward_mode are exactly the fields that differ between
// suites, and promoting any of them to a column breaks the viewer when a suite renames one.
let FILT={};
// Several training runs can be on screen at once, so selection is a set and the charts overlay.
// Comparing curves across runs is the whole reason to look at them side by side.
let RUNSEL=new Set(), RUNCACHE={}, SMOOTH=0.6, LOGX=false;
const RUNCOLORS=["#58a6ff","#3ec97a","#e6b455","#e25c5c","#bc8cff","#39c5cf","#f778ba","#a5d6ff"];
const api=async(p,o)=>{const r=await fetch(p,o);if(!r.ok)throw new Error(`${p}: ${r.status}`);return r.json()};
const n=(v,d=3)=>v==null?"–":(typeof v==="number"?(Number.isInteger(v)?v:v.toFixed(d)):v);
const pct=v=>v==null?"–":(v*100).toFixed(0)+"%";
const short=m=>String(m).split("/").pop();
const esc=s=>String(s??"").replace(/[<>&"]/g,c=>({"<":"&lt;",">":"&gt;","&":"&amp;",'"':"&quot;"}[c]));
// Heat for the aggregate matrices: one hue, alpha carries the value. Two hues would imply a midpoint
// that pass-rate does not have.
const heatBg=p=>p==null?"transparent":`rgba(62,201,122,${(0.08+0.5*p).toFixed(3)})`;
// Only fields that can actually partition the rows are offered: a key with one value filters nothing,
// and one with a value per task (an id, a free-text question) is a list, not a filter.
function metaFacets(tasks){
const counts={};
tasks.forEach(t=>Object.entries(t.meta||{}).forEach(([k,v])=>{
if(v==null||typeof v==="object")return;
counts[k]=counts[k]||{};
counts[k][String(v)]=(counts[k][String(v)]||0)+1;
}));
const out={};
for(const [k,vals] of Object.entries(counts)){
const nv=Object.keys(vals).length;
if(nv>1 && nv<=12 && nv<tasks.length) out[k]=vals;
}
return out;
}
function passesFilters(t){
return Object.entries(FILT).every(([k,set])=>
!set.size || set.has(String((t.meta||{})[k])));
}
const heatOf=(pass,measured)=>measured?pass/measured:null;
async function loadTree(){
TREE=await api("/api/projects");
const h=await api("/healthz");
$("#src").textContent=h.source;
$("#proj").innerHTML=TREE.map(p=>`<option value="${p.project_id}">${esc(p.label||p.project_id)}</option>`).join("");
renderRuns();
const f=document.querySelector(".kid"); if(f&&!SEL) f.click();
}
// One list, in the sidebar, scoped to the selected project. Two dropdowns made the project choice and
// the run choice look like peers when the second is nested inside the first.
function syncBoxes(){
document.querySelectorAll(".kid input[data-run]").forEach(cb=>cb.checked=RUNSEL.has(cb.dataset.run));
}
function renderRuns(){
const p=TREE.find(x=>x.project_id===$("#proj").value)||TREE[0]||{};
const ds=(p.datasets||[]), rs=(p.runs||[]);
$("#tree").innerHTML=`<div class="pgroup">Runs</div>
${ds.length?ds.map(d=>`<div class="kid" data-p="${p.project_id}" data-d="${d}"
title="${esc(d)}">▦ ${esc(d)}</div>`).join(""):""}
${rs.length?`<div class="pgroup">Training</div>`+rs.map(r=>`<div class="kid" data-p="${p.project_id}" data-r="${r}"
title="${esc(r)}"><input type="checkbox" data-run="${esc(r)}"${RUNSEL.has(r)?" checked":""}>${esc(r)}</div>`).join(""):""}
${!ds.length&&!rs.length?`<div class="empty-msg" style="padding:18px;font-size:11px">nothing here yet</div>`:""}`;
document.querySelectorAll(".kid").forEach(el=>el.onclick=(ev)=>{
if(ev.target.tagName==="INPUT")return; // the checkbox drives comparison, not navigation
document.querySelectorAll(".kid").forEach(k=>k.classList.remove("sel"));el.classList.add("sel");
if(window.matchMedia("(max-width:820px)").matches) document.body.classList.remove("nav-open");
if(el.dataset.d){RUNSEL.clear();openDataset(el.dataset.p,el.dataset.d)}
else{RUNSEL.clear();RUNSEL.add(el.dataset.r);syncBoxes();openRuns(el.dataset.p)}
});
document.querySelectorAll(".kid input[data-run]").forEach(cb=>cb.onchange=()=>{
cb.checked?RUNSEL.add(cb.dataset.run):RUNSEL.delete(cb.dataset.run);
if(RUNSEL.size) openRuns($("#proj").value);
});
}
$("#proj").onchange=()=>{renderRuns();const f=document.querySelector(".kid");if(f)f.click()};
$("#burger").onclick=()=>document.body.classList.toggle("nav-open");
async function openDataset(pid,did){
SEL={pid,did};$("#body").innerHTML=`<div class="empty-msg">loading ${did}…</div>`;
try{DS=await api(`/api/projects/${pid}/datasets/${did}`)}
catch(e){$("#body").innerHTML=`<div class="empty-msg">${e.message}</div>`;return}
KMAX=DS.k_max||4; $("#proj").value=pid;
renderGrid();
}
function renderGrid(){
const d=DS, models=d.models||[], harnesses=d.harnesses||[];
const cols = AXIS==="harness"?harnesses:models;
const subs = AXIS==="harness"?models:harnesses;
const keyFor=(c,s)=>AXIS==="harness"?`${s}|${c}`:`${c}|${s}`;
const s=d.summary||{};
const facets=metaFacets(d.tasks||[]);
const shown=(d.tasks||[]).filter(passesFilters);
const filterBar=Object.keys(facets).length?`<div class="filters">
${Object.entries(facets).map(([k,vals])=>`<span class="fkey">${esc(k)}</span>` +
Object.entries(vals).sort((a,b)=>a[0].localeCompare(b[0],undefined,{numeric:true})).map(([v,c])=>
`<span class="fchip ${(FILT[k]&&FILT[k].has(v))?"on":""}" data-fk="${esc(k)}" data-fv="${esc(v)}"
>${esc(v)} <span class="src">${c}</span></span>`).join("")).join("")}
${Object.values(FILT).some(x=>x.size)?`<span class="fchip" id="fclear">clear</span>`:""}
<span class="src">${shown.length}/${(d.tasks||[]).length} tasks</span></div>`:"";
const stats=[["tasks",n(s.tasks_total)],["solved ≤k",n(s.tasks_any_pass)],
["attempts",n(s.attempts_total)],["passed",n(s.attempts_passed)],
// Beside the pass rates on purpose: an all-infra task is unmeasured, not failed, and folding it in
// as a zero would read as a real (bad) measurement.
["unmeasured",n(s.n_all_infra)],["k",KMAX]]
.map(([l,v])=>`<div class="stat"><div class="num">${v}</div><div class="lbl">${l}</div></div>`).join("");
// aggregate matrix: model × harness, cells = pass@k
const agg={};
shown.forEach(t=>Object.entries(t.cells||{}).forEach(([k,c])=>{
const [m,h]=k.split("|"); agg[m]=agg[m]||{}; agg[m][h]=agg[m][h]||{cells:0,pass:0,measured:0};
const a=agg[m][h]; a.cells++;
const graded=(c.attempts||[]).filter(x=>x.reward!=null);
if(graded.length){a.measured++; if(c.passed_at)a.pass++}
}));
const mxh=`<table><thead><tr><th></th>${harnesses.map(h=>`<th>${esc(h)}</th>`).join("")}</tr></thead><tbody>
${models.map(m=>`<tr><td class="lbl">${esc(short(m))}</td>${harnesses.map(h=>{
const a=(agg[m]||{})[h];
if(!a||!a.measured) return `<td class="empty">—</td>`;
const p=a.pass/a.measured;
return `<td class="cell" style="background:${heatBg(p)}"><b>${pct(p)}</b><div class="det">${a.pass}/${a.measured}</div></td>`;
}).join("")}</tr>`).join("")}</tbody></table>`;
// Second matrix pivots on whichever metadata field has the most values — chosen from the data
// rather than hardcoded to difficulty, because that field is not guaranteed to exist.
const facetKey=Object.entries(facets).sort((a,b)=>Object.keys(b[1]).length-Object.keys(a[1]).length)[0]?.[0];
let mxd="";
if(facetKey){
const vals=[...new Set(shown.map(t=>String((t.meta||{})[facetKey])).filter(v=>v!=="undefined"))]
.sort((a,b)=>a.localeCompare(b,undefined,{numeric:true}));
const acc={};
shown.forEach(t=>Object.entries(t.cells||{}).forEach(([k,c])=>{
const h=k.split("|")[1], v=String((t.meta||{})[facetKey]); if(v==="undefined")return;
acc[h]=acc[h]||{}; acc[h][v]=acc[h][v]||{measured:0,pass:0};
if((c.attempts||[]).some(x=>x.reward!=null)){acc[h][v].measured++; if(c.passed_at)acc[h][v].pass++}
}));
mxd=`<table><thead><tr><th></th>${vals.map(v=>`<th>${esc(v)}</th>`).join("")}</tr></thead><tbody>
${harnesses.map(h=>`<tr><td class="lbl">${esc(h)}</td>${vals.map(v=>{
const a=(acc[h]||{})[v];
if(!a||!a.measured)return `<td class="empty">—</td>`;
const pr=heatOf(a.pass,a.measured);
return `<td class="cell" style="background:${heatBg(pr)}"><b>${pct(pr)}</b><div class="det">${a.pass}/${a.measured}</div></td>`;
}).join("")}</tr>`).join("")}</tbody></table>`;
}
// the per-task heatmap
const head=`<tr><th class="col-task">task</th>${cols.flatMap(c=>
(subs.length?subs:[null]).map(sb=>`<th title="${esc(c)}${sb?" · "+esc(sb):""}">${esc(subs.length>1?short(sb):c)}</th>`)).join("")}</tr>`
+ (subs.length>1?`<tr><th class="col-task"></th>${cols.flatMap(c=>subs.map(()=>`<th>${esc(c)}</th>`)).join("")}</tr>`:"");
const rows=shown.map(t=>{
const tds=cols.flatMap(c=>(subs.length?subs:[null]).map(sb=>{
const cell=(t.cells||{})[keyFor(c,sb)];
if(!cell) return `<td class="cell empty">—</td>`;
const graded=(cell.attempts||[]).filter(x=>x.reward!=null);
// pass-N shades by WHICH attempt passed: darker = solved sooner, so the grid shows reliability
// rather than a flat pass/fail.
const cls=cell.passed_at?`pass-${Math.min(cell.passed_at,4)}`:graded.length?"fail":"na";
const mark=cell.passed_at?`✓${cell.passed_at}`:graded.length?"✗":"·";
return `<td class="cell ${cls}" data-t="${esc(t.id)}" data-k="${esc(keyFor(c,sb))}"
title="${graded.length}/${(cell.attempts||[]).length} graded">${mark}</td>`;
})).join("");
// Whichever facet is active labels the row, so the pill reflects the data instead of assuming a
// difficulty scale exists.
const lbl=facetKey?String((t.meta||{})[facetKey]??""):"";
return `<tr><td class="col-task" data-task="${esc(t.id)}">
<div>${lbl?`<span class="diff ${diffLike(facetKey,lbl)}">${esc(lbl)}</span>`:""}<span class="tid">${esc(t.id)}</span></div>
<div class="q">${esc((t.question||"").slice(0,150))}</div></td>${tds}</tr>`;
}).join("");
// Collapsed by default: the grid is what you came for, and six counters above it push it below the
// fold on a laptop.
const statSummary=`${n(s.tasks_total)} tasks · ${n(s.tasks_any_pass)} solved ≤k · ${n(s.attempts_total)} attempts`
+ (s.n_all_infra?` · ${n(s.n_all_infra)} unmeasured`:"");
$("#body").innerHTML=`${filterBar}
<details class="statwrap"><summary>${statSummary}</summary><div class="stats">${stats}</div></details>
<div class="matrices">
<div class="matrix-card"><h4>model × harness <span class="sub">pass@${KMAX}</span></h4>${mxh}</div>
${mxd?`<div class="matrix-card"><h4>harness × ${esc(facetKey)} <span class="sub">pass@${KMAX}</span></h4>${mxd}</div>`:""}
</div>
<div class="legend">
<span><i style="background:rgba(62,201,122,.55)"></i>passed 1st</span>
<span><i style="background:rgba(62,201,122,.18)"></i>passed 4th</span>
<span><i style="background:rgba(226,92,92,.25)"></i>graded, failed</span>
<span><i style="background:rgba(230,180,85,.16)"></i>unmeasured (infra)</span>
<span class="src">click a cell for attempts + trace</span>
</div>
<div class="hwrap"><table class="heatmap"><thead>${head}</thead><tbody>${rows}</tbody></table></div>`;
document.querySelectorAll("td.cell[data-t]").forEach(td=>td.onclick=()=>openCell(td.dataset.t,td.dataset.k));
document.querySelectorAll("td.col-task[data-task]").forEach(td=>td.onclick=()=>openTask(td.dataset.task));
document.querySelectorAll(".fchip[data-fk]").forEach(el=>el.onclick=()=>{
const k=el.dataset.fk,v=el.dataset.fv;
FILT[k]=FILT[k]||new Set();
FILT[k].has(v)?FILT[k].delete(v):FILT[k].add(v);
renderGrid();
});
const fc=$("#fclear"); if(fc) fc.onclick=()=>{FILT={};renderGrid()};
}
function diffLike(key,val){
// Colour a facet value only when it plausibly orders low->high; otherwise leave it neutral rather
// than implying a ranking the field may not have.
const num=Number(val);
if(!/level|difficulty|tier|rank/i.test(key||"")||Number.isNaN(num))return "";
return num<=0?"easy":num<=2?"medium":"hard";
}
function openTask(taskId){
const t=(DS.tasks||[]).find(x=>String(x.id)===String(taskId))||{};
const cells=Object.entries(t.cells||{});
$("#dtitle").textContent=t.id||taskId;
$("#dpills").innerHTML=`<span class="pill">${cells.length} cell(s)</span>`;
const meta=Object.entries(t.meta||{}).map(([k,v])=>
`<div><em>${esc(k)}</em>${esc(typeof v==="object"?JSON.stringify(v):v)}</div>`).join("");
const perCell=cells.map(([k,c])=>{
const [m,h]=k.split("|");
const graded=(c.attempts||[]).filter(a=>a.reward!=null);
return `<tr><td>${esc(h)}</td><td>${esc(short(m))}</td>
<td class="reward ${c.passed_at?"pass":graded.length?"fail":"na"}">
${c.passed_at?`✓ attempt ${c.passed_at}`:graded.length?"✗ failed":"unmeasured"}</td>
<td>${graded.length}/${(c.attempts||[]).length}</td>
<td>${c.attempts?.[0]?.n_turns??"–"}</td>
<td><button data-cell="${esc(k)}">open</button></td></tr>`;
}).join("");
$("#dbody").innerHTML=`
<div class="taskcard"><div class="msg-head" style="background:none;padding:0 0 4px">question</div>
<pre class="msg-body" style="padding:0">${esc(t.question||"(none recorded)")}</pre></div>
${t.answer!=null?`<div class="taskcard"><div class="msg-head" style="background:none;padding:0 0 4px">gold answer</div>
<pre class="msg-body gold" style="padding:0">${esc(t.answer)}</pre></div>`:""}
${meta?`<div class="taskcard"><div class="msg-head" style="background:none;padding:0">metadata</div>
<div class="metagrid">${meta}</div></div>`:""}
<table class="trial-table"><thead><tr><th>harness</th><th>model</th><th>result</th><th>graded</th><th>turns</th><th></th></tr></thead>
<tbody>${perCell}</tbody></table>`;
$("#dlg").showModal();
document.querySelectorAll("#dbody button[data-cell]").forEach(b=>
b.onclick=()=>openCell(taskId,b.dataset.cell));
}
async function openCell(taskId,cellKey){
const t=(DS.tasks||[]).find(x=>String(x.id)===String(taskId))||{};
const cell=(t.cells||{})[cellKey]||{};
const [model,harness]=cellKey.split("|");
const graded=(cell.attempts||[]).filter(a=>a.reward!=null);
const pass1=graded.length?graded.filter(a=>a.reward>0).length/graded.length:null;
$("#dtitle").textContent=`${taskId}`;
$("#dpills").innerHTML=`<span class="pill">${esc(harness)}</span> <span class="pill">${esc(short(model))}</span>
<span class="pill ${cell.passed_at?"ok":"exp"}">${cell.passed_at?`pass@${KMAX} ✓ (attempt ${cell.passed_at})`:graded.length?"failed":"unmeasured"}</span>`;
$("#dbody").innerHTML=`
<div class="stats" style="padding:0 0 8px">
<div class="stat"><div class="num">${pass1==null?"–":pct(pass1)}</div><div class="lbl">pass@1</div></div>
<div class="stat"><div class="num">${graded.length}/${(cell.attempts||[]).length}</div><div class="lbl">graded</div></div>
<div class="stat"><div class="num">${cell.passed_at??"–"}</div><div class="lbl">first pass</div></div>
</div>
${t.question?`<div class="msg"><div class="msg-head">question</div><pre class="msg-body">${esc(t.question)}</pre></div>`:""}
${t.answer!=null?`<div class="msg"><div class="msg-head">gold</div><pre class="msg-body gold">${esc(t.answer)}</pre></div>`:""}
<table class="trial-table"><thead><tr><th>#</th><th>reward</th><th>turns</th><th>elapsed</th><th></th></tr></thead>
<tbody>${(cell.attempts||[]).map(a=>`<tr>
<td>${a.attempt}</td>
<td class="reward ${a.reward==null?"na":a.reward>0?"pass":"fail"}">${a.reward==null?"unmeasured":n(a.reward,2)}</td>
<td>${n(a.n_turns)}</td><td>${a.elapsed_sec?n(a.elapsed_sec,1)+"s":"–"}</td>
<td>${a.trace?`<button data-tr="${esc(a.trace)}">trace</button>`:`<span class="src">no trace recorded</span>`}</td>
</tr>`).join("")}</tbody></table>
<div id="tr"></div>`;
$("#dlg").showModal();
document.querySelectorAll("#dbody button[data-tr]").forEach(b=>b.onclick=()=>showTrace(b.dataset.tr));
}
async function showTrace(path){
$("#tr").innerHTML=`<div class="empty-msg">loading…</div>`;
let tr;
try{tr=await api(`/api/projects/${SEL.pid}/datasets/${SEL.did}/trace?path=${encodeURIComponent(path)}`)}
catch(e){$("#tr").innerHTML=`<div class="empty-msg">${e.message}</div>`;return}
// reward_method is surfaced because the grader is exact -> numeric -> LLM judge: a 0 from the judge is
// a wrong answer, a 0 from exact match may only be a formatting mismatch.
const meta=["reward","reward_method","n_turns","elapsed_sec","gold","predicted"].filter(k=>tr[k]!==undefined)
.map(k=>`<div class="stat"><div class="num">${esc(n(tr[k],2))}</div><div class="lbl">${k}</div></div>`).join("");
const msgs=(tr.messages||[]).map(m=>{
const calls=(m.tool_calls||[]).map(c=>JSON.stringify(c)).join("\n");
const body=(typeof m.content==="string"?m.content:JSON.stringify(m.content,null,2))||"";
return `<div class="msg"><div class="msg-head">${esc(m.role)}${calls?" · tool_calls":""}</div>
<pre class="msg-body">${esc(body)}${calls?"\n\n"+esc(calls):""}</pre></div>`;
}).join("");
$("#tr").innerHTML=`<div class="stats" style="padding:10px 0 6px">${meta}</div>
${msgs||`<pre class="msg-body">${esc(JSON.stringify(tr,null,2).slice(0,4000))}</pre>`}`;
}
async function openRuns(pid){
const ids=[...RUNSEL];
if(!ids.length){$("#body").innerHTML=`<div class="empty-msg">select a training run</div>`;return}
SEL={pid,rid:ids[0],runs:ids};
$("#body").innerHTML=`<div class="empty-msg">loading ${ids.length} run(s)…</div>`;
const loaded=[];
for(const rid of ids){
const key=`${pid}/${rid}`;
if(!RUNCACHE[key]){
let meta={},d={steps:[]};
try{meta=await api(`/api/projects/${pid}/runs/${rid}`)}catch(e){}
try{d=await api(`/api/projects/${pid}/runs/${rid}/train`)}
catch(e){RUNCACHE[key]={rid,meta,steps:[],error:e.message};loaded.push(RUNCACHE[key]);continue}
RUNCACHE[key]={rid,meta,steps:d.steps||[],skipped:d.skipped_lines,rollouts:d.rollout_steps||[]};
}
loaded.push(RUNCACHE[key]);
}
renderTrain(loaded);
}
function renderTrain(runs){
// Chart every numeric series the runs actually logged, rather than a fixed list: a trainer that adds
// a metric should show it without a code change here, the same reason task metadata is discovered.
const keys=[...new Set(runs.flatMap(r=>r.steps.flatMap(s=>
Object.entries(s).filter(([k,v])=>typeof v==="number"&&k!=="step").map(([k])=>k))))];
const preferred=["reward","reward_std","loss","ratio","kl","entropy","learning_rate"];
keys.sort((a,b)=>{
const ia=preferred.indexOf(a),ib=preferred.indexOf(b);
return (ia<0?99:ia)-(ib<0?99:ib) || a.localeCompare(b);
});
const summary=runs.map((r,i)=>{
const g=r.steps.filter(s=>(s.reward_std||0)>0).length;
return `<div class="stat" style="border-left:3px solid ${RUNCOLORS[i%RUNCOLORS.length]}">
<div class="num">${r.steps.length}<span class="src"> steps</span></div>
<div class="lbl">${esc(r.rid)}</div>
<div class="lbl">${g}/${r.steps.length} with a gradient${r.error?" · "+esc(r.error):""}</div></div>`;
}).join("");
const legend=runs.map((r,i)=>
`<span><i style="background:${RUNCOLORS[i%RUNCOLORS.length]}"></i>${esc(r.rid)}</span>`).join("");
const charts=keys.map(k=>{
const series=runs.map((r,i)=>({
pts:r.steps.map((s,j)=>[s.step??j,s[k]]).filter(pt=>typeof pt[1]==="number"),
color:RUNCOLORS[i%RUNCOLORS.length],
})).filter(x=>x.pts.length);
if(!series.length)return "";
return `<div class="chartbox"><h5>${esc(k)}<span>${series.map(x=>x.pts.length).join(" / ")} pts</span></h5>
${multiChart(series)}</div>`;
}).join("");
const anyRollouts=runs.some(r=>(r.rollouts||[]).length);
$("#body").innerHTML=`
<div class="ctrls">
<label>smoothing <input type="range" id="sm" min="0" max="0.95" step="0.05" value="${SMOOTH}">
<span class="src" id="smv">${SMOOTH.toFixed(2)}</span></label>
<label><input type="checkbox" id="logx"${LOGX?" checked":""}> log x</label>
<span class="src">${runs.length} run(s) overlaid — tick more in the sidebar to compare</span>
${anyRollouts?`<button id="rollbtn">rollout traces</button>`:`<span class="src">no rollout traces recorded for these runs</span>`}
</div>
<details class="statwrap" open><summary>${runs.length} run(s)</summary><div class="stats">${summary}</div></details>
<div class="rlegend">${legend}</div>
<div class="chartgrid">${charts||`<div class="empty-msg">no numeric series logged</div>`}</div>`;
$("#sm").oninput=(e)=>{SMOOTH=+e.target.value;$("#smv").textContent=SMOOTH.toFixed(2);renderTrain(runs)};
$("#logx").onchange=(e)=>{LOGX=e.target.checked;renderTrain(runs)};
const rb=$("#rollbtn"); if(rb) rb.onclick=()=>openRollouts(runs);
}
// EMA, applied left to right. Shown WITH the raw series rather than instead of it: a smoothed curve
// hides the variance, and on a GRPO run the variance is the signal — a flat reward with high spread
// means different things from a flat reward with none.
function ema(pts,alpha){
if(alpha<=0)return pts;
const out=[];let acc=null;
for(const [x,y] of pts){acc = acc===null ? y : alpha*acc+(1-alpha)*y; out.push([x,acc])}
return out;
}
function multiChart(series){
const W=330,H=140,P=34,B=20;
const all=series.flatMap(s=>s.pts);
const xs=all.map(p=>p[0]),ys=all.map(p=>p[1]);
let x0=Math.min(...xs),x1=Math.max(...xs);if(x0===x1)x1=x0+1;
let y0=Math.min(...ys),y1=Math.max(...ys);if(y0===y1){y0-=.5;y1+=.5}
const pad=(y1-y0)*0.08;y0-=pad;y1+=pad;
const fx=v=>LOGX?Math.log10(Math.max(v,1)):v;
const fx0=fx(x0),fx1=Math.max(fx(x1),fx0+1e-9);
const sx=v=>P+(fx(v)-fx0)/(fx1-fx0)*(W-P-8);
const sy=v=>H-B-(v-y0)/(y1-y0)*(H-B-14);
const grid=[0,.5,1].map(f=>{const y=y0+(y1-y0)*f;
return `<line x1="${P}" x2="${W-8}" y1="${sy(y).toFixed(1)}" y2="${sy(y).toFixed(1)}" stroke="#30363d" stroke-width=".5"/>
<text x="2" y="${(sy(y)+3).toFixed(1)}" fill="#6e7681" font-size="8.5">${y.toFixed(2)}</text>`}).join("");
const paths=series.map(s=>{
const raw=s.pts.map((p,i)=>`${i?"L":"M"}${sx(p[0]).toFixed(1)},${sy(p[1]).toFixed(1)}`).join("");
const sm=ema(s.pts,SMOOTH).map((p,i)=>`${i?"L":"M"}${sx(p[0]).toFixed(1)},${sy(p[1]).toFixed(1)}`).join("");
return `<path d="${raw}" fill="none" stroke="${s.color}" stroke-width="1" opacity="${SMOOTH>0?0.22:1}"/>
${SMOOTH>0?`<path d="${sm}" fill="none" stroke="${s.color}" stroke-width="1.8"/>`:""}`;
}).join("");
return `<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:auto">${grid}${paths}
<text x="${P}" y="${H-4}" fill="#6e7681" font-size="8.5">${x0}</text>
<text x="${W-30}" y="${H-4}" fill="#6e7681" font-size="8.5">${x1}</text></svg>`;
}
async function openRollouts(runs){
const withR=runs.filter(r=>(r.rollouts||[]).length);
$("#dtitle").textContent="rollout traces";
$("#dpills").innerHTML=`<span class="pill">${withR.length} run(s)</span>`;
$("#dbody").innerHTML=withR.map(r=>`<div class="taskcard"><b>${esc(r.rid)}</b>
<table class="trial-table"><thead><tr><th>step</th><th>rollouts</th><th>rewards</th><th></th></tr></thead>
<tbody>${r.rollouts.map(st=>`<tr><td>${st.step}</td><td>${st.n}</td>
<td>${(st.rewards||[]).map(x=>x==null?"·":Number(x).toFixed(2)).join(" ")}</td>
<td><button data-run="${esc(r.rid)}" data-step="${st.step}">open</button></td></tr>`).join("")}
</tbody></table></div>`).join("") || `<div class="empty-msg">no rollout traces recorded</div>`;
$("#dlg").showModal();
document.querySelectorAll("#dbody button[data-step]").forEach(b=>b.onclick=()=>showRolloutStep(b.dataset.run,b.dataset.step));
}
async function showRolloutStep(rid,step){
const box=$("#dbody");
box.insertAdjacentHTML("afterbegin",`<div id="rs" class="empty-msg">loading step ${step}…</div>`);
let rows;
try{rows=await api(`/api/projects/${SEL.pid}/runs/${rid}/rollouts/${step}`)}
catch(e){$("#rs").outerHTML=`<div class="warnbox">${e.message}</div>`;return}
$("#rs").outerHTML=`<div class="taskcard"><b>step ${esc(step)} · ${rows.rollouts.length} rollout(s)</b>
${rows.rollouts.map((ro,i)=>`
<details><summary class="src">#${i+1} · reward ${ro.reward==null?"unmeasured":n(ro.reward,2)}
· ${n(ro.n_turns)} turns · ${esc(ro.task_id||"")}</summary>
${(ro.messages||[]).map(m=>`<div class="msg"><div class="msg-head">${esc(m.role)}</div>
<pre class="msg-body">${esc(typeof m.content==="string"?m.content:JSON.stringify(m.content,null,2))}</pre></div>`).join("")
|| `<div class="src" style="padding:6px">no conversation recorded for this rollout</div>`}
</details>`).join("")}</div>`;
}
function chart(steps,key){
const pts=steps.map((s,i)=>[s.step??i,s[key]]).filter(p=>typeof p[1]==="number");
if(!pts.length)return"";
const W=380,H=112,P=30,xs=pts.map(p=>p[0]),ys=pts.map(p=>p[1]);
const x0=Math.min(...xs),x1=Math.max(...xs,x0+1);let y0=Math.min(...ys),y1=Math.max(...ys);
if(y0===y1){y0-=.5;y1+=.5}
const sx=v=>P+(v-x0)/(x1-x0)*(W-P-8),sy=v=>H-P+4-(v-y0)/(y1-y0)*(H-P-12);
return `<svg width="${W}" height="${H}" style="margin:0 8px 8px 0">
<text x="6" y="13" fill="#8b949e" font-size="10.5">${key}</text>
<text x="6" y="${H-6}" fill="#6e7681" font-size="9">${y0.toFixed(2)}</text>
<text x="${W-44}" y="${H-6}" fill="#6e7681" font-size="9">step ${x1}</text>
<path d="${pts.map((p,i)=>`${i?"L":"M"}${sx(p[0]).toFixed(1)},${sy(p[1]).toFixed(1)}`).join("")}"
fill="none" stroke="#58a6ff" stroke-width="1.5"/></svg>`;
}
$("#axH").onclick=()=>{AXIS="harness";$("#axH").classList.add("on");$("#axM").classList.remove("on");if(DS&&SEL?.did)renderGrid()};
$("#axM").onclick=()=>{AXIS="model";$("#axM").classList.add("on");$("#axH").classList.remove("on");if(DS&&SEL?.did)renderGrid()};
$("#refresh").onclick=async()=>{$("#refresh").textContent="…";
try{await api("/api/refresh",{method:"POST"});RUNCACHE={};const k=SEL;await loadTree();
if(k?.did)await openDataset(k.pid,k.did);else if(RUNSEL.size)await openRuns(k.pid)}
finally{$("#refresh").textContent="⟳"}};
loadTree();
</script>