| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <title>ag — run report</title> |
| <script src="plotly.min.js"></script> |
| |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;600;700&display=swap" rel="stylesheet"> |
| <script> |
| |
| (function () { |
| var p = new URLSearchParams(location.search).get('__theme'); |
| var dark = p ? p === 'dark' : (window.matchMedia |
| && window.matchMedia('(prefers-color-scheme: dark)').matches); |
| document.documentElement.classList.add(dark ? 'dark' : 'light'); |
| })(); |
| </script> |
| <style> |
| |
| |
| :root { |
| --bg:#ffffff; --panel:#ffffff; --subtle:#f8f7f4; --line:#ebe9e4; |
| --fg:#1b1b18; --dim:#6b7280; --accent:#ff9d00; --accent-fg:#92400e; |
| --good:#16a34a; --bad:#dc2626; --warn:#d97706; |
| --hover:#f8f7f4; --chip-on:#fff8eb; --code-bg:#f5f4f0; --drill-bg:#faf9f6; |
| --brand1:#ffd21e; --brand2:#ff9d00; |
| --shadow:0 1px 2px rgba(27,27,24,.05), 0 1px 3px rgba(27,27,24,.04); |
| } |
| |
| :root.dark { |
| --bg:#0b0f17; --panel:#161b22; --subtle:#11161f; --line:#2b313b; |
| --fg:#e6edf3; --dim:#9aa4b2; --accent:#ffb224; --accent-fg:#ffce6e; |
| --good:#3fb950; --bad:#f85149; --warn:#d29922; |
| --hover:#1b212b; --chip-on:#2a2113; --code-bg:#1b212b; --drill-bg:#10151d; |
| --brand1:#ffd21e; --brand2:#ff9d00; |
| --shadow:none; |
| } |
| @media (prefers-color-scheme: dark) { |
| :root:not(.light):not(.dark) { |
| --bg:#0b0f17; --panel:#161b22; --subtle:#11161f; --line:#2b313b; |
| --fg:#e6edf3; --dim:#9aa4b2; --accent:#ffb224; --accent-fg:#ffce6e; |
| --good:#3fb950; --bad:#f85149; --warn:#d29922; |
| --hover:#1b212b; --chip-on:#2a2113; --code-bg:#1b212b; --drill-bg:#10151d; |
| --brand1:#ffd21e; --brand2:#ff9d00; |
| --shadow:none; |
| } |
| } |
| * { box-sizing:border-box; } |
| body { margin:0; background:var(--bg); color:var(--fg); |
| font:15px/1.55 "Source Sans 3","Source Sans Pro",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; |
| -webkit-font-smoothing:antialiased; } |
| header, section, footer { max-width:1400px; margin:0 auto; padding:0 24px; } |
| header { padding-top:30px; display:flex; justify-content:space-between; align-items:flex-start; gap:16px; } |
| h1 { margin:0 0 6px; font-size:26px; font-weight:700; letter-spacing:-0.01em; |
| display:inline-block; } |
| #theme-toggle { flex:none; cursor:pointer; border:1px solid var(--line); background:var(--panel); |
| color:var(--fg); border-radius:999px; padding:7px 12px; font-size:13px; line-height:1; |
| display:inline-flex; align-items:center; gap:6px; box-shadow:var(--shadow); } |
| #theme-toggle, #cfg-toggle { flex:none; cursor:pointer; border:1px solid var(--line); background:var(--panel); |
| color:var(--fg); border-radius:999px; padding:7px 12px; font-size:13px; line-height:1; |
| display:inline-flex; align-items:center; gap:6px; box-shadow:var(--shadow); } |
| #theme-toggle:hover, #cfg-toggle:hover { border-color:var(--accent); } |
| .hdr-btns { display:flex; gap:8px; align-items:center; flex:none; } |
| |
| #backdrop { position:fixed; inset:0; background:rgba(0,0,0,.4); z-index:40; } |
| #sidebar { position:fixed; top:0; left:0; height:100vh; width:340px; max-width:88vw; z-index:50; |
| background:var(--panel); border-right:1px solid var(--line); overflow-y:auto; |
| box-shadow:0 0 24px rgba(0,0,0,.18); display:flex; flex-direction:column; } |
| |
| #sidebar[hidden], #backdrop[hidden] { display:none; } |
| .side-head { display:flex; justify-content:space-between; align-items:center; padding:14px 16px; |
| border-bottom:1px solid var(--line); position:sticky; top:0; background:var(--panel); } |
| .side-head b { font-size:15px; } |
| .side-close { cursor:pointer; border:0; background:none; color:var(--dim); font-size:18px; line-height:1; } |
| .side-close:hover { color:var(--fg); } |
| .side-body { padding:16px; display:flex; flex-direction:column; gap:18px; } |
| .side-grp > label { display:block; color:var(--dim); font-size:12px; margin-bottom:6px; font-weight:600; } |
| .side-grp .chips { max-width:none; } |
| .btn { cursor:pointer; border:1px solid var(--line); background:var(--panel); color:var(--fg); |
| border-radius:8px; padding:6px 12px; font-size:13px; } |
| .btn:hover { border-color:var(--accent); } |
| .chk { display:inline-flex; align-items:center; gap:5px; cursor:pointer; color:var(--fg); font-size:13px; } |
| .chk input { accent-color:var(--accent); } |
| |
| #tabs { display:flex; gap:2px; max-width:1400px; margin:22px auto 0; padding:0 24px; |
| border-bottom:1px solid var(--line); } |
| .tab { background:none; border:0; border-bottom:2px solid transparent; color:var(--dim); |
| cursor:pointer; font-weight:600; font-size:14px; padding:10px 16px; margin-bottom:-1px; } |
| .tab:hover { color:var(--fg); } |
| .tab.active { color:var(--fg); border-bottom-color:var(--accent); } |
| .pane > section:first-of-type h2 { margin-top:20px; } |
| |
| h1::after { content:""; display:block; height:3px; margin-top:6px; border-radius:3px; |
| background:linear-gradient(90deg,var(--brand1),var(--brand2)); } |
| h2 { font-size:16px; font-weight:600; border-bottom:1px solid var(--line); padding-bottom:6px; margin:28px 0 12px; } |
| .dim { color:var(--dim); } |
| .panel { background:var(--panel); border:1px solid var(--line); border-radius:12px; box-shadow:var(--shadow); } |
| #rev-list { display:flex; flex-wrap:wrap; gap:10px; } |
| .rev { padding:10px 14px; min-width:140px; cursor:pointer; user-select:none; } |
| .rev:hover { border-color:var(--accent); } |
| .rev.on { border-color:var(--accent); } |
| .rev.off { opacity:0.45; } |
| .rev .nm { font-weight:600; } |
| |
| #task-list { display:flex; flex-direction:column; gap:8px; } |
| .task { padding:0; overflow:hidden; } |
| .task-head { display:flex; align-items:center; gap:8px; cursor:pointer; padding:11px 14px; } |
| .task-head:hover { background:var(--hover); } |
| .task-head .caret { color:var(--dim); width:1em; flex:none; } |
| .task-head b { font-size:14px; } |
| .task-sum { margin-left:auto; font-size:12px; white-space:nowrap; } |
| .task-body { padding:2px 14px 14px; } |
| .task-prompt { white-space:pre-wrap; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; |
| font-size:12.5px; line-height:1.5; background:var(--code-bg); color:var(--fg); |
| border-radius:8px; padding:10px 12px; margin:0 0 8px; overflow:auto; max-height:200px; } |
| .task-match { font-size:13px; } |
| .task-match .pill { color:var(--accent-fg); border-color:var(--accent); background:var(--chip-on); font-weight:600; } |
| .task-media { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin:0 0 10px; } |
| .task-media img { max-height:72px; max-width:130px; border-radius:6px; border:1px solid var(--line); object-fit:cover; } |
| .task-media audio { height:34px; max-width:260px; } |
| .answers { width:100%; border-collapse:collapse; font-size:13px; margin-top:6px; max-width:760px; } |
| .answers td { padding:5px 8px; border-top:1px solid var(--line); vertical-align:top; } |
| .ans-row { cursor:pointer; } .ans-row:hover { background:var(--hover); } |
| .answers .m { white-space:nowrap; } |
| .answers .v { white-space:nowrap; font-variant-numeric:tabular-nums; } |
| .answers .a { color:var(--dim); white-space:nowrap; } |
| .ans-ok { color:var(--good); } .ans-bad { color:var(--bad); } .ans-mid { color:var(--warn); } |
| .task .final { white-space:pre-wrap; } |
| .ad-h { font-size:12px; font-weight:600; margin:8px 0 4px; } |
| .ad-n { color:var(--dim); font-variant-numeric:tabular-nums; margin-right:6px; } |
| .badge { font-size:11px; padding:1px 8px; border-radius:999px; border:1px solid var(--line); margin-left:6px; font-weight:600; } |
| .badge.tag { color:var(--good); border-color:var(--good); } |
| #filters { display:flex; flex-wrap:wrap; gap:18px; align-items:flex-start; margin-top:10px; } |
| #filters label { display:block; color:var(--dim); font-size:12px; margin-bottom:4px; } |
| select { background:var(--panel); color:var(--fg); border:1px solid var(--line); |
| border-radius:8px; padding:6px 10px; font-size:13px; } |
| select:hover { border-color:var(--accent); } |
| .chips { display:flex; flex-wrap:wrap; gap:6px; max-width:560px; } |
| .chip { font-size:12px; padding:3px 10px; border:1px solid var(--line); border-radius:999px; |
| cursor:pointer; user-select:none; color:var(--dim); background:var(--panel); } |
| .chip.on { color:var(--accent-fg); border-color:var(--accent); background:var(--chip-on); font-weight:600; } |
| .plot { height:340px; } |
| .chartlegend { display:flex; flex-wrap:wrap; gap:16px; } |
| .chartlegend .it { display:flex; align-items:center; gap:6px; font-size:12px; color:var(--dim); } |
| .chartlegend .sw { width:12px; height:12px; border-radius:3px; display:inline-block; } |
| .grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; } |
| .grid.one-col { grid-template-columns:1fr; } |
| .grid .plot { height:300px; } |
| #side-revs-grp { display:none; } |
| @media (max-width:760px) { |
| .grid, .grid.one-col { grid-template-columns:1fr; } |
| #rev-section { display:none; } |
| #side-revs-grp { display:block; } |
| } |
| table { width:100%; border-collapse:collapse; font-size:13px; } |
| th, td { text-align:left; padding:7px 8px; border-bottom:1px solid var(--line); white-space:nowrap; } |
| th { color:var(--dim); font-weight:600; } |
| tr.cell { cursor:pointer; } tr.cell:hover { background:var(--hover); } |
| tr.grp { cursor:pointer; background:var(--subtle); } |
| tr.grp:hover { background:var(--hover); } |
| tr.grp td { color:var(--fg); font-weight:600; } |
| .caret { color:var(--dim); display:inline-block; width:1em; } |
| td.num { text-align:right; font-variant-numeric:tabular-nums; } |
| .pill { font-size:11px; padding:1px 8px; border-radius:999px; border:1px solid var(--line); margin-right:4px; } |
| .ok { color:var(--good); } .bad { color:var(--bad); } .warn { color:var(--warn); } |
| .drill { background:var(--drill-bg); border:1px solid var(--line); border-radius:8px; margin:6px 0 14px; padding:10px 14px; } |
| .step { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; color:var(--dim); } |
| .step .err { color:var(--bad); } |
| .final { margin-top:6px; padding-left:10px; border-left:2px solid var(--accent); } |
| code { background:var(--code-bg); padding:1px 5px; border-radius:4px; font-size:12px; } |
| footer { color:var(--dim); padding:30px 20px 50px; font-size:12px; border-top:1px solid var(--line); margin-top:30px; } |
| </style> |
| </head> |
| <body> |
| <header> |
| <div> |
| <h1 id="title">run report</h1> |
| <div id="meta" class="dim"></div> |
| </div> |
| <div class="hdr-btns"> |
| <button id="cfg-toggle" title="Configuration" aria-label="Configuration">⚙ Configure</button> |
| <button id="theme-toggle" title="Toggle light / dark" aria-label="Toggle light / dark theme"></button> |
| </div> |
| </header> |
|
|
| <aside id="sidebar" hidden> |
| <div class="side-head"><b>⚙ Configuration</b><button id="cfg-close" class="side-close" aria-label="Close">✕</button></div> |
| <div class="side-body"> |
| <div class="side-grp"><label>Models</label><div id="f-ns" class="chips"></div></div> |
| <div class="side-grp"><label>Tiers — how much help the agent gets</label><div id="f-tiers" class="chips"></div></div> |
| <div class="side-grp"><label>Tasks</label><div id="f-tasks" class="chips"></div></div> |
| <div class="side-grp" id="side-revs-grp"><label>Revisions — include / exclude commits</label><div id="f-revs" class="chips"></div></div> |
| <div class="side-grp"><label>Fair comparison — shared tasks only</label> |
| <label class="chk"><input type="checkbox" id="f-shared-models"> across models</label> |
| <label class="chk" style="margin-left:14px"><input type="checkbox" id="f-shared-revs"> across revisions</label> |
| <div id="scope-note" class="dim" style="font-size:12px;margin-top:8px"></div> |
| </div> |
| </div> |
| </aside> |
| <div id="backdrop" hidden></div> |
|
|
| <section id="rev-section"> |
| <h2>Revisions <span class="dim">— click a commit to include / exclude it everywhere · oldest → newest</span></h2> |
| <div id="rev-list"></div> |
| </section> |
|
|
| <nav id="tabs"> |
| <button class="tab active" data-tab="overview">Overview</button> |
| <button class="tab" data-tab="coverage">Coverage</button> |
| <button class="tab" data-tab="results">Results</button> |
| </nav> |
|
|
| <div id="tab-overview" class="pane"> |
| <section> |
| <h2>Overview</h2> |
| <div id="filters"> |
| <div><label>X axis</label><select id="f-x"></select></div> |
| <div><label>Series (one line per)</label><select id="f-series"></select></div> |
| <div><label>Chart</label><select id="f-chart"> |
| <option value="line">lines</option><option value="bar">bars</option></select></div> |
| </div> |
| <div id="ov-legend" class="chartlegend" style="margin-top:12px"></div> |
| <div id="ov-grid" class="grid" style="margin-top:8px"> |
| <div id="ov-match" class="plot"></div> |
| <div id="ov-time" class="plot"></div> |
| <div id="ov-newtok" class="plot"></div> |
| <div id="ov-err" class="plot"></div> |
| </div> |
| </section> |
|
|
| <section id="markers-section"> |
| <h2>Label adoption</h2> |
| <div id="filters"> |
| <div><label>Marker</label><select id="f-metric"></select></div> |
| </div> |
| <div id="marker-desc" class="dim" style="font-size:13px;margin-top:8px"></div> |
| <div id="chart" class="plot" style="margin-top:10px"></div> |
| </section> |
|
|
| <section> |
| <h2>Distributions <span class="dim">box = median + quartiles, dashed line = mean (hover a point for model/tier/task/run)</span></h2> |
| <div id="filters" style="margin-bottom:6px"> |
| <div><label>Model</label><select id="f-dist-model"></select></div> |
| <div><label>Boxes by</label><select id="f-dist"> |
| <option value="revision">revision</option> |
| <option value="model">model (per revision)</option></select></div> |
| <div><label>Points</label><select id="f-dist-points"> |
| <option value="none">none (clean boxes)</option> |
| <option value="outliers">outliers only</option> |
| <option value="all">every run</option></select></div> |
| <div><label>Scale</label><select id="f-dist-scale"> |
| <option value="log">logarithmic</option> |
| <option value="linear">linear</option> |
| <option value="linear-clip">linear (clip outliers)</option></select></div> |
| </div> |
| <div id="dist-hint" class="dim" style="font-size:12px;margin-bottom:8px"></div> |
| <div id="dist-legend" class="chartlegend" style="margin-bottom:8px"></div> |
| <div class="grid"> |
| <div id="dist-time" class="plot"></div> |
| <div id="dist-new" class="plot"></div> |
| <div id="dist-rep" class="plot"></div> |
| <div id="dist-out" class="plot"></div> |
| </div> |
| </section> |
| </div> |
|
|
| <div id="tab-coverage" class="pane" hidden> |
| <section> |
| <h2>Coverage <span class="dim">— % of expected runs done per task × revision (hover for counts) · click a cell for the per-model breakdown (⚙ to pick models)</span></h2> |
| <div id="cov-heatmap" style="margin-top:10px"></div> |
| <div id="cov-detail" style="margin-top:12px"></div> |
| </section> |
| </div> |
|
|
| <div id="tab-results" class="pane" hidden> |
|
|
| <section> |
| <h2>Tasks <span class="dim">— click a task to expand it: what it asks, how a match is scored, and what each model answered</span></h2> |
| <div id="task-list"></div> |
| </section> |
|
|
| <section> |
| <h2>Run results <span id="results-hint" class="dim"></span></h2> |
| <div id="filters" style="margin-bottom:10px"> |
| <div><label>Group by</label><select id="f-group"> |
| <option value="model">model</option> |
| <option value="task">task</option> |
| <option value="both">model & task</option> |
| </select></div> |
| </div> |
| <div id="table"></div> |
| </section> |
| </div> |
|
|
| <footer id="foot"></footer> |
|
|
| <script src="data.js"></script> |
| <script> |
| "use strict"; |
| const DATA = window.__AG_DATA__ || {}; |
| const REVS = DATA.revisions || []; |
| const RUNS = DATA.runs || []; |
| const MARKER_NAMES = DATA.marker_names || []; |
| const MARKER_DESC = DATA.marker_desc || {}; |
| |
| |
| function themeColors() { |
| const cs = getComputedStyle(document.documentElement); |
| const v = (n, d) => (cs.getPropertyValue(n).trim() || d); |
| return { fg: v('--fg', '#1f2937'), grid: v('--line', '#e5e7eb') }; |
| } |
| |
| const esc = s => String(s ?? '').replace(/[&<>"']/g, c => |
| ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); |
| const pct = (k, n) => n ? Math.round(1000 * k / n) / 10 : null; |
| function median(xs) { |
| if (!xs.length) return null; |
| const s = [...xs].sort((a, b) => a - b), n = s.length; |
| return n % 2 ? s[(n - 1) / 2] : (s[n / 2 - 1] + s[n / 2]) / 2; |
| } |
| |
| |
| const revBySha = Object.fromEntries(REVS.map(c => [c.sha, c])); |
| const baseName = c => c.name || c.ref || c.sha; |
| const NAME_COUNT = {}; |
| REVS.forEach(c => { const n = baseName(c); NAME_COUNT[n] = (NAME_COUNT[n] || 0) + 1; }); |
| const dispName = c => NAME_COUNT[baseName(c)] > 1 ? `${baseName(c)} (${c.sha.slice(0,7)})` : baseName(c); |
| const revOrder = REVS.map(c => c.sha); |
| |
| |
| const TIER_RANK = { bare:0, clone:1, skill:2 }; |
| const TIERS = [...new Set(RUNS.map(r => r.tier))] |
| .sort((a, b) => (TIER_RANK[a] ?? 9) - (TIER_RANK[b] ?? 9) || a.localeCompare(b)); |
| const TIER_COLOR = { bare:'#8a8a9a', clone:'#6366f1', skill:'#22c55e' }; |
| const PALETTE = ['#8a8a9a','#6366f1','#22c55e','#e8590c','#9333ea','#0891b2']; |
| const colorForTier = t => TIER_COLOR[t] || PALETTE[TIERS.indexOf(t) % PALETTE.length]; |
| const TASKS = [...new Set(RUNS.map(r => r.task))].sort(); |
| |
| |
| |
| |
| |
| const MODEL_PARAMS = DATA.model_params || {}; |
| function paramB(ns) { |
| if (MODEL_PARAMS[ns] != null) return MODEL_PARAMS[ns] / 1e9; |
| const id = (ns.split('/').pop() || ns); |
| const hits = [...id.matchAll(/(\d+(?:\.\d+)?)\s*[bB]\b/g)].map(m => parseFloat(m[1])); |
| return hits.length ? Math.max(...hits) : null; |
| } |
| const NSS = ((DATA.namespaces && DATA.namespaces.length) |
| ? DATA.namespaces : [...new Set(RUNS.map(r => r.ns))]).slice() |
| .sort((a, b) => { |
| const pa = paramB(a), pb = paramB(b); |
| if (pa === null && pb === null) return a.localeCompare(b); |
| if (pa === null) return 1; |
| if (pb === null) return -1; |
| return pa - pb || a.localeCompare(b); |
| }); |
| const REV_PALETTE = ['#6366f1','#e8590c','#22c55e','#9333ea','#0891b2','#be185d','#a16207','#64748b']; |
| const REV_COLOR = Object.fromEntries(REVS.map((c, i) => [c.sha, REV_PALETTE[i % REV_PALETTE.length]])); |
| const revColor = sha => REV_COLOR[sha] || '#6366f1'; |
| |
| |
| const METRICS = []; |
| MARKER_NAMES.forEach(m => METRICS.push({ |
| key: 'mk:' + m, label: '🏷 ' + m + ' adoption %', |
| fn: rs => pct(rs.filter(r => (r.markers || []).includes(m)).length, rs.length), |
| })); |
| METRICS.push({ key:'match', label:'match %', |
| fn: rs => { const seen = rs.filter(r => r.matched !== null); |
| return pct(seen.filter(r => r.matched).length, seen.length); } }); |
| |
| |
| |
| const runErrored = r => r.exit !== 0 || (r.status && r.status !== 'ok'); |
| METRICS.push({ key:'err', label:'runs with error %', |
| fn: rs => pct(rs.filter(runErrored).length, rs.length) }); |
| METRICS.push({ key:'time', label:'median time (s)', |
| fn: rs => { const v = median(rs.map(r => r.elapsed)); return v === null ? null : Math.round(v * 10) / 10; } }); |
| METRICS.push({ key:'newtok', label:'median new tokens', |
| fn: rs => median(rs.map(r => (r.tok || {}).new || 0)) }); |
| const metricByKey = Object.fromEntries(METRICS.map(m => [m.key, m])); |
| |
| |
| |
| |
| |
| const OVERVIEW_KEYS = ['match', 'time', 'newtok', 'err']; |
| const CHOICE_METRICS = METRICS.filter(m => m.key.startsWith('mk:')); |
| const CHOICE = CHOICE_METRICS.length ? CHOICE_METRICS : METRICS.filter(m => !OVERVIEW_KEYS.includes(m.key)); |
| |
| |
| const NS_PALETTE = ['#6366f1','#e8590c','#22c55e','#9333ea','#0891b2','#be185d','#a16207','#64748b']; |
| const NS_COLOR = Object.fromEntries(NSS.map((n, i) => [n, NS_PALETTE[i % NS_PALETTE.length]])); |
| |
| |
| |
| |
| const shortModel = ns => (ns.split('/').pop() || ns).split(':')[0].replace(/^.*?--/, ''); |
| |
| const modelTick = ns => { const t = shortModel(ns); return t.length > 22 ? t.slice(0, 21) + '…' : t; }; |
| |
| |
| const DIMS = { |
| revision: { label:'revision', values: revOrder, val: r => r.sha, |
| tick: sha => dispName(revBySha[sha]), color: sha => revColor(sha) }, |
| model: { label:'model', values: NSS, val: r => r.ns, |
| tick: ns => shortModel(ns), color: ns => NS_COLOR[ns] || '#6366f1' }, |
| tier: { label:'tier', values: TIERS, val: r => r.tier, |
| tick: t => t, color: t => colorForTier(t) }, |
| }; |
| |
| const state = { |
| nss: new Set(NSS), tiers: new Set(TIERS), tasks: new Set(TASKS), revs: new Set(REVS.map(c => c.sha)), |
| metric: (CHOICE[0] || METRICS[0]).key, xdim: 'revision', series: 'tier', |
| chart: 'line', group: 'model', distSplit: 'revision', distModel: '', distScale: 'log', |
| distPoints: 'none', sharedModels: false, sharedRevisions: false, |
| }; |
| |
| |
| document.getElementById('title').textContent = `🤗 ${DATA.profile || 'ag'} — run report`; |
| document.getElementById('meta').innerHTML = |
| `${RUNS.length} runs · ${REVS.length} revisions · ${NSS.length} namespaces · ` + |
| `generated ${esc(DATA.generated_at || '')}` + (DATA.bucket ? ` · bucket <code>${esc(DATA.bucket)}</code>` : ''); |
| |
| |
| |
| function renderRevList() { |
| const host = document.getElementById('rev-list'); |
| host.innerHTML = REVS.map(c => { |
| const on = state.revs.has(c.sha); |
| const badge = c.kind === 'tag' ? '<span class="badge tag">release</span>' : ''; |
| const n = RUNS.filter(r => r.sha === c.sha).length; |
| return `<div class="rev panel ${on ? 'on' : 'off'}" data-sha="${esc(c.sha)}" title="click to toggle"> |
| <div class="nm">${esc(dispName(c))}${badge}</div> |
| <div class="dim" style="font-size:12px;margin-top:3px">${n} runs</div></div>`; |
| }).join('') || '<div class="dim">no revisions</div>'; |
| host.querySelectorAll('.rev').forEach(el => el.addEventListener('click', () => toggleRev(el.dataset.sha))); |
| } |
| |
| |
| function renderRevChips() { |
| const host = document.getElementById('f-revs'); |
| if (!host) return; |
| host.innerHTML = REVS.map(c => |
| `<span class="chip ${state.revs.has(c.sha) ? 'on' : ''}" data-sha="${esc(c.sha)}">${esc(dispName(c))}</span>` |
| ).join('') || '<span class="dim">no revisions</span>'; |
| host.querySelectorAll('.chip').forEach(el => el.addEventListener('click', () => toggleRev(el.dataset.sha))); |
| } |
| |
| function toggleRev(sha) { |
| state.revs.has(sha) ? state.revs.delete(sha) : state.revs.add(sha); |
| renderRevList(); renderRevChips(); applyConfig(); |
| } |
| renderRevList(); |
| renderRevChips(); |
| |
| |
| const TASK_META = Object.fromEntries((DATA.tasks || []).map(t => [t.id, t])); |
| function matchDesc(t) { |
| const m = t.match || 'substring'; |
| const e = esc(String(t.expected)); |
| if (t.expected === null || t.expected === undefined || t.expected === '') |
| return 'Open-ended — not auto-scored for correctness (compared via markers, tokens & time only).'; |
| if (m === 'substring') return `Match when the final answer contains “${e}” (case-insensitive).`; |
| if (m === 'exact') return `Match when the final answer equals “${e}”.`; |
| if (m === 'regex') return `Match when the final answer matches the pattern /${e}/i.`; |
| if (m === 'judge') return `Judged against “${e}”.`; |
| return `Match (${esc(m)}): “${e}”.`; |
| } |
| let INPUTS = DATA.inputs || {}; |
| function mediaHTML(t) { |
| const items = (t.media || []).map(fn => { |
| const m = INPUTS[fn]; |
| if (!m) return ''; |
| if (m.kind === 'image') return `<img src="${m.uri}" alt="${esc(fn)}" title="${esc(fn)}">`; |
| if (m.kind === 'audio') return `<audio controls preload="none" src="${m.uri}" title="${esc(fn)}"></audio>`; |
| return ''; |
| }).filter(Boolean).join(''); |
| return items ? `<div class="task-media">${items}</div>` : ''; |
| } |
| |
| const cfgScope = r => state.nss.has(r.ns) && state.tiers.has(r.tier) && state.revs.has(r.sha); |
| |
| function taskSummary(id) { |
| const rs = RUNS.filter(r => r.task === id && cfgScope(r)); |
| const models = NSS.filter(ns => state.nss.has(ns) && rs.some(r => r.ns === ns)); |
| let scored = 0, matched = 0; |
| models.forEach(ns => { |
| const seen = rs.filter(r => r.ns === ns && r.matched !== null); |
| if (seen.length) { scored++; if (seen.every(r => r.matched)) matched++; } |
| }); |
| const tag = scored |
| ? `<span class="ans-${matched === scored ? 'ok' : matched === 0 ? 'bad' : 'mid'}">✓ ${matched}/${scored} models match</span>` |
| : `<span class="dim">open-ended</span>`; |
| return `${tag} <span class="dim">· ${models.length} model(s)</span>`; |
| } |
| |
| function groupFinals(runs, cap) { |
| const g = new Map(); |
| runs.forEach(r => { const f = (r.final || '').trim() || '(no answer text)'; g.set(f, (g.get(f) || 0) + 1); }); |
| const entries = [...g.entries()].sort((a, b) => b[1] - a[1]); |
| const shown = entries.slice(0, cap).map(([f, n]) => |
| `<div class="final"><span class="ad-n">${n}×</span>${esc(f)}</div>`).join(''); |
| const extra = entries.length > cap ? `<div class="dim" style="font-size:12px">+ ${entries.length - cap} more distinct answer(s)</div>` : ''; |
| return shown + extra; |
| } |
| |
| |
| function answerDetail(mr) { |
| const fail = mr.filter(r => r.matched === false); |
| const none = mr.filter(r => r.matched === null); |
| const ok = mr.filter(r => r.matched === true); |
| const out = []; |
| if (fail.length) { |
| out.push(`<div class="ad-h bad">✗ didn't match (${fail.length}) — what they answered instead</div>`); |
| out.push(groupFinals(fail, 8)); |
| } |
| if (none.length) { |
| const byStatus = {}; |
| none.forEach(r => { const s = r.status || '?'; byStatus[s] = (byStatus[s] || 0) + 1; }); |
| const summary = Object.entries(byStatus).map(([s, n]) => `${n}× ${esc(s)}`).join(', '); |
| out.push(`<div class="ad-h warn">⚠ no scored answer (${none.length}): ${summary}</div>`); |
| const withText = none.filter(r => (r.final || '').trim()); |
| if (withText.length) out.push(groupFinals(withText, 4)); |
| } |
| if (ok.length) { |
| out.push(`<div class="ad-h ok">✓ matched (${ok.length}) — example</div>`); |
| out.push(`<div class="final">${esc(((ok.find(r => r.final) || {}).final || '(no text)'))}</div>`); |
| } |
| return out.join('') || '<div class="dim">no runs</div>'; |
| } |
| |
| |
| function answersHTML(id) { |
| const rs = RUNS.filter(r => r.task === id && cfgScope(r)); |
| if (!rs.length) return ''; |
| const rows = NSS.filter(ns => state.nss.has(ns) && rs.some(r => r.ns === ns)).map(ns => { |
| const mr = rs.filter(r => r.ns === ns); |
| const ok = mr.filter(r => r.matched === true).length; |
| const fail = mr.filter(r => r.matched === false).length; |
| const none = mr.filter(r => r.matched === null).length; |
| const scored = ok + fail; |
| let tally; |
| if (scored) { |
| tally = `<span class="ans-ok">✓ ${ok}</span>` |
| + (fail ? ` · <span class="ans-bad">✗ ${fail}</span>` : '') |
| + (none ? ` · <span class="ans-mid">⚠ ${none}</span>` : ''); |
| } else { |
| tally = `<span class="dim">${none} run(s), open-ended</span>`; |
| } |
| const key = `ans_${id}_${ns}`.replace(/[^\w]/g, '_'); |
| return `<tr class="ans-row" data-k="${key}"> |
| <td class="m">${esc(shortModel(ns))}</td> |
| <td class="v">${tally}</td> |
| <td class="a">${fail ? 'show failures ▸' : 'show responses ▸'}</td></tr> |
| <tr id="${key}" class="ans-detail" style="display:none"><td colspan="3">${answerDetail(mr)}</td></tr>`; |
| }).join(''); |
| return `<div class="dim" style="font-size:11px;margin-top:12px">model answers <span class="dim">— click a model to see failing & matched responses</span></div> |
| <table class="answers">${rows}</table>`; |
| } |
| let openTask = TASKS[0] || null; |
| function renderTasks() { |
| const host = document.getElementById('task-list'); |
| const ids = TASKS.filter(id => state.tasks.has(id)); |
| if (!ids.includes(openTask)) openTask = ids[0] || null; |
| host.innerHTML = ids.map(id => { |
| const t = TASK_META[id] || { id }; |
| const open = id === openTask; |
| const cat = t.category ? `<span class="badge">${esc(t.category)}</span>` : ''; |
| const scored = !(t.expected === null || t.expected === undefined || t.expected === ''); |
| const tag = scored ? `<span class="pill">expected: ${esc(String(t.expected))}</span>` |
| : `<span class="pill">open-ended</span>`; |
| const prompt = (t.prompt || '').trim(); |
| const body = `<div class="task-body"${open ? '' : ' hidden'}> |
| ${mediaHTML(t)} |
| ${prompt ? `<pre class="task-prompt">${esc(prompt)}</pre>` : ''} |
| <div class="task-match">${tag} <span class="dim">${matchDesc(t)}</span></div> |
| ${answersHTML(id)} |
| </div>`; |
| return `<div class="task panel"> |
| <div class="task-head" data-id="${esc(id)}"> |
| <span class="caret">${open ? '▾' : '▸'}</span><b>${esc(id)}</b> ${cat} |
| <span class="task-sum">${taskSummary(id)}</span> |
| </div>${body}</div>`; |
| }).join('') || '<div class="dim">no tasks in this report</div>'; |
| |
| host.querySelectorAll('.task-head').forEach(h => h.addEventListener('click', () => { |
| openTask = (openTask === h.dataset.id) ? null : h.dataset.id; |
| renderTasks(); |
| })); |
| host.querySelectorAll('.ans-row').forEach(r => r.addEventListener('click', () => { |
| const d = document.getElementById(r.dataset.k); |
| if (d) d.style.display = d.style.display === 'none' ? '' : 'none'; |
| })); |
| } |
| |
| |
| const mSel = document.getElementById('f-metric'); |
| if (CHOICE.length) { |
| mSel.innerHTML = CHOICE.map(m => `<option value="${esc(m.key)}">${esc(m.label)}</option>`).join(''); |
| mSel.value = state.metric; |
| mSel.addEventListener('change', () => { state.metric = mSel.value; renderChart(); }); |
| } else { |
| document.getElementById('markers-section').style.display = 'none'; |
| } |
| |
| const dimOpts = Object.entries(DIMS).map(([k, d]) => `<option value="${k}">${esc(d.label)}</option>`).join(''); |
| const xSel = document.getElementById('f-x'); |
| const sSel = document.getElementById('f-series'); |
| xSel.innerHTML = dimOpts; xSel.value = state.xdim; |
| sSel.innerHTML = dimOpts; sSel.value = state.series; |
| |
| function syncDims(changed) { |
| if (state.xdim !== state.series) return; |
| const other = Object.keys(DIMS).find(k => k !== state.xdim); |
| if (changed === 'x') { state.series = other; sSel.value = other; } |
| else { state.xdim = other; xSel.value = other; } |
| } |
| xSel.addEventListener('change', () => { state.xdim = xSel.value; syncDims('x'); renderViz(); }); |
| sSel.addEventListener('change', () => { state.series = sSel.value; syncDims('series'); renderViz(); }); |
| |
| const cSel = document.getElementById('f-chart'); |
| cSel.value = state.chart; |
| cSel.addEventListener('change', () => { state.chart = cSel.value; renderViz(); }); |
| |
| const gSel = document.getElementById('f-group'); |
| gSel.value = state.group; |
| gSel.addEventListener('change', () => { state.group = gSel.value; renderTable(); }); |
| |
| const smChk = document.getElementById('f-shared-models'); |
| smChk.checked = state.sharedModels; |
| smChk.addEventListener('change', () => { state.sharedModels = smChk.checked; applyConfig(); }); |
| const srChk = document.getElementById('f-shared-revs'); |
| srChk.checked = state.sharedRevisions; |
| srChk.addEventListener('change', () => { state.sharedRevisions = srChk.checked; applyConfig(); }); |
| |
| const dSel = document.getElementById('f-dist'); |
| dSel.value = state.distSplit; |
| dSel.addEventListener('change', () => { state.distSplit = dSel.value; renderDistributions(); }); |
| |
| const dmSel = document.getElementById('f-dist-model'); |
| dmSel.innerHTML = `<option value="">all models</option>` + |
| NSS.map(ns => `<option value="${esc(ns)}">${esc(shortModel(ns))}</option>`).join(''); |
| dmSel.value = state.distModel; |
| dmSel.addEventListener('change', () => { state.distModel = dmSel.value; renderDistributions(); }); |
| |
| const dscSel = document.getElementById('f-dist-scale'); |
| dscSel.value = state.distScale; |
| dscSel.addEventListener('change', () => { state.distScale = dscSel.value; renderDistributions(); }); |
| |
| const dpSel = document.getElementById('f-dist-points'); |
| dpSel.value = state.distPoints; |
| dpSel.addEventListener('change', () => { state.distPoints = dpSel.value; renderDistributions(); }); |
| |
| |
| function chipGroup(elId, keys, set, onchange, labelFn) { |
| const el = document.getElementById(elId); |
| keys.forEach(k => { |
| const chip = document.createElement('span'); |
| chip.className = set.has(k) ? 'chip on' : 'chip'; |
| chip.textContent = labelFn ? labelFn(k) : k; |
| chip.addEventListener('click', () => { |
| if (set.has(k)) { set.delete(k); chip.classList.remove('on'); } |
| else { set.add(k); chip.classList.add('on'); } |
| onchange(); |
| }); |
| el.appendChild(chip); |
| }); |
| } |
| |
| function applyConfig() { renderAll(); renderCoverage(); } |
| chipGroup('f-ns', NSS, state.nss, applyConfig, shortModel); |
| chipGroup('f-tiers', TIERS, state.tiers, applyConfig); |
| chipGroup('f-tasks', TASKS, state.tasks, applyConfig); |
| |
| let covOpen = false; |
| |
| |
| |
| |
| |
| |
| |
| |
| let scopeTasks = new Set(state.tasks); |
| function computeScopeTasks() { |
| let common = new Set(state.tasks); |
| const tierOk = r => state.tiers.has(r.tier); |
| if (state.sharedModels) { |
| for (const ns of NSS.filter(n => state.nss.has(n))) { |
| const ran = new Set(RUNS.filter(r => r.ns === ns && tierOk(r)).map(r => r.task)); |
| common = new Set([...common].filter(t => ran.has(t))); |
| } |
| } |
| if (state.sharedRevisions) { |
| const scoped = RUNS.filter(r => state.nss.has(r.ns) && tierOk(r) && state.revs.has(r.sha)); |
| for (const sha of new Set(scoped.map(r => r.sha))) { |
| const ran = new Set(scoped.filter(r => r.sha === sha).map(r => r.task)); |
| common = new Set([...common].filter(t => ran.has(t))); |
| } |
| } |
| return common; |
| } |
| function refreshScope() { |
| scopeTasks = computeScopeTasks(); |
| const note = document.getElementById('scope-note'); |
| const modes = []; |
| if (state.sharedModels) modes.push('all selected models'); |
| if (state.sharedRevisions) modes.push('all revisions'); |
| if (modes.length) { |
| const dropped = [...state.tasks].filter(t => !scopeTasks.has(t)).length; |
| note.textContent = `Comparing ${scopeTasks.size} task(s) shared by ${modes.join(' and ')}` |
| + (dropped ? ` · ${dropped} task(s) excluded for uneven coverage` : ''); |
| } else { |
| note.textContent = ''; |
| } |
| } |
| const inScope = r => state.nss.has(r.ns) && state.tiers.has(r.tier) |
| && state.revs.has(r.sha) && scopeTasks.has(r.task); |
| |
| |
| |
| function pivotTraces(m) { |
| const xd = DIMS[state.xdim], sd = DIMS[state.series]; |
| const rs = RUNS.filter(inScope); |
| const xPresent = new Set(rs.map(xd.val)); |
| const xvals = xd.values.filter(v => xPresent.has(v)); |
| const xtick = state.xdim === 'model' ? modelTick : xd.tick; |
| const x = xvals.map(xtick); |
| const sPresent = new Set(rs.map(sd.val)); |
| const svals = sd.values.filter(v => sPresent.has(v)); |
| const isLine = state.chart === 'line'; |
| return svals.map(sv => { |
| const y = xvals.map(xv => m.fn(rs.filter(r => xd.val(r) === xv && sd.val(r) === sv))); |
| const color = sd.color(sv), name = sd.tick(sv); |
| return isLine |
| ? { type: 'scatter', mode: 'lines+markers', name, x, y, |
| line: { color, width: 2 }, marker: { color, size: 7 }, connectgaps: true } |
| : { type: 'bar', name, x, y, marker: { color } }; |
| }); |
| } |
| |
| function presentVals(dim) { |
| const d = DIMS[dim], present = new Set(RUNS.filter(inScope).map(d.val)); |
| return d.values.filter(v => present.has(v)); |
| } |
| function chartInto(divId, m, { mini = false, legend = true } = {}) { |
| |
| |
| |
| |
| |
| const longX = presentVals(state.xdim).length > 5; |
| const baseB = mini ? 64 : 70; |
| const T = themeColors(); |
| const layout = { |
| barmode: 'group', height: mini ? 300 : 360, |
| margin: { l: mini ? 46 : 50, r: mini ? 8 : 10, t: mini ? 28 : 10, b: longX ? 120 : baseB }, |
| paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', |
| font: { color: T.fg }, showlegend: legend, |
| legend: { orientation: 'h', y: mini ? -0.3 : 1.15, font: { size: 11 } }, |
| yaxis: { title: mini ? '' : m.label, gridcolor: T.grid, zeroline: false, rangemode: 'tozero' }, |
| xaxis: { gridcolor: T.grid, automargin: true, tickangle: longX ? -35 : 0 }, |
| }; |
| if (mini) layout.title = { text: m.label, font: { size: 13 }, x: 0, xanchor: 'left' }; |
| Plotly.react(divId, pivotTraces(m), layout, { displayModeBar: false, responsive: true }); |
| } |
| |
| function legendHTML(dim) { |
| const d = DIMS[dim]; |
| return presentVals(dim).map(v => |
| `<span class="it"><span class="sw" style="background:${d.color(v)}"></span>${esc(d.tick(v))}</span>` |
| ).join(''); |
| } |
| |
| |
| function renderOverview() { |
| document.getElementById('ov-legend').innerHTML = legendHTML(state.series); |
| const wide = presentVals(state.xdim).length > 5; |
| |
| document.getElementById('ov-grid').classList.toggle('one-col', wide); |
| OVERVIEW_KEYS.forEach(k => chartInto('ov-' + k, metricByKey[k], { mini: true, legend: false })); |
| } |
| function renderChart() { |
| if (!CHOICE.length) return; |
| |
| const name = state.metric.startsWith('mk:') ? state.metric.slice(3) : ''; |
| document.getElementById('marker-desc').textContent = MARKER_DESC[name] || ''; |
| chartInto('chart', metricByKey[state.metric]); |
| } |
| |
| |
| function cellRuns(ns, sha, tier, task) { |
| return RUNS.filter(r => r.ns === ns && r.sha === sha && r.tier === tier && r.task === task); |
| } |
| function markerSummary(rs) { |
| return MARKER_NAMES.map(m => { |
| const k = rs.filter(r => (r.markers || []).includes(m)).length; |
| return k ? `<span class="pill">${esc(m)} ${k}/${rs.length}</span>` : ''; |
| }).join(''); |
| } |
| function drill(rs) { |
| return rs.slice().sort((a, b) => a.run - b.run).map(r => { |
| const head = `run ${r.run} · ` + |
| (r.matched === true ? '<span class="ok">✓ match</span>' : |
| r.matched === false ? '<span class="bad">✗ no-match</span>' : '<span class="dim">—</span>') + |
| ` · ${Math.round(r.elapsed)}s · ${r.calls} tools` + |
| (r.errs ? ` · <span class="bad">errors ${r.errs}</span>` : '') + |
| (r.status && r.status !== 'ok' ? ` · <span class="warn">${esc(r.status)}</span>` : '') + |
| ` · tok new:${(r.tok||{}).new ?? 0} out:${(r.tok||{}).out ?? 0} · ` + |
| (r.markers || []).map(m => `<span class="pill">${esc(m)}</span>`).join(''); |
| const steps = (r.steps || []).map(s => |
| `<div class="step">${s.e ? '❗' : '·'} <b>${esc(s.n)}</b> ${esc(s.x || '')}` + |
| (s.e && s.r ? `<br><span class="err">↳ ${esc(s.r)}</span>` : '') + `</div>`).join('') || |
| '<div class="step dim">(no tool calls)</div>'; |
| const final = r.final ? `<div class="final">${esc(r.final)}</div>` : ''; |
| const trace = r.trace_rel ? `<div class="dim" style="font-size:11px;margin-top:4px">trace: <code>${esc(r.trace_rel)}</code></div>` : ''; |
| return `<div style="margin-bottom:12px"><div>${head}</div>${steps}${final}${trace}</div>`; |
| }).join(''); |
| } |
| |
| const GROUP_DIMS = { model: ['model'], task: ['task'], both: ['model', 'task'] }; |
| const GROUP_LABEL = { model: 'model', task: 'task' }; |
| const keyOf = (c, dim) => dim === 'model' ? c.ns : c.task; |
| |
| |
| function statCells(rs) { |
| const seen = rs.filter(r => r.matched !== null); |
| const matched = seen.length ? `${seen.filter(r => r.matched).length}/${seen.length}` : '—'; |
| const errs = rs.filter(runErrored).length; |
| const retried = rs.filter(r => !runErrored(r) && r.retry).length; |
| const flags = (errs ? ` · <span class="bad">!${errs} err</span>` : '') |
| + (retried ? ` · <span class="warn" title="tool call(s) failed but the run recovered">↻${retried}</span>` : ''); |
| return `<td class="num">${rs.length}</td> |
| <td>✓ ${matched}${flags}</td> |
| <td>${markerSummary(rs) || '<span class="dim">—</span>'}</td> |
| <td class="num">${Math.round(median(rs.map(r => r.elapsed)) || 0)}s</td>`; |
| } |
| function dataRow(c) { |
| const { ns, sha, tier, task, rs } = c; |
| return `<td>${esc(ns)}</td><td>${esc(dispName(revBySha[sha]))}</td><td>${esc(tier)}</td><td>${esc(task)}</td>${statCells(rs)}`; |
| } |
| function renderTable() { |
| |
| |
| const cells = []; |
| for (const ns of NSS.filter(n => state.nss.has(n))) |
| for (const sha of revOrder.filter(s => state.revs.has(s))) |
| for (const tier of TIERS) for (const task of TASKS) { |
| if (!state.tiers.has(tier) || !state.tasks.has(task)) continue; |
| const rs = cellRuns(ns, sha, tier, task); |
| if (rs.length) cells.push({ ns, sha, tier, task, rs }); |
| } |
| |
| const dims = GROUP_DIMS[state.group]; |
| const shaRank = s => revOrder.indexOf(s), tierRank = t => TIERS.indexOf(t); |
| cells.sort((a, b) => { |
| for (const d of dims) { const x = keyOf(a, d), y = keyOf(b, d); if (x !== y) return x < y ? -1 : 1; } |
| if (a.ns !== b.ns) return a.ns < b.ns ? -1 : 1; |
| if (a.task !== b.task) return a.task < b.task ? -1 : 1; |
| if (a.sha !== b.sha) return shaRank(a.sha) - shaRank(b.sha); |
| return tierRank(a.tier) - tierRank(b.tier); |
| }); |
| |
| |
| |
| const prefixes = (labels, n) => Array.from({ length: n }, (_, i) => labels.slice(0, i + 1).join('§')); |
| |
| const groupRuns = {}; |
| for (const c of cells) { |
| const labels = dims.map(d => keyOf(c, d)); |
| prefixes(labels, labels.length).forEach(k => (groupRuns[k] = groupRuns[k] || []).push(...c.rs)); |
| } |
| const groupKeys = []; |
| const rows = []; |
| let prev = []; |
| for (const c of cells) { |
| const labels = dims.map(d => keyOf(c, d)); |
| let diff = 0; while (diff < labels.length && prev[diff] === labels[diff]) diff++; |
| for (let i = diff; i < labels.length; i++) { |
| const selfKey = labels.slice(0, i + 1).join('§'); |
| const owners = prefixes(labels, i).join('|'); |
| groupKeys.push(selfKey); |
| rows.push(`<tr class="grp" data-key="${esc(selfKey)}" data-owners="${esc(owners)}"> |
| <td colspan="4" style="padding-left:${6 + i * 18}px"><span class="caret">▸</span> |
| <b>${esc(labels[i])}</b> <span class="dim">${esc(GROUP_LABEL[dims[i]])}</span></td> |
| ${statCells(groupRuns[selfKey])}</tr>`); |
| } |
| prev = labels; |
| const owners = prefixes(labels, labels.length).join('|'); |
| const id = `d_${c.ns}_${c.sha}_${c.tier}_${c.task}`.replace(/[^\w]/g, '_'); |
| rows.push(`<tr class="cell grp-row" data-detail="${id}" data-owners="${esc(owners)}">${dataRow(c)}</tr>`); |
| rows.push(`<tr id="${id}" class="drill-row" data-owners="${esc(owners)}" style="display:none"><td colspan="8"><div class="drill">${drill(c.rs)}</div></td></tr>`); |
| } |
| |
| document.getElementById('results-hint').textContent = |
| `· ${cells.length} cell(s) across ${new Set(cells.map(c => c.ns)).size} model(s) · click a category to unfold it, a row to drill into its runs`; |
| document.getElementById('table').innerHTML = rows.length ? `<table> |
| <thead><tr><th>Model</th><th>Revision</th><th>Tier</th><th>Task</th><th class="num">runs</th> |
| <th>✓match · errors</th><th>markers</th><th class="num">med time</th></tr></thead> |
| <tbody>${rows.join('')}</tbody></table>` |
| : '<div class="dim">no runs for this model / tier / task selection</div>'; |
| |
| const collapsed = new Set(groupKeys), expanded = new Set(); |
| const owned = tr => (tr.dataset.owners || '').split('|').filter(Boolean); |
| function applyCollapse() { |
| document.querySelectorAll('#table tr.grp, #table tr.grp-row').forEach(tr => { |
| tr.style.display = owned(tr).some(k => collapsed.has(k)) ? 'none' : ''; |
| }); |
| document.querySelectorAll('#table tr.drill-row').forEach(tr => { |
| const hidden = owned(tr).some(k => collapsed.has(k)); |
| tr.style.display = (!hidden && expanded.has(tr.id)) ? '' : 'none'; |
| }); |
| document.querySelectorAll('#table tr.grp').forEach(tr => { |
| const car = tr.querySelector('.caret'); |
| if (car) car.textContent = collapsed.has(tr.dataset.key) ? '▸' : '▾'; |
| }); |
| } |
| document.querySelectorAll('#table tr.grp').forEach(tr => tr.addEventListener('click', () => { |
| const k = tr.dataset.key; collapsed.has(k) ? collapsed.delete(k) : collapsed.add(k); applyCollapse(); |
| })); |
| document.querySelectorAll('#table tr.grp-row').forEach(tr => tr.addEventListener('click', () => { |
| const id = tr.dataset.detail; expanded.has(id) ? expanded.delete(id) : expanded.add(id); applyCollapse(); |
| })); |
| applyCollapse(); |
| } |
| |
| |
| |
| |
| function distRuns() { |
| const base = RUNS.filter(r => state.tiers.has(r.tier) && state.tasks.has(r.task) && state.revs.has(r.sha)); |
| return state.distModel ? base.filter(r => r.ns === state.distModel) |
| : base.filter(r => state.nss.has(r.ns)); |
| } |
| |
| |
| |
| |
| |
| |
| function quantileSorted(sorted, q) { |
| if (!sorted.length) return 0; |
| const pos = (sorted.length - 1) * q, base = Math.floor(pos), rest = pos - base; |
| return sorted[base + 1] !== undefined ? sorted[base] + rest * (sorted[base + 1] - sorted[base]) : sorted[base]; |
| } |
| |
| |
| |
| |
| function clipBound(values) { |
| const s = values.filter(v => v != null && isFinite(v)).sort((a, b) => a - b); |
| if (!s.length) return null; |
| const q1 = quantileSorted(s, 0.25), q3 = quantileSorted(s, 0.75); |
| const fence = q3 + 1.5 * (q3 - q1); |
| const bound = fence > 0 ? fence : (quantileSorted(s, 0.95) || s[s.length - 1]); |
| return bound > 0 ? bound : null; |
| } |
| function boxFig(divId, title, valueFn) { |
| const rs = distRuns(); |
| const byModel = !state.distModel && state.distSplit === 'model'; |
| const tip = r => `${shortModel(r.ns)} · ${r.tier} · ${r.task} · run${r.run}`; |
| |
| |
| const pts = state.distPoints === 'all' ? 'all' : state.distPoints === 'outliers' ? 'outliers' : false; |
| const common = { |
| type: 'box', boxmean: true, boxpoints: pts, |
| ...(pts === 'all' ? { jitter: 0.3, pointpos: 0 } : {}), |
| }; |
| let traces; |
| if (byModel) { |
| traces = NSS.filter(n => state.nss.has(n)).map(ns => { |
| const cr = rs.filter(r => r.ns === ns), col = DIMS.model.color(ns); |
| return { |
| ...common, name: shortModel(ns), |
| x: cr.map(r => dispName(revBySha[r.sha])), y: cr.map(valueFn), |
| text: cr.map(tip), hoverinfo: 'y+text', |
| marker: { color: col, size: 4, opacity: 0.6 }, line: { color: col }, fillcolor: col + '22', |
| }; |
| }); |
| } else { |
| traces = REVS.filter(c => state.revs.has(c.sha)).map(c => { |
| const cr = rs.filter(r => r.sha === c.sha), col = revColor(c.sha); |
| return { |
| ...common, name: dispName(c), y: cr.map(valueFn), |
| text: cr.map(tip), hoverinfo: 'y+text', |
| marker: { color: col, size: 4, opacity: 0.6 }, line: { color: col }, fillcolor: col + '22', |
| }; |
| }); |
| } |
| const T = themeColors(); |
| const layout = { |
| title: { text: title, font: { size: 13 }, x: 0, xanchor: 'left' }, |
| height: 300, showlegend: false, margin: { l: 52, r: 10, t: 30, b: 70 }, |
| paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', font: { color: T.fg }, |
| yaxis: { type: state.distScale === 'log' ? 'log' : 'linear', gridcolor: T.grid, zeroline: false }, |
| xaxis: { gridcolor: T.grid, automargin: true }, |
| }; |
| |
| if (state.distScale === 'linear-clip') { |
| const bound = clipBound(rs.map(valueFn)); |
| if (bound) layout.yaxis.range = [0, bound * 1.05]; |
| } |
| |
| |
| if (byModel) { |
| layout.boxmode = 'group'; |
| layout.xaxis.categoryorder = 'array'; |
| layout.xaxis.categoryarray = REVS.filter(c => state.revs.has(c.sha)).map(dispName); |
| } |
| Plotly.react(divId, traces, layout, { displayModeBar: false, responsive: true }); |
| } |
| function renderDistributions() { |
| const byModel = !state.distModel && state.distSplit === 'model'; |
| |
| document.getElementById('dist-legend').innerHTML = byModel ? legendHTML('model') : ''; |
| document.getElementById('dist-hint').textContent = state.distModel |
| ? `Showing ${shortModel(state.distModel)} across revisions. Pick "all models" to compare, or use the model chips up top.` |
| : 'Pick a single model to see it across revisions, or use the model chips up top to focus the set.'; |
| boxFig('dist-time', 'elapsed (s)', r => r.elapsed); |
| boxFig('dist-new', 'new tokens', r => (r.tok || {}).new || 0); |
| boxFig('dist-rep', 'repeat tokens', r => (r.tok || {}).rep || 0); |
| boxFig('dist-out', 'out tokens', r => (r.tok || {}).out || 0); |
| resizePlots(['dist-time', 'dist-new', 'dist-rep', 'dist-out']); |
| } |
| |
| |
| |
| |
| function resizePlots(ids) { |
| requestAnimationFrame(() => ids.forEach(id => { |
| const el = document.getElementById(id); |
| if (el && el.data) Plotly.Plots.resize(el); |
| })); |
| } |
| function renderViz() { |
| renderOverview(); renderChart(); |
| resizePlots(['ov-match', 'ov-time', 'ov-newtok', 'ov-err', 'chart']); |
| } |
| |
| |
| const COV_SCALE = [[0, '#fff8eb'], [0.25, '#ffe7ad'], [0.5, '#ffce6e'], [0.75, '#ff9d00'], [1, '#b45309']]; |
| const _hex = h => { h = h.replace('#', ''); return [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16)); }; |
| function _covColor(t) { |
| t = Math.max(0, Math.min(1, t)); |
| for (let i = 1; i < COV_SCALE.length; i++) { |
| if (t <= COV_SCALE[i][0]) { |
| const [t0, c0] = COV_SCALE[i - 1], [t1, c1] = COV_SCALE[i]; |
| const f = (t - t0) / ((t1 - t0) || 1), a = _hex(c0), b = _hex(c1); |
| return [0, 1, 2].map(k => Math.round(a[k] + f * (b[k] - a[k]))); |
| } |
| } |
| return _hex(COV_SCALE[COV_SCALE.length - 1][1]); |
| } |
| |
| function _covText(t) { |
| const [r, g, b] = _covColor(t); |
| return (0.299 * r + 0.587 * g + 0.114 * b) > 150 ? '#111111' : '#ffffff'; |
| } |
| |
| |
| const _maxRunByTask = {}; |
| RUNS.forEach(r => { _maxRunByTask[r.task] = Math.max(_maxRunByTask[r.task] || 0, r.run || 0); }); |
| function expectedRuns(task) { |
| return Math.max(_maxRunByTask[task] || 0, (TASK_META[task] || {}).runs || 3); |
| } |
| |
| |
| const _tiersBySha = {}; |
| RUNS.forEach(r => { (_tiersBySha[r.sha] = _tiersBySha[r.sha] || new Set()).add(r.tier); }); |
| function availTiers(sha) { |
| const seen = _tiersBySha[sha] || new Set(); |
| return TIERS.filter(t => state.tiers.has(t) && seen.has(t)); |
| } |
| function covExpected(task, sha) { |
| return NSS.filter(n => state.nss.has(n)).length * availTiers(sha).length * expectedRuns(task); |
| } |
| |
| function covDetail(task, sha) { |
| const er = expectedRuns(task), tiers = availTiers(sha); |
| const models = NSS.filter(n => state.nss.has(n)); |
| const head = tiers.map(t => `<td class="m">${esc(t)}</td>`).join(''); |
| const rows = models.map(ns => { |
| const cells = tiers.map(tier => { |
| const have = new Set(RUNS.filter(r => r.task === task && r.sha === sha && r.ns === ns && r.tier === tier).map(r => r.run)); |
| const missing = []; |
| for (let i = 1; i <= er; i++) if (!have.has(i)) missing.push(i); |
| const cls = have.size >= er ? 'ans-ok' : have.size === 0 ? 'ans-bad' : 'ans-mid'; |
| const miss = (missing.length && missing.length < er) ? ` <span class="dim">(missing ${missing.join(',')})</span>` : ''; |
| return `<td class="v ${cls}">${have.size}/${er}${miss}</td>`; |
| }).join(''); |
| return `<tr><td class="m">${esc(shortModel(ns))}</td>${cells}</tr>`; |
| }).join(''); |
| return `<div class="dim" style="font-size:12px;margin-bottom:6px"><b>${esc(task)}</b> @ ${esc(dispName(revBySha[sha]))} — completed / expected per model × tier</div> |
| <table class="answers"><tr><td class="m dim">model</td>${head}</tr>${rows}</table>`; |
| } |
| |
| function renderCoverage() { |
| if (!covOpen) return; |
| const T = themeColors(); |
| const sel = REVS.filter(c => state.revs.has(c.sha)); |
| const x = sel.map(dispName), shas = sel.map(c => c.sha); |
| const y = TASKS.filter(t => state.tasks.has(t)); |
| const done = y.map(t => shas.map(sha => |
| RUNS.filter(r => r.task === t && r.sha === sha && state.nss.has(r.ns) && state.tiers.has(r.tier)).length)); |
| const exp = y.map(t => shas.map(sha => covExpected(t, sha))); |
| |
| const z = done.map((row, i) => row.map((v, j) => exp[i][j] ? Math.round(100 * v / exp[i][j]) : null)); |
| const ann = []; |
| z.forEach((row, i) => row.forEach((pct, j) => ann.push({ |
| x: x[j], y: y[i], text: pct === null ? '—' : `${pct}%`, showarrow: false, |
| font: { size: 11, color: _covText((pct || 0) / 100) }, |
| }))); |
| const custom = done.map((row, i) => row.map((v, j) => [v, exp[i][j]])); |
| const layout = { |
| height: Math.max(240, y.length * 26 + 130), |
| margin: { l: 180, r: 10, t: 8, b: 96 }, |
| paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', font: { color: T.fg }, |
| xaxis: { tickangle: -30, automargin: true }, |
| yaxis: { automargin: true, autorange: 'reversed' }, |
| annotations: ann, |
| }; |
| Plotly.react('cov-heatmap', [{ |
| type: 'heatmap', z, x, y, customdata: custom, colorscale: COV_SCALE, zmin: 0, zmax: 100, |
| colorbar: { title: '% done', ticksuffix: '%' }, |
| hovertemplate: '%{y} · %{x}<br>%{customdata[0]}/%{customdata[1]} runs done (%{z}%)<extra></extra>', |
| }], layout, { displayModeBar: false, responsive: true }); |
| resizePlots(['cov-heatmap']); |
| const gd = document.getElementById('cov-heatmap'); |
| if (gd && !gd._covClick && gd.on) { |
| gd._covClick = true; |
| gd.on('plotly_click', ev => { |
| const p = ev.points && ev.points[0]; if (!p) return; |
| const c = REVS.find(rev => dispName(rev) === p.x); if (!c) return; |
| document.getElementById('cov-detail').innerHTML = covDetail(p.y, c.sha); |
| }); |
| } |
| } |
| |
| |
| |
| let DETAILS = null; |
| const _detailCbs = []; |
| function ensureDetails(cb) { |
| if (DETAILS) { cb && cb(); return; } |
| if (cb) _detailCbs.push(cb); |
| if (ensureDetails._loading) return; |
| ensureDetails._loading = true; |
| const s = document.createElement('script'); |
| s.src = 'details.js'; |
| s.onload = s.onerror = () => { |
| DETAILS = window.__AG_DETAILS__ || { runs: [], inputs: {} }; |
| RUNS.forEach((r, i) => Object.assign(r, (DETAILS.runs || [])[i] || {})); |
| INPUTS = DETAILS.inputs || {}; |
| _detailCbs.splice(0).forEach(f => f()); |
| }; |
| document.head.appendChild(s); |
| } |
| let resOpen = false; |
| function renderResults() { |
| if (!resOpen) return; |
| if (DETAILS) { renderTasks(); renderTable(); return; } |
| document.getElementById('task-list').innerHTML = '<div class="dim">Loading run details…</div>'; |
| document.getElementById('results-hint').textContent = ''; |
| ensureDetails(() => { renderTasks(); renderTable(); }); |
| } |
| function renderAll() { refreshScope(); renderViz(); renderResults(); renderDistributions(); } |
| renderAll(); |
| |
| setTimeout(() => ensureDetails(() => { if (resOpen) renderResults(); }), 300); |
| |
| |
| const PANES = [...document.querySelectorAll('.pane')]; |
| const TABS = [...document.querySelectorAll('.tab')]; |
| function showTab(name) { |
| PANES.forEach(p => { p.hidden = p.id !== 'tab-' + name; }); |
| TABS.forEach(b => b.classList.toggle('active', b.dataset.tab === name)); |
| |
| if (name === 'overview') { renderViz(); renderDistributions(); } |
| else if (name === 'coverage') { covOpen = true; renderCoverage(); } |
| else if (name === 'results') { resOpen = true; renderResults(); } |
| } |
| TABS.forEach(b => b.addEventListener('click', () => showTab(b.dataset.tab))); |
| |
| |
| const sidebar = document.getElementById('sidebar'); |
| const backdrop = document.getElementById('backdrop'); |
| function setSidebar(open) { sidebar.hidden = !open; backdrop.hidden = !open; } |
| document.getElementById('cfg-toggle').addEventListener('click', () => setSidebar(sidebar.hidden)); |
| document.getElementById('cfg-close').addEventListener('click', () => setSidebar(false)); |
| backdrop.addEventListener('click', () => setSidebar(false)); |
| document.addEventListener('keydown', e => { if (e.key === 'Escape') setSidebar(false); }); |
| |
| |
| |
| |
| let manualTheme = !!new URLSearchParams(location.search).get('__theme'); |
| function applyTheme(dark) { |
| const root = document.documentElement.classList; |
| root.remove('dark', 'light'); |
| root.add(dark ? 'dark' : 'light'); |
| document.getElementById('theme-toggle').textContent = dark ? '☀ Light' : '☾ Dark'; |
| renderViz(); renderDistributions(); renderCoverage(); |
| } |
| applyTheme(document.documentElement.classList.contains('dark')); |
| document.getElementById('theme-toggle').addEventListener('click', () => { |
| manualTheme = true; |
| applyTheme(!document.documentElement.classList.contains('dark')); |
| }); |
| if (window.matchMedia) { |
| const mq = window.matchMedia('(prefers-color-scheme: dark)'); |
| const onThemeChange = e => { if (!manualTheme) applyTheme(e.matches); }; |
| mq.addEventListener ? mq.addEventListener('change', onThemeChange) |
| : mq.addListener(onThemeChange); |
| } |
| |
| document.getElementById('foot').innerHTML = |
| `Static report generated by <code>ag report</code>. Profile: <code>${esc(DATA.profile || '')}</code>. ` + |
| `Markers: ${MARKER_NAMES.map(m => `<code>${esc(m)}</code>`).join(', ') || '(none)'}.`; |
| </script> |
| </body> |
| </html> |
|
|