(() => {
const state = {
meta: null,
selectionMode: 'suite',
suite: 'official',
activeJobId: null,
pollTimer: null,
logOffset: 0,
};
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();
}
async function api(path, options) {
const res = await fetch(path, {
headers: { 'Content-Type': 'application/json' },
...options,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const detail = data.detail || res.statusText || 'request failed';
throw new Error(typeof detail === 'string' ? detail : JSON.stringify(detail));
}
return data;
}
function fillDefaults(meta) {
const d = meta.defaults;
$('model').value = d.model || '';
$('api_url').value = d.api_url || '';
$('api_key').value = d.api_key || 'EMPTY';
$('seed').value = d.seed;
$('batch_size').value = d.batch_size;
$('thinking').checked = !!d.thinking;
$('dataset_dir').value = d.dataset_dir || '';
$('output_dir').value = d.output_dir || '';
$('config').value = d.config || '';
$('tokenizer_path').value = d.tokenizer_path || '';
$('judge_model').value = d.judge_model || '';
$('judge_api_url').value = d.judge_api_url || '';
$('judge_max_tokens').value = d.judge_max_tokens || '';
state.suite = d.suite || 'official';
}
function renderSuites() {
const box = $('suiteList');
box.innerHTML = '';
const suites = state.meta.suites;
Object.keys(suites).forEach((name) => {
const info = suites[name];
const btn = document.createElement('button');
btn.type = 'button';
btn.className = `suite-card${state.suite === name ? ' active' : ''}`;
btn.innerHTML = `${name}${info.all.length} benchmarks
${info.all.slice(0, 6).join(', ')}${info.all.length > 6 ? '…' : ''}`;
btn.addEventListener('click', () => {
state.suite = name;
renderSuites();
});
box.appendChild(btn);
});
}
function renderBenchmarks() {
const box = $('benchmarkList');
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}`;
section.dataset.category = cat.id;
const head = document.createElement('div');
head.className = 'bench-cat-head';
head.innerHTML = `
${cat.name}
${cat.items.length}
`;
head.querySelector('.cat-select').addEventListener('click', () => {
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = true; });
updateSelectedCount();
});
head.querySelector('.cat-clear').addEventListener('click', () => {
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = false; });
updateSelectedCount();
});
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 = `${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', updateSelectedCount);
grid.appendChild(label);
});
section.appendChild(grid);
box.appendChild(section);
});
updateSelectedCount();
}
function updateSelectedCount() {
const n = [...document.querySelectorAll('#benchmarkList input:checked')].length;
$('selectedCount').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() {
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)
: [];
const payload = {
model: $('model').value.trim(),
api_url: $('api_url').value.trim(),
api_key: $('api_key').value.trim() || 'EMPTY',
thinking: $('thinking').checked,
selection_mode: state.selectionMode,
suite: state.suite,
datasets,
exclude,
folder_name: $('folder_name').value.trim() || null,
limit: $('limit').value.trim() || null,
seed: Number($('seed').value || 42),
batch_size: Number($('batch_size').value || 4),
thinking_max_tokens_scale: Number($('thinking_max_tokens_scale').value || 1),
max_tokens_add: Number($('max_tokens_add').value || 0),
dataset_dir: $('dataset_dir').value.trim() || null,
output_dir: $('output_dir').value.trim() || null,
config: $('config').value.trim() || null,
tokenizer_path: $('tokenizer_path').value.trim() || null,
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,
write_summary: $('write_summary').value === 'true',
};
return payload;
}
function statusClass(status) {
return status || 'idle';
}
function renderActive(job) {
const card = $('activeCard');
if (!job) {
card.className = 'active-card idle';
$('activeStatus').className = 'badge';
$('activeStatus').textContent = 'idle';
$('activeJobId').textContent = '—';
$('activeCmd').textContent = '尚未启动任务';
$('stopBtn').disabled = true;
return;
}
card.className = `active-card ${statusClass(job.status)}`;
$('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;
}
function renderJobs(jobs, activeId) {
jobList.innerHTML = '';
if (!jobs.length) {
jobList.innerHTML = '暂无历史任务
';
return;
}
jobs.forEach((job) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'job-item';
const model = job.payload?.model || '-';
const thinking = job.payload?.thinking ? 'thinking' : 'no-thinking';
btn.innerHTML = `
${job.id}
${job.status}
${model} · ${thinking}
`;
btn.addEventListener('click', () => followJob(job.id, true));
if (job.id === activeId) btn.style.borderColor = 'rgba(214,162,74,0.75)';
jobList.appendChild(btn);
});
}
async function refreshJobs() {
const data = await api('/api/jobs');
renderJobs(data.jobs || [], data.active_job_id);
if (data.active_job_id) {
const active = (data.jobs || []).find((j) => j.id === data.active_job_id);
if (active) renderActive(active);
} else if (state.activeJobId) {
const current = (data.jobs || []).find((j) => j.id === state.activeJobId);
if (current) renderActive(current);
}
return data;
}
async function pullLogs(reset = false) {
if (!state.activeJobId) return;
if (reset) {
state.logOffset = 0;
logView.textContent = '';
}
const data = await api(`/api/jobs/${state.activeJobId}/logs?offset=${state.logOffset}`);
if (data.content) {
logView.textContent += data.content;
state.logOffset = data.next_offset;
if ($('autoScroll').checked) logView.scrollTop = logView.scrollHeight;
}
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) stopPolling();
}
function stopPolling() {
if (state.pollTimer) {
clearInterval(state.pollTimer);
state.pollTimer = null;
}
}
function startPolling() {
stopPolling();
state.pollTimer = setInterval(async () => {
try {
await pullLogs(false);
await refreshJobs();
} catch (e) {
console.error(e);
}
}, 1200);
}
async function followJob(jobId, resetLog = false) {
const job = await api(`/api/jobs/${jobId}`);
renderActive(job);
await pullLogs(resetLog);
if (job.status === 'running' || job.status === 'queued') startPolling();
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();
renderBenchmarks();
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')));
form.addEventListener('submit', async (e) => {
e.preventDefault();
setMsg('');
const payload = collectPayload();
if (payload.selection_mode === 'datasets' && !payload.datasets.length) {
setMsg('请至少选择一个 benchmark', 'error');
return;
}
$('launchBtn').disabled = true;
try {
const job = await api('/api/jobs', {
method: 'POST',
body: JSON.stringify(payload),
});
setMsg(`任务已启动: ${job.id}`, 'ok');
renderActive(job);
state.logOffset = 0;
logView.textContent = '';
await refreshJobs();
startPolling();
await pullLogs(true);
} catch (err) {
setMsg(err.message, 'error');
} finally {
$('launchBtn').disabled = false;
}
});
$('stopBtn').addEventListener('click', async () => {
if (!state.activeJobId) return;
if (!confirm(`确认停止任务 ${state.activeJobId}?`)) return;
try {
const job = await api(`/api/jobs/${state.activeJobId}/stop`, { method: 'POST' });
renderActive(job);
setMsg('任务已停止', 'ok');
await refreshJobs();
await pullLogs(false);
} catch (err) {
setMsg(err.message, 'error');
}
});
init();
})();