(() => { const state = { overview: null, compare: null, charts: { bar: null, radar: null, catBar: null }, }; const $ = (id) => document.getElementById(id); const COLORS = [ '#d6a24a', '#5ec4a0', '#6aa8d6', '#d9584c', '#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: 3, pointHoverRadius: 5, }; if (type === 'bar') { return { ...base, borderWidth: 0, borderRadius: 3 }; } if (type === 'radar') { return { ...base, fill: true, borderWidth: 2 }; } return { ...base, fill: false, borderWidth: 2 }; }); } function axisOptions(maxRotation = 45) { return { x: { ticks: { color: '#9aa89d', maxRotation, minRotation: 0, font: { size: 11 } }, grid: { color: 'rgba(40,48,42,0.8)' }, }, y: { min: 0, max: 100, ticks: { color: '#9aa89d', callback: (v) => v + '%', font: { size: 11 } }, grid: { color: 'rgba(40,48,42,0.8)' }, title: { display: true, text: 'Score (%)', color: '#9aa89d', font: { size: 11 } }, }, }; } function markEmpty(el, msg) { if (!el) return; const wrap = el.parentElement || el; wrap.classList.add('empty'); wrap.dataset.empty = msg; } function clearEmpty(el) { if (!el) return; const wrap = el.parentElement || el; wrap.classList.remove('empty'); delete wrap.dataset.empty; } function makeChart(key, canvas, config) { if (typeof Chart === 'undefined') { markEmpty(canvas, 'Chart.js 未加载,请强制刷新页面 (Ctrl+F5)'); return null; } if (!canvas) return null; try { clearEmpty(canvas); const chart = new Chart(canvas, config); state.charts[key] = chart; return chart; } catch (err) { console.error('chart error', key, err); markEmpty(canvas, `图表渲染失败: ${err.message || err}`); return null; } } function renderCharts(compare) { destroyCharts(); const labels = compare.benchmarks || []; const common = { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#c5d0c7', boxWidth: 10, font: { family: 'Inter', size: 12 } }, }, tooltip: { callbacks: { label(ctx) { const v = ctx.parsed.y ?? ctx.parsed.r; return `${ctx.dataset.label}: ${v == null ? '—' : v.toFixed(2) + '%'}`; }, }, }, }, scales: {}, }; const catLabels = compare.category_labels || []; const catSeries = compare.category_series || []; const catBarCanvas = $('categoryBarChart'); if (!catLabels.length || !catSeries.length) { markEmpty(catBarCanvas, '当前选择下无可用能力分类数据'); } else { makeChart('catBar', catBarCanvas, { type: 'bar', data: { labels: catLabels, datasets: chartDatasets(catSeries, 'bar') }, options: { ...common, scales: { x: { ...axisOptions(0).x, grid: { display: false } }, y: axisOptions(0).y, }, }, }); } if (!labels.length) { markEmpty($('barChart'), '暂无 benchmark 得分'); } else { makeChart('bar', $('barChart'), { type: 'bar', data: { labels, datasets: chartDatasets(compare.series, 'bar') }, options: { ...common, scales: { x: { ...axisOptions(45).x, grid: { display: false } }, y: axisOptions(45).y, }, }, }); } const radarLabels = labels; const canRadar = radarLabels.length >= 3 && (compare.series || []).length > 0; const radarCanvas = $('radarChart'); if (!canRadar) { markEmpty(radarCanvas, '共同 benchmark 不足 3 个,暂不绘制雷达图'); } else { makeChart('radar', radarCanvas, { type: 'radar', data: { labels: radarLabels, datasets: chartDatasets(compare.series, 'radar') }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#c5d0c7', boxWidth: 10 } }, }, scales: { r: { min: 0, max: 100, ticks: { color: '#9aa89d', backdropColor: 'transparent', stepSize: 20, font: { size: 10 } }, grid: { color: 'rgba(40,48,42,0.8)' }, angleLines: { color: 'rgba(40,48,42,0.8)' }, pointLabels: { color: '#c5d0c7', 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 renderWinMatrix(compare) { const table = $('winMatrixTable'); const thead = table.querySelector('thead'); const tbody = table.querySelector('tbody'); thead.innerHTML = ''; tbody.innerHTML = ''; const folders = compare.series.map((s) => s.folder); if (folders.length < 2) { tbody.innerHTML = '至少选择 2 个模型才能生成胜负矩阵'; return; } const matrix = compare.win_matrix || {}; const head = document.createElement('tr'); head.innerHTML = `模型${folders.map((f) => `${f}`).join('')}`; thead.appendChild(head); folders.forEach((f) => { const tr = document.createElement('tr'); const cells = folders.map((g) => { const val = matrix[f]?.[g] ?? 0; return `${val}`; }).join(''); tr.innerHTML = `${f}${cells}`; tbody.appendChild(tr); }); } function renderOverallRank(compare) { const box = $('overallRank'); box.innerHTML = ''; const overall = compare.overall || []; const wins = compare.win_counts || {}; if (!overall.length) { box.innerHTML = '
无数据
'; return; } overall.forEach((item, idx) => { const card = document.createElement('div'); card.className = 'rank-card'; const pct = item.mean != null ? (item.mean * 100).toFixed(2) + '%' : '—'; const win = wins[item.folder] != null ? wins[item.folder].toFixed(1) : '0'; card.innerHTML = `
#${idx + 1} ${item.folder} ${pct}
${item.n} benches · ${win} 项第一 `; box.appendChild(card); }); } 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); renderWinMatrix(data); renderOverallRank(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; }); }); api('/api/meta').then((meta) => { $('outputDir').value = meta.defaults?.output_dir || ''; return loadOverview(); }).catch(() => loadOverview()); })();