lysandre's picture
lysandre HF Staff
Upload folder using huggingface_hub
1742c87 verified
Raw
History Blame Contribute Delete
65.3 kB
<!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>
<!-- Source Sans 3: Hugging Face's typeface (graceful fallback to the system stack if offline). -->
<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>
// Honor the Hugging Face Space theme (?__theme=light|dark); else the OS theme.
(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>
/* Light theme (default) — Hugging Face palette: warm off-white surface,
near-black ink, amber/orange brand accent. */
: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);
}
/* Dark theme — explicit (?__theme=dark / .dark) and the OS fallback. */
: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; }
/* configuration sidebar (opened by the gear) */
#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; }
/* explicit author rules beat the UA [hidden] rule, so re-assert it */
#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); }
/* top-level tabs */
#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; }
/* HF brand underline accent under the title */
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; }
/* Tasks accordion: one task expanded, the rest folded to one-line headers. */
#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; } /* on desktop, revisions live in the top strip */
@media (max-width:760px) {
.grid, .grid.one-col { grid-template-columns:1fr; } /* one column of graphs on phones */
#rev-section { display:none; } /* hide the top revisions strip … */
#side-revs-grp { display:block; } /* … and surface revisions in the config sidebar instead */
}
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 &amp; 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 || {};
// Plotly colors follow the active (light/dark) theme — read from CSS variables.
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 =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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;
}
// revision display names (disambiguate duplicate names by short sha)
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); // already date-sorted by the generator
// tiers / tasks / namespaces present
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();
// Parameter count (in billions) used to order models by size. Prefer the exact
// count resolved from the Hub at generate time (DATA.model_params, in params);
// otherwise parse the largest "<n>B" token out of the model id — for MoE ids
// like "…-550B-A55B" that's the total (550B), not the active experts (55B).
// Returns null when neither is available (sorted last, alphabetically).
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; // unknown size → last
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';
// metrics (generic over the profile's markers)
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); } });
// A run "errored" only if it ended badly (bad status / nonzero exit) — NOT if an
// intermediate tool call failed and the agent recovered (those are tracked
// separately as per-run "tool errors"; a warning on stderr is not a failure).
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]));
// the four headline metrics get their own always-on charts up top; the
// configurable chart below then lets you pick among the remaining ones — the
// profile's labels (markers). Falls back to the leftover metrics if a profile
// declares no markers.
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));
// namespace (model) colors, for when "model" is the series dimension
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]]));
// compact model label for legends/tooltips: drop the "<harness>/" prefix, the
// "<org>--" prefix, and any ":provider" routing suffix
// (e.g. "pi/Qwen--Qwen2.5-7B-Instruct:together" → "Qwen2.5-7B-Instruct").
const shortModel = ns => (ns.split('/').pop() || ns).split(':')[0].replace(/^.*?--/, '');
// even shorter for crowded axis ticks (truncate long names; legend keeps full)
const modelTick = ns => { const t = shortModel(ns); return t.length > 22 ? t.slice(0, 21) + '…' : t; };
// the three pivot dimensions — any can be the X axis or the per-line series
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,
};
// header
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>` : '');
// the Revisions strip (top, oldest → newest by commit date) doubles as a global
// toggle: click a card to include/exclude that commit from every pane.
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)));
}
// A mirror of the Revisions strip inside the config sidebar — shown on mobile (where the
// top strip is hidden) so commit selection stays reachable on small screens.
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)));
}
// single toggle path shared by the strip and the sidebar chips, kept in sync
function toggleRev(sha) {
state.revs.has(sha) ? state.revs.delete(sha) : state.revs.add(sha);
renderRevList(); renderRevChips(); applyConfig();
}
renderRevList();
renderRevChips();
// tasks: show exactly what each run asks for + how a match is scored
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 || {}; // populated from details.js once the Results tab needs it
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>` : '';
}
// runs included in the Tasks/Results panes (honors the global config)
const cfgScope = r => state.nss.has(r.ns) && state.tiers.has(r.tier) && state.revs.has(r.sha);
// one-line summary for a folded task header: how many models match
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>`;
}
// group runs by their (truncated) final answer, most frequent first
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;
}
// the expandable breakdown for one model: failures first (what went wrong),
// no-answer/errored runs, then a matched example
function answerDetail(mr) {
const fail = mr.filter(r => r.matched === false);
const none = mr.filter(r => r.matched === null); // scored task but no comparable answer (errored/empty)
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>';
}
// per-task: one row per model — matched / failed / no-answer at a glance,
// click to see the failing (and matched) responses
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 &amp; 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; // single-open; click again to fold
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';
}));
}
// filters
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;
// X and series must differ — bump the other one when they'd collide
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(); });
// multi-select chip groups (toggle freely; empty = nothing in scope)
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);
});
}
// One global configuration (the sidebar) drives every pane.
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; // Coverage heatmap renders when its tab is first shown
// Effective task set for the aggregate views. The two "shared" toggles restrict
// to tasks with even coverage so metrics aren't biased (e.g. a model that only
// ran the quick tasks, or a revision where only the fast batch finished):
// • across models — keep only tasks EVERY selected model ran
// • across revisions — keep only tasks present on EVERY revision in scope
// Both on ≈ "shared by all models and revisions". `scopeTasks` is refreshed on
// any selection / toggle change (refreshScope, called at the top of renderAll).
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);
// a pivot — metric over the chosen X dimension, one line/bar per series value.
// Used both by the headline overview charts and the configurable marker chart.
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; // truncate long model names on the axis
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 } };
});
}
// values of the current X dimension actually present in scope, and of the series
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 } = {}) {
// many categories → angle the ticks (labels truncated to ≤22 chars upstream).
// automargin lets Plotly grow the L/B margins to fully fit the angled labels so
// the leftmost one isn't clipped; height is fixed, so this only shrinks the plot
// area, it can't blow the chart height open. The b floor keeps short-label charts
// from hugging the axis.
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 });
}
// a shared HTML swatch legend (avoids four overlapping in-plot legends)
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('');
}
// headline charts share one series + one legend; widen to full width when the
// X axis has many categories (e.g. all models) so long labels stop colliding.
function renderOverview() {
document.getElementById('ov-legend').innerHTML = legendHTML(state.series);
const wide = presentVals(state.xdim).length > 5;
// class (not an inline style) so the mobile media query can force a single column
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;
// show the profile's one-line explanation of the selected marker
const name = state.metric.startsWith('mk:') ? state.metric.slice(3) : '';
document.getElementById('marker-desc').textContent = MARKER_DESC[name] || '';
chartInto('chart', metricByKey[state.metric]);
}
// results table (revision × tier × task), click a row to drill into its runs
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('');
}
// results grouped into collapsible categories (by model, task, or both)
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;
// the four right-hand summary columns (runs · ✓match·errors · markers · med time),
// shared by data rows and by folded group headers so errors stay visible folded.
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; // runs that failed
const retried = rs.filter(r => !runErrored(r) && r.retry).length; // recovered tool hiccups
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() {
// the Results tab honors the global configuration (sidebar + Revisions strip);
// organize the included runs via Group by + fold/drill.
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);
});
// §-joined prefixes of a label path; used both as group keys and as the
// "owner" keys that hide a row when an ancestor group is collapsed.
const prefixes = (labels, n) => Array.from({ length: n }, (_, i) => labels.slice(0, i + 1).join('§'));
// aggregate every group's runs up front so a folded header can summarize them
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('|'); // ancestors only
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('|'); // all ancestors
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(); // start fully folded
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();
}
// runs in scope for the distributions: a single model when one is picked from
// the dropdown (ignoring the model chips), else the chip-selected models.
// Tier/task chips always apply.
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));
}
// distributions box+strip, log scale. Layouts:
// • one box per revision (default, or whenever a single model is picked) — see
// how that model / the selected set behaves across commits.
// • "model" split with all models: within each revision, one box per model,
// colored by model, grouped — compare models on each commit.
// linear interpolation quantile over a pre-sorted array
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];
}
// Tukey upper fence (Q3 + 1.5·IQR) — the same cutoff Plotly uses to flag
// outliers. Used to clip the y-axis so a few extreme runs don't squash every box
// to the floor on the linear scale. Falls back to p95, then the max, when the
// IQR is 0 (e.g. most runs near zero) so the range is never degenerate.
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}`;
// Default to clean boxes (median + quartiles + mean), since plotting every run
// overplots into an unreadable cloud; the Points control opts back in.
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 },
};
// clip the view past the outliers so the boxes are readable on the natural scale
if (state.distScale === 'linear-clip') {
const bound = clipBound(rs.map(valueFn));
if (bound) layout.yaxis.range = [0, bound * 1.05];
}
// only the per-model view groups multiple boxes per revision category; the
// revision view keeps one wide box per trace (default boxmode), as before
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';
// a shared swatch legend maps colors → models only in the multi-model compare view
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']);
}
// Plotly sizes to the container at react() time; when we switch the overview to
// full-width (single column) the grid hasn't reflowed yet, so the plot would
// keep a stale (half) width. Resize on the next frame, once layout has settled.
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']);
}
// explicit colorscale (so we can match annotation text to actual cell luminance)
// HF-brand amber ramp (matches the accent); pale cream → yellow → orange → brown.
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) { // interpolate COV_SCALE at t∈[0,1] → [r,g,b]
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]);
}
// readable text color for a cell: white on dark fills, near-black on light ones
function _covText(t) {
const [r, g, b] = _covColor(t);
return (0.299 * r + 0.587 * g + 0.114 * b) > 150 ? '#111111' : '#ffffff';
}
// expected sample count for a task: the intended runs (max run index seen, or
// the task's declared `runs`, default 3) so partial cells stand out.
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);
}
// tiers a revision actually supports (seen in any run there) ∩ selected tiers —
// so releases without the skill tier don't count it as "missing".
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);
}
// per-(task,revision) breakdown shown on click: model × tier done/expected + missing run indices
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>`;
}
// tasks × revisions coverage heatmap — cell shows done/expected; click for detail
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)));
// color + cell value = % of expected runs that are done; null when nothing's expected
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);
});
}
}
// Per-run details (steps/final + task media) are heavy and only the Results tab
// needs them, so they live in a separate details.js loaded on demand. The charts
// paint immediately from the lightweight data.js; details.js is merged in by index.
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; // Results pane renders (and pulls details.js) on first open
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();
// warm the details cache shortly after first paint so Results is instant when opened
setTimeout(() => ensureDetails(() => { if (resOpen) renderResults(); }), 300);
// --- top-level tabs: Overview / Coverage / Results ---
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));
// (re)draw the now-visible Plotly charts so they size to the real width
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)));
// --- configuration sidebar (gear) ---
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); });
// --- theme: manual toggle + OS-change follow ---
// `manualTheme` is set once the user clicks the toggle (or the Space pins
// ?__theme=), after which we stop following the OS preference.
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(); // re-render so Plotly picks up theme colors
}
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>