(() => { const state = { meta: null, tab: 'scope', selectionMode: 'suite', suiteKind: 'builtin', suite: 'official', customSuites: {}, activeJobId: null, pollTimer: null, logOffset: 0, activeCategory: 'all', searchQuery: '', }; const $ = (id) => document.getElementById(id); const formMsg = $('formMsg'); const logView = $('logView'); const jobList = $('jobList'); function setMsg(text, type = '') { formMsg.textContent = text || ''; formMsg.className = `form-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 escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } 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 || {}; } // ---- 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 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' : ''}`; btn.innerHTML = ` ${escapeHtml(name)}
`; btn.addEventListener('click', () => { state.suiteKind = 'builtin'; state.suite = name; renderSuites(); renderCustomSuites(); updateEstimate(); }); box.appendChild(btn); }); } function renderCustomSuites() { const box = $('customSuiteList'); const names = Object.keys(state.customSuites || {}); 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${state.suiteKind === 'custom' && state.suite === name ? ' active' : ''}`; wrap.innerHTML = ` ${escapeHtml(name)} `; wrap.querySelector('.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'; } renderCustomSuites(); renderSuites(); setMsg(`已删除组合: ${name}`, 'ok'); } catch (err) { setMsg(err.message, 'error'); } }); wrap.addEventListener('click', (e) => { if (e.target.closest('.del')) return; state.suiteKind = 'custom'; state.suite = name; renderSuites(); renderCustomSuites(); updateEstimate(); }); box.appendChild(wrap); }); } // ---- 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() { 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 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 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); 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; let finalDatasets = datasets; if (selectionMode === 'suite' && state.suiteKind === 'custom') { selectionMode = 'datasets'; finalDatasets = [...(state.customSuites[state.suite] || [])]; } return { 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, 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', }; } function updateCommandPreview(cmd) { $('activeCmd').textContent = (cmd || []).join(' '); $('copyCmdBtn').disabled = !cmd; } // ---- Jobs ---- function statusClass(status) { return status || 'idle'; } function renderActive(job) { const card = $('activeCard'); if (!job) { card.className = 'active-card idle'; $('activeStatus').className = 'badge idle'; $('activeStatus').textContent = 'idle'; $('activeJobId').textContent = '—'; $('progressBar').style.width = '0%'; $('progressText').textContent = ''; updateCommandPreview(null); $('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; $('stopBtn').disabled = job.status !== 'running'; state.activeJobId = job.id; updateCommandPreview(job.command); } function renderJobs(jobs, activeId) { jobList.innerHTML = ''; if (!jobs.length) { jobList.innerHTML = '