monaco-dev-viewer / viewer.js
timchen0618's picture
Monaco dev viewer β€” initial 12-tab launch
61b5f50 verified
Raw
History Blame Contribute Delete
54.6 kB
// MoNaCo dev viewer β€” sibling of open-wikitable-smoke-viewer, adapted for the
// 100-question monaco_dev subset. Key divergences from the smoke viewer:
//
// β€’ scoring column is LLM judge_score (not deterministic F1). Every metric
// the trajectory / compare cards expose comes from the AML judge output
// (parsed.judge_score / precision / recall).
// β€’ dataset tab renders the monaco raw schema (question_decomposition +
// steps + docs[] β€” no SQL).
// β€’ 3 e2e runs (v0, v2, v3) instead of 2.
// β€’ sidebar has NO subset/pill filters β€” just the 100 qids.
//
// View modes:
// - "dataset" β€” source monaco raw record per qid
// - "unified" β€” unified eval bundle
// - "trajectories" β€” standalone agentic Β· structure per q (cell4_agentic_a)
// - "trajectories_corpus" β€” standalone agentic Β· structure per ds (cell5_agentic_b)
// - "trajectories_rawtext" β€” standalone agentic Β· DCI (cell6_agentic_c v2)
// - "e2e_v0" / "e2e_v2" / "e2e_v3" β€” e2e_pipeline runs (agent trajectories)
// - "e2e_structures_v0/2/3" β€” per-qid supporting doc + generated structures
// - "compare" β€” side-by-side grid across all 9 configs
//
// State machine: load index.json β†’ render sidebar β†’ on click/nav fetch
// records/<qid>.json and render question + tables / trajectory / compare grid.
(function () {
'use strict';
const MODES = {
dataset: {
indexUrl: 'index.json',
recordsDir: 'records',
topbarSub: 'dev subset Β· 100 questions',
indexShape: 'array',
},
unified: {
indexUrl: 'unified/index.json',
recordsDir: 'unified/records',
topbarSub: 'unified eval bundle Β· dev 100-qid subset',
indexShape: 'array',
},
trajectories: {
indexUrl: 'trajectories/index.json',
recordsDir: 'trajectories/records',
indexShape: 'wrapped',
},
trajectories_corpus: {
indexUrl: 'trajectories_corpus/index.json',
recordsDir: 'trajectories_corpus/records',
indexShape: 'wrapped',
},
trajectories_rawtext: {
indexUrl: 'trajectories_rawtext/index.json',
recordsDir: 'trajectories_rawtext/records',
indexShape: 'wrapped',
},
e2e_v0: {
indexUrl: 'trajectories_e2e_v0/index.json',
recordsDir: 'trajectories_e2e_v0/records',
indexShape: 'wrapped',
},
e2e_v2: {
indexUrl: 'trajectories_e2e_v2/index.json',
recordsDir: 'trajectories_e2e_v2/records',
indexShape: 'wrapped',
},
e2e_v3: {
indexUrl: 'trajectories_e2e_v3/index.json',
recordsDir: 'trajectories_e2e_v3/records',
indexShape: 'wrapped',
},
e2e_structures_v0: {
indexUrl: 'e2e_structures_v0/index.json',
recordsDir: 'e2e_structures_v0/records',
indexShape: 'wrapped',
},
e2e_structures_v2: {
indexUrl: 'e2e_structures_v2/index.json',
recordsDir: 'e2e_structures_v2/records',
indexShape: 'wrapped',
},
e2e_structures_v3: {
indexUrl: 'e2e_structures_v3/index.json',
recordsDir: 'e2e_structures_v3/records',
indexShape: 'wrapped',
},
compare: {
indexUrl: 'compare/index.json',
recordsDir: 'compare/records',
indexShape: 'wrapped',
},
};
const modeState = Object.fromEntries(
Object.keys(MODES).map((k) => [k, { index: [], filtered: [], selectedQid: null, cache: new Map(), meta: null }])
);
const state = {
viewMode: 'dataset',
search: '',
stopReasonFilter: 'all',
scoreFilter: 'all',
compareDisagreeFilter: 'all',
activeBucket: 'supporting',
};
function curMode() { return modeState[state.viewMode]; }
// Trajectory-shape modes share the same index schema
// (qid, question, judge_score, correct, has_error, stop_reason, n_turns).
const TRAJ_MODES = new Set([
'trajectories', 'trajectories_corpus', 'trajectories_rawtext',
'e2e_v0', 'e2e_v2', 'e2e_v3',
]);
const E2E_STRUCT_MODES = new Set(['e2e_structures_v0', 'e2e_structures_v2', 'e2e_structures_v3']);
// ── DOM ────────────────────────────────────────────────────────────
const dom = {
list: document.getElementById('qidList'),
main: document.getElementById('mainPane'),
search: document.getElementById('searchBox'),
stopFilter: document.getElementById('stopReasonFilter'),
scoreFilter: document.getElementById('scoreFilter'),
compareDisagreeFilter: document.getElementById('compareDisagreeFilter'),
e2eMeta: document.getElementById('e2eMeta'),
viewModeRow: document.getElementById('viewModeRow'),
topbarSub: document.getElementById('topbarSub'),
prev: document.getElementById('prevBtn'),
next: document.getElementById('nextBtn'),
counter: document.getElementById('counter'),
};
// ── Utils ──────────────────────────────────────────────────────────
const escapeHtml = (s) =>
String(s ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const debounce = (fn, ms) => {
let t;
return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
};
const num = (x) => (x === null || x === undefined ? 'β€”' : String(x));
const fmtJson = (o) => {
if (o === null || o === undefined) return '';
try { return JSON.stringify(o, null, 2); } catch (e) { return String(o); }
};
function scoreClass(judgeScore, correct) {
if (correct) return 'good';
const j = judgeScore || 0;
if (j >= 0.5) return 'mid';
if (j > 0) return 'low';
return 'zero';
}
function renderMarkdown(src) {
if (typeof window.marked !== 'undefined' && window.marked && window.marked.parse) {
try {
return `<div class="md-body">${window.marked.parse(String(src ?? ''))}</div>`;
} catch (e) {
// fall through
}
}
return `<pre class="md-fallback">${escapeHtml(src ?? '')}</pre>`;
}
// ── Filtering ──────────────────────────────────────────────────────
function recomputeFiltered() {
if (TRAJ_MODES.has(state.viewMode)) return recomputeFilteredE2e();
if (state.viewMode === 'compare') return recomputeFilteredCompare();
const m = curMode();
const q = state.search.trim().toLowerCase();
const filtered = [];
for (let i = 0; i < m.index.length; i++) {
const e = m.index[i];
if (q) {
const hay = (e.qid + ' ' + e.question).toLowerCase();
if (!hay.includes(q)) continue;
}
filtered.push(i);
}
m.filtered = filtered;
}
function recomputeFilteredE2e() {
const m = curMode();
const q = state.search.trim().toLowerCase();
const stop = state.stopReasonFilter;
const score = state.scoreFilter;
const filtered = [];
for (let i = 0; i < m.index.length; i++) {
const e = m.index[i];
if (stop !== 'all' && e.stop_reason !== stop) continue;
const j = e.judge_score || 0;
if (score === 'correct' && !e.correct) continue;
if (score === 'partial' && !(j > 0 && !e.correct)) continue;
if (score === 'zero' && !(j === 0)) continue;
if (q) {
const hay = (e.qid + ' ' + e.question).toLowerCase();
if (!hay.includes(q)) continue;
}
filtered.push(i);
}
m.filtered = filtered;
}
function recomputeFilteredCompare() {
const m = modeState.compare;
const q = state.search.trim().toLowerCase();
const dis = state.compareDisagreeFilter;
const filtered = [];
for (let i = 0; i < m.index.length; i++) {
const e = m.index[i];
if (dis !== 'all') {
const cells = e.cells || [];
const allHigh = cells.length > 0 && cells.every((c) => c.correct === 1);
const allZero = cells.length > 0 && cells.every((c) => (c.judge_score || 0) === 0);
if (dis === 'agree_high' && !allHigh) continue;
if (dis === 'agree_zero' && !allZero) continue;
if (dis === 'disagree' && (allHigh || allZero)) continue;
}
if (q) {
const hay = (e.qid + ' ' + e.question).toLowerCase();
if (!hay.includes(q)) continue;
}
filtered.push(i);
}
m.filtered = filtered;
}
// ── Sidebar render ─────────────────────────────────────────────────
function renderSidebar() {
if (TRAJ_MODES.has(state.viewMode)) return renderSidebarE2e();
if (state.viewMode === 'unified') return renderSidebarUnified();
if (state.viewMode === 'compare') return renderSidebarCompare();
if (E2E_STRUCT_MODES.has(state.viewMode)) return renderSidebarE2eStructures();
return renderSidebarDataset();
}
function renderSidebarDataset() {
const m = modeState.dataset;
const html = [];
for (const i of m.filtered) {
const e = m.index[i];
const sel = e.qid === m.selectedQid ? ' selected' : '';
html.push(
`<li class="qid-row${sel}" data-qid="${escapeHtml(e.qid)}">` +
`<span class="qid-id">${escapeHtml(e.qid)}</span>` +
`<span class="qid-question">${escapeHtml(e.question)}</span>` +
`<span class="qid-row-meta">` +
`<span class="meta-mini">docs ${num(e.n_docs)}</span>` +
`<span class="meta-mini">ans ${num(e.n_answers)}</span>` +
`</span>` +
`</li>`
);
}
dom.list.innerHTML = html.join('');
updateCounter();
}
function renderSidebarUnified() {
const m = modeState.unified;
const html = [];
for (const i of m.filtered) {
const e = m.index[i];
const sel = e.qid === m.selectedQid ? ' selected' : '';
html.push(
`<li class="qid-row${sel}" data-qid="${escapeHtml(e.qid)}">` +
`<span class="qid-id">${escapeHtml(e.qid)}</span>` +
`<span class="qid-question">${escapeHtml(e.question)}</span>` +
`<span class="qid-row-meta">` +
`<span class="meta-mini">docs ${num(e.n_docs)}</span>` +
`<span class="meta-mini">ans ${num(e.n_answers)}</span>` +
`</span>` +
`</li>`
);
}
dom.list.innerHTML = html.join('');
updateCounter();
}
// Trajectory sidebar: judge_score pill + correct tick + stop + turns.
function renderSidebarE2e() {
const m = curMode();
const html = [];
for (const i of m.filtered) {
const e = m.index[i];
const sel = e.qid === m.selectedQid ? ' selected' : '';
const j = e.judge_score || 0;
const cls = scoreClass(j, e.correct);
const correctPill = e.correct ? `<span class="metric-pill em">βœ“</span>` : '';
const errPill = e.has_error ? `<span class="metric-pill err">ERR</span>` : '';
const stop = e.stop_reason || '';
const stopPill = stop ? `<span class="stop-pill stop-${stop}">${escapeHtml(stop)}</span>` : '';
html.push(
`<li class="qid-row${sel}" data-qid="${escapeHtml(e.qid)}">` +
`<span class="qid-id">` +
`<span class="metric-pill f1 ${cls}" title="LLM judge_score">${(j * 100).toFixed(0)}</span>` +
correctPill + errPill +
`<span class="qid-monoid">${escapeHtml(e.qid)}</span>` +
`</span>` +
`<span class="qid-question">${escapeHtml(e.question)}</span>` +
`<span class="qid-row-meta">` +
stopPill +
(e.n_turns != null ? `<span class="meta-mini">turns ${num(e.n_turns)}</span>` : '') +
`<span class="meta-mini">gold ${num(e.gold_answers_length)}</span>` +
`</span>` +
`</li>`
);
}
dom.list.innerHTML = html.join('');
updateCounter();
}
// Compare sidebar: sparkline of judge-score buckets per cell.
function renderSidebarCompare() {
const m = modeState.compare;
const html = [];
for (const i of m.filtered) {
const e = m.index[i];
const sel = e.qid === m.selectedQid ? ' selected' : '';
const sparks = (e.cells || []).map((c) => {
const cls = scoreClass(c.judge_score, c.correct);
return `<span class="compare-spark ${cls}" title="${escapeHtml(c.label)} Β· J ${((c.judge_score || 0) * 100).toFixed(0)}${c.correct ? ' Β· βœ“' : ''}"></span>`;
}).join('');
html.push(
`<li class="qid-row${sel}" data-qid="${escapeHtml(e.qid)}">` +
`<span class="qid-id"><span class="qid-monoid">${escapeHtml(e.qid)}</span></span>` +
`<span class="qid-question">${escapeHtml(e.question)}</span>` +
`<span class="qid-row-meta">` +
`<span class="compare-spark-row">${sparks}</span>` +
`<span class="meta-mini">gold ${num(e.gold_answers_length)}</span>` +
`</span>` +
`</li>`
);
}
dom.list.innerHTML = html.join('');
updateCounter();
}
function renderSidebarE2eStructures() {
const m = curMode();
const html = [];
for (const i of m.filtered) {
const e = m.index[i];
const sel = e.qid === m.selectedQid ? ' selected' : '';
html.push(
`<li class="qid-row${sel}" data-qid="${escapeHtml(e.qid)}">` +
`<span class="qid-id"><span class="qid-monoid">${escapeHtml(e.qid)}</span></span>` +
`<span class="qid-question">${escapeHtml(e.question)}</span>` +
`<span class="qid-row-meta">` +
`<span class="meta-mini">docs ${num(e.n_docs)}</span>` +
`<span class="meta-mini">struct ${num(e.n_structures)}</span>` +
`</span>` +
`</li>`
);
}
dom.list.innerHTML = html.join('');
updateCounter();
}
function updateCounter() {
const m = curMode();
const total = m.index.length;
const shown = m.filtered.length;
const sel = m.selectedQid;
let pos = 0;
if (sel) {
const idx = m.filtered.findIndex((i) => m.index[i].qid === sel);
pos = idx >= 0 ? idx + 1 : 0;
}
if (pos) {
dom.counter.textContent = `${pos} / ${shown}` + (shown !== total ? ` (of ${total})` : '');
} else {
dom.counter.textContent = `${shown}` + (shown !== total ? ` of ${total}` : '');
}
dom.prev.disabled = !sel || pos <= 1;
dom.next.disabled = !sel || pos >= shown;
}
function scrollSelectedIntoView() {
const node = dom.list.querySelector('li.selected');
if (node && typeof node.scrollIntoView === 'function') {
node.scrollIntoView({ block: 'nearest' });
}
}
// ── Record fetching ───────────────────────────────────────────────
async function loadRecord(qid) {
const m = curMode();
if (m.cache.has(qid)) return m.cache.get(qid);
const dir = MODES[state.viewMode].recordsDir;
const res = await fetch(`${dir}/${encodeURIComponent(qid)}.json`);
if (!res.ok) throw new Error(`fetch ${dir}/${qid}.json failed: ${res.status}`);
const rec = await res.json();
m.cache.set(qid, rec);
return rec;
}
// ── Main pane render ──────────────────────────────────────────────
function renderEmpty() {
let msg;
switch (state.viewMode) {
case 'unified': msg = 'Select a question on the left to inspect its unified-bundle gold answers + supporting docs.'; break;
case 'trajectories': msg = 'Select a question on the left to inspect the structure-per-q (Baseline A / cell 4) agent trajectory.'; break;
case 'trajectories_corpus': msg = 'Select a question on the left to inspect the structure-per-ds (Baseline B / cell 5) agent trajectory.'; break;
case 'trajectories_rawtext': msg = 'Select a question on the left to inspect the DCI (Baseline C / cell 6) agent trajectory.'; break;
case 'e2e_v0': msg = 'Select a question on the left to inspect the e2e v0 pipeline trajectory (per-qid LLM judge_score).'; break;
case 'e2e_v2': msg = 'Select a question on the left to inspect the e2e v2 pipeline trajectory (per-qid LLM judge_score).'; break;
case 'e2e_v3': msg = 'Select a question on the left to inspect the e2e v3 pipeline trajectory (per-qid LLM judge_score).'; break;
case 'e2e_structures_v0': msg = 'Select a question on the left to inspect its supporting doc(s) and the e2e v0 pipeline-generated structures.'; break;
case 'e2e_structures_v2': msg = 'Select a question on the left to inspect its supporting doc(s) and the e2e v2 pipeline-generated structures.'; break;
case 'e2e_structures_v3': msg = 'Select a question on the left to inspect its supporting doc(s) and the e2e v3 pipeline-generated structures.'; break;
case 'compare': msg = 'Select a question on the left to see all 9 configs side-by-side (closed-book / with-docs / with-structures / structure per q / structure per ds / DCI / e2e v0 / e2e v2 / e2e v3).'; break;
default: msg = 'Select a question on the left to begin.';
}
dom.main.innerHTML = `<div class="placeholder">${msg}</div>`;
}
function renderLoading() {
dom.main.innerHTML = `<div class="placeholder">Loading record…</div>`;
}
function pickInitialBucket(rec) {
return 'supporting';
}
function renderRecord(rec) {
if (TRAJ_MODES.has(state.viewMode)) return renderTrajectoryRecord(rec);
if (state.viewMode === 'compare') return renderCompareRecord(rec);
if (E2E_STRUCT_MODES.has(state.viewMode)) return renderE2eStructuresRecord(rec);
return state.viewMode === 'unified' ? renderUnifiedRecord(rec) : renderDatasetRecord(rec);
}
// ── Dataset rendering (monaco raw schema) ─────────────────────────
function renderDatasetRecord(rec) {
state.activeBucket = pickInitialBucket(rec);
const answers = Array.isArray(rec.answers) ? rec.answers : (rec.answers ? [rec.answers] : []);
const answersHtml = answers.length
? answers.map((a) => `<span class="answer-pill">${escapeHtml(String(a))}</span>`).join('')
: `<span class="answer-pill empty">(empty)</span>`;
const supDocs = rec.docs || [];
const docsHtml = supDocs.length
? supDocs.map((d, i) => renderMonacoRawDoc(d, i + 1)).join('')
: `<div class="empty-bucket">No supporting doc recorded for this question.</div>`;
const html = `
<div class="record-header">
<div class="record-pills">
${rec.dataset ? `<span class="pill dataset ${escapeHtml(rec.dataset)}">${escapeHtml(rec.dataset)}</span>` : ''}
<span class="pill mono" title="qid (ex_num)">${escapeHtml(rec.qid)}</span>
${rec.ex_num != null ? `<span class="pill mono" title="ex_num">ex_num=${rec.ex_num}</span>` : ''}
<button class="copy-btn" id="copyQidBtn" title="Copy qid">πŸ“‹ copy qid</button>
</div>
<div class="question-text">${escapeHtml(rec.question)}</div>
<div class="answer-row">
<span class="answer-label">Gold answer${answers.length > 1 ? 's' : ''} (${answers.length})</span>
${answersHtml}
</div>
</div>
<div class="section">
<h3 class="section-title">Supporting documents <span class="section-sub">(${supDocs.length} β€” from monaco raw <code>docs[]</code>; every entry is <code>is_supporting=true</code>)</span></h3>
${docsHtml}
</div>
`;
dom.main.innerHTML = html;
wireCopyBtn(rec.qid);
}
function renderMonacoRawDoc(d, idx) {
const subq = d.sub_question_text ? `<div class="unified-doc-head" style="margin-top:6px"><span class="meta-mini" style="max-width:none;white-space:normal">↳ ${escapeHtml(d.sub_question_text)}</span></div>` : '';
return `
<div class="chunk-card hard_positive">
<div class="chunk-head">
<div class="chunk-bread">
<span class="crumb-page">${escapeHtml(d.title || '(untitled)')}</span>
${d.section ? `<span class="crumb-sep">β€Ί</span><span class="crumb-section">${escapeHtml(d.section)}</span>` : ''}
</div>
<div class="chunk-meta">
<span class="pill mono" title="doc id (matches unified eval doc.id)">πŸ“„ doc ${escapeHtml(String(d.id))}</span>
<span>${(d.text || '').length} chars</span>
</div>
</div>
${subq}
<div class="chunk-table-wrap">
${renderMarkdown(d.text || '')}
</div>
</div>
`;
}
// ── Unified rendering ─────────────────────────────────────────────
function renderUnifiedRecord(rec) {
const answers = Array.isArray(rec.answers) ? rec.answers : (rec.answers ? [rec.answers] : []);
const answersHtml = answers.length
? answers.map((a) => `<span class="answer-pill">${escapeHtml(String(a))}</span>`).join('')
: `<span class="answer-pill empty">(empty)</span>`;
const docs = rec.docs || [];
const docsHtml = docs.length
? docs.map((d, i) => `
<div class="unified-doc">
<div class="unified-doc-head">
<span class="unified-doc-num">[Doc ${i + 1}]</span>
<span class="pill mono" title="doc id">id=${escapeHtml(String(d.id ?? ''))}</span>
</div>
${renderMarkdown(d.contents || '')}
</div>
`).join('')
: `<div class="empty-bucket">No gold supporting docs.</div>`;
const html = `
<div class="record-header">
<div class="record-pills">
<span class="pill mono" title="qid">${escapeHtml(rec.qid)}</span>
${rec.dataset ? `<span class="pill dataset ${escapeHtml(rec.dataset)}">${escapeHtml(rec.dataset)}</span>` : ''}
<button class="copy-btn" id="copyQidBtn" title="Copy qid">πŸ“‹ copy qid</button>
</div>
<div class="question-text">${escapeHtml(rec.question)}</div>
<div class="answer-row">
<span class="answer-label">Gold answer${answers.length > 1 ? 's' : ''} (${answers.length})</span>
${answersHtml}
</div>
</div>
<div class="section">
<h3 class="section-title">Gold Supporting Documents <span class="section-sub">(${docs.length})</span></h3>
${docsHtml}
</div>
`;
dom.main.innerHTML = html;
wireCopyBtn(rec.qid);
}
function wireCopyBtn(qid) {
const copyBtn = document.getElementById('copyQidBtn');
if (!copyBtn) return;
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(qid).then(() => {
copyBtn.textContent = 'βœ“ copied';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = 'πŸ“‹ copy qid';
copyBtn.classList.remove('copied');
}, 1200);
});
});
}
// ── Trajectory rendering (LLM judge scores) ────────────────────────
function pill(label, cls) {
return `<span class="pill ${cls || ''}">${label}</span>`;
}
function renderTrajectoryRecord(rec) {
const m = rec.metrics || {};
const j = m.judge_score ?? 0;
const correct = m.correct ?? 0;
const cls = scoreClass(j, correct);
const goldHtml = (rec.gold_answers || []).map(
(a) => `<span class="answer-pill">${escapeHtml(String(a))}</span>`
).join('') || `<span class="answer-pill empty">(empty)</span>`;
const predList = rec.pred_items || [];
const predHtml = predList.length
? predList.map((a) => `<span class="answer-pill pred">${escapeHtml(String(a))}</span>`).join('')
: `<span class="answer-pill empty">(no extracted final answer)</span>`;
const errBlock = rec.error
? `<div class="error-banner"><strong>Error:</strong> ${escapeHtml(rec.error.slice(0, 1200))}${rec.error.length > 1200 ? '…' : ''}</div>`
: '';
const tokens = rec.tokens || {};
const metaPills = [
rec.n_turns != null ? pill(`turns ${num(rec.n_turns)}${rec.max_turns ? '/' + rec.max_turns : ''}`, 'meta') : '',
rec.stop_reason ? pill(`stop=${escapeHtml(rec.stop_reason)}`, `meta stop-${rec.stop_reason}`) : '',
tokens.total_tokens != null ? pill(`tokens ${tokens.total_tokens}`, 'meta') : '',
rec.latency_ms != null ? pill(`${(rec.latency_ms / 1000).toFixed(1)}s`, 'meta') : '',
rec.model ? pill(escapeHtml(rec.model), 'meta mono') : '',
rec.mode ? pill(`mode=${escapeHtml(rec.mode)}`, 'meta mono') : '',
].filter(Boolean).join('');
const extractedRow = m.extracted_final_answer
? `<div class="answer-row"><span class="answer-label">Judge-extracted</span><span class="answer-pill pred">${escapeHtml(String(m.extracted_final_answer))}</span></div>`
: '';
const trajSection = (rec.events && rec.events.length)
? `<div class="section">
<h3 class="section-title">Trajectory <span class="section-sub">(${rec.events.length} events Β· ${escapeHtml(rec.system_prompt_file || '')})</span></h3>
<div class="trajectory">${renderEvents(rec.events)}</div>
</div>`
: (rec.user_prompt ? `<div class="section">
<h3 class="section-title">Prompt <span class="section-sub">(single-turn β€” no trajectory)</span></h3>
<details class="compare-sql-details"><summary>system prompt (${(rec.system_prompt || '').length} chars)</summary><pre class="raw-block">${escapeHtml(rec.system_prompt || '')}</pre></details>
<details class="compare-sql-details" open><summary>user prompt (${(rec.user_prompt || '').length} chars)</summary><pre class="raw-block">${escapeHtml((rec.user_prompt || '').slice(0, 8000))}${(rec.user_prompt || '').length > 8000 ? '\n\n… (truncated)' : ''}</pre></details>
</div>` : '');
const html = `
<div class="record-header e2e">
<div class="record-pills">
<span class="pill dataset monaco">${escapeHtml(rec.dataset_origin || rec.dataset || 'monaco_dev')}</span>
<span class="pill mono" title="qid">${escapeHtml(rec.qid)}</span>
<button class="copy-btn" id="copyQidBtn" title="Copy qid">πŸ“‹ copy qid</button>
</div>
<div class="question-text">${escapeHtml(rec.question)}</div>
<div class="metrics-row">
<span class="metric-badge f1 ${cls}" title="LLM judge_score β€” --deterministic-extract mirror of canonical monaco protocol">J: ${(j * 100).toFixed(1)}</span>
<span class="metric-badge ${correct ? 'good' : 'zero'}" title="1 iff judge_score β‰₯ 0.99">correct: ${correct}</span>
<span class="metric-badge mid" title="judge precision (list-based)">P: ${((m.precision ?? 0) * 100).toFixed(1)}</span>
<span class="metric-badge mid" title="judge recall (list-based)">R: ${((m.recall ?? 0) * 100).toFixed(1)}</span>
<span class="metric-badge sep">|P|=${predList.length} Β· |G|=${(rec.gold_answers || []).length}</span>
</div>
<div class="answer-row">
<span class="answer-label">Gold (${(rec.gold_answers || []).length})</span>
${goldHtml}
</div>
<div class="answer-row">
<span class="answer-label">Model answer items (${predList.length})</span>
${predHtml}
</div>
${extractedRow}
<div class="meta-row">${metaPills}</div>
${errBlock}
</div>
<div class="section">
<h3 class="section-title">Model's full answer <span class="section-sub">(raw text before judge extraction)</span></h3>
<pre class="raw-block">${escapeHtml(rec.model_answer || '(empty)')}</pre>
</div>
${trajSection}
`;
dom.main.innerHTML = html;
wireCopyBtn(rec.qid);
}
function renderEvents(events) {
if (!events.length) return `<div class="empty-bucket">No events recorded.</div>`;
const parts = [];
for (let i = 0; i < events.length; i++) {
const ev = events[i];
if (ev.type === 'tool_call') {
const next = events[i + 1];
const paired = next && next.type === 'tool_result' ? next : null;
parts.push(renderToolPair(ev, paired, i));
if (paired) i += 1;
continue;
}
if (ev.type === 'tool_result') {
parts.push(renderToolPair(null, ev, i));
continue;
}
if (ev.type === 'text') {
parts.push(`<div class="ev ev-text"><div class="ev-head">πŸ’¬ assistant text</div><pre class="ev-body">${escapeHtml(ev.content || '')}</pre></div>`);
continue;
}
if (ev.type === 'error') {
parts.push(`<div class="ev ev-error"><div class="ev-head">⚠ error</div><pre class="ev-body">${escapeHtml(ev.content || '')}</pre></div>`);
continue;
}
if (ev.type === 'done') {
parts.push(`<div class="ev ev-done"><div class="ev-head">🏁 done · stop=${escapeHtml(ev.content || '?')}</div></div>`);
continue;
}
parts.push(`<div class="ev"><div class="ev-head">${escapeHtml(ev.type || '?')}</div><pre class="ev-body">${escapeHtml(ev.content || '')}</pre></div>`);
}
return parts.join('');
}
function renderToolPair(call, result, idx) {
const name = (call && call.name) || (result && result.name) || '?';
const callBody = call
? `<pre class="ev-body call">${escapeHtml(fmtJson(call.input))}</pre>`
: `<pre class="ev-body call empty">(no matching tool_call)</pre>`;
const trunc = result && result.truncated ? ' <span class="trunc-tag">truncated</span>' : '';
const resultBody = result
? `<pre class="ev-body result">${escapeHtml(result.content || '')}</pre>`
: `<pre class="ev-body result empty">(no matching tool_result)</pre>`;
return (
`<div class="ev ev-tool">` +
`<div class="ev-head">πŸ”§ ${escapeHtml(name)}${trunc}</div>` +
`<div class="ev-cols">` +
`<div class="ev-col"><div class="ev-col-label">input</div>${callBody}</div>` +
`<div class="ev-col"><div class="ev-col-label">output</div>${resultBody}</div>` +
`</div>` +
`</div>`
);
}
function renderE2eMeta(meta) {
if (!meta) return '';
if (meta.n_structures_total != null) return renderE2eStructMeta(meta);
if (meta.configs && meta.mean_metrics) {
const cells = meta.configs.map((c) => {
const mm = meta.mean_metrics[c] || {};
const j = ((mm.mean_judge_score || 0) * 100).toFixed(1);
const cor = ((mm.mean_correct || 0) * 100).toFixed(0);
return `<span class="meta-stat" title="${escapeHtml(c)} Β· correct ${cor}">${escapeHtml(c)}: <strong>${j}</strong></span>`;
}).join('');
return `
<div class="e2e-meta-row"><span class="meta-label">Compare</span> <span class="meta-value mono">${meta.n} qids Β· ${(meta.configs || []).length} configs</span></div>
<div class="e2e-meta-row">${cells}</div>
`;
}
const stops = Object.entries(meta.stop_reason_counts || {})
.map(([k, v]) => `<span class="meta-stat">${escapeHtml(k)}: <strong>${v}</strong></span>`)
.join('');
return `
<div class="e2e-meta-row"><span class="meta-label">Run</span> <span class="meta-value mono">${escapeHtml(meta.label || '')}</span></div>
<div class="e2e-meta-row">
<span class="meta-stat">n: <strong>${meta.n}</strong></span>
<span class="meta-stat">J: <strong>${((meta.mean_judge_score || 0) * 100).toFixed(2)}</strong></span>
<span class="meta-stat">βœ“: <strong>${((meta.mean_correct || 0) * 100).toFixed(2)}</strong></span>
<span class="meta-stat">P: <strong>${((meta.mean_precision || 0) * 100).toFixed(2)}</strong></span>
<span class="meta-stat">R: <strong>${((meta.mean_recall || 0) * 100).toFixed(2)}</strong></span>
<span class="meta-stat">err: <strong>${((meta.fraction_error || 0) * 100).toFixed(2)}%</strong></span>
</div>
<div class="e2e-meta-row">${stops}</div>
`;
}
function metaSubtitle(meta) {
if (!meta) return '';
if (meta.n_structures_total != null) {
return `e2e structures Β· ${meta.n_qids} questions Β· ${meta.n_docs_unique} unique docs Β· ${meta.n_structures_total} structure files`;
}
if (meta.configs && meta.mean_metrics) {
const cells = meta.configs.map((c) => {
const mm = meta.mean_metrics[c] || {};
return `${c} J ${((mm.mean_judge_score || 0) * 100).toFixed(1)}`;
}).join(' Β· ');
return `${meta.n} questions Β· ${cells}`;
}
return `${meta.label || 'agentic run'} Β· ${meta.n} questions Β· J ${((meta.mean_judge_score || 0) * 100).toFixed(2)} Β· βœ“ ${((meta.mean_correct || 0) * 100).toFixed(2)}`;
}
// ── Compare record (side-by-side grid of 9 cells) ──────────────────
function renderCompareRecord(rec) {
const configsOrder = rec.configs_order || Object.keys(rec.configs || {});
const configs = rec.configs || {};
const gold = rec.gold_answers || [];
const goldHtml = gold.length
? gold.map((g) => `<span class="answer-pill">${escapeHtml(String(g))}</span>`).join('')
: `<span class="answer-pill empty">(empty)</span>`;
let bestJ = -1;
for (const k of configsOrder) {
const c = configs[k];
const j = (c && c.metrics && c.metrics.judge_score) || 0;
if (j > bestJ) bestJ = j;
}
const cardHtml = configsOrder.map((label) => {
const c = configs[label];
if (!c) {
return `<div class="compare-card"><div class="compare-card-hdr"><span class="compare-card-label">${escapeHtml(label)}</span></div><div class="compare-card-body empty">No response</div></div>`;
}
const m = c.metrics || {};
const j = m.judge_score || 0;
const correct = m.correct || 0;
const cls = scoreClass(j, correct);
const isWinner = bestJ > 0 && j === bestJ;
const winnerCls = isWinner ? ' is-winner' : '';
const answer = c.answer || '';
const answerHtml = answer
? `<pre class="compare-card-answer">${escapeHtml(answer.length > 1500 ? answer.slice(0, 1500) + '…' : answer)}</pre>`
: `<div class="compare-card-body empty">(empty)</div>`;
const predItems = c.pred_items || [];
const predPills = predItems.length
? predItems.map((p) => `<span class="answer-pill answer-pill-pred">${escapeHtml(String(p))}</span>`).join('')
: `<span class="answer-pill empty">(no extracted final answer)</span>`;
const footerPills = [];
if (c.mode === 'agentic') {
if (c.stop_reason) footerPills.push(`<span class="meta-mini stop-pill stop-${escapeHtml(c.stop_reason)}">stop=${escapeHtml(c.stop_reason)}</span>`);
if (c.n_turns != null) footerPills.push(`<span class="meta-mini">turns ${c.n_turns}/${num(c.max_turns)}</span>`);
const tc = c.tool_call_counts || {};
const tcKeys = Object.keys(tc);
if (tcKeys.length) {
const txt = tcKeys.map((k) => `${k}:${tc[k]}`).join(' Β· ');
footerPills.push(`<span class="meta-mini" title="tool_call_counts">${escapeHtml(txt)}</span>`);
}
const tk = c.tokens || {};
if (tk.total_tokens != null) footerPills.push(`<span class="meta-mini">${tk.total_tokens} tok</span>`);
} else {
if (c.finish_reason) footerPills.push(`<span class="meta-mini">finish=${escapeHtml(c.finish_reason)}</span>`);
const u = c.usage || {};
if (u.total_tokens != null) footerPills.push(`<span class="meta-mini">${u.total_tokens} tok</span>`);
}
if (c.latency_ms != null) footerPills.push(`<span class="meta-mini">${(c.latency_ms / 1000).toFixed(1)}s</span>`);
if (c.model) footerPills.push(`<span class="meta-mini mono">${escapeHtml(c.model)}</span>`);
return `
<div class="compare-card${winnerCls}">
<div class="compare-card-hdr">
<span class="compare-card-label">${escapeHtml(label)}${isWinner ? ' <span class="compare-winner-pill" title="best judge_score on this question">πŸ†</span>' : ''}</span>
<span class="compare-card-metrics">
<span class="metric-badge f1 ${cls}" title="LLM judge_score">J ${(j * 100).toFixed(0)}</span>
<span class="metric-badge ${correct ? 'good' : 'zero'}" title="correct = judge_score β‰₯ 0.99">βœ“ ${correct}</span>
<span class="metric-badge mid" title="judge precision">P ${((m.precision || 0) * 100).toFixed(0)}</span>
<span class="metric-badge mid" title="judge recall">R ${((m.recall || 0) * 100).toFixed(0)}</span>
</span>
</div>
<div class="compare-card-preds">
<span class="compare-card-sublabel">Extracted (${predItems.length}):</span>
${predPills}
</div>
${answerHtml}
<div class="compare-card-footer">${footerPills.join('')}</div>
</div>
`;
}).join('');
const html = `
<div class="record-header e2e">
<div class="record-pills">
<span class="pill dataset monaco">monaco_dev</span>
<span class="pill mono" title="qid">${escapeHtml(rec.qid)}</span>
<button class="copy-btn" id="copyQidBtn" title="Copy qid">πŸ“‹ copy qid</button>
</div>
<div class="question-text">${escapeHtml(rec.question)}</div>
<div class="answer-row">
<span class="answer-label">Gold (${gold.length})</span>
${goldHtml}
</div>
</div>
<div class="section">
<h3 class="section-title">All ${configsOrder.length} cells <span class="section-sub">side-by-side Β· LLM judge_score / correct / P / R (canonical --deterministic-extract judge)</span></h3>
<div class="compare-grid">${cardHtml}</div>
</div>
`;
dom.main.innerHTML = html;
wireCopyBtn(rec.qid);
}
// ── e2e structures record (supporting docs + per-shape structure files) ─
const SHAPE_ICON = {
tabular_records: 'πŸ“Š',
chronology_and_timeline_indexes: 'πŸ•’',
claim_and_theme_summaries: 'πŸ’¬',
qa_shortcuts_and_templates: '❓',
relation_graphs_and_mappings: 'πŸ•Έ',
};
function renderE2eStructMeta(meta) {
if (!meta) return '';
const shapeRows = (meta.shapes || []).map((s) => {
const desc = (meta.shape_descriptions || {})[s] || '';
const icon = SHAPE_ICON[s] || '🧩';
return `<div class="e2e-meta-row"><span class="meta-label mono">${icon} ${escapeHtml(s)}</span> <span class="meta-stat" title="${escapeHtml(desc)}">${escapeHtml(desc.slice(0, 80))}${desc.length > 80 ? '…' : ''}</span></div>`;
}).join('');
return `
<div class="e2e-meta-row"><span class="meta-label">E2E Structures</span> <span class="meta-value mono">${escapeHtml(meta.label || '')}</span></div>
<div class="e2e-meta-row">
<span class="meta-stat">qids: <strong>${meta.n_qids}</strong></span>
<span class="meta-stat">unique docs: <strong>${meta.n_docs_unique}</strong></span>
<span class="meta-stat" title="files where the e2e pipeline emitted at least one shape">structures: <strong>${meta.n_structures_total}</strong></span>
<span class="meta-stat" title="avg structures per qid">avg/qid: <strong>${meta.n_structures_avg_per_qid}</strong></span>
</div>
${shapeRows}
`;
}
function renderE2eStructuresRecord(rec) {
const goldHtml = (rec.gold_answers || []).map(
(a) => `<span class="answer-pill">${escapeHtml(String(a))}</span>`
).join('') || `<span class="answer-pill empty">(empty)</span>`;
const docs = rec.docs || [];
const docsHtml = docs.length
? docs.map((d) => renderE2eStructDoc(d)).join('')
: `<div class="empty-bucket">No supporting docs recorded for this question.</div>`;
const html = `
<div class="record-header">
<div class="record-pills">
<span class="pill dataset monaco">monaco_dev</span>
<span class="pill mono" title="qid">${escapeHtml(rec.qid)}</span>
<span class="pill mono" title="number of supporting docs Β· total e2e structure files">πŸ“„ ${rec.n_docs} doc Β· πŸ— ${rec.n_structures} struct</span>
<button class="copy-btn" id="copyQidBtn" title="Copy qid">πŸ“‹ copy qid</button>
</div>
<div class="question-text">${escapeHtml(rec.question)}</div>
<div class="answer-row">
<span class="answer-label">Gold answer${(rec.gold_answers || []).length > 1 ? 's' : ''} (${(rec.gold_answers || []).length})</span>
${goldHtml}
</div>
</div>
<div class="section">
<h3 class="section-title">πŸ“„ Supporting doc${docs.length > 1 ? 's' : ''} + πŸ— E2E-pipeline structures
<span class="section-sub">(5 shapes: tabular_records, chronology_and_timeline_indexes, claim_and_theme_summaries, qa_shortcuts_and_templates, relation_graphs_and_mappings)</span>
</h3>
${docsHtml}
</div>
`;
dom.main.innerHTML = html;
wireCopyBtn(rec.qid);
}
function renderE2eStructDoc(d) {
const supportBadge = d.is_supporting
? `<span class="pill" style="background:#dcecdc;color:#1f5a32;border-color:#b9d4b9" title="This doc is in the qid's gold supporting-docs list">βœ“ supporting</span>`
: `<span class="pill" style="background:#f3eedb;color:var(--ink-mute)">distractor</span>`;
const structs = d.structures || [];
const structsHtml = structs.length
? structs.map((s) => renderE2eStructItem(s, d.doc_id)).join('')
: `<div class="empty-bucket">No e2e structures were extracted from this doc.</div>`;
return `
<div class="e2estruct-doc">
<div class="e2estruct-doc-head">
<span class="pill mono" title="doc id">πŸ“„ doc ${escapeHtml(String(d.doc_id))}</span>
${supportBadge}
<span class="pill mono" style="background:#f6e9e0;color:#7a4a1d;border-color:#e9c9a6">πŸ— ${structs.length} structure file${structs.length === 1 ? '' : 's'}</span>
</div>
<details class="e2estruct-doc-source" open>
<summary class="e2estruct-summary"><span>πŸ“– source document</span><span class="meta-mini">${(d.contents || '').length} chars</span></summary>
<div class="e2estruct-doc-body">${renderMarkdown(d.contents || '')}</div>
</details>
<div class="e2estruct-structs">${structsHtml}</div>
</div>
`;
}
function renderE2eStructItem(s, docId) {
const icon = SHAPE_ICON[s.shape_id] || '🧩';
const body = renderStructContent(s.format, s.content || '');
return `
<details class="e2estruct-card e2estruct-${escapeHtml(s.shape_id)}" open>
<summary class="e2estruct-summary">
<span>${icon} <span class="mono">${escapeHtml(s.shape_id)}</span></span>
<span class="meta-mini mono">${escapeHtml(s.file)}</span>
<span class="meta-mini">${escapeHtml(s.format)}</span>
</summary>
${s.description ? `<div class="e2estruct-desc">${escapeHtml(s.description)}</div>` : ''}
<div class="e2estruct-body">${body}</div>
</details>
`;
}
function renderStructContent(fmt, content) {
if (!content) return `<div class="empty-bucket">(empty file)</div>`;
if (fmt === 'csv') return renderCsvAsTable(content);
if (fmt === 'jsonl') return renderJsonl(content);
if (fmt === 'json') {
try {
const parsed = JSON.parse(content);
return `<pre class="e2estruct-pre">${escapeHtml(JSON.stringify(parsed, null, 2))}</pre>`;
} catch (e) {
return `<pre class="e2estruct-pre">${escapeHtml(content)}</pre>`;
}
}
if (fmt === 'md') return renderMarkdown(content);
return `<pre class="e2estruct-pre">${escapeHtml(content)}</pre>`;
}
function parseCsv(text) {
const rows = [];
let row = [];
let cell = '';
let inQuote = false;
let i = 0;
while (i < text.length) {
const c = text[i];
if (inQuote) {
if (c === '"') {
if (text[i + 1] === '"') { cell += '"'; i += 2; continue; }
inQuote = false; i++; continue;
}
cell += c; i++; continue;
}
if (c === '"') { inQuote = true; i++; continue; }
if (c === ',') { row.push(cell); cell = ''; i++; continue; }
if (c === '\n') { row.push(cell); rows.push(row); row = []; cell = ''; i++; continue; }
if (c === '\r') { i++; continue; }
cell += c; i++;
}
if (cell.length > 0 || row.length > 0) { row.push(cell); rows.push(row); }
return rows;
}
function renderCsvAsTable(text) {
const rows = parseCsv(text);
if (!rows.length) return `<pre class="e2estruct-pre">${escapeHtml(text)}</pre>`;
const head = rows[0];
const body = rows.slice(1);
const headHtml = head.map((h) => `<th>${escapeHtml(h)}</th>`).join('');
const bodyHtml = body
.filter((r) => r.length && !(r.length === 1 && r[0] === ''))
.map((r) => `<tr>${r.map((c) => `<td>${escapeHtml(c)}</td>`).join('')}</tr>`)
.join('');
return `
<div class="e2estruct-table-wrap">
<table class="e2estruct-table">
<thead><tr>${headHtml}</tr></thead>
<tbody>${bodyHtml}</tbody>
</table>
</div>
`;
}
function renderJsonl(text) {
const lines = text.split('\n').map((l) => l.trim()).filter(Boolean);
if (!lines.length) return `<div class="empty-bucket">(empty)</div>`;
const items = lines.map((line) => {
let parsed;
try { parsed = JSON.parse(line); } catch (e) { parsed = null; }
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const rows = Object.entries(parsed).map(([k, v]) => {
const vstr = (v === null || v === undefined) ? 'βˆ…' :
(typeof v === 'string') ? v :
JSON.stringify(v);
return `<div class="jsonl-row"><span class="jsonl-key">${escapeHtml(k)}</span><span class="jsonl-val">${escapeHtml(vstr)}</span></div>`;
}).join('');
return `<div class="jsonl-card">${rows}</div>`;
}
return `<pre class="e2estruct-pre">${escapeHtml(line)}</pre>`;
}).join('');
return `<div class="jsonl-list">${items}</div>`;
}
// ── Selection / nav ───────────────────────────────────────────────
async function selectQid(qid, pushHash = true) {
const m = curMode();
m.selectedQid = qid;
renderSidebar();
scrollSelectedIntoView();
if (pushHash) {
const h = `#${state.viewMode}:${encodeURIComponent(qid)}`;
if (window.location.hash !== h) window.history.replaceState(null, '', h);
}
renderLoading();
try {
const rec = await loadRecord(qid);
if (m.selectedQid !== qid) return;
renderRecord(rec);
} catch (e) {
dom.main.innerHTML = `<div class="placeholder">Failed to load record: ${escapeHtml(e.message)}</div>`;
}
}
function step(delta) {
const m = curMode();
if (!m.selectedQid) {
if (m.filtered.length) selectQid(m.index[m.filtered[0]].qid);
return;
}
const cur = m.filtered.findIndex((i) => m.index[i].qid === m.selectedQid);
if (cur < 0) return;
const next = cur + delta;
if (next < 0 || next >= m.filtered.length) return;
selectQid(m.index[m.filtered[next]].qid);
}
// ── Mode switching ────────────────────────────────────────────────
async function setViewMode(mode) {
if (!MODES[mode] || mode === state.viewMode) return;
state.viewMode = mode;
dom.viewModeRow.querySelectorAll('.view-mode-btn').forEach((b) => {
const on = b.getAttribute('data-view-mode') === mode;
b.classList.toggle('active', on);
b.setAttribute('aria-selected', on ? 'true' : 'false');
});
document.querySelectorAll('[data-show-in]').forEach((el) => {
const want = (el.getAttribute('data-show-in') || '').split(/\s+/).filter(Boolean);
el.hidden = !want.includes(mode);
});
renderEmpty();
await ensureIndexLoaded(mode);
dom.topbarSub.textContent = MODES[mode].topbarSub || metaSubtitle(modeState[mode].meta);
if ((TRAJ_MODES.has(mode) || mode === 'compare' || E2E_STRUCT_MODES.has(mode)) && modeState[mode].meta && dom.e2eMeta) {
dom.e2eMeta.innerHTML = renderE2eMeta(modeState[mode].meta);
}
recomputeFiltered();
renderSidebar();
const sel = curMode().selectedQid;
if (sel) {
selectQid(sel, /*pushHash=*/false);
} else if (curMode().filtered.length) {
renderEmpty();
} else {
dom.main.innerHTML = `<div class="placeholder">No items in this view.</div>`;
}
}
async function ensureIndexLoaded(mode) {
const m = modeState[mode];
if (m.index.length) return;
const url = MODES[mode].indexUrl;
const res = await fetch(url);
if (!res.ok) throw new Error(`fetch ${url} failed: ${res.status}`);
const payload = await res.json();
if (MODES[mode].indexShape === 'wrapped') {
m.meta = payload.meta || null;
m.index = payload.rows || [];
} else {
m.index = Array.isArray(payload) ? payload : (payload.rows || []);
}
}
// ── Wiring ────────────────────────────────────────────────────────
function wire() {
dom.list.addEventListener('click', (ev) => {
const li = ev.target.closest('li.qid-row');
if (li) selectQid(li.getAttribute('data-qid'));
});
dom.search.addEventListener('input', debounce(() => {
state.search = dom.search.value;
recomputeFiltered();
renderSidebar();
}, 120));
dom.viewModeRow.addEventListener('click', (ev) => {
const btn = ev.target.closest('.view-mode-btn');
if (!btn) return;
const mode = btn.getAttribute('data-view-mode');
setViewMode(mode).catch((e) => {
dom.main.innerHTML = `<div class="placeholder">Failed to switch mode: ${escapeHtml(e.message)}</div>`;
});
});
if (dom.stopFilter) {
dom.stopFilter.addEventListener('click', (ev) => {
const btn = ev.target.closest('.filter-pill');
if (!btn) return;
const v = btn.getAttribute('data-stop');
if (v === state.stopReasonFilter) return;
state.stopReasonFilter = v;
dom.stopFilter.querySelectorAll('.filter-pill').forEach((b) => b.classList.toggle('active', b === btn));
recomputeFiltered();
renderSidebar();
});
}
if (dom.scoreFilter) {
dom.scoreFilter.addEventListener('click', (ev) => {
const btn = ev.target.closest('.filter-pill');
if (!btn) return;
const v = btn.getAttribute('data-score');
if (v === state.scoreFilter) return;
state.scoreFilter = v;
dom.scoreFilter.querySelectorAll('.filter-pill').forEach((b) => b.classList.toggle('active', b === btn));
recomputeFiltered();
renderSidebar();
});
}
if (dom.compareDisagreeFilter) {
dom.compareDisagreeFilter.addEventListener('click', (ev) => {
const btn = ev.target.closest('.filter-pill');
if (!btn) return;
const v = btn.getAttribute('data-disagree');
if (v === state.compareDisagreeFilter) return;
state.compareDisagreeFilter = v;
dom.compareDisagreeFilter.querySelectorAll('.filter-pill').forEach((b) => b.classList.toggle('active', b === btn));
recomputeFiltered();
renderSidebar();
});
}
dom.prev.addEventListener('click', () => step(-1));
dom.next.addEventListener('click', () => step(+1));
document.addEventListener('keydown', (ev) => {
if (ev.target && (ev.target.tagName === 'INPUT' || ev.target.tagName === 'TEXTAREA')) {
if (ev.key === 'Escape') ev.target.blur();
return;
}
if (ev.key === 'ArrowLeft') { ev.preventDefault(); step(-1); }
else if (ev.key === 'ArrowRight') { ev.preventDefault(); step(+1); }
else if (ev.key === '/') { ev.preventDefault(); dom.search.focus(); dom.search.select(); }
});
window.addEventListener('hashchange', () => {
const parsed = parseHash(window.location.hash);
if (!parsed) return;
if (parsed.mode !== state.viewMode) {
setViewMode(parsed.mode).then(() => {
if (parsed.qid && parsed.qid !== curMode().selectedQid) selectQid(parsed.qid, /*pushHash=*/false);
});
} else if (parsed.qid && parsed.qid !== curMode().selectedQid) {
selectQid(parsed.qid, /*pushHash=*/false);
}
});
}
function parseHash(hash) {
const s = (hash || '').replace(/^#/, '');
if (!s) return null;
if (s.includes(':')) {
const [mode, ...rest] = s.split(':');
if (MODES[mode]) return { mode, qid: decodeURIComponent(rest.join(':')) };
}
return { mode: 'dataset', qid: decodeURIComponent(s) };
}
// ── Boot ──────────────────────────────────────────────────────────
async function boot() {
wire();
dom.main.innerHTML = `<div class="placeholder">Loading index…</div>`;
const parsed = parseHash(window.location.hash);
const initialMode = (parsed && MODES[parsed.mode]) ? parsed.mode : 'dataset';
try {
await ensureIndexLoaded('dataset');
} catch (e) {
dom.main.innerHTML = `<div class="placeholder">Failed to load index.json: ${escapeHtml(e.message)}</div>`;
return;
}
if (initialMode === 'dataset') {
recomputeFiltered();
renderSidebar();
} else {
try {
await setViewMode(initialMode);
} catch (e) {
dom.main.innerHTML = `<div class="placeholder">Failed to load ${escapeHtml(initialMode)} index: ${escapeHtml(e.message)}</div>`;
return;
}
}
if (parsed && parsed.qid) {
const m = curMode();
if (m.index.some((e) => e.qid === parsed.qid)) {
selectQid(parsed.qid, /*pushHash=*/false);
return;
}
}
renderEmpty();
}
boot();
})();