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 @@ 启动台 结果分析 -
+
connecting…
+ +
+ + + + +
+ + +
+
+
-
-
-
-

路径配置(必填)

-
- - - - + +
+
+ + + +
+ + +
+
+ 选择方式 +
+ +
+
-
-

模型接入

-
- - - - +
+
内置 Suite
+
+ +
+ 自定义组合 + 在「自选」模式勾选并保存
+
-
-

Thinking

-
- - - + +
-
-

评测范围

-
- - -
- -
-

内置 Suite

-
- -

已保存的自定义组合

-
-

暂无自定义组合,可在下方勾选并保存

- -
-

新建 / 更新自定义组合

-
- -
- - -
-
-
- 已勾选 0 -
- - -
- - + +
+
+ + + +
+
+ 路径预设 + + + +
+
-
- 其他参数 + +
+
+ + + + + + + +
+
+ Judge 模型配置
- - - -
- -
- - - -
- -
- -
-
-

运行状态

-
+
+
+ + +
+
+

运行状态

+ idle +
-
- idle - +
+
+
+
+
+
+ +
+
+ 命令预览 +
尚未启动任务
-
-

实时日志

- +
+
+ 实时日志 + +
+

         
-

 
-        
-

历史任务

+
+
+

历史任务

+ +
+
-
- + diff --git a/webui/static/results.html b/webui/static/results.html index d01be0e..002ea3b 100644 --- a/webui/static/results.html +++ b/webui/static/results.html @@ -6,7 +6,7 @@ EvalStone Results - + @@ -66,9 +66,9 @@
-
+

能力分类走势

- 横轴 = 能力域(数学/代码/智能体等),纵轴 = 该域平均分 + 横轴 = 能力域,纵轴 = 该域平均分
@@ -76,7 +76,7 @@
-
+

能力分类柱状对比

各模型在同一能力域的平均得分
@@ -86,7 +86,7 @@
-
+

得分走势

横轴 = benchmark,纵轴 = score
@@ -96,7 +96,7 @@
-
+

柱状对比

同 benchmark 下各模型得分
@@ -106,7 +106,7 @@
-
+

雷达图

能力轮廓(需 ≥3 个共同 benchmark)
@@ -116,7 +116,7 @@
-
+

得分表

平均值(跨 seed / multi-run)
@@ -129,7 +129,7 @@
-
+

各 Benchmark 排名

