(() => { 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(); })();