(() => { const state = { overview: null, compare: null, charts: { line: null, bar: null, radar: null }, }; const $ = (id) => document.getElementById(id); const COLORS = [ '#d6a24a', '#6fbf8a', '#6aa8d6', '#d86a5b', '#8b7ec8', '#e0c36a', '#5ec4b0', '#c98a6a', ]; function setMsg(text, type = '') { const el = $('scanMsg'); el.textContent = text || ''; el.className = `msg ${type}`.trim(); } async function api(path) { const res = await fetch(path); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data.detail || res.statusText || 'request failed'); } return data; } function selectedValues(containerId) { return [...document.querySelectorAll(`#${containerId} input:checked`)].map((el) => el.value); } function renderModelList(models) { const box = $('modelList'); box.innerHTML = ''; if (!models.length) { box.innerHTML = '
未发现模型结果目录
'; return; } models.forEach((m, i) => { const label = document.createElement('label'); label.className = 'pick-item'; label.innerHTML = ` ${m.folder} ${m.model_name} · ${m.n_benchmarks} benches `; box.appendChild(label); }); } function renderBenchList(categories, benchmarks) { const box = $('benchList'); box.innerHTML = ''; if (!benchmarks.length) { box.innerHTML = '
暂无 benchmark 结果
'; return; } const cats = categories && categories.length ? categories : [{ id: 'all', name: '全部', items: benchmarks }]; cats.forEach((cat) => { const section = document.createElement('div'); section.className = `pick-cat cat-${cat.id}`; section.innerHTML = `
${cat.name} ${cat.items.length}
`; const grid = document.createElement('div'); grid.className = 'pick-grid'; cat.items.forEach((name) => { const label = document.createElement('label'); label.className = 'pick-chip'; label.innerHTML = `${name}`; grid.appendChild(label); }); section.querySelector('.cat-all').addEventListener('click', () => { grid.querySelectorAll('input').forEach((el) => { el.checked = true; }); }); section.querySelector('.cat-none').addEventListener('click', () => { grid.querySelectorAll('input').forEach((el) => { el.checked = false; }); }); section.appendChild(grid); box.appendChild(section); }); } function destroyCharts() { Object.keys(state.charts).forEach((k) => { if (state.charts[k]) { state.charts[k].destroy(); state.charts[k] = null; } }); } function chartDatasets(series, type) { return series.map((s, i) => { const color = COLORS[i % COLORS.length]; const base = { label: s.display, data: s.scores.map((v) => (v == null ? null : Number((v * 100).toFixed(2)))), borderColor: color, backgroundColor: type === 'radar' ? color + '33' : color + 'cc', tension: 0.25, spanGaps: false, pointRadius: 4, pointHoverRadius: 6, }; if (type === 'bar') { return { ...base, borderWidth: 0, borderRadius: 4 }; } if (type === 'radar') { return { ...base, fill: true, borderWidth: 2 }; } return { ...base, fill: false, borderWidth: 2 }; }); } function renderCharts(compare) { destroyCharts(); const labels = compare.benchmarks; const common = { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#c9d4cb', boxWidth: 12, font: { family: 'IBM Plex Sans' } }, }, tooltip: { callbacks: { label(ctx) { const v = ctx.parsed.y ?? ctx.parsed.r; return `${ctx.dataset.label}: ${v == null ? '—' : v.toFixed(2) + '%'}`; }, }, }, }, scales: {}, }; state.charts.line = new Chart($('lineChart'), { type: 'line', data: { labels, datasets: chartDatasets(compare.series, 'line') }, options: { ...common, scales: { x: { ticks: { color: '#92a197', maxRotation: 45, minRotation: 0 }, grid: { color: 'rgba(51,64,56,0.6)' }, }, y: { min: 0, max: 100, ticks: { color: '#92a197', callback: (v) => v + '%', }, grid: { color: 'rgba(51,64,56,0.6)' }, title: { display: true, text: 'Score (%)', color: '#92a197' }, }, }, }, }); state.charts.bar = new Chart($('barChart'), { type: 'bar', data: { labels, datasets: chartDatasets(compare.series, 'bar') }, options: { ...common, scales: { x: { ticks: { color: '#92a197', maxRotation: 45 }, grid: { display: false }, }, y: { min: 0, max: 100, ticks: { color: '#92a197', callback: (v) => v + '%' }, grid: { color: 'rgba(51,64,56,0.6)' }, }, }, }, }); const radarLabels = labels; const canRadar = radarLabels.length >= 3 && compare.series.length > 0; const radarCanvas = $('radarChart'); if (!canRadar) { radarCanvas.parentElement.classList.add('empty'); radarCanvas.parentElement.dataset.empty = '共同 benchmark 不足 3 个,暂不绘制雷达图'; } else { radarCanvas.parentElement.classList.remove('empty'); delete radarCanvas.parentElement.dataset.empty; state.charts.radar = new Chart(radarCanvas, { type: 'radar', data: { labels: radarLabels, datasets: chartDatasets(compare.series, 'radar') }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#c9d4cb', boxWidth: 12 } }, }, scales: { r: { min: 0, max: 100, ticks: { color: '#92a197', backdropColor: 'transparent', stepSize: 20 }, grid: { color: 'rgba(51,64,56,0.7)' }, angleLines: { color: 'rgba(51,64,56,0.7)' }, pointLabels: { color: '#c9d4cb', font: { size: 11 } }, }, }, }, }); } } function renderTable(compare) { const thead = $('scoreTable').querySelector('thead'); const tbody = $('scoreTable').querySelector('tbody'); thead.innerHTML = ''; tbody.innerHTML = ''; const head = document.createElement('tr'); head.innerHTML = `Benchmark${compare.series.map((s) => `${s.display}`).join('')}最佳`; thead.appendChild(head); compare.benchmarks.forEach((b, i) => { const tr = document.createElement('tr'); let best = -1; let bestFolder = '—'; compare.series.forEach((s) => { const v = s.scores[i]; if (v != null && v > best) { best = v; bestFolder = s.display; } }); const cells = compare.series.map((s) => { const v = s.scores[i]; if (v == null) return '—'; const pct = (v * 100).toFixed(2); const isBest = v === best && best >= 0; return `${pct}%`; }).join(''); tr.innerHTML = `${b}${cells}${bestFolder}`; tbody.appendChild(tr); }); } function renderRanking(compare) { const box = $('rankList'); box.innerHTML = ''; compare.ranking.forEach((item) => { const card = document.createElement('div'); card.className = 'rank-card'; const rows = item.rows.length ? item.rows.map((r, idx) => `
#${idx + 1} ${r.folder} ${(r.score * 100).toFixed(2)}%
`).join('') : '
无数据
'; card.innerHTML = `${item.benchmark}${rows}`; box.appendChild(card); }); } async function loadOverview() { const dir = $('outputDir').value.trim(); const qs = dir ? `?output_dir=${encodeURIComponent(dir)}` : ''; setMsg('扫描中…'); try { const data = await api(`/api/results/overview${qs}`); state.overview = data; if (!$('outputDir').value) $('outputDir').value = data.output_dir || ''; $('scanMeta').textContent = `${data.models.length} 个模型 · ${data.benchmarks.length} 个 benchmark · ${data.output_dir}`; renderModelList(data.models); renderBenchList(data.categories, data.benchmarks); setMsg('扫描完成', 'ok'); await runCompare(); } catch (e) { setMsg(e.message, 'error'); } } async function runCompare() { const models = selectedValues('modelList'); const benchmarks = selectedValues('benchList'); if (!models.length) { setMsg('请至少选择一个模型', 'error'); return; } if (!benchmarks.length) { setMsg('请至少选择一个 benchmark', 'error'); return; } const dir = $('outputDir').value.trim(); const params = new URLSearchParams({ models: models.join(','), benchmarks: benchmarks.join(','), }); if (dir) params.set('output_dir', dir); const data = await api(`/api/results/compare?${params}`); state.compare = data; renderCharts(data); renderTable(data); renderRanking(data); setMsg(`已对比 ${data.series.length} 模型 × ${data.benchmarks.length} benches`, 'ok'); } $('reloadBtn').addEventListener('click', () => loadOverview()); $('compareBtn').addEventListener('click', () => runCompare().catch((e) => setMsg(e.message, 'error'))); $('modelAllBtn').addEventListener('click', () => { document.querySelectorAll('#modelList input').forEach((el) => { el.checked = true; }); }); $('modelClearBtn').addEventListener('click', () => { document.querySelectorAll('#modelList input').forEach((el) => { el.checked = false; }); }); $('benchAllBtn').addEventListener('click', () => { document.querySelectorAll('#benchList input').forEach((el) => { el.checked = true; }); }); $('benchClearBtn').addEventListener('click', () => { document.querySelectorAll('#benchList input').forEach((el) => { el.checked = false; }); }); // bootstrap defaults from meta api('/api/meta').then((meta) => { $('outputDir').value = meta.defaults?.output_dir || ''; return loadOverview(); }).catch(() => loadOverview()); })();