Redesign webui: compact layout, search/filter, command preview, progress bar, presets
This commit is contained in:
parent
541b0b477d
commit
28f22c11e3
@ -184,7 +184,7 @@ class LaunchRequest(BaseModel):
|
||||
BENCHMARK_CATEGORIES = [
|
||||
{
|
||||
'id': 'math',
|
||||
'name': '数学推理',
|
||||
'name': '数学',
|
||||
'items': [
|
||||
'aime24', 'aime25', 'aime26', 'hmmt26',
|
||||
'imo_answerbench', 'competition_math', 'gsm8k',
|
||||
@ -197,12 +197,12 @@ BENCHMARK_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
'id': 'science',
|
||||
'name': '科学 / 高难推理',
|
||||
'name': '科学',
|
||||
'items': ['gpqa_diamond', 'super_gpqa', 'hle'],
|
||||
},
|
||||
{
|
||||
'id': 'knowledge',
|
||||
'name': '知识与通用能力',
|
||||
'name': '知识',
|
||||
'items': [
|
||||
'mmlu', 'mmlu_pro', 'cmmlu', 'bbh', 'arc',
|
||||
'drop', 'hellaswag', 'winogrande', 'simple_qa', 'trivia_qa',
|
||||
@ -215,11 +215,34 @@ BENCHMARK_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
'id': 'tool_agent',
|
||||
'name': '工具调用 / 智能体',
|
||||
'name': '智能体',
|
||||
'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:
|
||||
all_benchmarks = sorted(
|
||||
@ -247,6 +270,8 @@ def _meta() -> dict:
|
||||
'benchmarks': all_benchmarks,
|
||||
'categories': categories,
|
||||
'multi_run': run_module.MULTI_RUN_CONFIG,
|
||||
'suite_estimates': SUITE_ESTIMATES,
|
||||
'benchmark_requirements': BENCHMARK_REQUIREMENTS,
|
||||
'defaults': {
|
||||
'model': run_module.DEFAULT_MODEL,
|
||||
'api_url': run_module.DEFAULT_API_URL,
|
||||
|
||||
@ -5,7 +5,7 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$DIR"
|
||||
|
||||
HOST="${WEBUI_HOST:-0.0.0.0}"
|
||||
PORT="${WEBUI_PORT:-7860}"
|
||||
PORT="${WEBUI_PORT:-7861}"
|
||||
|
||||
echo "EvalStone Launch Panel"
|
||||
echo " project : $(dirname "$DIR")"
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
(() => {
|
||||
const state = {
|
||||
meta: null,
|
||||
tab: 'scope',
|
||||
selectionMode: 'suite',
|
||||
suiteKind: 'builtin', // builtin | custom
|
||||
suiteKind: 'builtin',
|
||||
suite: 'official',
|
||||
customSuites: {},
|
||||
activeJobId: null,
|
||||
pollTimer: null,
|
||||
logOffset: 0,
|
||||
activeCategory: 'all',
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const form = $('launchForm');
|
||||
const formMsg = $('formMsg');
|
||||
const logView = $('logView');
|
||||
const jobList = $('jobList');
|
||||
|
||||
function setMsg(text, type = '') {
|
||||
formMsg.textContent = text || '';
|
||||
formMsg.className = `msg ${type}`.trim();
|
||||
formMsg.className = `form-msg ${type}`.trim();
|
||||
}
|
||||
|
||||
async function api(path, options) {
|
||||
@ -34,6 +36,14 @@
|
||||
return data;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function fillDefaults(meta) {
|
||||
const d = meta.defaults;
|
||||
$('model').value = d.model || '';
|
||||
@ -54,34 +64,60 @@
|
||||
state.customSuites = meta.custom_suites || {};
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
// ---- Tabs ----
|
||||
function setTab(tab) {
|
||||
state.tab = tab;
|
||||
document.querySelectorAll('.tabs .tab').forEach((el) => {
|
||||
el.classList.toggle('active', el.dataset.tab === tab);
|
||||
});
|
||||
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() {
|
||||
const box = $('suiteList');
|
||||
box.innerHTML = '';
|
||||
const suites = state.meta.suites;
|
||||
const suites = state.meta.suites || {};
|
||||
const estimates = state.meta.suite_estimates || {};
|
||||
Object.keys(suites).forEach((name) => {
|
||||
const info = suites[name];
|
||||
const count = info.all.length;
|
||||
const est = estimates[name];
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = `suite-card${state.suiteKind === 'builtin' && state.suite === name ? ' active' : ''}`;
|
||||
const names = info.all.map(escapeHtml).join(', ');
|
||||
btn.innerHTML = `
|
||||
<strong>${escapeHtml(name)}</strong>
|
||||
<span class="suite-count">${info.all.length} benchmarks</span>
|
||||
<div class="suite-names">${names}</div>
|
||||
<div class="suite-meta">
|
||||
<span>${count} benches</span>
|
||||
${est ? `<span>${formatHours(est)}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
btn.addEventListener('click', () => {
|
||||
state.suiteKind = 'builtin';
|
||||
state.suite = name;
|
||||
renderSuites();
|
||||
renderCustomSuites();
|
||||
updateEstimate();
|
||||
});
|
||||
box.appendChild(btn);
|
||||
});
|
||||
@ -89,43 +125,23 @@
|
||||
|
||||
function renderCustomSuites() {
|
||||
const box = $('customSuiteList');
|
||||
const empty = $('customSuiteEmpty');
|
||||
box.innerHTML = '';
|
||||
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) => {
|
||||
const items = state.customSuites[name] || [];
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = `suite-card custom${state.suiteKind === 'custom' && state.suite === name ? ' active' : ''}`;
|
||||
|
||||
const main = document.createElement('button');
|
||||
main.type = 'button';
|
||||
main.className = 'suite-card-main';
|
||||
main.innerHTML = `
|
||||
wrap.className = `suite-card${state.suiteKind === 'custom' && state.suite === name ? ' active' : ''}`;
|
||||
wrap.innerHTML = `
|
||||
<strong>${escapeHtml(name)}</strong>
|
||||
<span class="suite-count">${items.length} benchmarks · 自定义</span>
|
||||
<div class="suite-names">${items.map(escapeHtml).join(', ')}</div>
|
||||
<div class="suite-meta">
|
||||
<span>${items.length} benches</span>
|
||||
<span>自定义</span>
|
||||
</div>
|
||||
<button type="button" class="ghost del">删除</button>
|
||||
`;
|
||||
main.addEventListener('click', () => {
|
||||
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) => {
|
||||
wrap.querySelector('.del').addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm(`删除自定义组合「${name}」?`)) return;
|
||||
try {
|
||||
@ -135,110 +151,141 @@
|
||||
state.suiteKind = 'builtin';
|
||||
state.suite = state.meta.defaults?.suite || 'official';
|
||||
}
|
||||
renderSuites();
|
||||
renderCustomSuites();
|
||||
renderSuites();
|
||||
setMsg(`已删除组合: ${name}`, 'ok');
|
||||
} catch (err) {
|
||||
setMsg(err.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
wrap.appendChild(main);
|
||||
wrap.appendChild(del);
|
||||
wrap.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.del')) return;
|
||||
state.suiteKind = 'custom';
|
||||
state.suite = name;
|
||||
renderSuites();
|
||||
renderCustomSuites();
|
||||
updateEstimate();
|
||||
});
|
||||
box.appendChild(wrap);
|
||||
});
|
||||
}
|
||||
|
||||
function renderBenchmarkPicker(containerId, withCountCb) {
|
||||
const box = $(containerId);
|
||||
box.innerHTML = '';
|
||||
const multi = new Set(Object.keys(state.meta.multi_run || {}));
|
||||
const categories = state.meta.categories || [
|
||||
{ id: 'all', name: '全部', items: state.meta.benchmarks || [] },
|
||||
];
|
||||
|
||||
categories.forEach((cat) => {
|
||||
const section = document.createElement('section');
|
||||
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);
|
||||
grid.appendChild(label);
|
||||
});
|
||||
section.appendChild(grid);
|
||||
box.appendChild(section);
|
||||
});
|
||||
withCountCb();
|
||||
// ---- Benchmark picker ----
|
||||
function benchmarkTags(name) {
|
||||
const tags = [];
|
||||
const multi = state.meta.multi_run || {};
|
||||
const req = state.meta.benchmark_requirements || {};
|
||||
if (multi[name]) tags.push({ text: `×${multi[name]}`, cls: '' });
|
||||
if (req[name]) {
|
||||
if (req[name].sandbox) tags.push({ text: 'sandbox', cls: 'warn' });
|
||||
else if (req[name].swe) tags.push({ text: 'swe', cls: 'warn' });
|
||||
else if (req[name].agent) tags.push({ text: 'agent', cls: 'warn' });
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
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() {
|
||||
renderBenchmarkPicker('customPickList', updateCustomPickCount);
|
||||
function renderCategoryChips() {
|
||||
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() {
|
||||
const n = [...document.querySelectorAll('#benchmarkList input:checked')].length;
|
||||
$('selectedCount').textContent = `已选 ${n}`;
|
||||
updateEstimate();
|
||||
}
|
||||
|
||||
function updateCustomPickCount() {
|
||||
const n = [...document.querySelectorAll('#customPickList input:checked')].length;
|
||||
$('customPickCount').textContent = `已勾选 ${n}`;
|
||||
}
|
||||
|
||||
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() {
|
||||
function updateEstimate() {
|
||||
const el = $('scopeEstimate');
|
||||
if (state.selectionMode === 'suite') {
|
||||
const est = (state.meta.suite_estimates || {})[state.suite];
|
||||
el.textContent = est ? `预计 ${formatHours(est)}` : '';
|
||||
return;
|
||||
}
|
||||
const datasets = [...document.querySelectorAll('#benchmarkList input:checked')].map((el) => el.value);
|
||||
const excludeRaw = $('exclude').value.trim();
|
||||
const exclude = excludeRaw
|
||||
? excludeRaw.split(',').map((s) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
if (!datasets.length) {
|
||||
el.textContent = '';
|
||||
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 suite = state.suite;
|
||||
@ -247,10 +294,9 @@
|
||||
if (selectionMode === 'suite' && state.suiteKind === 'custom') {
|
||||
selectionMode = 'datasets';
|
||||
finalDatasets = [...(state.customSuites[state.suite] || [])];
|
||||
suite = state.suite;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
return {
|
||||
model: $('model').value.trim(),
|
||||
api_url: $('api_url').value.trim(),
|
||||
api_key: $('api_key').value.trim() || 'EMPTY',
|
||||
@ -258,7 +304,7 @@
|
||||
selection_mode: selectionMode,
|
||||
suite,
|
||||
datasets: finalDatasets,
|
||||
exclude: selectionMode === 'suite' ? exclude : [],
|
||||
exclude,
|
||||
folder_name: $('folder_name').value.trim() || null,
|
||||
limit: $('limit').value.trim() || null,
|
||||
seed: Number($('seed').value || 42),
|
||||
@ -272,27 +318,17 @@
|
||||
judge_model: $('judge_model').value.trim() || null,
|
||||
judge_api_url: $('judge_api_url').value.trim() || null,
|
||||
judge_api_key: $('judge_api_key').value.trim() || null,
|
||||
judge_max_tokens: $('judge_max_tokens').value
|
||||
? Number($('judge_max_tokens').value)
|
||||
: null,
|
||||
judge_max_tokens: $('judge_max_tokens').value ? Number($('judge_max_tokens').value) : null,
|
||||
write_summary: $('write_summary').value === 'true',
|
||||
};
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function saveCustomSuite(name, datasets) {
|
||||
const data = await api('/api/custom-suites', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, datasets }),
|
||||
});
|
||||
state.customSuites = data.suites || {};
|
||||
state.suiteKind = 'custom';
|
||||
state.suite = name;
|
||||
renderSuites();
|
||||
renderCustomSuites();
|
||||
setMsg(`已永久保存组合: ${name}(${datasets.length} 个)`, 'ok');
|
||||
function updateCommandPreview(cmd) {
|
||||
$('activeCmd').textContent = (cmd || []).join(' ');
|
||||
$('copyCmdBtn').disabled = !cmd;
|
||||
}
|
||||
|
||||
// ---- Jobs ----
|
||||
function statusClass(status) {
|
||||
return status || 'idle';
|
||||
}
|
||||
@ -301,10 +337,12 @@
|
||||
const card = $('activeCard');
|
||||
if (!job) {
|
||||
card.className = 'active-card idle';
|
||||
$('activeStatus').className = 'badge';
|
||||
$('activeStatus').className = 'badge idle';
|
||||
$('activeStatus').textContent = 'idle';
|
||||
$('activeJobId').textContent = '—';
|
||||
$('activeCmd').textContent = '尚未启动任务';
|
||||
$('progressBar').style.width = '0%';
|
||||
$('progressText').textContent = '';
|
||||
updateCommandPreview(null);
|
||||
$('stopBtn').disabled = true;
|
||||
return;
|
||||
}
|
||||
@ -312,9 +350,9 @@
|
||||
$('activeStatus').className = `badge ${statusClass(job.status)}`;
|
||||
$('activeStatus').textContent = job.status;
|
||||
$('activeJobId').textContent = job.id;
|
||||
$('activeCmd').textContent = (job.command || []).join(' ');
|
||||
$('stopBtn').disabled = job.status !== 'running';
|
||||
state.activeJobId = job.id;
|
||||
updateCommandPreview(job.command);
|
||||
}
|
||||
|
||||
function renderJobs(jobs, activeId) {
|
||||
@ -326,18 +364,20 @@
|
||||
jobs.forEach((job) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'job-item';
|
||||
btn.className = `job-item${job.id === activeId ? ' active' : ''}`;
|
||||
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 = `
|
||||
<div class="row">
|
||||
<div class="job-row">
|
||||
<strong>${escapeHtml(job.id)}</strong>
|
||||
<span class="badge ${statusClass(job.status)}">${escapeHtml(job.status)}</span>
|
||||
</div>
|
||||
<small>${escapeHtml(model)} · ${thinking}</small>
|
||||
<small>${escapeHtml(model)} · ${mode} · ${escapeHtml(scope)}</small>
|
||||
`;
|
||||
btn.addEventListener('click', () => followJob(job.id, true));
|
||||
if (job.id === activeId) btn.style.borderColor = 'rgba(214,162,74,0.75)';
|
||||
jobList.appendChild(btn);
|
||||
});
|
||||
}
|
||||
@ -355,6 +395,19 @@
|
||||
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) {
|
||||
if (!state.activeJobId) return;
|
||||
if (reset) {
|
||||
@ -366,12 +419,22 @@
|
||||
logView.textContent += data.content;
|
||||
state.logOffset = data.next_offset;
|
||||
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) {
|
||||
$('activeStatus').className = `badge ${statusClass(data.status)}`;
|
||||
$('activeStatus').textContent = data.status;
|
||||
$('activeCard').className = `active-card ${statusClass(data.status)}`;
|
||||
$('stopBtn').disabled = data.status !== 'running';
|
||||
if (data.done) {
|
||||
$('progressBar').style.width = '100%';
|
||||
$('progressText').textContent = data.status === 'completed' ? '已完成' : `结束: ${data.status}`;
|
||||
}
|
||||
}
|
||||
if (data.done) stopPolling();
|
||||
}
|
||||
@ -403,114 +466,26 @@
|
||||
else stopPolling();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
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();
|
||||
async function createJob() {
|
||||
setMsg('');
|
||||
const requiredPaths = [
|
||||
const required = [
|
||||
['dataset_dir', 'Dataset Dir'],
|
||||
['output_dir', 'Output Dir'],
|
||||
['config', 'Config YAML'],
|
||||
['tokenizer_path', 'Tokenizer Path'],
|
||||
];
|
||||
for (const [id, label] of requiredPaths) {
|
||||
for (const [id, label] of required) {
|
||||
if (!$(id).value.trim()) {
|
||||
setMsg(`请填写必填路径: ${label}`, 'error');
|
||||
setTab('config');
|
||||
$(id).focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const payload = collectPayload();
|
||||
const payload = buildCommandPayload();
|
||||
if (payload.selection_mode === 'datasets' && !payload.datasets.length) {
|
||||
setMsg('请至少选择一个 benchmark / 自定义组合', 'error');
|
||||
setMsg('请至少选择一个 benchmark', 'error');
|
||||
setTab('scope');
|
||||
return;
|
||||
}
|
||||
$('launchBtn').disabled = true;
|
||||
@ -531,8 +506,139 @@
|
||||
} finally {
|
||||
$('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 () => {
|
||||
if (!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();
|
||||
})();
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<title>EvalStone Launch</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<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" />
|
||||
</head>
|
||||
<body>
|
||||
@ -16,7 +16,7 @@
|
||||
<span class="brand-mark"></span>
|
||||
<div>
|
||||
<h1>EvalStone Launch</h1>
|
||||
<p>本地评测启动台 · 基于 <code>bash/run.py</code></p>
|
||||
<p>基于 <code>bash/run.py</code> 的评测启动台</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
@ -24,206 +24,214 @@
|
||||
<a href="/" class="active">启动台</a>
|
||||
<a href="/results">结果分析</a>
|
||||
</nav>
|
||||
<div class="top-meta">
|
||||
<div class="health">
|
||||
<span id="healthDot" class="dot"></span>
|
||||
<span id="healthText">connecting…</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Top action bar -->
|
||||
<section class="action-bar">
|
||||
<label class="field-grow">
|
||||
<span>Model</span>
|
||||
<input id="model" placeholder="DeepSeek-V4-Flash-Int8" />
|
||||
</label>
|
||||
<label class="field-grow">
|
||||
<span>API URL</span>
|
||||
<input id="api_url" placeholder="http://localhost:30000/v1" />
|
||||
</label>
|
||||
<label>
|
||||
<span>API Key</span>
|
||||
<input id="api_key" type="password" placeholder="EMPTY" />
|
||||
</label>
|
||||
<label class="switch-inline">
|
||||
<input type="checkbox" id="thinking" />
|
||||
<span class="switch-slider"></span>
|
||||
<span class="switch-label">Thinking</span>
|
||||
</label>
|
||||
<div class="action-btns">
|
||||
<button type="button" id="launchBtn" class="primary">▶ 启动</button>
|
||||
<button type="button" id="stopBtn" class="danger" disabled>■ 停止</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="layout">
|
||||
<section class="panel form-panel">
|
||||
<form id="launchForm">
|
||||
<div class="block">
|
||||
<h2>路径配置(必填)</h2>
|
||||
<div class="grid-2">
|
||||
<label class="full">
|
||||
<span>Dataset Dir</span>
|
||||
<input id="dataset_dir" name="dataset_dir" required />
|
||||
</label>
|
||||
<label class="full">
|
||||
<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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Scope tab -->
|
||||
<div id="scopeTab" class="tab-pane active">
|
||||
<div class="mode-bar">
|
||||
<span class="mode-label">选择方式</span>
|
||||
<div class="mode-toggle">
|
||||
<button type="button" class="mode-btn active" data-mode="suite">按 Suite</button>
|
||||
<button type="button" class="mode-btn" data-mode="datasets">自选 Benchmark</button>
|
||||
</div>
|
||||
<span id="scopeEstimate" class="estimate"></span>
|
||||
</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>
|
||||
<input id="api_url" name="api_url" required placeholder="http://localhost:30000/v1" />
|
||||
</label>
|
||||
<label>
|
||||
<span>API Key</span>
|
||||
<input id="api_key" name="api_key" placeholder="EMPTY" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Folder Name(可选)</span>
|
||||
<input id="folder_name" name="folder_name" placeholder="默认取 model / model_THINKING" />
|
||||
</label>
|
||||
<div id="suitePane">
|
||||
<div class="section-title">内置 Suite</div>
|
||||
<div id="suiteList" class="suite-grid"></div>
|
||||
|
||||
<div class="section-title flex-between">
|
||||
<span>自定义组合</span>
|
||||
<span id="customSuiteHint" class="hint">在「自选」模式勾选并保存</span>
|
||||
</div>
|
||||
<div id="customSuiteList" class="suite-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2>Thinking</h2>
|
||||
<div class="thinking-row">
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="thinking" name="thinking" />
|
||||
<span class="slider"></span>
|
||||
<span class="switch-text">启用 thinking 模式</span>
|
||||
</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 id="datasetsPane" class="hidden">
|
||||
<div class="bench-toolbar">
|
||||
<input id="benchSearch" class="search" placeholder="搜索 benchmark…" />
|
||||
<button type="button" id="selectAllBtn" class="ghost">全选</button>
|
||||
<button type="button" id="clearAllBtn" class="ghost">清空</button>
|
||||
<button type="button" id="saveCustomBtn" class="ghost">保存组合</button>
|
||||
<span id="selectedCount" class="counter">已选 0</span>
|
||||
</div>
|
||||
<div id="categoryFilters" class="category-chips"></div>
|
||||
<div id="benchmarkList" class="benchmark-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2>评测范围</h2>
|
||||
<div class="mode-tabs" role="tablist">
|
||||
<button type="button" class="tab active" data-mode="suite">按 Suite / 自定义组合</button>
|
||||
<button type="button" class="tab" data-mode="datasets">自选 Benchmark</button>
|
||||
</div>
|
||||
|
||||
<div id="suitePane" class="pane">
|
||||
<h3 class="subhead">内置 Suite</h3>
|
||||
<div id="suiteList" class="suite-list"></div>
|
||||
|
||||
<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="clearAllBtn" class="ghost">清空</button>
|
||||
<button type="button" id="saveFromDatasetsBtn" class="ghost">保存为自定义组合</button>
|
||||
<span id="selectedCount" class="muted">已选 0</span>
|
||||
</div>
|
||||
<div id="benchmarkList" class="benchmark-list"></div>
|
||||
</div>
|
||||
<!-- Config tab -->
|
||||
<div id="configTab" class="tab-pane">
|
||||
<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>
|
||||
|
||||
<details class="block advanced">
|
||||
<summary>其他参数</summary>
|
||||
<!-- Advanced tab -->
|
||||
<div id="advancedTab" class="tab-pane">
|
||||
<div class="grid-4">
|
||||
<label>
|
||||
<span>Folder Name</span>
|
||||
<input id="folder_name" placeholder="默认 model / model_THINKING" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Limit</span>
|
||||
<input id="limit" placeholder="none" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Seed</span>
|
||||
<input type="number" id="seed" value="42" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Batch Size</span>
|
||||
<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>
|
||||
<span>Write Summary</span>
|
||||
<select id="write_summary">
|
||||
<option value="true" selected>是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<details class="fold">
|
||||
<summary>Judge 模型配置</summary>
|
||||
<div class="grid-2 mt">
|
||||
<label>
|
||||
<span>Limit(none = 全量)</span>
|
||||
<input id="limit" name="limit" placeholder="none" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Seed</span>
|
||||
<input type="number" id="seed" name="seed" value="42" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Batch Size</span>
|
||||
<input type="number" id="batch_size" name="batch_size" value="4" min="1" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Write Summary</span>
|
||||
<select id="write_summary" name="write_summary">
|
||||
<option value="true" selected>是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Judge Model</span>
|
||||
<input id="judge_model" name="judge_model" />
|
||||
<input id="judge_model" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Judge API URL</span>
|
||||
<input id="judge_api_url" name="judge_api_url" />
|
||||
<input id="judge_api_url" />
|
||||
</label>
|
||||
<label>
|
||||
<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>
|
||||
<span>Judge Max Tokens</span>
|
||||
<input type="number" id="judge_max_tokens" name="judge_max_tokens" />
|
||||
<input type="number" id="judge_max_tokens" />
|
||||
</label>
|
||||
</div>
|
||||
</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>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel side-panel">
|
||||
<div class="side-head">
|
||||
<h2>运行状态</h2>
|
||||
<button type="button" id="refreshJobsBtn" class="ghost">刷新</button>
|
||||
</div>
|
||||
|
||||
<div id="formMsg" class="form-msg"></div>
|
||||
</section>
|
||||
|
||||
<!-- Right: status panel -->
|
||||
<section class="panel status-panel">
|
||||
<div class="active-head">
|
||||
<h2>运行状态</h2>
|
||||
<span id="activeStatus" class="badge idle">idle</span>
|
||||
</div>
|
||||
<div id="activeCard" class="active-card idle">
|
||||
<div class="active-title">
|
||||
<span id="activeStatus" class="badge">idle</span>
|
||||
<code id="activeJobId">—</code>
|
||||
<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 class="cmd-wrap">
|
||||
<div class="cmd-head">
|
||||
<span>命令预览</span>
|
||||
<button type="button" id="copyCmdBtn" class="ghost" disabled>复制</button>
|
||||
</div>
|
||||
<pre id="activeCmd" class="cmd">尚未启动任务</pre>
|
||||
</div>
|
||||
|
||||
<div class="log-head">
|
||||
<h3>实时日志</h3>
|
||||
<label class="check-inline">
|
||||
<input type="checkbox" id="autoScroll" checked />
|
||||
自动滚动
|
||||
</label>
|
||||
<div class="log-wrap">
|
||||
<div class="log-head">
|
||||
<span>实时日志</span>
|
||||
<label class="check-inline">
|
||||
<input type="checkbox" id="autoScroll" checked />
|
||||
自动滚动
|
||||
</label>
|
||||
</div>
|
||||
<pre id="logView" class="log-view"></pre>
|
||||
</div>
|
||||
<pre id="logView" class="log-view"></pre>
|
||||
|
||||
<div class="jobs-head">
|
||||
<h3>历史任务</h3>
|
||||
<div class="jobs-wrap">
|
||||
<div class="jobs-head">
|
||||
<h3>历史任务</h3>
|
||||
<button type="button" id="refreshJobsBtn" class="ghost">刷新</button>
|
||||
</div>
|
||||
<div id="jobList" class="job-list"></div>
|
||||
</div>
|
||||
<div id="jobList" class="job-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/static/app.js"></script>
|
||||
<script src="/static/app.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<title>EvalStone Results</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<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" />
|
||||
<script src="/static/vendor/chart.umd.min.js"></script>
|
||||
</head>
|
||||
@ -66,9 +66,9 @@
|
||||
|
||||
<section class="panel charts-panel">
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<div class="chart-head">
|
||||
<h2>能力分类走势</h2>
|
||||
<span class="muted">横轴 = 能力域(数学/代码/智能体等),纵轴 = 该域平均分</span>
|
||||
<span class="muted">横轴 = 能力域,纵轴 = 该域平均分</span>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<canvas id="categoryLineChart"></canvas>
|
||||
@ -76,7 +76,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<div class="chart-head">
|
||||
<h2>能力分类柱状对比</h2>
|
||||
<span class="muted">各模型在同一能力域的平均得分</span>
|
||||
</div>
|
||||
@ -86,7 +86,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<div class="chart-head">
|
||||
<h2>得分走势</h2>
|
||||
<span class="muted">横轴 = benchmark,纵轴 = score</span>
|
||||
</div>
|
||||
@ -96,7 +96,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<div class="chart-head">
|
||||
<h2>柱状对比</h2>
|
||||
<span class="muted">同 benchmark 下各模型得分</span>
|
||||
</div>
|
||||
@ -106,7 +106,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<div class="chart-head">
|
||||
<h2>雷达图</h2>
|
||||
<span class="muted">能力轮廓(需 ≥3 个共同 benchmark)</span>
|
||||
</div>
|
||||
@ -116,7 +116,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<div class="chart-head">
|
||||
<h2>得分表</h2>
|
||||
<span class="muted">平均值(跨 seed / multi-run)</span>
|
||||
</div>
|
||||
@ -129,7 +129,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<div class="chart-head">
|
||||
<h2>各 Benchmark 排名</h2>
|
||||
</div>
|
||||
<div id="rankList" class="rank-list"></div>
|
||||
@ -137,6 +137,6 @@
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/static/results.js?v=20260729"></script>
|
||||
<script src="/static/results.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const COLORS = [
|
||||
'#d6a24a', '#6fbf8a', '#6aa8d6', '#d86a5b',
|
||||
'#d6a24a', '#5ec4a0', '#6aa8d6', '#d9584c',
|
||||
'#8b7ec8', '#e0c36a', '#5ec4b0', '#c98a6a',
|
||||
];
|
||||
|
||||
@ -112,11 +112,11 @@
|
||||
backgroundColor: type === 'radar' ? color + '33' : color + 'cc',
|
||||
tension: 0.25,
|
||||
spanGaps: false,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5,
|
||||
};
|
||||
if (type === 'bar') {
|
||||
return { ...base, borderWidth: 0, borderRadius: 4 };
|
||||
return { ...base, borderWidth: 0, borderRadius: 3 };
|
||||
}
|
||||
if (type === 'radar') {
|
||||
return { ...base, fill: true, borderWidth: 2 };
|
||||
@ -128,18 +128,15 @@
|
||||
function axisOptions(maxRotation = 45) {
|
||||
return {
|
||||
x: {
|
||||
ticks: { color: '#92a197', maxRotation, minRotation: 0, font: { size: 12 } },
|
||||
grid: { color: 'rgba(51,64,56,0.6)' },
|
||||
ticks: { color: '#9aa89d', maxRotation, minRotation: 0, font: { size: 11 } },
|
||||
grid: { color: 'rgba(40,48,42,0.8)' },
|
||||
},
|
||||
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' },
|
||||
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 } },
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -184,7 +181,7 @@
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: { color: '#c9d4cb', boxWidth: 12, font: { family: 'IBM Plex Sans', size: 13 } },
|
||||
labels: { color: '#c5d0c7', boxWidth: 10, font: { family: 'Inter', size: 12 } },
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
@ -198,7 +195,6 @@
|
||||
scales: {},
|
||||
};
|
||||
|
||||
// Capability-domain charts
|
||||
const catLabels = compare.category_labels || [];
|
||||
const catSeries = compare.category_series || [];
|
||||
const catLineCanvas = $('categoryLineChart');
|
||||
@ -260,16 +256,16 @@
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { labels: { color: '#c9d4cb', boxWidth: 12 } },
|
||||
legend: { labels: { color: '#c5d0c7', boxWidth: 10 } },
|
||||
},
|
||||
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 } },
|
||||
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 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -387,7 +383,6 @@
|
||||
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();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user