| <!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; |
| |
| |
| |
| let FILT={}; |
| |
| |
| 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=>({"<":"<",">":">","&":"&",'"':"""}[c])); |
| |
| |
| |
| const heatBg=p=>p==null?"transparent":`rgba(62,201,122,${(0.08+0.5*p).toFixed(3)})`; |
| |
| |
| 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(); |
| } |
| |
| |
| |
| 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; |
| 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)], |
| |
| |
| ["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(""); |
| |
| |
| 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>`; |
| |
| |
| |
| 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>`; |
| } |
| |
| |
| 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); |
| |
| |
| 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(""); |
| |
| |
| 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(""); |
| |
| |
| |
| 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){ |
| |
| |
| 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} |
| |
| |
| 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){ |
| |
| |
| 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); |
| } |
| |
| |
| |
| |
| 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> |
|
|