@@ -137,6 +137,6 @@
- + diff --git a/webui/static/results.js b/webui/static/results.js index f052284..038fcd6 100644 --- a/webui/static/results.js +++ b/webui/static/results.js @@ -7,7 +7,7 @@ const $ = (id) => document.getElementById(id); const COLORS = [ - '#d6a24a', '#6fbf8a', '#6aa8d6', '#d86a5b', + '#d6a24a', '#5ec4a0', '#6aa8d6', '#d9584c', '#8b7ec8', '#e0c36a', '#5ec4b0', '#c98a6a', ]; @@ -112,11 +112,11 @@ backgroundColor: type === 'radar' ? color + '33' : color + 'cc', tension: 0.25, spanGaps: false, - pointRadius: 4, - pointHoverRadius: 6, + pointRadius: 3, + pointHoverRadius: 5, }; if (type === 'bar') { - return { ...base, borderWidth: 0, borderRadius: 4 }; + return { ...base, borderWidth: 0, borderRadius: 3 }; } if (type === 'radar') { return { ...base, fill: true, borderWidth: 2 }; @@ -128,18 +128,15 @@ function axisOptions(maxRotation = 45) { return { x: { - ticks: { color: '#92a197', maxRotation, minRotation: 0, font: { size: 12 } }, - grid: { color: 'rgba(51,64,56,0.6)' }, + ticks: { color: '#9aa89d', maxRotation, minRotation: 0, font: { size: 11 } }, + grid: { color: 'rgba(40,48,42,0.8)' }, }, y: { min: 0, max: 100, - ticks: { - color: '#92a197', - callback: (v) => v + '%', - }, - grid: { color: 'rgba(51,64,56,0.6)' }, - title: { display: true, text: 'Score (%)', color: '#92a197' }, + ticks: { color: '#9aa89d', callback: (v) => v + '%', font: { size: 11 } }, + grid: { color: 'rgba(40,48,42,0.8)' }, + title: { display: true, text: 'Score (%)', color: '#9aa89d', font: { size: 11 } }, }, }; } @@ -184,7 +181,7 @@ maintainAspectRatio: false, plugins: { legend: { - labels: { color: '#c9d4cb', boxWidth: 12, font: { family: 'IBM Plex Sans', size: 13 } }, + labels: { color: '#c5d0c7', boxWidth: 10, font: { family: 'Inter', size: 12 } }, }, tooltip: { callbacks: { @@ -198,7 +195,6 @@ scales: {}, }; - // Capability-domain charts const catLabels = compare.category_labels || []; const catSeries = compare.category_series || []; const catLineCanvas = $('categoryLineChart'); @@ -260,16 +256,16 @@ responsive: true, maintainAspectRatio: false, plugins: { - legend: { labels: { color: '#c9d4cb', boxWidth: 12 } }, + legend: { labels: { color: '#c5d0c7', boxWidth: 10 } }, }, scales: { r: { min: 0, max: 100, - ticks: { color: '#92a197', backdropColor: 'transparent', stepSize: 20 }, - grid: { color: 'rgba(51,64,56,0.7)' }, - angleLines: { color: 'rgba(51,64,56,0.7)' }, - pointLabels: { color: '#c9d4cb', font: { size: 12 } }, + ticks: { color: '#9aa89d', backdropColor: 'transparent', stepSize: 20, font: { size: 10 } }, + grid: { color: 'rgba(40,48,42,0.8)' }, + angleLines: { color: 'rgba(40,48,42,0.8)' }, + pointLabels: { color: '#c5d0c7', font: { size: 11 } }, }, }, }, @@ -387,7 +383,6 @@ document.querySelectorAll('#benchList input').forEach((el) => { el.checked = false; }); }); - // bootstrap defaults from meta api('/api/meta').then((meta) => { $('outputDir').value = meta.defaults?.output_dir || ''; return loadOverview(); diff --git a/webui/static/styles.css b/webui/static/styles.css index 7d49be5..034b5be 100644 --- a/webui/static/styles.css +++ b/webui/static/styles.css @@ -1,16 +1,18 @@ :root { - --bg0: #121714; - --bg1: #1a211c; - --bg2: #232c26; - --line: #334038; - --text: #e7eee8; - --muted: #92a197; + --bg: #0b0f0d; + --bg-elev: #111613; + --bg-hover: #161b18; + --border: #28302a; + --border-strong: #3a4540; + --text: #e8eeea; + --text-dim: #9aa89d; + --text-muted: #6e7c70; --accent: #d6a24a; - --accent-2: #6fbf8a; - --danger: #d86a5b; - --shadow: 0 18px 40px rgba(0, 0, 0, 0.28); - --radius: 14px; - --font: "IBM Plex Sans", "PingFang SC", "Noto Sans SC", sans-serif; + --accent-2: #5ec4a0; + --danger: #d9584c; + --radius: 10px; + --radius-sm: 6px; + --font: "Inter", "PingFang SC", "Noto Sans SC", system-ui, sans-serif; --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace; } @@ -19,602 +21,729 @@ html, body { margin: 0; min-height: 100%; } body { font-family: var(--font); color: var(--text); - background: - radial-gradient(1200px 600px at 10% -10%, rgba(214, 162, 74, 0.16), transparent 55%), - radial-gradient(900px 500px at 100% 0%, rgba(111, 191, 138, 0.12), transparent 50%), - linear-gradient(180deg, #0f1411 0%, var(--bg0) 40%, #101612 100%); + background: var(--bg); + font-size: 13px; + line-height: 1.45; +} + +.page { + max-width: 1400px; + margin: 0 auto; + padding: 18px 20px 24px; } code, pre, .mono { font-family: var(--mono); } -.page { - max-width: 1440px; - margin: 0 auto; - padding: 28px 24px 40px; -} - +/* Header */ .top { display: flex; justify-content: space-between; - align-items: flex-end; - gap: 16px; - margin-bottom: 22px; + align-items: center; + gap: 12px; + margin-bottom: 14px; } - -.brand { display: flex; gap: 14px; align-items: center; } +.brand { display: flex; align-items: center; gap: 10px; } .brand-mark { - width: 14px; height: 42px; border-radius: 999px; + width: 10px; height: 32px; border-radius: 999px; background: linear-gradient(180deg, var(--accent), var(--accent-2)); - box-shadow: 0 0 24px rgba(214, 162, 74, 0.35); } .brand h1 { margin: 0; - font-size: 28px; - letter-spacing: 0.02em; + font-size: 20px; font-weight: 700; + letter-spacing: -0.02em; } .brand p { - margin: 4px 0 0; - color: var(--muted); - font-size: 13px; + margin: 1px 0 0; + color: var(--text-muted); + font-size: 12px; } .brand code { color: var(--accent); - background: rgba(214, 162, 74, 0.1); - padding: 1px 6px; - border-radius: 6px; -} - -.top-meta { - display: flex; align-items: center; gap: 8px; - color: var(--muted); font-size: 13px; -} -.dot { - width: 9px; height: 9px; border-radius: 50%; - background: #667; - box-shadow: 0 0 0 3px rgba(102, 102, 119, 0.2); -} -.dot.ok { background: var(--accent-2); box-shadow: 0 0 0 3px rgba(111, 191, 138, 0.2); } -.dot.bad { background: var(--danger); box-shadow: 0 0 0 3px rgba(216, 106, 91, 0.2); } - -.layout { - display: grid; - grid-template-columns: minmax(0, 1.15fr) minmax(340px, 0.85fr); - gap: 18px; - align-items: start; -} - -.panel { - background: linear-gradient(180deg, rgba(26, 33, 28, 0.96), rgba(18, 23, 20, 0.96)); - border: 1px solid var(--line); - border-radius: var(--radius); - box-shadow: var(--shadow); - padding: 20px; -} - -.block + .block { margin-top: 18px; } -.block h2, .side-head h2, .log-head h3, .jobs-head h3 { - margin: 0 0 10px; - font-size: 16px; - font-weight: 600; - letter-spacing: 0.03em; - text-transform: uppercase; - color: #c7d2c9; -} -.subhead { - margin: 0 0 8px; - font-size: 14px; - font-weight: 600; - color: #c7d2c9; -} - -label { - display: flex; - flex-direction: column; - gap: 5px; - font-size: 14px; - color: var(--muted); -} -label.full { grid-column: 1 / -1; } -label span { letter-spacing: 0.02em; color: #b7c4ba; } - -input, select, button, summary { - font: inherit; -} -input, select { - width: 100%; - background: var(--bg0); - color: var(--text); - border: 1px solid var(--line); - border-radius: 8px; - padding: 7px 10px; - font-size: 15px; - line-height: 1.35; - outline: none; - transition: border-color .15s, box-shadow .15s; -} -input:focus, select:focus { - border-color: rgba(214, 162, 74, 0.7); - box-shadow: 0 0 0 3px rgba(214, 162, 74, 0.15); -} - -.grid-2 { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; -} - -.thinking-row { - display: flex; - flex-wrap: wrap; - gap: 14px 18px; - align-items: end; -} -.thinking-row .inline { min-width: 140px; } - -.switch { - flex-direction: row; - align-items: center; - gap: 10px; - cursor: pointer; - user-select: none; -} -.switch input { display: none; } -.slider { - width: 42px; height: 24px; border-radius: 999px; - background: #2c3630; position: relative; transition: .2s; - border: 1px solid var(--line); -} -.slider::after { - content: ""; - position: absolute; top: 2px; left: 2px; - width: 18px; height: 18px; border-radius: 50%; - background: #c5d0c7; transition: .2s; -} -.switch input:checked + .slider { - background: rgba(214, 162, 74, 0.35); - border-color: rgba(214, 162, 74, 0.7); -} -.switch input:checked + .slider::after { - transform: translateX(18px); - background: var(--accent); -} -.switch-text { color: var(--text); font-size: 15px; } - -.mode-tabs { - display: inline-flex; - background: var(--bg0); - border: 1px solid var(--line); - border-radius: 999px; - padding: 3px; - margin-bottom: 12px; -} -.tab { - border: 0; - background: transparent; - color: var(--muted); - padding: 7px 12px; - border-radius: 999px; - cursor: pointer; - font-size: 14px; -} -.tab.active { - background: rgba(214, 162, 74, 0.18); - color: var(--text); -} - -.suite-list { - display: grid; - grid-template-columns: 1fr; - gap: 8px; -} -.suite-card { - text-align: left; - border: 1px solid var(--line); - background: var(--bg0); - color: var(--text); - border-radius: 10px; - padding: 10px 12px; - cursor: pointer; - transition: border-color .15s, transform .15s; - display: flex; - flex-direction: column; - gap: 4px; - position: relative; -} -.suite-card:hover { transform: translateY(-1px); } -.suite-card.active { - border-color: rgba(214, 162, 74, 0.75); - background: rgba(214, 162, 74, 0.08); -} -.suite-card.custom { - padding-right: 64px; -} -.suite-card-main { - all: unset; - cursor: pointer; - display: block; - width: 100%; -} -.suite-card strong { - display: block; - margin-bottom: 2px; - font-size: 15px; -} -.suite-count { - display: block; - color: var(--muted); - font-size: 13px; - margin-bottom: 4px; -} -.suite-names { - color: #c9d4cb; - font-size: 13px; - line-height: 1.55; - white-space: normal; - word-break: break-word; - overflow: visible; - text-overflow: unset; -} -.suite-del { - position: absolute; - top: 8px; - right: 8px; - color: #ffb3a8 !important; -} -.danger-text { color: #ffb3a8; } -.custom-editor { - border: 1px dashed var(--line); - border-radius: 10px; - padding: 12px; - background: rgba(12, 16, 14, 0.35); -} -.custom-editor-row { - display: grid; - grid-template-columns: 1fr auto; - gap: 10px; - align-items: end; - margin-bottom: 10px; -} -.custom-editor-actions { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.benchmark-list { - display: flex; - flex-direction: column; - gap: 10px; - max-height: 420px; - overflow: auto; - padding-right: 4px; -} -.benchmark-list.compact { max-height: 280px; } -.bench-category { - border: 1px solid var(--line); - border-radius: 10px; - background: rgba(12, 16, 14, 0.55); - padding: 8px; -} -.bench-category.cat-math { border-left: 3px solid #6fbf8a; } -.bench-category.cat-code { border-left: 3px solid #6aa8d6; } -.bench-category.cat-science { border-left: 3px solid #c79a5a; } -.bench-category.cat-knowledge { border-left: 3px solid #9aa4b2; } -.bench-category.cat-long_context { border-left: 3px solid #8b7ec8; } -.bench-category.cat-tool_agent { border-left: 3px solid #d6a24a; } -.bench-category.cat-other { border-left: 3px solid #667; } - -.bench-cat-head { - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; - margin-bottom: 8px; -} -.bench-cat-title { - display: flex; - align-items: baseline; - gap: 8px; -} -.bench-cat-title strong { - font-size: 14px; - letter-spacing: 0.02em; - color: var(--text); -} -.bench-cat-actions { display: flex; gap: 4px; } -.bench-cat-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 6px; -} -.bench-item { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 8px; - border-radius: 8px; - border: 1px solid var(--line); - background: var(--bg0); - cursor: pointer; - font-size: 14px; - color: var(--text); - min-height: 0; -} -.bench-item input { width: auto; } -.bench-item.multi { border-color: rgba(111, 191, 138, 0.35); } -.bench-item .run-tag { - margin-left: auto; - font-style: normal; - font-size: 12px; - color: #9ee0b2; - opacity: 0.85; -} - -.bench-actions { - display: flex; gap: 8px; align-items: center; margin-bottom: 10px; -} -.muted { color: var(--muted); font-size: 13px; } -.mt { margin-top: 12px; } -.hidden { display: none !important; } - -.advanced { - border: 1px dashed var(--line); - border-radius: 12px; - padding: 10px 14px; -} -.advanced summary { - cursor: pointer; - color: var(--muted); - font-size: 13px; -} - -.actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - align-items: center; - margin-top: 22px; -} -button { - border-radius: 8px; - border: 1px solid var(--line); - padding: 8px 14px; - cursor: pointer; - background: var(--bg2); - color: var(--text); - font-size: 14px; -} -button:disabled { opacity: 0.45; cursor: not-allowed; } -button.primary { - background: linear-gradient(180deg, #e0b05a, #c48c2f); - border-color: #b9852d; - color: #1a1408; - font-weight: 600; -} -button.danger { - background: rgba(216, 106, 91, 0.15); - border-color: rgba(216, 106, 91, 0.45); - color: #ffc2ba; -} -button.ghost { - background: transparent; - padding: 5px 9px; - font-size: 13px; -} -.msg { color: var(--muted); font-size: 14px; } -.msg.error { color: var(--danger); } -.msg.ok { color: var(--accent-2); } - -.side-head, .log-head, .jobs-head { - display: flex; - justify-content: space-between; - align-items: center; -} -.active-card { - border: 1px solid var(--line); - border-radius: 12px; - padding: 12px; - background: var(--bg0); - margin-bottom: 14px; -} -.active-card.running { border-color: rgba(111, 191, 138, 0.55); } -.active-card.failed, .active-card.stopped { border-color: rgba(216, 106, 91, 0.45); } -.active-card.completed { border-color: rgba(214, 162, 74, 0.45); } -.active-title { - display: flex; gap: 10px; align-items: center; margin-bottom: 8px; -} -.badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 999px; + background: rgba(214, 162, 74, 0.12); + padding: 1px 5px; + border-radius: 4px; font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - background: #2a332d; - color: var(--muted); } -.badge.running { background: rgba(111, 191, 138, 0.18); color: #9ee0b2; } -.badge.completed { background: rgba(214, 162, 74, 0.18); color: #f0d08a; } -.badge.failed, .badge.stopped { background: rgba(216, 106, 91, 0.18); color: #ffb3a8; } - -.cmd, .log-view { - margin: 0; - white-space: pre-wrap; - word-break: break-word; - font-size: 12px; - line-height: 1.5; - color: #c9d4cb; -} -.cmd { color: var(--muted); max-height: 72px; overflow: auto; } -.log-view { - height: 360px; - overflow: auto; - background: #0c100e; - border: 1px solid var(--line); - border-radius: 12px; - padding: 12px; - margin: 8px 0 16px; -} - -.check-inline { - flex-direction: row; - align-items: center; - gap: 6px; - font-size: 12px; -} -.check-inline input { width: auto; } - -.job-list { - display: flex; - flex-direction: column; - gap: 8px; - max-height: 220px; - overflow: auto; -} -.job-item { - text-align: left; - width: 100%; - border: 1px solid var(--line); - background: var(--bg0); - border-radius: 10px; - padding: 10px 12px; - cursor: pointer; -} -.job-item:hover { border-color: rgba(214, 162, 74, 0.45); } -.job-item .row { - display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px; -} -.job-item small { color: var(--muted); } - .top-right { display: flex; - flex-direction: column; - align-items: flex-end; - gap: 10px; + align-items: center; + gap: 14px; } .nav-tabs { display: inline-flex; - gap: 4px; - padding: 4px; - border: 1px solid var(--line); + gap: 3px; + padding: 3px; + background: var(--bg-elev); + border: 1px solid var(--border); border-radius: 999px; - background: rgba(18, 23, 20, 0.7); } .nav-tabs a { - color: var(--muted); + color: var(--text-dim); text-decoration: none; - padding: 7px 14px; + padding: 5px 12px; border-radius: 999px; - font-size: 13px; + font-size: 12px; + font-weight: 500; } .nav-tabs a.active, .nav-tabs a:hover { color: var(--text); background: rgba(214, 162, 74, 0.18); } +.health { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-muted); + font-size: 12px; +} +.dot { + width: 7px; height: 7px; border-radius: 50%; + background: #555; +} +.dot.ok { background: var(--accent-2); box-shadow: 0 0 0 2px rgba(94, 196, 160, 0.25); } +.dot.bad { background: var(--danger); box-shadow: 0 0 0 2px rgba(217, 88, 76, 0.25); } -.results-layout { +/* Action bar */ +.action-bar { + display: flex; + align-items: flex-end; + gap: 10px; + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px 14px; + margin-bottom: 14px; +} +.action-bar label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 11px; + color: var(--text-dim); + font-weight: 500; +} +.action-bar input { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + padding: 6px 9px; + font-size: 13px; + outline: none; + min-width: 110px; +} +.action-bar input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(214, 162, 74, 0.15); +} +.field-grow { flex: 1 1 0; } +.action-btns { display: flex; gap: 8px; } + +.switch-inline { + flex-direction: row; + align-items: center; + gap: 7px; + cursor: pointer; + user-select: none; + padding-bottom: 6px; +} +.switch-inline input { display: none; } +.switch-slider { + width: 34px; height: 18px; border-radius: 999px; + background: var(--border-strong); + position: relative; + transition: .15s; +} +.switch-slider::after { + content: ""; + position: absolute; top: 2px; left: 2px; + width: 14px; height: 14px; border-radius: 50%; + background: #c5d0c7; + transition: .15s; +} +.switch-inline input:checked + .switch-slider { + background: rgba(94, 196, 160, 0.35); +} +.switch-inline input:checked + .switch-slider::after { + transform: translateX(16px); + background: var(--accent-2); +} +.switch-label { color: var(--text); font-size: 12px; } + +/* Layout */ +.layout { display: grid; - grid-template-columns: minmax(280px, 0.85fr) minmax(0, 1.4fr); - gap: 18px; + grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr); + gap: 14px; align-items: start; } -.filters-panel { position: sticky; top: 16px; } +.panel { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); +} +.main-panel { padding: 0; overflow: hidden; } +.status-panel { padding: 14px; } + +/* Tabs */ +.tabs { + display: flex; + gap: 0; + border-bottom: 1px solid var(--border); + background: rgba(0,0,0,0.15); +} +.tab { + border: 0; + background: transparent; + color: var(--text-dim); + padding: 10px 16px; + cursor: pointer; + font-size: 13px; + font-weight: 500; + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} +.tab.active, +.tab:hover { + color: var(--text); +} +.tab.active { + border-bottom-color: var(--accent); +} +.tab-pane { display: none; padding: 14px; } +.tab-pane.active { display: block; } + +/* Section titles */ +.section-title { + font-size: 11px; + font-weight: 600; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 14px 0 8px; +} +.section-title:first-child { margin-top: 0; } +.flex-between { display: flex; justify-content: space-between; align-items: center; } +.hint { color: var(--text-muted); font-size: 11px; text-transform: none; font-weight: 400; } + +/* Mode bar */ +.mode-bar { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} +.mode-label { color: var(--text-dim); font-size: 12px; } +.mode-toggle { + display: inline-flex; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 999px; + padding: 2px; +} +.mode-btn { + border: 0; + background: transparent; + color: var(--text-dim); + padding: 5px 12px; + border-radius: 999px; + cursor: pointer; + font-size: 12px; +} +.mode-btn.active { + background: rgba(214, 162, 74, 0.18); + color: var(--text); +} +.estimate { + margin-left: auto; + color: var(--accent); + font-size: 12px; + font-weight: 500; +} + +/* Suite grid */ +.suite-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 8px; +} +.suite-card { + text-align: left; + border: 1px solid var(--border); + background: var(--bg); + color: var(--text); + border-radius: var(--radius-sm); + padding: 10px 11px; + cursor: pointer; + transition: .12s; + position: relative; +} +.suite-card:hover { border-color: var(--border-strong); background: var(--bg-hover); } +.suite-card.active { + border-color: var(--accent); + background: rgba(214, 162, 74, 0.1); +} +.suite-card strong { + display: block; + font-size: 13px; + margin-bottom: 3px; +} +.suite-meta { + display: flex; + gap: 8px; + color: var(--text-muted); + font-size: 11px; +} +.suite-card .del { + position: absolute; + top: 5px; right: 5px; + padding: 2px 5px; + font-size: 11px; + color: #ffafa3; + opacity: 0; + transition: .12s; +} +.suite-card:hover .del { opacity: 1; } + +/* Benchmark picker */ +.bench-toolbar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} +.search { + flex: 1; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + padding: 6px 10px; + font-size: 13px; + outline: none; +} +.search:focus { border-color: var(--accent); } +.counter { + color: var(--text-muted); + font-size: 12px; + margin-left: auto; +} +.category-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 10px; +} +.chip { + border: 1px solid var(--border); + background: var(--bg); + color: var(--text-dim); + padding: 3px 9px; + border-radius: 999px; + cursor: pointer; + font-size: 11px; + font-weight: 500; +} +.chip.active { + border-color: var(--accent); + color: var(--accent); + background: rgba(214, 162, 74, 0.1); +} +.benchmark-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 6px; + max-height: 360px; + overflow: auto; + padding-right: 4px; +} +.bench-item { + display: flex; + align-items: center; + gap: 5px; + padding: 5px 8px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--bg); + cursor: pointer; + font-size: 12px; + color: var(--text); + user-select: none; +} +.bench-item:hover { background: var(--bg-hover); } +.bench-item input { width: auto; margin: 0; } +.bench-item .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.bench-item .tag { + font-size: 10px; + padding: 1px 4px; + border-radius: 4px; + background: rgba(94, 196, 160, 0.15); + color: var(--accent-2); +} +.bench-item .warn { + background: rgba(217, 88, 76, 0.15); + color: #ffafa3; +} +.bench-item.hidden-item { display: none; } + +/* Forms */ +.path-grid, +.grid-2, +.grid-4 { + display: grid; + gap: 10px; +} +.path-grid, +.grid-2 { grid-template-columns: 1fr 1fr; } +.grid-4 { grid-template-columns: repeat(4, 1fr); } +.path-grid .full { grid-column: 1 / -1; } +label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 11px; + color: var(--text-dim); + font-weight: 500; +} +label span small { font-weight: 400; } +input, select { + width: 100%; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + padding: 6px 9px; + font-size: 13px; + outline: none; +} +input:focus, select:focus { border-color: var(--accent); } +.muted { color: var(--text-muted); } + +.preset-bar { + display: flex; + align-items: center; + gap: 8px; + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid var(--border); +} + +.fold { + margin-top: 14px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px 12px; +} +.fold summary { + cursor: pointer; + color: var(--text-dim); + font-size: 12px; + font-weight: 500; +} +.fold > .grid-2 { margin-top: 10px; } +.mt { margin-top: 10px; } + +.form-msg { + margin-top: 12px; + font-size: 12px; + min-height: 18px; +} +.form-msg.error { color: #ffafa3; } +.form-msg.ok { color: var(--accent-2); } + +/* Status panel */ +.active-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} +.active-head h2 { + margin: 0; + font-size: 14px; + font-weight: 600; +} +.badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 600; + background: var(--bg); + color: var(--text-muted); + border: 1px solid var(--border); +} +.badge.running { color: var(--accent-2); border-color: rgba(94, 196, 160, 0.4); } +.badge.completed { color: var(--accent); border-color: rgba(214, 162, 74, 0.4); } +.badge.failed, .badge.stopped { color: #ffafa3; border-color: rgba(217, 88, 76, 0.4); } + +.active-card { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px 12px; + margin-bottom: 12px; +} +.active-card.running { border-color: rgba(94, 196, 160, 0.45); } +.active-card.failed, .active-card.stopped { border-color: rgba(217, 88, 76, 0.35); } +.active-card.completed { border-color: rgba(214, 162, 74, 0.35); } +.active-id { + font-size: 12px; + color: var(--text-dim); + margin-bottom: 8px; +} +.progress-wrap { + height: 5px; + background: var(--border); + border-radius: 999px; + overflow: hidden; +} +.progress-bar { + height: 100%; + background: linear-gradient(90deg, var(--accent-2), var(--accent)); + transition: width .3s; +} +.progress-text { + font-size: 11px; + color: var(--text-muted); + margin-top: 5px; +} + +.cmd-wrap, .log-wrap, .jobs-wrap { margin-bottom: 12px; } +.cmd-head, .log-head, .jobs-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} +.cmd-head span, .log-head span, .jobs-head h3 { + font-size: 12px; + font-weight: 600; + color: var(--text-dim); +} +.jobs-head h3 { margin: 0; } +.cmd { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-size: 11px; + line-height: 1.5; + color: var(--text-dim); + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 8px 10px; + max-height: 90px; + overflow: auto; +} +.log-view { + height: 280px; + overflow: auto; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px; + margin: 0; + font-size: 11px; + line-height: 1.5; + color: #c5d0c7; +} +.check-inline { + flex-direction: row; + align-items: center; + gap: 5px; + font-size: 11px; + color: var(--text-muted); + cursor: pointer; +} +.check-inline input { width: auto; } + +.job-list { + display: flex; + flex-direction: column; + gap: 6px; + max-height: 160px; + overflow: auto; +} +.job-item { + text-align: left; + width: 100%; + border: 1px solid var(--border); + background: var(--bg); + border-radius: var(--radius-sm); + padding: 7px 10px; + cursor: pointer; + font-size: 12px; +} +.job-item:hover { border-color: var(--border-strong); } +.job-item.active { border-color: var(--accent); } +.job-row { + display: flex; + justify-content: space-between; + gap: 8px; +} +.job-row strong { + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.job-row small { color: var(--text-muted); } + +/* Buttons */ +button { + border-radius: var(--radius-sm); + border: 1px solid var(--border); + padding: 6px 12px; + cursor: pointer; + background: var(--bg); + color: var(--text); + font-size: 12px; + font-weight: 500; + transition: .12s; +} +button:hover { background: var(--bg-hover); } +button:disabled { opacity: .45; cursor: not-allowed; } +button.primary { + background: var(--accent); + border-color: var(--accent); + color: #1a1408; +} +button.primary:hover { filter: brightness(1.08); } +button.danger { + background: rgba(217, 88, 76, 0.12); + border-color: rgba(217, 88, 76, 0.4); + color: #ffafa3; +} +button.ghost { + background: transparent; + border-color: transparent; + color: var(--text-dim); + padding: 5px 8px; +} +button.ghost:hover { background: rgba(255,255,255,0.05); color: var(--text); } + +.hidden { display: none !important; } + +/* Results page */ +.results-layout { + display: grid; + grid-template-columns: minmax(260px, 0.75fr) minmax(0, 1.45fr); + gap: 14px; + align-items: start; +} +.filters-panel { padding: 14px; } +.filters-panel .block + .block { margin-top: 14px; } +.filters-panel h2 { + margin: 0 0 8px; + font-size: 12px; + font-weight: 600; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.04em; +} +.charts-panel { padding: 14px; } + .pick-list { display: flex; flex-direction: column; - gap: 8px; - max-height: 240px; + gap: 6px; + max-height: 200px; overflow: auto; } -.pick-list.bench-pick { max-height: 320px; } +.pick-list.bench-pick { max-height: 280px; } .pick-item { flex-direction: row; align-items: center; - gap: 10px; - padding: 10px 12px; - border: 1px solid var(--line); - border-radius: 10px; - background: var(--bg0); + gap: 8px; + padding: 7px 10px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg); cursor: pointer; + font-size: 12px; } .pick-item input { width: auto; } -.pick-main { display: flex; flex-direction: column; gap: 2px; flex: 1; } -.pick-main strong { color: var(--text); font-size: 13px; } -.pick-main small { color: var(--muted); font-size: 11px; } -.swatch { - width: 10px; height: 28px; border-radius: 999px; display: inline-block; -} +.pick-main { display: flex; flex-direction: column; gap: 1px; flex: 1; } +.pick-main strong { color: var(--text); font-size: 12px; } +.pick-main small { color: var(--text-muted); font-size: 11px; } +.swatch { width: 8px; height: 24px; border-radius: 999px; } + .pick-cat { - border: 1px solid var(--line); - border-radius: 10px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); padding: 8px; - background: rgba(12, 16, 14, 0.45); + background: var(--bg); } -.pick-cat + .pick-cat { margin-top: 8px; } +.pick-cat + .pick-cat { margin-top: 6px; } .pick-cat-head { - display: flex; align-items: center; gap: 8px; margin-bottom: 8px; -} -.pick-cat-head strong { color: var(--text); font-size: 12px; } -.pick-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 6px; + display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } +.pick-cat-head strong { color: var(--text); font-size: 11px; } +.pick-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; } .pick-chip { flex-direction: row; align-items: center; - gap: 6px; - padding: 6px 8px; - border-radius: 8px; - border: 1px solid var(--line); - background: var(--bg0); + gap: 5px; + padding: 5px 7px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--bg); cursor: pointer; - font-size: 12px; + font-size: 11px; color: var(--text); } .pick-chip input { width: auto; } -.chart-block + .chart-block { margin-top: 22px; } +.chart-block + .chart-block { margin-top: 18px; } +.chart-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} +.chart-head h2 { + margin: 0; + font-size: 13px; + font-weight: 600; +} .chart-wrap { - height: 340px; - background: #0c100e; - border: 1px solid var(--line); - border-radius: 12px; - padding: 12px; + height: 300px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px; position: relative; } -.chart-wrap.radar-wrap { height: 380px; } +.chart-wrap.radar-wrap { height: 340px; } .chart-wrap.empty::after { content: attr(data-empty); - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - color: var(--muted); - font-size: 13px; - background: rgba(12, 16, 14, 0.85); - border-radius: 12px; + position: absolute; inset: 0; + display: flex; align-items: center; justify-content: center; + color: var(--text-muted); font-size: 12px; + background: rgba(11,15,13,0.85); + border-radius: var(--radius-sm); } - .table-wrap { overflow: auto; - border: 1px solid var(--line); - border-radius: 12px; - background: #0c100e; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg); } #scoreTable { width: 100%; border-collapse: collapse; - font-size: 13px; + font-size: 12px; font-family: var(--mono); } #scoreTable th, #scoreTable td { - padding: 10px 12px; - border-bottom: 1px solid rgba(51, 64, 56, 0.8); + padding: 8px 10px; + border-bottom: 1px solid var(--border); text-align: right; white-space: nowrap; } @@ -622,72 +751,47 @@ button.ghost { #scoreTable td:first-child, #scoreTable .bench-col { text-align: left; - position: sticky; - left: 0; - background: #0c100e; + position: sticky; left: 0; + background: var(--bg); } -#scoreTable th { - color: var(--muted); - font-weight: 500; - background: #121714; -} -#scoreTable td.best { color: #f0d08a; font-weight: 600; } -#scoreTable td.na { color: #5a665e; } +#scoreTable th { color: var(--text-dim); font-weight: 500; background: var(--bg-elev); } +#scoreTable td.best { color: var(--accent); font-weight: 600; } +#scoreTable td.na { color: var(--text-muted); } #scoreTable .best-name { color: var(--accent-2); } .rank-list { display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 10px; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 8px; } .rank-card { - border: 1px solid var(--line); - border-radius: 12px; - background: var(--bg0); - padding: 12px; -} -.rank-card strong { - display: block; - margin-bottom: 8px; - font-size: 13px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg); + padding: 10px; } +.rank-card strong { display: block; margin-bottom: 6px; font-size: 12px; } .rank-row { display: grid; - grid-template-columns: 28px minmax(0, 1fr) auto; - gap: 8px; + grid-template-columns: 22px minmax(0, 1fr) auto; + gap: 6px; align-items: start; - font-size: 13px; - padding: 6px 0; - color: var(--muted); + font-size: 12px; + padding: 4px 0; + color: var(--text-dim); } .rank-idx { color: var(--accent); font-family: var(--mono); } -.rank-name { - color: var(--text); - white-space: normal; - overflow: visible; - text-overflow: unset; - word-break: break-all; - line-height: 1.4; - font-size: 13px; -} -.rank-score { - font-family: var(--mono); - color: #c9d4cb; - white-space: nowrap; - padding-top: 1px; -} +.rank-name { color: var(--text); word-break: break-all; line-height: 1.3; } +.rank-score { font-family: var(--mono); color: var(--text); white-space: nowrap; } @media (max-width: 1100px) { - .layout { grid-template-columns: 1fr; } - .results-layout { grid-template-columns: 1fr; } - .filters-panel { position: static; } - .bench-cat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .layout, .results-layout { grid-template-columns: 1fr; } + .grid-4 { grid-template-columns: repeat(2, 1fr); } + .action-bar { flex-wrap: wrap; } } @media (max-width: 720px) { - .grid-2, .suite-list, .bench-cat-grid, .pick-grid { grid-template-columns: 1fr; } - .page { padding: 18px 14px 28px; } - .brand h1 { font-size: 22px; } - .bench-cat-head { flex-wrap: wrap; } + .grid-2, .grid-4, .path-grid { grid-template-columns: 1fr; } + .suite-grid { grid-template-columns: repeat(2, 1fr); } + .benchmark-grid { grid-template-columns: repeat(2, 1fr); } .top { flex-direction: column; align-items: flex-start; } - .top-right { align-items: flex-start; } }