(() => { const state = { meta: null, selectionMode: 'suite', suiteKind: 'builtin', // builtin | custom suite: 'official', customSuites: {}, 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'; state.suiteKind = 'builtin'; state.customSuites = meta.custom_suites || {}; } function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } 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.suiteKind === 'builtin' && state.suite === name ? ' active' : ''}`; const names = info.all.map(escapeHtml).join(', '); btn.innerHTML = ` ${escapeHtml(name)} ${info.all.length} benchmarks
${names}
`; btn.addEventListener('click', () => { state.suiteKind = 'builtin'; state.suite = name; renderSuites(); renderCustomSuites(); }); box.appendChild(btn); }); } function renderCustomSuites() { const box = $('customSuiteList'); const empty = $('customSuiteEmpty'); box.innerHTML = ''; const names = Object.keys(state.customSuites || {}); empty.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 = ` ${escapeHtml(name)} ${items.length} benchmarks · 自定义
${items.map(escapeHtml).join(', ')}
`; 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) => { e.stopPropagation(); if (!confirm(`删除自定义组合「${name}」?`)) return; try { const data = await api(`/api/custom-suites/${encodeURIComponent(name)}`, { method: 'DELETE' }); state.customSuites = data.suites || {}; if (state.suiteKind === 'custom' && state.suite === name) { state.suiteKind = 'builtin'; state.suite = state.meta.defaults?.suite || 'official'; } renderSuites(); renderCustomSuites(); setMsg(`已删除组合: ${name}`, 'ok'); } catch (err) { setMsg(err.message, 'error'); } }); wrap.appendChild(main); wrap.appendChild(del); 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(); } function renderBenchmarks() { renderBenchmarkPicker('benchmarkList', updateSelectedCount); } function renderCustomPicker() { renderBenchmarkPicker('customPickList', updateCustomPickCount); } function updateSelectedCount() { const n = [...document.querySelectorAll('#benchmarkList input:checked')].length; $('selectedCount').textContent = `已选 ${n}`; } 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() { 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) : []; let selectionMode = state.selectionMode; let suite = state.suite; let finalDatasets = datasets; if (selectionMode === 'suite' && state.suiteKind === 'custom') { selectionMode = 'datasets'; finalDatasets = [...(state.customSuites[state.suite] || [])]; suite = state.suite; } const payload = { model: $('model').value.trim(), api_url: $('api_url').value.trim(), api_key: $('api_key').value.trim() || 'EMPTY', thinking: $('thinking').checked, selection_mode: selectionMode, suite, datasets: finalDatasets, exclude: selectionMode === 'suite' ? 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; } 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 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 = `
${escapeHtml(job.id)} ${escapeHtml(job.status)}
${escapeHtml(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(); renderCustomSuites(); renderBenchmarks(); renderCustomPicker(); setMode('suite'); const jobs = await refreshJobs(); if (jobs.active_job_id) { await followJob(jobs.active_job_id, true); } else if (jobs.jobs?.[0]) { await followJob(jobs.jobs[0].id, true); } } document.querySelectorAll('.mode-tabs .tab').forEach((el) => { el.addEventListener('click', () => setMode(el.dataset.mode)); }); $('selectAllBtn').addEventListener('click', () => { document.querySelectorAll('#benchmarkList input').forEach((el) => { el.checked = true; }); updateSelectedCount(); }); $('clearAllBtn').addEventListener('click', () => { document.querySelectorAll('#benchmarkList input').forEach((el) => { el.checked = false; }); updateSelectedCount(); }); $('refreshJobsBtn').addEventListener('click', () => refreshJobs().catch((e) => setMsg(e.message, 'error'))); $('saveCustomSuiteBtn').addEventListener('click', async () => { const name = $('customSuiteName').value.trim(); const datasets = [...document.querySelectorAll('#customPickList input:checked')].map((el) => el.value); if (!name) { setMsg('请填写组合名称', 'error'); return; } if (!datasets.length) { setMsg('请至少勾选一个 benchmark', 'error'); return; } try { await saveCustomSuite(name, datasets); } catch (err) { setMsg(err.message, 'error'); } }); $('loadCheckedToCustomBtn').addEventListener('click', () => { // no-op helper text: already editing in place; just focus name $('customSuiteName').focus(); setMsg('请在下方勾选数据集并填写组合名称后保存', 'ok'); }); $('saveFromDatasetsBtn').addEventListener('click', async () => { const datasets = [...document.querySelectorAll('#benchmarkList input:checked')].map((el) => el.value); if (!datasets.length) { setMsg('请先勾选 benchmark', 'error'); return; } const name = prompt('输入要永久保存的组合名称:'); if (!name || !name.trim()) return; try { // sync into custom picker too const set = new Set(datasets); document.querySelectorAll('#customPickList input').forEach((el) => { el.checked = set.has(el.value); }); $('customSuiteName').value = name.trim(); updateCustomPickCount(); await saveCustomSuite(name.trim(), datasets); setMode('suite'); } catch (err) { setMsg(err.message, 'error'); } }); form.addEventListener('submit', async (e) => { e.preventDefault(); setMsg(''); const requiredPaths = [ ['dataset_dir', 'Dataset Dir'], ['output_dir', 'Output Dir'], ['config', 'Config YAML'], ['tokenizer_path', 'Tokenizer Path'], ]; for (const [id, label] of requiredPaths) { if (!$(id).value.trim()) { setMsg(`请填写必填路径: ${label}`, 'error'); $(id).focus(); return; } } 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(); })();