Redesign webui: compact layout, search/filter, command preview, progress bar, presets

This commit is contained in:
sora 2026-07-29 08:08:06 +00:00
parent 541b0b477d
commit 28f22c11e3
7 changed files with 1256 additions and 1014 deletions

View File

@ -184,7 +184,7 @@ class LaunchRequest(BaseModel):
BENCHMARK_CATEGORIES = [ BENCHMARK_CATEGORIES = [
{ {
'id': 'math', 'id': 'math',
'name': '数学推理', 'name': '数学',
'items': [ 'items': [
'aime24', 'aime25', 'aime26', 'hmmt26', 'aime24', 'aime25', 'aime26', 'hmmt26',
'imo_answerbench', 'competition_math', 'gsm8k', 'imo_answerbench', 'competition_math', 'gsm8k',
@ -197,12 +197,12 @@ BENCHMARK_CATEGORIES = [
}, },
{ {
'id': 'science', 'id': 'science',
'name': '科学 / 高难推理', 'name': '科学',
'items': ['gpqa_diamond', 'super_gpqa', 'hle'], 'items': ['gpqa_diamond', 'super_gpqa', 'hle'],
}, },
{ {
'id': 'knowledge', 'id': 'knowledge',
'name': '知识与通用能力', 'name': '知识',
'items': [ 'items': [
'mmlu', 'mmlu_pro', 'cmmlu', 'bbh', 'arc', 'mmlu', 'mmlu_pro', 'cmmlu', 'bbh', 'arc',
'drop', 'hellaswag', 'winogrande', 'simple_qa', 'trivia_qa', 'drop', 'hellaswag', 'winogrande', 'simple_qa', 'trivia_qa',
@ -215,11 +215,34 @@ BENCHMARK_CATEGORIES = [
}, },
{ {
'id': 'tool_agent', 'id': 'tool_agent',
'name': '工具调用 / 智能体', 'name': '智能体',
'items': ['bfcl_v3', 'general_fc', 'tau2_bench'], 'items': ['bfcl_v3', 'general_fc', 'tau2_bench'],
}, },
] ]
# Approximate wall-clock hours for each suite (from run.py comments / measured data).
SUITE_ESTIMATES = {
'lite': 5,
'mid': 26,
'full': 72,
'group1': 23,
'group2': 26,
'group3': 27,
'official': 28,
}
# Benchmarks that need extra environment setup before running.
BENCHMARK_REQUIREMENTS = {
'humaneval': {'sandbox': True, 'hint': '需要 python:3.11-slim 镜像'},
'bigcodebench': {'sandbox': True, 'hint': '需要 bigcodebench-sandbox 镜像'},
'swe_bench_verified': {'swe': True, 'hint': '需要预加载 swebench 实例镜像'},
'swe_bench_pro': {'swe': True, 'hint': '需要预加载 swebench 实例镜像'},
'swe_bench_multilingual_agentic': {'swe': True, 'hint': '需要预加载 swebench 实例镜像'},
'tau2_bench': {'agent': True, 'hint': '需要安装 tau2-bench 包'},
'general_fc': {'agent': True, 'hint': 'Agent benchmark'},
'bfcl_v3': {'agent': True, 'hint': 'Agent benchmark'},
}
def _meta() -> dict: def _meta() -> dict:
all_benchmarks = sorted( all_benchmarks = sorted(
@ -247,6 +270,8 @@ def _meta() -> dict:
'benchmarks': all_benchmarks, 'benchmarks': all_benchmarks,
'categories': categories, 'categories': categories,
'multi_run': run_module.MULTI_RUN_CONFIG, 'multi_run': run_module.MULTI_RUN_CONFIG,
'suite_estimates': SUITE_ESTIMATES,
'benchmark_requirements': BENCHMARK_REQUIREMENTS,
'defaults': { 'defaults': {
'model': run_module.DEFAULT_MODEL, 'model': run_module.DEFAULT_MODEL,
'api_url': run_module.DEFAULT_API_URL, 'api_url': run_module.DEFAULT_API_URL,

View File

@ -5,7 +5,7 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR" cd "$DIR"
HOST="${WEBUI_HOST:-0.0.0.0}" HOST="${WEBUI_HOST:-0.0.0.0}"
PORT="${WEBUI_PORT:-7860}" PORT="${WEBUI_PORT:-7861}"
echo "EvalStone Launch Panel" echo "EvalStone Launch Panel"
echo " project : $(dirname "$DIR")" echo " project : $(dirname "$DIR")"

View File

@ -1,24 +1,26 @@
(() => { (() => {
const state = { const state = {
meta: null, meta: null,
tab: 'scope',
selectionMode: 'suite', selectionMode: 'suite',
suiteKind: 'builtin', // builtin | custom suiteKind: 'builtin',
suite: 'official', suite: 'official',
customSuites: {}, customSuites: {},
activeJobId: null, activeJobId: null,
pollTimer: null, pollTimer: null,
logOffset: 0, logOffset: 0,
activeCategory: 'all',
searchQuery: '',
}; };
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const form = $('launchForm');
const formMsg = $('formMsg'); const formMsg = $('formMsg');
const logView = $('logView'); const logView = $('logView');
const jobList = $('jobList'); const jobList = $('jobList');
function setMsg(text, type = '') { function setMsg(text, type = '') {
formMsg.textContent = text || ''; formMsg.textContent = text || '';
formMsg.className = `msg ${type}`.trim(); formMsg.className = `form-msg ${type}`.trim();
} }
async function api(path, options) { async function api(path, options) {
@ -34,6 +36,14 @@
return data; return data;
} }
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function fillDefaults(meta) { function fillDefaults(meta) {
const d = meta.defaults; const d = meta.defaults;
$('model').value = d.model || ''; $('model').value = d.model || '';
@ -54,34 +64,60 @@
state.customSuites = meta.custom_suites || {}; state.customSuites = meta.custom_suites || {};
} }
function escapeHtml(s) { // ---- Tabs ----
return String(s) function setTab(tab) {
.replace(/&/g, '&amp;') state.tab = tab;
.replace(/</g, '&lt;') document.querySelectorAll('.tabs .tab').forEach((el) => {
.replace(/>/g, '&gt;') el.classList.toggle('active', el.dataset.tab === tab);
.replace(/"/g, '&quot;'); });
document.querySelectorAll('.tab-pane').forEach((el) => {
el.classList.toggle('active', el.id === `${tab}Tab`);
});
}
// ---- Mode ----
function setMode(mode) {
state.selectionMode = mode;
document.querySelectorAll('.mode-btn').forEach((el) => {
el.classList.toggle('active', el.dataset.mode === mode);
});
$('suitePane').classList.toggle('hidden', mode !== 'suite');
$('datasetsPane').classList.toggle('hidden', mode !== 'datasets');
updateEstimate();
}
// ---- Suites ----
function formatHours(h) {
if (h == null || h <= 0) return '';
if (h < 1) return `${Math.round(h * 60)}min`;
return `~${h}h`;
} }
function renderSuites() { function renderSuites() {
const box = $('suiteList'); const box = $('suiteList');
box.innerHTML = ''; box.innerHTML = '';
const suites = state.meta.suites; const suites = state.meta.suites || {};
const estimates = state.meta.suite_estimates || {};
Object.keys(suites).forEach((name) => { Object.keys(suites).forEach((name) => {
const info = suites[name]; const info = suites[name];
const count = info.all.length;
const est = estimates[name];
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.type = 'button'; btn.type = 'button';
btn.className = `suite-card${state.suiteKind === 'builtin' && state.suite === name ? ' active' : ''}`; btn.className = `suite-card${state.suiteKind === 'builtin' && state.suite === name ? ' active' : ''}`;
const names = info.all.map(escapeHtml).join(', ');
btn.innerHTML = ` btn.innerHTML = `
<strong>${escapeHtml(name)}</strong> <strong>${escapeHtml(name)}</strong>
<span class="suite-count">${info.all.length} benchmarks</span> <div class="suite-meta">
<div class="suite-names">${names}</div> <span>${count} benches</span>
${est ? `<span>${formatHours(est)}</span>` : ''}
</div>
`; `;
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
state.suiteKind = 'builtin'; state.suiteKind = 'builtin';
state.suite = name; state.suite = name;
renderSuites(); renderSuites();
renderCustomSuites(); renderCustomSuites();
updateEstimate();
}); });
box.appendChild(btn); box.appendChild(btn);
}); });
@ -89,43 +125,23 @@
function renderCustomSuites() { function renderCustomSuites() {
const box = $('customSuiteList'); const box = $('customSuiteList');
const empty = $('customSuiteEmpty');
box.innerHTML = '';
const names = Object.keys(state.customSuites || {}); const names = Object.keys(state.customSuites || {});
empty.classList.toggle('hidden', names.length > 0); box.innerHTML = '';
$('customSuiteHint').classList.toggle('hidden', names.length > 0);
names.forEach((name) => { names.forEach((name) => {
const items = state.customSuites[name] || []; const items = state.customSuites[name] || [];
const wrap = document.createElement('div'); const wrap = document.createElement('div');
wrap.className = `suite-card custom${state.suiteKind === 'custom' && state.suite === name ? ' active' : ''}`; wrap.className = `suite-card${state.suiteKind === 'custom' && state.suite === name ? ' active' : ''}`;
wrap.innerHTML = `
const main = document.createElement('button');
main.type = 'button';
main.className = 'suite-card-main';
main.innerHTML = `
<strong>${escapeHtml(name)}</strong> <strong>${escapeHtml(name)}</strong>
<span class="suite-count">${items.length} benchmarks · 自定义</span> <div class="suite-meta">
<div class="suite-names">${items.map(escapeHtml).join(', ')}</div> <span>${items.length} benches</span>
<span>自定义</span>
</div>
<button type="button" class="ghost del">删除</button>
`; `;
main.addEventListener('click', () => { wrap.querySelector('.del').addEventListener('click', async (e) => {
state.suiteKind = 'custom';
state.suite = name;
renderSuites();
renderCustomSuites();
// sync editor checkboxes
const set = new Set(items);
document.querySelectorAll('#customPickList input').forEach((el) => {
el.checked = set.has(el.value);
});
$('customSuiteName').value = name;
updateCustomPickCount();
});
const del = document.createElement('button');
del.type = 'button';
del.className = 'ghost danger-text suite-del';
del.textContent = '删除';
del.addEventListener('click', async (e) => {
e.stopPropagation(); e.stopPropagation();
if (!confirm(`删除自定义组合「${name}」?`)) return; if (!confirm(`删除自定义组合「${name}」?`)) return;
try { try {
@ -135,110 +151,141 @@
state.suiteKind = 'builtin'; state.suiteKind = 'builtin';
state.suite = state.meta.defaults?.suite || 'official'; state.suite = state.meta.defaults?.suite || 'official';
} }
renderSuites();
renderCustomSuites(); renderCustomSuites();
renderSuites();
setMsg(`已删除组合: ${name}`, 'ok'); setMsg(`已删除组合: ${name}`, 'ok');
} catch (err) { } catch (err) {
setMsg(err.message, 'error'); setMsg(err.message, 'error');
} }
}); });
wrap.addEventListener('click', (e) => {
wrap.appendChild(main); if (e.target.closest('.del')) return;
wrap.appendChild(del); state.suiteKind = 'custom';
state.suite = name;
renderSuites();
renderCustomSuites();
updateEstimate();
});
box.appendChild(wrap); box.appendChild(wrap);
}); });
} }
function renderBenchmarkPicker(containerId, withCountCb) { // ---- Benchmark picker ----
const box = $(containerId); function benchmarkTags(name) {
box.innerHTML = ''; const tags = [];
const multi = new Set(Object.keys(state.meta.multi_run || {})); const multi = state.meta.multi_run || {};
const categories = state.meta.categories || [ const req = state.meta.benchmark_requirements || {};
{ id: 'all', name: '全部', items: state.meta.benchmarks || [] }, if (multi[name]) tags.push({ text: `×${multi[name]}`, cls: '' });
]; if (req[name]) {
if (req[name].sandbox) tags.push({ text: 'sandbox', cls: 'warn' });
categories.forEach((cat) => { else if (req[name].swe) tags.push({ text: 'swe', cls: 'warn' });
const section = document.createElement('section'); else if (req[name].agent) tags.push({ text: 'agent', cls: 'warn' });
section.className = `bench-category cat-${cat.id}`;
const head = document.createElement('div');
head.className = 'bench-cat-head';
head.innerHTML = `
<div class="bench-cat-title">
<strong>${escapeHtml(cat.name)}</strong>
<span class="muted">${cat.items.length}</span>
</div>
<div class="bench-cat-actions">
<button type="button" class="ghost cat-select">全选本组</button>
<button type="button" class="ghost cat-clear">清空</button>
</div>
`;
head.querySelector('.cat-select').addEventListener('click', () => {
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = true; });
withCountCb();
});
head.querySelector('.cat-clear').addEventListener('click', () => {
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = false; });
withCountCb();
});
section.appendChild(head);
const grid = document.createElement('div');
grid.className = 'bench-cat-grid';
cat.items.forEach((name) => {
const label = document.createElement('label');
const isMulti = multi.has(name);
label.className = `bench-item${isMulti ? ' multi' : ''}`;
label.title = isMulti ? `multi-run x${state.meta.multi_run[name]}` : cat.name;
label.innerHTML = `<input type="checkbox" value="${escapeHtml(name)}" /><span>${escapeHtml(name)}</span>`;
if (isMulti) {
const tag = document.createElement('em');
tag.className = 'run-tag';
tag.textContent = `×${state.meta.multi_run[name]}`;
label.appendChild(tag);
} }
label.querySelector('input').addEventListener('change', withCountCb); return tags;
grid.appendChild(label);
});
section.appendChild(grid);
box.appendChild(section);
});
withCountCb();
} }
function renderBenchmarks() { function renderBenchmarks() {
renderBenchmarkPicker('benchmarkList', updateSelectedCount); const box = $('benchmarkList');
box.innerHTML = '';
const multi = new Set(Object.keys(state.meta.multi_run || {}));
const categories = state.meta.categories || [];
categories.forEach((cat) => {
const section = document.createElement('div');
section.className = 'bench-category';
section.dataset.category = cat.id;
const grid = document.createElement('div');
grid.className = 'benchmark-grid';
cat.items.forEach((name) => {
const label = document.createElement('label');
label.className = 'bench-item';
label.dataset.name = name;
label.dataset.category = cat.id;
const tags = benchmarkTags(name);
label.innerHTML = `
<input type="checkbox" value="${escapeHtml(name)}" />
<span class="name">${escapeHtml(name)}</span>
${tags.map((t) => `<span class="tag ${t.cls}" title="${escapeHtml(t.text)}">${escapeHtml(t.text)}</span>`).join('')}
`;
label.querySelector('input').addEventListener('change', updateSelectedCount);
grid.appendChild(label);
});
if (grid.children.length) {
section.appendChild(grid);
box.appendChild(section);
}
});
updateSelectedCount();
applyFilters();
} }
function renderCustomPicker() { function renderCategoryChips() {
renderBenchmarkPicker('customPickList', updateCustomPickCount); const box = $('categoryFilters');
box.innerHTML = '';
const all = document.createElement('button');
all.type = 'button';
all.className = `chip${state.activeCategory === 'all' ? ' active' : ''}`;
all.textContent = '全部';
all.addEventListener('click', () => { state.activeCategory = 'all'; renderCategoryChips(); applyFilters(); });
box.appendChild(all);
(state.meta.categories || []).forEach((cat) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = `chip${state.activeCategory === cat.id ? ' active' : ''}`;
btn.textContent = `${cat.name} (${cat.items.length})`;
btn.addEventListener('click', () => { state.activeCategory = cat.id; renderCategoryChips(); applyFilters(); });
box.appendChild(btn);
});
}
function applyFilters() {
const q = state.searchQuery.toLowerCase().trim();
document.querySelectorAll('.bench-item').forEach((el) => {
const name = el.dataset.name.toLowerCase();
const cat = el.dataset.category;
const matchCat = state.activeCategory === 'all' || cat === state.activeCategory;
const matchSearch = !q || name.includes(q);
el.classList.toggle('hidden-item', !(matchCat && matchSearch));
});
document.querySelectorAll('.bench-category').forEach((cat) => {
const visible = cat.querySelectorAll('.bench-item:not(.hidden-item)').length > 0;
cat.classList.toggle('hidden', !visible);
});
} }
function updateSelectedCount() { function updateSelectedCount() {
const n = [...document.querySelectorAll('#benchmarkList input:checked')].length; const n = [...document.querySelectorAll('#benchmarkList input:checked')].length;
$('selectedCount').textContent = `已选 ${n}`; $('selectedCount').textContent = `已选 ${n}`;
updateEstimate();
} }
function updateCustomPickCount() { function updateEstimate() {
const n = [...document.querySelectorAll('#customPickList input:checked')].length; const el = $('scopeEstimate');
$('customPickCount').textContent = `已勾选 ${n}`; if (state.selectionMode === 'suite') {
const est = (state.meta.suite_estimates || {})[state.suite];
el.textContent = est ? `预计 ${formatHours(est)}` : '';
return;
} }
function setMode(mode) {
state.selectionMode = mode;
document.querySelectorAll('.mode-tabs .tab').forEach((el) => {
el.classList.toggle('active', el.dataset.mode === mode);
});
$('suitePane').classList.toggle('hidden', mode !== 'suite');
$('datasetsPane').classList.toggle('hidden', mode !== 'datasets');
}
function collectPayload() {
const datasets = [...document.querySelectorAll('#benchmarkList input:checked')].map((el) => el.value); const datasets = [...document.querySelectorAll('#benchmarkList input:checked')].map((el) => el.value);
const excludeRaw = $('exclude').value.trim(); if (!datasets.length) {
const exclude = excludeRaw el.textContent = '';
? excludeRaw.split(',').map((s) => s.trim()).filter(Boolean) return;
: []; }
const multi = state.meta.multi_run || {};
// rough estimate: single-run bench ~0.5h, multi-run scaled
let hours = 0;
datasets.forEach((d) => {
hours += multi[d] ? multi[d] * 0.5 : 0.5;
});
el.textContent = `预计 ${formatHours(hours)}`;
}
// ---- Command preview ----
function buildCommandPayload() {
const datasets = [...document.querySelectorAll('#benchmarkList input:checked')].map((el) => el.value);
const excludeRaw = state.selectionMode === 'suite' ? '' : '';
const exclude = [];
let selectionMode = state.selectionMode; let selectionMode = state.selectionMode;
let suite = state.suite; let suite = state.suite;
@ -247,10 +294,9 @@
if (selectionMode === 'suite' && state.suiteKind === 'custom') { if (selectionMode === 'suite' && state.suiteKind === 'custom') {
selectionMode = 'datasets'; selectionMode = 'datasets';
finalDatasets = [...(state.customSuites[state.suite] || [])]; finalDatasets = [...(state.customSuites[state.suite] || [])];
suite = state.suite;
} }
const payload = { return {
model: $('model').value.trim(), model: $('model').value.trim(),
api_url: $('api_url').value.trim(), api_url: $('api_url').value.trim(),
api_key: $('api_key').value.trim() || 'EMPTY', api_key: $('api_key').value.trim() || 'EMPTY',
@ -258,7 +304,7 @@
selection_mode: selectionMode, selection_mode: selectionMode,
suite, suite,
datasets: finalDatasets, datasets: finalDatasets,
exclude: selectionMode === 'suite' ? exclude : [], exclude,
folder_name: $('folder_name').value.trim() || null, folder_name: $('folder_name').value.trim() || null,
limit: $('limit').value.trim() || null, limit: $('limit').value.trim() || null,
seed: Number($('seed').value || 42), seed: Number($('seed').value || 42),
@ -272,27 +318,17 @@
judge_model: $('judge_model').value.trim() || null, judge_model: $('judge_model').value.trim() || null,
judge_api_url: $('judge_api_url').value.trim() || null, judge_api_url: $('judge_api_url').value.trim() || null,
judge_api_key: $('judge_api_key').value.trim() || null, judge_api_key: $('judge_api_key').value.trim() || null,
judge_max_tokens: $('judge_max_tokens').value judge_max_tokens: $('judge_max_tokens').value ? Number($('judge_max_tokens').value) : null,
? Number($('judge_max_tokens').value)
: null,
write_summary: $('write_summary').value === 'true', write_summary: $('write_summary').value === 'true',
}; };
return payload;
} }
async function saveCustomSuite(name, datasets) { function updateCommandPreview(cmd) {
const data = await api('/api/custom-suites', { $('activeCmd').textContent = (cmd || []).join(' ');
method: 'POST', $('copyCmdBtn').disabled = !cmd;
body: JSON.stringify({ name, datasets }),
});
state.customSuites = data.suites || {};
state.suiteKind = 'custom';
state.suite = name;
renderSuites();
renderCustomSuites();
setMsg(`已永久保存组合: ${name}${datasets.length} 个)`, 'ok');
} }
// ---- Jobs ----
function statusClass(status) { function statusClass(status) {
return status || 'idle'; return status || 'idle';
} }
@ -301,10 +337,12 @@
const card = $('activeCard'); const card = $('activeCard');
if (!job) { if (!job) {
card.className = 'active-card idle'; card.className = 'active-card idle';
$('activeStatus').className = 'badge'; $('activeStatus').className = 'badge idle';
$('activeStatus').textContent = 'idle'; $('activeStatus').textContent = 'idle';
$('activeJobId').textContent = '—'; $('activeJobId').textContent = '—';
$('activeCmd').textContent = '尚未启动任务'; $('progressBar').style.width = '0%';
$('progressText').textContent = '';
updateCommandPreview(null);
$('stopBtn').disabled = true; $('stopBtn').disabled = true;
return; return;
} }
@ -312,9 +350,9 @@
$('activeStatus').className = `badge ${statusClass(job.status)}`; $('activeStatus').className = `badge ${statusClass(job.status)}`;
$('activeStatus').textContent = job.status; $('activeStatus').textContent = job.status;
$('activeJobId').textContent = job.id; $('activeJobId').textContent = job.id;
$('activeCmd').textContent = (job.command || []).join(' ');
$('stopBtn').disabled = job.status !== 'running'; $('stopBtn').disabled = job.status !== 'running';
state.activeJobId = job.id; state.activeJobId = job.id;
updateCommandPreview(job.command);
} }
function renderJobs(jobs, activeId) { function renderJobs(jobs, activeId) {
@ -326,18 +364,20 @@
jobs.forEach((job) => { jobs.forEach((job) => {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.type = 'button'; btn.type = 'button';
btn.className = 'job-item'; btn.className = `job-item${job.id === activeId ? ' active' : ''}`;
const model = job.payload?.model || '-'; const model = job.payload?.model || '-';
const thinking = job.payload?.thinking ? 'thinking' : 'no-thinking'; const mode = job.payload?.thinking ? 'thinking' : 'no-thinking';
const scope = job.payload?.selection_mode === 'datasets'
? `${job.payload.datasets?.length || 0} benches`
: (job.payload?.suite || '-');
btn.innerHTML = ` btn.innerHTML = `
<div class="row"> <div class="job-row">
<strong>${escapeHtml(job.id)}</strong> <strong>${escapeHtml(job.id)}</strong>
<span class="badge ${statusClass(job.status)}">${escapeHtml(job.status)}</span> <span class="badge ${statusClass(job.status)}">${escapeHtml(job.status)}</span>
</div> </div>
<small>${escapeHtml(model)} · ${thinking}</small> <small>${escapeHtml(model)} · ${mode} · ${escapeHtml(scope)}</small>
`; `;
btn.addEventListener('click', () => followJob(job.id, true)); btn.addEventListener('click', () => followJob(job.id, true));
if (job.id === activeId) btn.style.borderColor = 'rgba(214,162,74,0.75)';
jobList.appendChild(btn); jobList.appendChild(btn);
}); });
} }
@ -355,6 +395,19 @@
return data; return data;
} }
function tryParseProgress(text) {
// Look for patterns like "Evaluating[mmlu_pro] 12%| 1450/12032"
const m = text.match(/\[eval\]\s*(\d+)%\|\s*(\d+)\/(\d+)/i);
if (m) {
return { pct: Number(m[1]), current: Number(m[2]), total: Number(m[3]) };
}
const m2 = text.match(/Evaluating\[(\w+)\]\s+(\d+)%\|\s*(\d+)\/(\d+)/);
if (m2) {
return { pct: Number(m2[2]), current: Number(m2[3]), total: Number(m2[4]) };
}
return null;
}
async function pullLogs(reset = false) { async function pullLogs(reset = false) {
if (!state.activeJobId) return; if (!state.activeJobId) return;
if (reset) { if (reset) {
@ -366,12 +419,22 @@
logView.textContent += data.content; logView.textContent += data.content;
state.logOffset = data.next_offset; state.logOffset = data.next_offset;
if ($('autoScroll').checked) logView.scrollTop = logView.scrollHeight; if ($('autoScroll').checked) logView.scrollTop = logView.scrollHeight;
const prog = tryParseProgress(logView.textContent);
if (prog) {
$('progressBar').style.width = `${prog.pct}%`;
$('progressText').textContent = `${prog.pct}% · ${prog.current}/${prog.total}`;
}
} }
if (data.status) { if (data.status) {
$('activeStatus').className = `badge ${statusClass(data.status)}`; $('activeStatus').className = `badge ${statusClass(data.status)}`;
$('activeStatus').textContent = data.status; $('activeStatus').textContent = data.status;
$('activeCard').className = `active-card ${statusClass(data.status)}`; $('activeCard').className = `active-card ${statusClass(data.status)}`;
$('stopBtn').disabled = data.status !== 'running'; $('stopBtn').disabled = data.status !== 'running';
if (data.done) {
$('progressBar').style.width = '100%';
$('progressText').textContent = data.status === 'completed' ? '已完成' : `结束: ${data.status}`;
}
} }
if (data.done) stopPolling(); if (data.done) stopPolling();
} }
@ -403,114 +466,26 @@
else stopPolling(); else stopPolling();
} }
async function init() { async function createJob() {
try {
await api('/api/health');
$('healthDot').className = 'dot ok';
$('healthText').textContent = 'server online';
} catch (e) {
$('healthDot').className = 'dot bad';
$('healthText').textContent = 'server offline';
setMsg(e.message, 'error');
return;
}
state.meta = await api('/api/meta');
fillDefaults(state.meta);
renderSuites();
renderCustomSuites();
renderBenchmarks();
renderCustomPicker();
setMode('suite');
const jobs = await refreshJobs();
if (jobs.active_job_id) {
await followJob(jobs.active_job_id, true);
} else if (jobs.jobs?.[0]) {
await followJob(jobs.jobs[0].id, true);
}
}
document.querySelectorAll('.mode-tabs .tab').forEach((el) => {
el.addEventListener('click', () => setMode(el.dataset.mode));
});
$('selectAllBtn').addEventListener('click', () => {
document.querySelectorAll('#benchmarkList input').forEach((el) => { el.checked = true; });
updateSelectedCount();
});
$('clearAllBtn').addEventListener('click', () => {
document.querySelectorAll('#benchmarkList input').forEach((el) => { el.checked = false; });
updateSelectedCount();
});
$('refreshJobsBtn').addEventListener('click', () => refreshJobs().catch((e) => setMsg(e.message, 'error')));
$('saveCustomSuiteBtn').addEventListener('click', async () => {
const name = $('customSuiteName').value.trim();
const datasets = [...document.querySelectorAll('#customPickList input:checked')].map((el) => el.value);
if (!name) {
setMsg('请填写组合名称', 'error');
return;
}
if (!datasets.length) {
setMsg('请至少勾选一个 benchmark', 'error');
return;
}
try {
await saveCustomSuite(name, datasets);
} catch (err) {
setMsg(err.message, 'error');
}
});
$('loadCheckedToCustomBtn').addEventListener('click', () => {
// no-op helper text: already editing in place; just focus name
$('customSuiteName').focus();
setMsg('请在下方勾选数据集并填写组合名称后保存', 'ok');
});
$('saveFromDatasetsBtn').addEventListener('click', async () => {
const datasets = [...document.querySelectorAll('#benchmarkList input:checked')].map((el) => el.value);
if (!datasets.length) {
setMsg('请先勾选 benchmark', 'error');
return;
}
const name = prompt('输入要永久保存的组合名称:');
if (!name || !name.trim()) return;
try {
// sync into custom picker too
const set = new Set(datasets);
document.querySelectorAll('#customPickList input').forEach((el) => {
el.checked = set.has(el.value);
});
$('customSuiteName').value = name.trim();
updateCustomPickCount();
await saveCustomSuite(name.trim(), datasets);
setMode('suite');
} catch (err) {
setMsg(err.message, 'error');
}
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
setMsg(''); setMsg('');
const requiredPaths = [ const required = [
['dataset_dir', 'Dataset Dir'], ['dataset_dir', 'Dataset Dir'],
['output_dir', 'Output Dir'], ['output_dir', 'Output Dir'],
['config', 'Config YAML'], ['config', 'Config YAML'],
['tokenizer_path', 'Tokenizer Path'], ['tokenizer_path', 'Tokenizer Path'],
]; ];
for (const [id, label] of requiredPaths) { for (const [id, label] of required) {
if (!$(id).value.trim()) { if (!$(id).value.trim()) {
setMsg(`请填写必填路径: ${label}`, 'error'); setMsg(`请填写必填路径: ${label}`, 'error');
setTab('config');
$(id).focus(); $(id).focus();
return; return;
} }
} }
const payload = buildCommandPayload();
const payload = collectPayload();
if (payload.selection_mode === 'datasets' && !payload.datasets.length) { if (payload.selection_mode === 'datasets' && !payload.datasets.length) {
setMsg('请至少选择一个 benchmark / 自定义组合', 'error'); setMsg('请至少选择一个 benchmark', 'error');
setTab('scope');
return; return;
} }
$('launchBtn').disabled = true; $('launchBtn').disabled = true;
@ -531,8 +506,139 @@
} finally { } finally {
$('launchBtn').disabled = false; $('launchBtn').disabled = false;
} }
}
// ---- Presets (localStorage) ----
const PRESET_KEY = 'evalstone_path_presets';
function getPresets() {
try {
return JSON.parse(localStorage.getItem(PRESET_KEY) || '[]');
} catch {
return [];
}
}
function savePreset() {
const name = prompt('预设名称');
if (!name) return;
const presets = getPresets();
presets.push({
name,
dataset_dir: $('dataset_dir').value,
output_dir: $('output_dir').value,
config: $('config').value,
tokenizer_path: $('tokenizer_path').value,
});
localStorage.setItem(PRESET_KEY, JSON.stringify(presets));
setMsg('预设已保存', 'ok');
}
function loadPreset() {
const presets = getPresets();
if (!presets.length) {
setMsg('没有保存的预设', 'error');
return;
}
const list = presets.map((p, i) => `${i + 1}. ${p.name}`).join('\n');
const idx = prompt(`选择预设编号:\n${list}`);
const n = Number(idx);
if (!n || n < 1 || n > presets.length) return;
const p = presets[n - 1];
$('dataset_dir').value = p.dataset_dir || '';
$('output_dir').value = p.output_dir || '';
$('config').value = p.config || '';
$('tokenizer_path').value = p.tokenizer_path || '';
setMsg(`已加载预设: ${p.name}`, 'ok');
}
function applyDefaultPreset() {
const d = state.meta.defaults;
$('dataset_dir').value = d.dataset_dir || '';
$('output_dir').value = d.output_dir || '';
$('config').value = d.config || '';
$('tokenizer_path').value = d.tokenizer_path || '';
}
// ---- Init ----
async function init() {
try {
await api('/api/health');
$('healthDot').className = 'dot ok';
$('healthText').textContent = 'online';
} catch (e) {
$('healthDot').className = 'dot bad';
$('healthText').textContent = 'offline';
setMsg(e.message, 'error');
return;
}
state.meta = await api('/api/meta');
fillDefaults(state.meta);
renderSuites();
renderCustomSuites();
renderBenchmarks();
renderCategoryChips();
setMode('suite');
setTab('scope');
updateEstimate();
const jobs = await refreshJobs();
if (jobs.active_job_id) {
await followJob(jobs.active_job_id, true);
} else if (jobs.jobs?.[0]) {
await followJob(jobs.jobs[0].id, true);
}
}
// ---- Events ----
document.querySelectorAll('.tabs .tab').forEach((el) => {
el.addEventListener('click', () => setTab(el.dataset.tab));
});
document.querySelectorAll('.mode-btn').forEach((el) => {
el.addEventListener('click', () => setMode(el.dataset.mode));
}); });
$('benchSearch').addEventListener('input', (e) => {
state.searchQuery = e.target.value;
applyFilters();
});
$('selectAllBtn').addEventListener('click', () => {
document.querySelectorAll('#benchmarkList input:not(.hidden-item input)').forEach((el) => { el.checked = true; });
updateSelectedCount();
});
$('clearAllBtn').addEventListener('click', () => {
document.querySelectorAll('#benchmarkList input').forEach((el) => { el.checked = false; });
updateSelectedCount();
});
$('saveCustomBtn').addEventListener('click', async () => {
const datasets = [...document.querySelectorAll('#benchmarkList input:checked')].map((el) => el.value);
if (!datasets.length) {
setMsg('请先勾选 benchmark', 'error');
return;
}
const name = prompt('输入自定义组合名称:');
if (!name || !name.trim()) return;
try {
const data = await api('/api/custom-suites', {
method: 'POST',
body: JSON.stringify({ name: name.trim(), datasets }),
});
state.customSuites = data.suites || {};
renderCustomSuites();
state.suiteKind = 'custom';
state.suite = name.trim();
renderSuites();
setMode('suite');
setMsg(`已保存组合: ${name.trim()}`, 'ok');
} catch (err) {
setMsg(err.message, 'error');
}
});
$('refreshJobsBtn').addEventListener('click', () => refreshJobs().catch((e) => setMsg(e.message, 'error')));
$('launchBtn').addEventListener('click', createJob);
$('copyCmdBtn').addEventListener('click', () => {
const text = $('activeCmd').textContent;
if (!text || text === '尚未启动任务') return;
navigator.clipboard.writeText(text).then(() => setMsg('命令已复制', 'ok')).catch(() => setMsg('复制失败', 'error'));
});
$('stopBtn').addEventListener('click', async () => { $('stopBtn').addEventListener('click', async () => {
if (!state.activeJobId) return; if (!state.activeJobId) return;
if (!confirm(`确认停止任务 ${state.activeJobId}`)) return; if (!confirm(`确认停止任务 ${state.activeJobId}`)) return;
@ -547,5 +653,9 @@
} }
}); });
$('presetDefaultBtn').addEventListener('click', applyDefaultPreset);
$('presetCurrentBtn').addEventListener('click', savePreset);
$('presetLoadBtn').addEventListener('click', loadPreset);
init(); init();
})(); })();

