dashboard.ts (7573B)
1 import { loadDashboard, type Dashboard, type Pipeline } from '../data'; 2 import { renderHistogram } from '../components/histogram'; 3 import { renderBarChart } from '../components/bar-chart'; 4 5 const GAME_DESCRIPTIONS: Record<string, string> = { 6 'Overclaiming': 'Abstract claims not supported by results, or results generalized beyond what was tested.', 7 'Big Numbers No Error Bars': 'Reports results with no confidence intervals, no variance, and no uncertainty quantification.', 8 'Contamination Dodge': 'Evaluates on benchmarks without discussing whether the model saw the test data during training.', 9 'Open Source Theater': 'Releases code but provides no environment specs or reproduction instructions. Technically "open" but practically unreproducible.', 10 'Trust Us': 'No raw data released and no code released. Claims are completely unverifiable by independent researchers.', 11 'Moving Goalpost': 'Makes causal claims ("X improves Y") from observational data without a causal study design.', 12 'The Black Box': 'No prompts provided and no hyperparameters reported. The experiment cannot be replicated even in principle.', 13 'Cherry-picked Comparisons': 'Baselines are outdated or suspiciously weak. Newer, stronger alternatives exist but are not compared against.', 14 'All Show No Substance': 'Strong evaluation design (baselines, metrics, ablations) but near-zero statistical rigor and no artifact release.', 15 'Limitation Theater': 'Has a limitations section, but it contains only generic boilerplate rather than specific threats to this study.', 16 }; 17 18 function renderProgressBar(p: Pipeline): string { 19 const total = p.registry_total; 20 const scanned = p.v5_opus + p.v5_haiku + p.deprecated_scan; 21 const opusPct = (p.v5_opus / total * 100).toFixed(1); 22 const haikuPct = (p.v5_haiku / total * 100).toFixed(1); 23 const depPct = (p.deprecated_scan / total * 100).toFixed(1); 24 const nonePct = (p.not_scanned / total * 100).toFixed(1); 25 const scannedPct = (scanned / total * 100).toFixed(1); 26 27 return `<div class="pipeline-bar"> 28 <div class="pipeline-header"> 29 <span class="pipeline-title">Survey Progress</span> 30 <span class="pipeline-stat">${scanned} of ${total} scanned (${scannedPct}%)</span> 31 </div> 32 <div class="pipeline-track"> 33 <div class="pipeline-seg v5opus" style="width:${opusPct}%" title="V5 Opus: ${p.v5_opus}"></div> 34 <div class="pipeline-seg v5haiku" style="width:${haikuPct}%" title="V5 Haiku/Sonnet: ${p.v5_haiku}"></div> 35 <div class="pipeline-seg deprecated" style="width:${depPct}%" title="Deprecated scan: ${p.deprecated_scan}"></div> 36 <div class="pipeline-seg notscan" style="width:${nonePct}%" title="Not scanned: ${p.not_scanned}"></div> 37 </div> 38 <div class="pipeline-legend"> 39 <span><span class="pipeline-dot v5opus"></span>V5 Opus (${p.v5_opus})</span> 40 <span><span class="pipeline-dot v5haiku"></span>V5 Haiku/Sonnet (${p.v5_haiku})</span> 41 <span><span class="pipeline-dot deprecated"></span>Deprecated (${p.deprecated_scan})</span> 42 <span><span class="pipeline-dot notscan"></span>Not scanned (${p.not_scanned})</span> 43 </div> 44 </div>`; 45 } 46 47 export async function renderDashboard(app: HTMLElement) { 48 app.innerHTML = '<div class="spinner"></div>'; 49 const agg = await loadDashboard(); 50 51 const topGame = Object.entries(agg.game_pcts).sort((a, b) => b[1] - a[1])[0]; 52 53 const p = agg.pipeline; 54 const scanned = p.v5_opus + p.v5_haiku + p.deprecated_scan; 55 const scanPct = Math.round(scanned / p.registry_total * 100); 56 57 app.innerHTML = ` 58 ${renderProgressBar(p)} 59 60 <div class="cards"> 61 <div class="card"><div class="label">Papers Analyzed</div><div class="value">${agg.n}</div></div> 62 <div class="card"><div class="label">Median Score</div><div class="value yellow">${agg.median}%</div></div> 63 <div class="card"><div class="label">Full Reproducibility</div><div class="value red">${agg.full_reproducibility_pct}%</div></div> 64 <div class="card"><div class="label">${topGame ? topGame[0] : 'N/A'}</div><div class="value red">${topGame ? topGame[1] + '%' : '—'}</div></div> 65 </div> 66 67 <div class="section"> 68 <h2>Score Distribution</h2> 69 ${renderHistogram(agg.histogram)} 70 </div> 71 72 <div class="section"> 73 <h2>Category Pass Rates</h2> 74 ${renderBarChart(Object.entries(agg.category_rates).map(([k, v]) => ({ label: k, value: v })))} 75 </div> 76 77 <div class="section"> 78 <h2>Year Trends</h2> 79 ${renderYearTrends(agg.year_trends)} 80 </div> 81 82 <div class="section"> 83 <h2>Named Games</h2> 84 <p style="font-size:0.82rem;color:var(--text-dim);margin-bottom:0.75rem">Recurring methodological anti-patterns detected by checklist combinations. Percentage = share of papers exhibiting the pattern.</p> 85 ${Object.entries(agg.game_pcts).sort((a, b) => b[1] - a[1]).map(([name, pct]) => 86 `<div class="game-row"><div><span class="game-name">${name}</span><div class="game-desc">${GAME_DESCRIPTIONS[name] || ''}</div></div><span class="game-pct">${pct}%</span></div>` 87 ).join('')} 88 </div> 89 90 <div class="section"> 91 <h2>Archetypes</h2> 92 ${Object.entries(agg.archetype_counts).sort((a, b) => b[1] - a[1]).map(([name, count]) => 93 `<div class="game-row"><span><span class="archetype ${name}">${name}</span></span><span style="color:var(--text-dim);font-family:var(--font)">${count} (${Math.round(count / agg.n * 100)}%)</span></div>` 94 ).join('')} 95 </div> 96 `; 97 } 98 99 function renderYearTrends(trends: Record<string, { n: number; mean: number; median: number }>): string { 100 const years = Object.keys(trends).map(Number).sort(); 101 if (years.length < 2) return '<p>Not enough data for trends.</p>'; 102 103 const w = 500, h = 180; 104 const pad = { l: 50, r: 20, t: 10, b: 30 }; 105 const chartW = w - pad.l - pad.r; 106 const chartH = h - pad.t - pad.b; 107 108 const minY = 0, maxY = 100; 109 const xScale = (y: number) => pad.l + ((y - years[0]) / (years[years.length - 1] - years[0])) * chartW; 110 const yScale = (v: number) => pad.t + chartH - ((v - minY) / (maxY - minY)) * chartH; 111 112 const medianPoints = years.map(y => `${xScale(y)},${yScale(trends[y].median)}`).join(' '); 113 const meanPoints = years.map(y => `${xScale(y)},${yScale(trends[y].mean)}`).join(' '); 114 115 let labels = years.map(y => 116 `<text x="${xScale(y)}" y="${h - 5}" text-anchor="middle">${y}</text>` 117 ).join(''); 118 119 for (let v = 0; v <= 100; v += 25) { 120 labels += `<text x="${pad.l - 8}" y="${yScale(v) + 4}" text-anchor="end">${v}%</text>`; 121 labels += `<line class="grid-line" x1="${pad.l}" x2="${w - pad.r}" y1="${yScale(v)}" y2="${yScale(v)}" stroke-dasharray="3"/>`; 122 } 123 124 const dataLabels = years.map(y => { 125 const t = trends[y]; 126 return `<text x="${xScale(y)}" y="${yScale(t.median) - 8}" text-anchor="middle" fill="var(--yellow)" font-size="10">${t.median}% (n=${t.n})</text>`; 127 }).join(''); 128 129 return `<svg viewBox="0 0 ${w} ${h}" class="trend-chart" style="width:100%;max-width:${w}px"> 130 ${labels} 131 <polyline points="${medianPoints}" fill="none" stroke="var(--yellow)" stroke-width="2"/> 132 <polyline points="${meanPoints}" fill="none" stroke="var(--accent)" stroke-width="2" stroke-dasharray="4"/> 133 ${years.map(y => `<circle cx="${xScale(y)}" cy="${yScale(trends[y].median)}" r="3" fill="var(--yellow)"/>`).join('')} 134 ${dataLabels} 135 <text x="${w - pad.r}" y="${pad.t + 12}" text-anchor="end" fill="var(--yellow)" font-size="10">median</text> 136 <text x="${w - pad.r}" y="${pad.t + 24}" text-anchor="end" fill="var(--accent)" font-size="10">mean</text> 137 </svg>`; 138 }