From 28f22c11e3483fbf1a90c9352921c43ec74a8790 Mon Sep 17 00:00:00 2001
From: sora <2075279110@qq.com>
Date: Wed, 29 Jul 2026 08:08:06 +0000
Subject: [PATCH] Redesign webui: compact layout, search/filter, command
preview, progress bar, presets
---
webui/server.py | 33 +-
webui/start.sh | 2 +-
webui/static/app.js | 600 ++++++++++--------
webui/static/index.html | 322 +++++-----
webui/static/results.html | 20 +-
webui/static/results.js | 35 +-
webui/static/styles.css | 1258 ++++++++++++++++++++-----------------
7 files changed, 1256 insertions(+), 1014 deletions(-)
diff --git a/webui/server.py b/webui/server.py
index 8f04dc7..5e1dba9 100644
--- a/webui/server.py
+++ b/webui/server.py
@@ -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,
diff --git a/webui/start.sh b/webui/start.sh
index 5454d27..99cc501 100755
--- a/webui/start.sh
+++ b/webui/start.sh
@@ -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")"
diff --git a/webui/static/app.js b/webui/static/app.js
index 0faf16d..f8e2586 100644
--- a/webui/static/app.js
+++ b/webui/static/app.js
@@ -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, '"');
+ }
+
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, '"');
+ // ---- 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 = `
${escapeHtml(name)}
- ${info.all.length} benchmarks
-
${names}
+
+ ${count} benches
+ ${est ? `${formatHours(est)}` : ''}
+
`;
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 = `
${escapeHtml(name)}
- ${items.length} benchmarks · 自定义
- ${items.map(escapeHtml).join(', ')}
+
+ ${items.length} benches
+ 自定义
+
+
`;
- 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 = `
-
- ${escapeHtml(cat.name)}
- ${cat.items.length}
-
-
-
-
-
- `;
- 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 = `${escapeHtml(name)}`;
- 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 = `
+
+ ${escapeHtml(name)}
+ ${tags.map((t) => `${escapeHtml(t.text)}`).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 = `
-
+
${escapeHtml(job.id)}
${escapeHtml(job.status)}
-
${escapeHtml(model)} · ${thinking}
+
${escapeHtml(model)} · ${mode} · ${escapeHtml(scope)}
`;
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();
})();
diff --git a/webui/static/index.html b/webui/static/index.html
index f3651db..ac6250d 100644
--- a/webui/static/index.html
+++ b/webui/static/index.html
@@ -6,7 +6,7 @@
EvalStone Launch
-
+
@@ -16,7 +16,7 @@
EvalStone Launch
-
本地评测启动台 · 基于 bash/run.py
+
基于 bash/run.py 的评测启动台
@@ -24,206 +24,214 @@
启动台
结果分析
-
+
+
+
-