View File

@ -6,7 +6,7 @@
<title>EvalStone Launch</title> <title>EvalStone Launch</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/static/styles.css" /> <link rel="stylesheet" href="/static/styles.css" />
</head> </head>
<body> <body>
@ -16,7 +16,7 @@
<span class="brand-mark"></span> <span class="brand-mark"></span>
<div> <div>
<h1>EvalStone Launch</h1> <h1>EvalStone Launch</h1>
<p>本地评测启动台 · 基于 <code>bash/run.py</code></p> <p>基于 <code>bash/run.py</code> 的评测启动台</p>
</div> </div>
</div> </div>
<div class="top-right"> <div class="top-right">
@ -24,206 +24,214 @@
<a href="/" class="active">启动台</a> <a href="/" class="active">启动台</a>
<a href="/results">结果分析</a> <a href="/results">结果分析</a>
</nav> </nav>
<div class="top-meta"> <div class="health">
<span id="healthDot" class="dot"></span> <span id="healthDot" class="dot"></span>
<span id="healthText">connecting…</span> <span id="healthText">connecting…</span>
</div> </div>
</div> </div>
</header> </header>
<main class="layout"> <!-- Top action bar -->
<section class="panel form-panel"> <section class="action-bar">
<form id="launchForm"> <label class="field-grow">
<div class="block"> <span>Model</span>
<h2>路径配置(必填)</h2> <input id="model" placeholder="DeepSeek-V4-Flash-Int8" />
<div class="grid-2">
<label class="full">
<span>Dataset Dir</span>
<input id="dataset_dir" name="dataset_dir" required />
</label> </label>
<label class="full"> <label class="field-grow">
<span>Output Dir</span>
<input id="output_dir" name="output_dir" required />
</label>
<label class="full">
<span>Config YAML</span>
<input id="config" name="config" required />
</label>
<label class="full">
<span>Tokenizer Path</span>
<input id="tokenizer_path" name="tokenizer_path" required />
</label>
</div>
</div>
<div class="block">
<h2>模型接入</h2>
<div class="grid-2">
<label>
<span>Model Name</span>
<input id="model" name="model" required placeholder="DeepSeek-V4-Flash-Int8" />
</label>
<label>
<span>API URL</span> <span>API URL</span>
<input id="api_url" name="api_url" required placeholder="http://localhost:30000/v1" /> <input id="api_url" placeholder="http://localhost:30000/v1" />
</label> </label>
<label> <label>
<span>API Key</span> <span>API Key</span>
<input id="api_key" name="api_key" placeholder="EMPTY" /> <input id="api_key" type="password" placeholder="EMPTY" />
</label> </label>
<label> <label class="switch-inline">
<span>Folder Name可选</span> <input type="checkbox" id="thinking" />
<input id="folder_name" name="folder_name" placeholder="默认取 model / model_THINKING" /> <span class="switch-slider"></span>
<span class="switch-label">Thinking</span>
</label> </label>
<div class="action-btns">
<button type="button" id="launchBtn" class="primary">▶ 启动</button>
<button type="button" id="stopBtn" class="danger" disabled>■ 停止</button>
</div> </div>
</section>
<main class="layout">
<!-- Left: configuration tabs -->
<section class="panel main-panel">
<div class="tabs" role="tablist">
<button type="button" class="tab active" data-tab="scope">评测范围</button>
<button type="button" class="tab" data-tab="config">路径配置</button>
<button type="button" class="tab" data-tab="advanced">高级参数</button>
</div> </div>
<div class="block"> <!-- Scope tab -->
<h2>Thinking</h2> <div id="scopeTab" class="tab-pane active">
<div class="thinking-row"> <div class="mode-bar">
<label class="switch"> <span class="mode-label">选择方式</span>
<input type="checkbox" id="thinking" name="thinking" /> <div class="mode-toggle">
<span class="slider"></span> <button type="button" class="mode-btn active" data-mode="suite">按 Suite</button>
<span class="switch-text">启用 thinking 模式</span> <button type="button" class="mode-btn" data-mode="datasets">自选 Benchmark</button>
</label>
<label class="inline">
<span>max_tokens_add</span>
<input type="number" id="max_tokens_add" name="max_tokens_add" value="0" min="0" step="1024" />
</label>
<label class="inline">
<span>thinking scale</span>
<input type="number" id="thinking_max_tokens_scale" name="thinking_max_tokens_scale" value="1.0" min="0.1" step="0.1" />
</label>
</div> </div>
<span id="scopeEstimate" class="estimate"></span>
</div> </div>
<div class="block"> <div id="suitePane">
<h2>评测范围</h2> <div class="section-title">内置 Suite</div>
<div class="mode-tabs" role="tablist"> <div id="suiteList" class="suite-grid"></div>
<button type="button" class="tab active" data-mode="suite">按 Suite / 自定义组合</button>
<button type="button" class="tab" data-mode="datasets">自选 Benchmark</button> <div class="section-title flex-between">
<span>自定义组合</span>
<span id="customSuiteHint" class="hint">在「自选」模式勾选并保存</span>
</div>
<div id="customSuiteList" class="suite-grid"></div>
</div> </div>
<div id="suitePane" class="pane"> <div id="datasetsPane" class="hidden">
<h3 class="subhead">内置 Suite</h3> <div class="bench-toolbar">
<div id="suiteList" class="suite-list"></div> <input id="benchSearch" class="search" placeholder="搜索 benchmark…" />
<h3 class="subhead mt">已保存的自定义组合</h3>
<div id="customSuiteList" class="suite-list"></div>
<p id="customSuiteEmpty" class="muted">暂无自定义组合,可在下方勾选并保存</p>
<div class="custom-editor mt">
<h3 class="subhead">新建 / 更新自定义组合</h3>
<div class="custom-editor-row">
<label>
<span>组合名称</span>
<input id="customSuiteName" placeholder="例如: my_math_code" />
</label>
<div class="custom-editor-actions">
<button type="button" id="saveCustomSuiteBtn" class="primary">永久保存组合</button>
<button type="button" id="loadCheckedToCustomBtn" class="ghost">从下方勾选填入</button>
</div>
</div>
<div id="customPickList" class="benchmark-list compact"></div>
<span id="customPickCount" class="muted">已勾选 0</span>
</div>
<label class="mt">
<span>Exclude逗号分隔可选仅对内置 Suite 生效)</span>
<input id="exclude" name="exclude" placeholder="例如: tau2_bench,hle" />
</label>
</div>
<div id="datasetsPane" class="pane hidden">
<div class="bench-actions">
<button type="button" id="selectAllBtn" class="ghost">全选</button> <button type="button" id="selectAllBtn" class="ghost">全选</button>
<button type="button" id="clearAllBtn" class="ghost">清空</button> <button type="button" id="clearAllBtn" class="ghost">清空</button>
<button type="button" id="saveFromDatasetsBtn" class="ghost">保存为自定义组合</button> <button type="button" id="saveCustomBtn" class="ghost">保存组合</button>
<span id="selectedCount" class="muted">已选 0</span> <span id="selectedCount" class="counter">已选 0</span>
</div> </div>
<div id="benchmarkList" class="benchmark-list"></div> <div id="categoryFilters" class="category-chips"></div>
<div id="benchmarkList" class="benchmark-grid"></div>
</div> </div>
</div> </div>
<details class="block advanced"> <!-- Config tab -->
<summary>其他参数</summary> <div id="configTab" class="tab-pane">
<div class="grid-2 mt"> <div class="path-grid">
<label class="full">
<span>Dataset Dir <small class="muted">evalscope 会自动在其下找 datasets/</small></span>
<input id="dataset_dir" />
</label>
<label class="full">
<span>Output Dir</span>
<input id="output_dir" />
</label>
<label class="full">
<span>Config YAML</span>
<input id="config" />
</label>
<label class="full">
<span>Tokenizer Path</span>
<input id="tokenizer_path" />
</label>
</div>
<div class="preset-bar">
<span class="muted">路径预设</span>
<button type="button" class="ghost" id="presetDefaultBtn">默认</button>
<button type="button" class="ghost" id="presetCurrentBtn">保存当前</button>
<button type="button" class="ghost" id="presetLoadBtn">加载预设</button>
</div>
</div>
<!-- Advanced tab -->
<div id="advancedTab" class="tab-pane">
<div class="grid-4">
<label> <label>
<span>Limitnone = 全量)</span> <span>Folder Name</span>
<input id="limit" name="limit" placeholder="none" /> <input id="folder_name" placeholder="默认 model / model_THINKING" />
</label>
<label>
<span>Limit</span>
<input id="limit" placeholder="none" />
</label> </label>
<label> <label>
<span>Seed</span> <span>Seed</span>
<input type="number" id="seed" name="seed" value="42" /> <input type="number" id="seed" value="42" />
</label> </label>
<label> <label>
<span>Batch Size</span> <span>Batch Size</span>
<input type="number" id="batch_size" name="batch_size" value="4" min="1" /> <input type="number" id="batch_size" value="4" min="1" />
</label>
<label>
<span>max_tokens_add</span>
<input type="number" id="max_tokens_add" value="0" min="0" step="1024" />
</label>
<label>
<span>thinking scale</span>
<input type="number" id="thinking_max_tokens_scale" value="1.0" min="0.1" step="0.1" />
</label> </label>
<label> <label>
<span>Write Summary</span> <span>Write Summary</span>
<select id="write_summary" name="write_summary"> <select id="write_summary">
<option value="true" selected></option> <option value="true" selected></option>
<option value="false"></option> <option value="false"></option>
</select> </select>
</label> </label>
</div>
<details class="fold">
<summary>Judge 模型配置</summary>
<div class="grid-2 mt">
<label> <label>
<span>Judge Model</span> <span>Judge Model</span>
<input id="judge_model" name="judge_model" /> <input id="judge_model" />
</label> </label>
<label> <label>
<span>Judge API URL</span> <span>Judge API URL</span>
<input id="judge_api_url" name="judge_api_url" /> <input id="judge_api_url" />
</label> </label>
<label> <label>
<span>Judge API Key</span> <span>Judge API Key</span>
<input id="judge_api_key" name="judge_api_key" type="password" /> <input id="judge_api_key" type="password" />
</label> </label>
<label> <label>
<span>Judge Max Tokens</span> <span>Judge Max Tokens</span>
<input type="number" id="judge_max_tokens" name="judge_max_tokens" /> <input type="number" id="judge_max_tokens" />
</label> </label>
</div> </div>
</details> </details>
<div class="actions">
<button type="submit" id="launchBtn" class="primary">启动评测</button>
<button type="button" id="stopBtn" class="danger" disabled>停止任务</button>
<span id="formMsg" class="msg"></span>
</div> </div>
</form>
<div id="formMsg" class="form-msg"></div>
</section> </section>
<section class="panel side-panel"> <!-- Right: status panel -->
<div class="side-head"> <section class="panel status-panel">
<div class="active-head">
<h2>运行状态</h2> <h2>运行状态</h2>
<button type="button" id="refreshJobsBtn" class="ghost">刷新</button> <span id="activeStatus" class="badge idle">idle</span>
</div>
<div id="activeCard" class="active-card idle">
<div class="active-id"><code id="activeJobId"></code></div>
<div class="progress-wrap">
<div id="progressBar" class="progress-bar" style="width:0%"></div>
</div>
<div id="progressText" class="progress-text"></div>
</div> </div>
<div id="activeCard" class="active-card idle"> <div class="cmd-wrap">
<div class="active-title"> <div class="cmd-head">
<span id="activeStatus" class="badge">idle</span> <span>命令预览</span>
<code id="activeJobId"></code> <button type="button" id="copyCmdBtn" class="ghost" disabled>复制</button>
</div> </div>
<pre id="activeCmd" class="cmd">尚未启动任务</pre> <pre id="activeCmd" class="cmd">尚未启动任务</pre>
</div> </div>
<div class="log-wrap">
<div class="log-head"> <div class="log-head">
<h3>实时日志</h3> <span>实时日志</span>
<label class="check-inline"> <label class="check-inline">
<input type="checkbox" id="autoScroll" checked /> <input type="checkbox" id="autoScroll" checked />
自动滚动 自动滚动
</label> </label>
</div> </div>
<pre id="logView" class="log-view"></pre> <pre id="logView" class="log-view"></pre>
</div>
<div class="jobs-wrap">
<div class="jobs-head"> <div class="jobs-head">
<h3>历史任务</h3> <h3>历史任务</h3>
<button type="button" id="refreshJobsBtn" class="ghost">刷新</button>
</div> </div>
<div id="jobList" class="job-list"></div> <div id="jobList" class="job-list"></div>
</div>
</section> </section>
</main> </main>
</div> </div>
<script src="/static/app.js"></script> <script src="/static/app.js?v=2"></script>
</body> </body>
</html> </html>

