Improve EvalStone web UI paths, custom suites, and results charts.
Move required path fields up front, persist custom benchmark suites, enlarge readable typography, show full suite benchmark names, add capability-domain trend charts with local Chart.js, and polish ranking name display.
This commit is contained in:
parent
7c449d3126
commit
541b0b477d
@ -304,6 +304,69 @@ def compare_models(
|
||||
rows.sort(key=lambda x: x['score'], reverse=True)
|
||||
ranking.append({'benchmark': b, 'rows': rows})
|
||||
|
||||
# Capability-domain aggregation: mean score over selected benches in each category
|
||||
bench_set = set(bench_list)
|
||||
active_categories = []
|
||||
for cat in overview['categories']:
|
||||
items = [b for b in cat['items'] if b in bench_set]
|
||||
if items:
|
||||
active_categories.append({
|
||||
'id': cat['id'],
|
||||
'name': cat['name'],
|
||||
'items': items,
|
||||
})
|
||||
|
||||
category_labels = [c['name'] for c in active_categories]
|
||||
category_series = []
|
||||
for folder in selected:
|
||||
scores = []
|
||||
details = []
|
||||
for cat in active_categories:
|
||||
vals = []
|
||||
for b in cat['items']:
|
||||
cell = overview['matrix'].get(folder, {}).get(b)
|
||||
if cell and cell.get('score') is not None:
|
||||
vals.append(float(cell['score']))
|
||||
if vals:
|
||||
avg = sum(vals) / len(vals)
|
||||
scores.append(round(avg, 6))
|
||||
else:
|
||||
avg = None
|
||||
scores.append(None)
|
||||
details.append({
|
||||
'category': cat['name'],
|
||||
'score': avg,
|
||||
'n_benchmarks': len(vals),
|
||||
'benchmarks': cat['items'],
|
||||
})
|
||||
model_meta = next((m for m in overview['models'] if m['folder'] == folder), None)
|
||||
category_series.append({
|
||||
'folder': folder,
|
||||
'label': model_meta['model_name'] if model_meta else folder,
|
||||
'display': folder,
|
||||
'scores': scores,
|
||||
'details': details,
|
||||
})
|
||||
|
||||
category_ranking = []
|
||||
for i, cat in enumerate(active_categories):
|
||||
rows = []
|
||||
for s in category_series:
|
||||
if s['scores'][i] is not None:
|
||||
rows.append({
|
||||
'folder': s['folder'],
|
||||
'label': s['label'],
|
||||
'score': s['scores'][i],
|
||||
'n_benchmarks': s['details'][i]['n_benchmarks'],
|
||||
})
|
||||
rows.sort(key=lambda x: x['score'], reverse=True)
|
||||
category_ranking.append({
|
||||
'category': cat['name'],
|
||||
'id': cat['id'],
|
||||
'items': cat['items'],
|
||||
'rows': rows,
|
||||
})
|
||||
|
||||
return {
|
||||
'output_dir': overview['output_dir'],
|
||||
'benchmarks': bench_list,
|
||||
@ -311,6 +374,9 @@ def compare_models(
|
||||
'models': [m for m in overview['models'] if m['folder'] in selected],
|
||||
'series': series,
|
||||
'ranking': ranking,
|
||||
'category_labels': category_labels,
|
||||
'category_series': category_series,
|
||||
'category_ranking': category_ranking,
|
||||
'matrix': {
|
||||
f: {b: overview['matrix'].get(f, {}).get(b) for b in bench_list}
|
||||
for f in selected
|
||||
|
||||
@ -35,10 +35,45 @@ JOBS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
DEFAULT_OUTPUT_DIR = Path(run_module.DEFAULT_OUTPUT_DIR)
|
||||
CUSTOM_SUITES_PATH = DATA_DIR / 'custom_suites.json'
|
||||
|
||||
app = FastAPI(title='EvalStone Launch Panel', version='1.0.0')
|
||||
|
||||
|
||||
def _load_custom_suites() -> Dict[str, List[str]]:
|
||||
if not CUSTOM_SUITES_PATH.exists():
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(CUSTOM_SUITES_PATH.read_text(encoding='utf-8'))
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
out: Dict[str, List[str]] = {}
|
||||
for name, items in data.items():
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
continue
|
||||
if not isinstance(items, list):
|
||||
continue
|
||||
cleaned = [str(x).strip() for x in items if str(x).strip()]
|
||||
if cleaned:
|
||||
out[name.strip()] = cleaned
|
||||
return out
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _save_custom_suites(suites: Dict[str, List[str]]) -> None:
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CUSTOM_SUITES_PATH.write_text(
|
||||
json.dumps(suites, ensure_ascii=False, indent=2),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
|
||||
class CustomSuiteRequest(BaseModel):
|
||||
name: str = Field(..., min_length=1)
|
||||
datasets: List[str] = Field(..., min_length=1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job store
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -208,6 +243,7 @@ def _meta() -> dict:
|
||||
}
|
||||
return {
|
||||
'suites': suites,
|
||||
'custom_suites': _load_custom_suites(),
|
||||
'benchmarks': all_benchmarks,
|
||||
'categories': categories,
|
||||
'multi_run': run_module.MULTI_RUN_CONFIG,
|
||||
@ -530,6 +566,44 @@ async def stream_logs(job_id: str, offset: int = 0):
|
||||
return StreamingResponse(event_gen(), media_type='text/event-stream')
|
||||
|
||||
|
||||
@app.get('/api/custom-suites')
|
||||
async def list_custom_suites():
|
||||
return {'suites': _load_custom_suites()}
|
||||
|
||||
|
||||
@app.post('/api/custom-suites')
|
||||
async def upsert_custom_suite(req: CustomSuiteRequest):
|
||||
name = req.name.strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail='组合名称不能为空')
|
||||
if name in run_module.SUITES:
|
||||
raise HTTPException(status_code=400, detail=f'名称与内置 suite 冲突: {name}')
|
||||
datasets = []
|
||||
seen = set()
|
||||
for item in req.datasets:
|
||||
d = str(item).strip()
|
||||
if not d or d in seen:
|
||||
continue
|
||||
seen.add(d)
|
||||
datasets.append(d)
|
||||
if not datasets:
|
||||
raise HTTPException(status_code=400, detail='请至少选择一个 benchmark')
|
||||
suites = _load_custom_suites()
|
||||
suites[name] = datasets
|
||||
_save_custom_suites(suites)
|
||||
return {'ok': True, 'name': name, 'datasets': datasets, 'suites': suites}
|
||||
|
||||
|
||||
@app.delete('/api/custom-suites/{name}')
|
||||
async def delete_custom_suite(name: str):
|
||||
suites = _load_custom_suites()
|
||||
if name not in suites:
|
||||
raise HTTPException(status_code=404, detail='自定义组合不存在')
|
||||
suites.pop(name, None)
|
||||
_save_custom_suites(suites)
|
||||
return {'ok': True, 'suites': suites}
|
||||
|
||||
|
||||
@app.get('/api/results/overview')
|
||||
async def results_overview(output_dir: Optional[str] = None):
|
||||
root = Path(output_dir) if output_dir else DEFAULT_OUTPUT_DIR
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
const state = {
|
||||
meta: null,
|
||||
selectionMode: 'suite',
|
||||
suiteKind: 'builtin', // builtin | custom
|
||||
suite: 'official',
|
||||
customSuites: {},
|
||||
activeJobId: null,
|
||||
pollTimer: null,
|
||||
logOffset: 0,
|
||||
@ -48,6 +50,16 @@
|
||||
$('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, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function renderSuites() {
|
||||
@ -58,18 +70,87 @@
|
||||
const info = suites[name];
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = `suite-card${state.suite === name ? ' active' : ''}`;
|
||||
btn.innerHTML = `<strong>${name}</strong><small>${info.all.length} benchmarks<br>${info.all.slice(0, 6).join(', ')}${info.all.length > 6 ? '…' : ''}</small>`;
|
||||
btn.className = `suite-card${state.suiteKind === 'builtin' && state.suite === name ? ' active' : ''}`;
|
||||
const names = info.all.map(escapeHtml).join(', ');
|
||||
btn.innerHTML = `
|
||||
<strong>${escapeHtml(name)}</strong>
|
||||
<span class="suite-count">${info.all.length} benchmarks</span>
|
||||
<div class="suite-names">${names}</div>
|
||||
`;
|
||||
btn.addEventListener('click', () => {
|
||||
state.suiteKind = 'builtin';
|
||||
state.suite = name;
|
||||
renderSuites();
|
||||
renderCustomSuites();
|
||||
});
|
||||
box.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
function renderBenchmarks() {
|
||||
const box = $('benchmarkList');
|
||||
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 = `
|
||||
<strong>${escapeHtml(name)}</strong>
|
||||
<span class="suite-count">${items.length} benchmarks · 自定义</span>
|
||||
<div class="suite-names">${items.map(escapeHtml).join(', ')}</div>
|
||||
`;
|
||||
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 || [
|
||||
@ -79,13 +160,12 @@
|
||||
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 = `
|
||||
<div class="bench-cat-title">
|
||||
<strong>${cat.name}</strong>
|
||||
<strong>${escapeHtml(cat.name)}</strong>
|
||||
<span class="muted">${cat.items.length}</span>
|
||||
</div>
|
||||
<div class="bench-cat-actions">
|
||||
@ -95,11 +175,11 @@
|
||||
`;
|
||||
head.querySelector('.cat-select').addEventListener('click', () => {
|
||||
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = true; });
|
||||
updateSelectedCount();
|
||||
withCountCb();
|
||||
});
|
||||
head.querySelector('.cat-clear').addEventListener('click', () => {
|
||||
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = false; });
|
||||
updateSelectedCount();
|
||||
withCountCb();
|
||||
});
|
||||
section.appendChild(head);
|
||||
|
||||
@ -110,20 +190,28 @@
|
||||
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 = `<input type="checkbox" value="${name}" /><span>${name}</span>`;
|
||||
label.innerHTML = `<input type="checkbox" value="${escapeHtml(name)}" /><span>${escapeHtml(name)}</span>`;
|
||||
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);
|
||||
label.querySelector('input').addEventListener('change', withCountCb);
|
||||
grid.appendChild(label);
|
||||
});
|
||||
section.appendChild(grid);
|
||||
box.appendChild(section);
|
||||
});
|
||||
updateSelectedCount();
|
||||
withCountCb();
|
||||
}
|
||||
|
||||
function renderBenchmarks() {
|
||||
renderBenchmarkPicker('benchmarkList', updateSelectedCount);
|
||||
}
|
||||
|
||||
function renderCustomPicker() {
|
||||
renderBenchmarkPicker('customPickList', updateCustomPickCount);
|
||||
}
|
||||
|
||||
function updateSelectedCount() {
|
||||
@ -131,6 +219,11 @@
|
||||
$('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) => {
|
||||
@ -147,15 +240,25 @@
|
||||
? 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: state.selectionMode,
|
||||
suite: state.suite,
|
||||
datasets,
|
||||
exclude,
|
||||
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),
|
||||
@ -177,6 +280,19 @@
|
||||
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';
|
||||
}
|
||||
@ -215,10 +331,10 @@
|
||||
const thinking = job.payload?.thinking ? 'thinking' : 'no-thinking';
|
||||
btn.innerHTML = `
|
||||
<div class="row">
|
||||
<strong>${job.id}</strong>
|
||||
<span class="badge ${statusClass(job.status)}">${job.status}</span>
|
||||
<strong>${escapeHtml(job.id)}</strong>
|
||||
<span class="badge ${statusClass(job.status)}">${escapeHtml(job.status)}</span>
|
||||
</div>
|
||||
<small>${model} · ${thinking}</small>
|
||||
<small>${escapeHtml(model)} · ${thinking}</small>
|
||||
`;
|
||||
btn.addEventListener('click', () => followJob(job.id, true));
|
||||
if (job.id === activeId) btn.style.borderColor = 'rgba(214,162,74,0.75)';
|
||||
@ -302,7 +418,9 @@
|
||||
state.meta = await api('/api/meta');
|
||||
fillDefaults(state.meta);
|
||||
renderSuites();
|
||||
renderCustomSuites();
|
||||
renderBenchmarks();
|
||||
renderCustomPicker();
|
||||
setMode('suite');
|
||||
|
||||
const jobs = await refreshJobs();
|
||||
@ -326,12 +444,73 @@
|
||||
});
|
||||
$('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');
|
||||
setMsg('请至少选择一个 benchmark / 自定义组合', 'error');
|
||||
return;
|
||||
}
|
||||
$('launchBtn').disabled = true;
|
||||
|
||||
@ -34,6 +34,28 @@
|
||||
<main class="layout">
|
||||
<section class="panel form-panel">
|
||||
<form id="launchForm">
|
||||
<div class="block">
|
||||
<h2>路径配置(必填)</h2>
|
||||
<div class="grid-2">
|
||||
<label class="full">
|
||||
<span>Dataset Dir</span>
|
||||
<input id="dataset_dir" name="dataset_dir" required />
|
||||
</label>
|
||||
<label class="full">
|
||||
<span>Output Dir</span>
|
||||
<input id="output_dir" name="output_dir" required />
|
||||
</label>
|
||||
<label class="full">
|
||||
<span>Config YAML</span>
|
||||
<input id="config" name="config" required />
|
||||
</label>
|
||||
<label class="full">
|
||||
<span>Tokenizer Path</span>
|
||||
<input id="tokenizer_path" name="tokenizer_path" required />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2>模型接入</h2>
|
||||
<div class="grid-2">
|
||||
@ -78,14 +100,36 @@
|
||||
<div class="block">
|
||||
<h2>评测范围</h2>
|
||||
<div class="mode-tabs" role="tablist">
|
||||
<button type="button" class="tab active" data-mode="suite">按 Suite</button>
|
||||
<button type="button" class="tab active" data-mode="suite">按 Suite / 自定义组合</button>
|
||||
<button type="button" class="tab" data-mode="datasets">自选 Benchmark</button>
|
||||
</div>
|
||||
|
||||
<div id="suitePane" class="pane">
|
||||
<h3 class="subhead">内置 Suite</h3>
|
||||
<div id="suiteList" class="suite-list"></div>
|
||||
|
||||
<h3 class="subhead mt">已保存的自定义组合</h3>
|
||||
<div id="customSuiteList" class="suite-list"></div>
|
||||
<p id="customSuiteEmpty" class="muted">暂无自定义组合,可在下方勾选并保存</p>
|
||||
|
||||
<div class="custom-editor mt">
|
||||
<h3 class="subhead">新建 / 更新自定义组合</h3>
|
||||
<div class="custom-editor-row">
|
||||
<label>
|
||||
<span>组合名称</span>
|
||||
<input id="customSuiteName" placeholder="例如: my_math_code" />
|
||||
</label>
|
||||
<div class="custom-editor-actions">
|
||||
<button type="button" id="saveCustomSuiteBtn" class="primary">永久保存组合</button>
|
||||
<button type="button" id="loadCheckedToCustomBtn" class="ghost">从下方勾选填入</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="customPickList" class="benchmark-list compact"></div>
|
||||
<span id="customPickCount" class="muted">已勾选 0</span>
|
||||
</div>
|
||||
|
||||
<label class="mt">
|
||||
<span>Exclude(逗号分隔,可选)</span>
|
||||
<span>Exclude(逗号分隔,可选,仅对内置 Suite 生效)</span>
|
||||
<input id="exclude" name="exclude" placeholder="例如: tau2_bench,hle" />
|
||||
</label>
|
||||
</div>
|
||||
@ -94,6 +138,7 @@
|
||||
<div class="bench-actions">
|
||||
<button type="button" id="selectAllBtn" class="ghost">全选</button>
|
||||
<button type="button" id="clearAllBtn" class="ghost">清空</button>
|
||||
<button type="button" id="saveFromDatasetsBtn" class="ghost">保存为自定义组合</button>
|
||||
<span id="selectedCount" class="muted">已选 0</span>
|
||||
</div>
|
||||
<div id="benchmarkList" class="benchmark-list"></div>
|
||||
@ -101,7 +146,7 @@
|
||||
</div>
|
||||
|
||||
<details class="block advanced">
|
||||
<summary>高级参数</summary>
|
||||
<summary>其他参数</summary>
|
||||
<div class="grid-2 mt">
|
||||
<label>
|
||||
<span>Limit(none = 全量)</span>
|
||||
@ -122,22 +167,6 @@
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Dataset Dir</span>
|
||||
<input id="dataset_dir" name="dataset_dir" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Output Dir</span>
|
||||
<input id="output_dir" name="output_dir" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Config YAML</span>
|
||||
<input id="config" name="config" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Tokenizer Path</span>
|
||||
<input id="tokenizer_path" name="tokenizer_path" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Judge Model</span>
|
||||
<input id="judge_model" name="judge_model" />
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/static/styles.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js"></script>
|
||||
<script src="/static/vendor/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
@ -65,6 +65,26 @@
|
||||
</section>
|
||||
|
||||
<section class="panel charts-panel">
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<h2>能力分类走势</h2>
|
||||
<span class="muted">横轴 = 能力域(数学/代码/智能体等),纵轴 = 该域平均分</span>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<canvas id="categoryLineChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<h2>能力分类柱状对比</h2>
|
||||
<span class="muted">各模型在同一能力域的平均得分</span>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<canvas id="categoryBarChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-block">
|
||||
<div class="side-head">
|
||||
<h2>得分走势</h2>
|
||||
@ -117,6 +137,6 @@
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/static/results.js"></script>
|
||||
<script src="/static/results.js?v=20260729"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
const state = {
|
||||
overview: null,
|
||||
compare: null,
|
||||
charts: { line: null, bar: null, radar: null },
|
||||
charts: { line: null, bar: null, radar: null, catLine: null, catBar: null },
|
||||
};
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
@ -125,15 +125,66 @@
|
||||
});
|
||||
}
|
||||
|
||||
function axisOptions(maxRotation = 45) {
|
||||
return {
|
||||
x: {
|
||||
ticks: { color: '#92a197', maxRotation, minRotation: 0, font: { size: 12 } },
|
||||
grid: { color: 'rgba(51,64,56,0.6)' },
|
||||
},
|
||||
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' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function markEmpty(el, msg) {
|
||||
if (!el) return;
|
||||
const wrap = el.parentElement || el;
|
||||
wrap.classList.add('empty');
|
||||
wrap.dataset.empty = msg;
|
||||
}
|
||||
|
||||
function clearEmpty(el) {
|
||||
if (!el) return;
|
||||
const wrap = el.parentElement || el;
|
||||
wrap.classList.remove('empty');
|
||||
delete wrap.dataset.empty;
|
||||
}
|
||||
|
||||
function makeChart(key, canvas, config) {
|
||||
if (typeof Chart === 'undefined') {
|
||||
markEmpty(canvas, 'Chart.js 未加载,请强制刷新页面 (Ctrl+F5)');
|
||||
return null;
|
||||
}
|
||||
if (!canvas) return null;
|
||||
try {
|
||||
clearEmpty(canvas);
|
||||
const chart = new Chart(canvas, config);
|
||||
state.charts[key] = chart;
|
||||
return chart;
|
||||
} catch (err) {
|
||||
console.error('chart error', key, err);
|
||||
markEmpty(canvas, `图表渲染失败: ${err.message || err}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderCharts(compare) {
|
||||
destroyCharts();
|
||||
const labels = compare.benchmarks;
|
||||
const labels = compare.benchmarks || [];
|
||||
const common = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: { color: '#c9d4cb', boxWidth: 12, font: { family: 'IBM Plex Sans' } },
|
||||
labels: { color: '#c9d4cb', boxWidth: 12, font: { family: 'IBM Plex Sans', size: 13 } },
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
@ -147,60 +198,62 @@
|
||||
scales: {},
|
||||
};
|
||||
|
||||
state.charts.line = new Chart($('lineChart'), {
|
||||
// Capability-domain charts
|
||||
const catLabels = compare.category_labels || [];
|
||||
const catSeries = compare.category_series || [];
|
||||
const catLineCanvas = $('categoryLineChart');
|
||||
const catBarCanvas = $('categoryBarChart');
|
||||
if (!catLabels.length || !catSeries.length) {
|
||||
markEmpty(catLineCanvas, '当前选择下无可用能力分类数据');
|
||||
markEmpty(catBarCanvas, '当前选择下无可用能力分类数据');
|
||||
} else {
|
||||
makeChart('catLine', catLineCanvas, {
|
||||
type: 'line',
|
||||
data: { labels, datasets: chartDatasets(compare.series, 'line') },
|
||||
data: { labels: catLabels, datasets: chartDatasets(catSeries, 'line') },
|
||||
options: { ...common, scales: axisOptions(0) },
|
||||
});
|
||||
makeChart('catBar', catBarCanvas, {
|
||||
type: 'bar',
|
||||
data: { labels: catLabels, datasets: chartDatasets(catSeries, 'bar') },
|
||||
options: {
|
||||
...common,
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: '#92a197', maxRotation: 45, minRotation: 0 },
|
||||
grid: { color: 'rgba(51,64,56,0.6)' },
|
||||
},
|
||||
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' },
|
||||
},
|
||||
x: { ...axisOptions(0).x, grid: { display: false } },
|
||||
y: axisOptions(0).y,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
state.charts.bar = new Chart($('barChart'), {
|
||||
if (!labels.length) {
|
||||
markEmpty($('lineChart'), '暂无 benchmark 得分');
|
||||
markEmpty($('barChart'), '暂无 benchmark 得分');
|
||||
} else {
|
||||
makeChart('line', $('lineChart'), {
|
||||
type: 'line',
|
||||
data: { labels, datasets: chartDatasets(compare.series, 'line') },
|
||||
options: { ...common, scales: axisOptions(45) },
|
||||
});
|
||||
makeChart('bar', $('barChart'), {
|
||||
type: 'bar',
|
||||
data: { labels, datasets: chartDatasets(compare.series, 'bar') },
|
||||
options: {
|
||||
...common,
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: '#92a197', maxRotation: 45 },
|
||||
grid: { display: false },
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
ticks: { color: '#92a197', callback: (v) => v + '%' },
|
||||
grid: { color: 'rgba(51,64,56,0.6)' },
|
||||
},
|
||||
x: { ...axisOptions(45).x, grid: { display: false } },
|
||||
y: axisOptions(45).y,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const radarLabels = labels;
|
||||
const canRadar = radarLabels.length >= 3 && compare.series.length > 0;
|
||||
const canRadar = radarLabels.length >= 3 && (compare.series || []).length > 0;
|
||||
const radarCanvas = $('radarChart');
|
||||
if (!canRadar) {
|
||||
radarCanvas.parentElement.classList.add('empty');
|
||||
radarCanvas.parentElement.dataset.empty = '共同 benchmark 不足 3 个,暂不绘制雷达图';
|
||||
markEmpty(radarCanvas, '共同 benchmark 不足 3 个,暂不绘制雷达图');
|
||||
} else {
|
||||
radarCanvas.parentElement.classList.remove('empty');
|
||||
delete radarCanvas.parentElement.dataset.empty;
|
||||
state.charts.radar = new Chart(radarCanvas, {
|
||||
makeChart('radar', radarCanvas, {
|
||||
type: 'radar',
|
||||
data: { labels: radarLabels, datasets: chartDatasets(compare.series, 'radar') },
|
||||
options: {
|
||||
@ -216,7 +269,7 @@
|
||||
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: 11 } },
|
||||
pointLabels: { color: '#c9d4cb', font: { size: 12 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -92,24 +92,31 @@ code, pre, .mono { font-family: var(--mono); }
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.block + .block { margin-top: 22px; }
|
||||
.block + .block { margin-top: 18px; }
|
||||
.block h2, .side-head h2, .log-head h3, .jobs-head h3 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
margin: 0 0 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
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: 6px;
|
||||
font-size: 12px;
|
||||
gap: 5px;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
label span { letter-spacing: 0.03em; }
|
||||
label.full { grid-column: 1 / -1; }
|
||||
label span { letter-spacing: 0.02em; color: #b7c4ba; }
|
||||
|
||||
input, select, button, summary {
|
||||
font: inherit;
|
||||
@ -119,8 +126,10 @@ input, select {
|
||||
background: var(--bg0);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
padding: 7px 10px;
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
@ -170,23 +179,24 @@ input:focus, select:focus {
|
||||
transform: translateX(18px);
|
||||
background: var(--accent);
|
||||
}
|
||||
.switch-text { color: var(--text); font-size: 14px; }
|
||||
.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: 4px;
|
||||
margin-bottom: 14px;
|
||||
padding: 3px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tab {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
padding: 8px 14px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.tab.active {
|
||||
background: rgba(214, 162, 74, 0.18);
|
||||
@ -195,40 +205,97 @@ input:focus, select:focus {
|
||||
|
||||
.suite-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.suite-card {
|
||||
text-align: left;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg0);
|
||||
color: var(--text);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
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 strong { display: block; margin-bottom: 4px; }
|
||||
.suite-card small { color: var(--muted); line-height: 1.4; }
|
||||
.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: 12px;
|
||||
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: 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(12, 16, 14, 0.55);
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
.bench-category.cat-math { border-left: 3px solid #6fbf8a; }
|
||||
.bench-category.cat-code { border-left: 3px solid #6aa8d6; }
|
||||
@ -251,32 +318,35 @@ input:focus, select:focus {
|
||||
gap: 8px;
|
||||
}
|
||||
.bench-cat-title strong {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.03em;
|
||||
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: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
.bench-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg0);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
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: 11px;
|
||||
font-size: 12px;
|
||||
color: #9ee0b2;
|
||||
opacity: 0.85;
|
||||
}
|
||||
@ -284,7 +354,7 @@ input:focus, select:focus {
|
||||
.bench-actions {
|
||||
display: flex; gap: 8px; align-items: center; margin-bottom: 10px;
|
||||
}
|
||||
.muted { color: var(--muted); font-size: 12px; }
|
||||
.muted { color: var(--muted); font-size: 13px; }
|
||||
.mt { margin-top: 12px; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
@ -307,12 +377,13 @@ input:focus, select:focus {
|
||||
margin-top: 22px;
|
||||
}
|
||||
button {
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
padding: 10px 16px;
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
background: var(--bg2);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
}
|
||||
button:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
button.primary {
|
||||
@ -328,10 +399,10 @@ button.danger {
|
||||
}
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
padding: 5px 9px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.msg { color: var(--muted); font-size: 13px; }
|
||||
.msg { color: var(--muted); font-size: 14px; }
|
||||
.msg.error { color: var(--danger); }
|
||||
.msg.ok { color: var(--accent-2); }
|
||||
|
||||
@ -582,15 +653,29 @@ button.ghost {
|
||||
}
|
||||
.rank-row {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr auto;
|
||||
grid-template-columns: 28px minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
align-items: start;
|
||||
font-size: 13px;
|
||||
padding: 6px 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
.rank-idx { color: var(--accent); font-family: var(--mono); }
|
||||
.rank-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
|
||||
.rank-score { font-family: var(--mono); color: #c9d4cb; }
|
||||
.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;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
|
||||
20
webui/static/vendor/chart.umd.min.js
vendored
Normal file
20
webui/static/vendor/chart.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user