(() => {
const state = {
overview: null,
compare: null,
charts: { line: null, bar: null, radar: null, catLine: null, catBar: 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 axisOptions(maxRotation = 45) {
return {
x: {
ticks: { color: '#92a197', maxRotation, minRotation: 0, font: { size: 12 } },
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' },
},
};
}
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: '#c9d4cb', boxWidth: 12, font: { family: 'IBM Plex Sans', size: 13 } },
},
tooltip: {
callbacks: {
label(ctx) {
const v = ctx.parsed.y ?? ctx.parsed.r;
return `${ctx.dataset.label}: ${v == null ? '—' : v.toFixed(2) + '%'}`;
},
},
},
},
scales: {},
};
// Capability-domain charts
const catLabels = compare.category_labels || [];
const catSeries = compare.category_series || [];
const catLineCanvas = $('categoryLineChart');
const catBarCanvas = $('categoryBarChart');
if (!catLabels.length || !catSeries.length) {
markEmpty(catLineCanvas, '当前选择下无可用能力分类数据');
markEmpty(catBarCanvas, '当前选择下无可用能力分类数据');
} else {
makeChart('catLine', catLineCanvas, {
type: 'line',
data: { labels: catLabels, datasets: chartDatasets(catSeries, 'line') },
options: { ...common, scales: axisOptions(0) },
});
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($('lineChart'), '暂无 benchmark 得分');
markEmpty($('barChart'), '暂无 benchmark 得分');
} else {
makeChart('line', $('lineChart'), {
type: 'line',
data: { labels, datasets: chartDatasets(compare.series, 'line') },
options: { ...common, scales: axisOptions(45) },
});
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: '#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: 12 } },
},
},
},
});
}
}
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());
})();