View File

@ -6,7 +6,7 @@
<title>EvalStone Results</title> <title>EvalStone Results</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/static/styles.css" /> <link rel="stylesheet" href="/static/styles.css" />
<script src="/static/vendor/chart.umd.min.js"></script> <script src="/static/vendor/chart.umd.min.js"></script>
</head> </head>
@ -66,9 +66,9 @@
<section class="panel charts-panel"> <section class="panel charts-panel">
<div class="chart-block"> <div class="chart-block">
<div class="side-head"> <div class="chart-head">
<h2>能力分类走势</h2> <h2>能力分类走势</h2>
<span class="muted">横轴 = 能力域(数学/代码/智能体等),纵轴 = 该域平均分</span> <span class="muted">横轴 = 能力域,纵轴 = 该域平均分</span>
</div> </div>
<div class="chart-wrap"> <div class="chart-wrap">
<canvas id="categoryLineChart"></canvas> <canvas id="categoryLineChart"></canvas>
@ -76,7 +76,7 @@
</div> </div>
<div class="chart-block"> <div class="chart-block">
<div class="side-head"> <div class="chart-head">
<h2>能力分类柱状对比</h2> <h2>能力分类柱状对比</h2>
<span class="muted">各模型在同一能力域的平均得分</span> <span class="muted">各模型在同一能力域的平均得分</span>
</div> </div>
@ -86,7 +86,7 @@
</div> </div>
<div class="chart-block"> <div class="chart-block">
<div class="side-head"> <div class="chart-head">
<h2>得分走势</h2> <h2>得分走势</h2>
<span class="muted">横轴 = benchmark纵轴 = score</span> <span class="muted">横轴 = benchmark纵轴 = score</span>
</div> </div>
@ -96,7 +96,7 @@
</div> </div>
<div class="chart-block"> <div class="chart-block">
<div class="side-head"> <div class="chart-head">
<h2>柱状对比</h2> <h2>柱状对比</h2>
<span class="muted">同 benchmark 下各模型得分</span> <span class="muted">同 benchmark 下各模型得分</span>
</div> </div>
@ -106,7 +106,7 @@
</div> </div>
<div class="chart-block"> <div class="chart-block">
<div class="side-head"> <div class="chart-head">
<h2>雷达图</h2> <h2>雷达图</h2>
<span class="muted">能力轮廓(需 ≥3 个共同 benchmark</span> <span class="muted">能力轮廓(需 ≥3 个共同 benchmark</span>
</div> </div>
@ -116,7 +116,7 @@
</div> </div>
<div class="chart-block"> <div class="chart-block">
<div class="side-head"> <div class="chart-head">
<h2>得分表</h2> <h2>得分表</h2>
<span class="muted">平均值(跨 seed / multi-run</span> <span class="muted">平均值(跨 seed / multi-run</span>
</div> </div>
@ -129,7 +129,7 @@
</div> </div>
<div class="chart-block"> <div class="chart-block">
<div class="side-head"> <div class="chart-head">
<h2>各 Benchmark 排名</h2> <h2>各 Benchmark 排名</h2>
</div> </div>
<div id="rankList" class="rank-list"></div> <div id="rankList" class="rank-list"></div>
@ -137,6 +137,6 @@
</section> </section>
</main> </main>
</div> </div>
<script src="/static/results.js?v=20260729"></script> <script src="/static/results.js?v=2"></script>
</body> </body>
</html> </html>

View File

@ -7,7 +7,7 @@
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const COLORS = [ const COLORS = [
'#d6a24a', '#6fbf8a', '#6aa8d6', '#d86a5b', '#d6a24a', '#5ec4a0', '#6aa8d6', '#d9584c',
'#8b7ec8', '#e0c36a', '#5ec4b0', '#c98a6a', '#8b7ec8', '#e0c36a', '#5ec4b0', '#c98a6a',
]; ];
@ -112,11 +112,11 @@
backgroundColor: type === 'radar' ? color + '33' : color + 'cc', backgroundColor: type === 'radar' ? color + '33' : color + 'cc',
tension: 0.25, tension: 0.25,
spanGaps: false, spanGaps: false,
pointRadius: 4, pointRadius: 3,
pointHoverRadius: 6, pointHoverRadius: 5,
}; };
if (type === 'bar') { if (type === 'bar') {
return { ...base, borderWidth: 0, borderRadius: 4 }; return { ...base, borderWidth: 0, borderRadius: 3 };
} }
if (type === 'radar') { if (type === 'radar') {
return { ...base, fill: true, borderWidth: 2 }; return { ...base, fill: true, borderWidth: 2 };
@ -128,18 +128,15 @@
function axisOptions(maxRotation = 45) { function axisOptions(maxRotation = 45) {
return { return {
x: { x: {
ticks: { color: '#92a197', maxRotation, minRotation: 0, font: { size: 12 } }, ticks: { color: '#9aa89d', maxRotation, minRotation: 0, font: { size: 11 } },
grid: { color: 'rgba(51,64,56,0.6)' }, grid: { color: 'rgba(40,48,42,0.8)' },
}, },
y: { y: {
min: 0, min: 0,
max: 100, max: 100,
ticks: { ticks: { color: '#9aa89d', callback: (v) => v + '%', font: { size: 11 } },
color: '#92a197', grid: { color: 'rgba(40,48,42,0.8)' },
callback: (v) => v + '%', title: { display: true, text: 'Score (%)', color: '#9aa89d', font: { size: 11 } },
},
grid: { color: 'rgba(51,64,56,0.6)' },
title: { display: true, text: 'Score (%)', color: '#92a197' },
}, },
}; };
} }
@ -184,7 +181,7 @@
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { legend: {
labels: { color: '#c9d4cb', boxWidth: 12, font: { family: 'IBM Plex Sans', size: 13 } }, labels: { color: '#c5d0c7', boxWidth: 10, font: { family: 'Inter', size: 12 } },
}, },
tooltip: { tooltip: {
callbacks: { callbacks: {
@ -198,7 +195,6 @@
scales: {}, scales: {},
}; };
// Capability-domain charts
const catLabels = compare.category_labels || []; const catLabels = compare.category_labels || [];
const catSeries = compare.category_series || []; const catSeries = compare.category_series || [];
const catLineCanvas = $('categoryLineChart'); const catLineCanvas = $('categoryLineChart');
@ -260,16 +256,16 @@
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { labels: { color: '#c9d4cb', boxWidth: 12 } }, legend: { labels: { color: '#c5d0c7', boxWidth: 10 } },
}, },
scales: { scales: {
r: { r: {
min: 0, min: 0,
max: 100, max: 100,
ticks: { color: '#92a197', backdropColor: 'transparent', stepSize: 20 }, ticks: { color: '#9aa89d', backdropColor: 'transparent', stepSize: 20, font: { size: 10 } },
grid: { color: 'rgba(51,64,56,0.7)' }, grid: { color: 'rgba(40,48,42,0.8)' },
angleLines: { color: 'rgba(51,64,56,0.7)' }, angleLines: { color: 'rgba(40,48,42,0.8)' },
pointLabels: { color: '#c9d4cb', font: { size: 12 } }, pointLabels: { color: '#c5d0c7', font: { size: 11 } },
}, },
}, },
}, },
@ -387,7 +383,6 @@
document.querySelectorAll('#benchList input').forEach((el) => { el.checked = false; }); document.querySelectorAll('#benchList input').forEach((el) => { el.checked = false; });
}); });
// bootstrap defaults from meta
api('/api/meta').then((meta) => { api('/api/meta').then((meta) => {
$('outputDir').value = meta.defaults?.output_dir || ''; $('outputDir').value = meta.defaults?.output_dir || '';
return loadOverview(); return loadOverview();

File diff suppressed because it is too large Load Diff