Spaces:
Running
Running
| // 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, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| 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(); | |
| })(); | |