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)
|
rows.sort(key=lambda x: x['score'], reverse=True)
|
||||||
ranking.append({'benchmark': b, 'rows': rows})
|
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 {
|
return {
|
||||||
'output_dir': overview['output_dir'],
|
'output_dir': overview['output_dir'],
|
||||||
'benchmarks': bench_list,
|
'benchmarks': bench_list,
|
||||||
@ -311,6 +374,9 @@ def compare_models(
|
|||||||
'models': [m for m in overview['models'] if m['folder'] in selected],
|
'models': [m for m in overview['models'] if m['folder'] in selected],
|
||||||
'series': series,
|
'series': series,
|
||||||
'ranking': ranking,
|
'ranking': ranking,
|
||||||
|
'category_labels': category_labels,
|
||||||
|
'category_series': category_series,
|
||||||
|
'category_ranking': category_ranking,
|
||||||
'matrix': {
|
'matrix': {
|
||||||
f: {b: overview['matrix'].get(f, {}).get(b) for b in bench_list}
|
f: {b: overview['matrix'].get(f, {}).get(b) for b in bench_list}
|
||||||
for f in selected
|
for f in selected
|
||||||
|
|||||||
@ -35,10 +35,45 @@ JOBS_DIR.mkdir(parents=True, exist_ok=True)
|
|||||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
DEFAULT_OUTPUT_DIR = Path(run_module.DEFAULT_OUTPUT_DIR)
|
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')
|
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
|
# Job store
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -208,6 +243,7 @@ def _meta() -> dict:
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'suites': suites,
|
'suites': suites,
|
||||||
|
'custom_suites': _load_custom_suites(),
|
||||||
'benchmarks': all_benchmarks,
|
'benchmarks': all_benchmarks,
|
||||||
'categories': categories,
|
'categories': categories,
|
||||||
'multi_run': run_module.MULTI_RUN_CONFIG,
|
'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')
|
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')
|
@app.get('/api/results/overview')
|
||||||
async def results_overview(output_dir: Optional[str] = None):
|
async def results_overview(output_dir: Optional[str] = None):
|
||||||
root = Path(output_dir) if output_dir else DEFAULT_OUTPUT_DIR
|
root = Path(output_dir) if output_dir else DEFAULT_OUTPUT_DIR
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
const state = {
|
const state = {
|
||||||
meta: null,
|
meta: null,
|
||||||
selectionMode: 'suite',
|
selectionMode: 'suite',
|
||||||
|
suiteKind: 'builtin', // builtin | custom
|
||||||
suite: 'official',
|
suite: 'official',
|
||||||
|
customSuites: {},
|
||||||
activeJobId: null,
|
activeJobId: null,
|
||||||
pollTimer: null,
|
pollTimer: null,
|
||||||
logOffset: 0,
|
logOffset: 0,
|
||||||
@ -48,6 +50,16 @@
|
|||||||
$('judge_api_url').value = d.judge_api_url || '';
|
$('judge_api_url').value = d.judge_api_url || '';
|
||||||
$('judge_max_tokens').value = d.judge_max_tokens || '';
|
$('judge_max_tokens').value = d.judge_max_tokens || '';
|
||||||
state.suite = d.suite || 'official';
|
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() {
|
function renderSuites() {
|
||||||
@ -58,18 +70,87 @@
|
|||||||
const info = suites[name];
|
const info = suites[name];
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
btn.className = `suite-card${state.suite === name ? ' active' : ''}`;
|
btn.className = `suite-card${state.suiteKind === 'builtin' && 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>`;
|
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', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
state.suiteKind = 'builtin';
|
||||||
state.suite = name;
|
state.suite = name;
|
||||||
renderSuites();
|
renderSuites();
|
||||||
|
renderCustomSuites();
|
||||||
});
|
});
|
||||||
box.appendChild(btn);
|
box.appendChild(btn);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBenchmarks() {
|
function renderCustomSuites() {
|
||||||
const box = $('benchmarkList');
|
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 = '';
|
box.innerHTML = '';
|
||||||
const multi = new Set(Object.keys(state.meta.multi_run || {}));
|
const multi = new Set(Object.keys(state.meta.multi_run || {}));
|
||||||
const categories = state.meta.categories || [
|
const categories = state.meta.categories || [
|
||||||
@ -79,13 +160,12 @@
|
|||||||
categories.forEach((cat) => {
|
categories.forEach((cat) => {
|
||||||
const section = document.createElement('section');
|
const section = document.createElement('section');
|
||||||
section.className = `bench-category cat-${cat.id}`;
|
section.className = `bench-category cat-${cat.id}`;
|
||||||
section.dataset.category = cat.id;
|
|
||||||
|
|
||||||
const head = document.createElement('div');
|
const head = document.createElement('div');
|
||||||
head.className = 'bench-cat-head';
|
head.className = 'bench-cat-head';
|
||||||
head.innerHTML = `
|
head.innerHTML = `
|
||||||
<div class="bench-cat-title">
|
<div class="bench-cat-title">
|
||||||
<strong>${cat.name}</strong>
|
<strong>${escapeHtml(cat.name)}</strong>
|
||||||
<span class="muted">${cat.items.length}</span>
|
<span class="muted">${cat.items.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bench-cat-actions">
|
<div class="bench-cat-actions">
|
||||||
@ -95,11 +175,11 @@
|
|||||||
`;
|
`;
|
||||||
head.querySelector('.cat-select').addEventListener('click', () => {
|
head.querySelector('.cat-select').addEventListener('click', () => {
|
||||||
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = true; });
|
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = true; });
|
||||||
updateSelectedCount();
|
withCountCb();
|
||||||
});
|
});
|
||||||
head.querySelector('.cat-clear').addEventListener('click', () => {
|
head.querySelector('.cat-clear').addEventListener('click', () => {
|
||||||
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = false; });
|
section.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = false; });
|
||||||
updateSelectedCount();
|
withCountCb();
|
||||||
});
|
});
|
||||||
section.appendChild(head);
|
section.appendChild(head);
|
||||||
|
|
||||||
@ -110,20 +190,28 @@
|
|||||||
const isMulti = multi.has(name);
|
const isMulti = multi.has(name);
|
||||||
label.className = `bench-item${isMulti ? ' multi' : ''}`;
|
label.className = `bench-item${isMulti ? ' multi' : ''}`;
|
||||||
label.title = isMulti ? `multi-run x${state.meta.multi_run[name]}` : cat.name;
|
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) {
|
if (isMulti) {
|
||||||
const tag = document.createElement('em');
|
const tag = document.createElement('em');
|
||||||
tag.className = 'run-tag';
|
tag.className = 'run-tag';
|
||||||
tag.textContent = `×${state.meta.multi_run[name]}`;
|
tag.textContent = `×${state.meta.multi_run[name]}`;
|
||||||
label.appendChild(tag);
|
label.appendChild(tag);
|
||||||
}
|
}
|
||||||
label.querySelector('input').addEventListener('change', updateSelectedCount);
|
label.querySelector('input').addEventListener('change', withCountCb);
|
||||||
grid.appendChild(label);
|
grid.appendChild(label);
|
||||||
});
|
});
|
||||||
section.appendChild(grid);
|
section.appendChild(grid);
|
||||||
box.appendChild(section);
|
box.appendChild(section);
|
||||||
});
|
});
|
||||||
updateSelectedCount();
|
withCountCb();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBenchmarks() {
|
||||||
|
renderBenchmarkPicker('benchmarkList', updateSelectedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCustomPicker() {
|
||||||
|
renderBenchmarkPicker('customPickList', updateCustomPickCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelectedCount() {
|
function updateSelectedCount() {
|
||||||
@ -131,6 +219,11 @@
|
|||||||
$('selectedCount').textContent = `已选 ${n}`;
|
$('selectedCount').textContent = `已选 ${n}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateCustomPickCount() {
|
||||||
|
const n = [...document.querySelectorAll('#customPickList input:checked')].length;
|
||||||
|
$('customPickCount').textContent = `已勾选 ${n}`;
|
||||||
|
}
|
||||||
|
|
||||||
function setMode(mode) {
|
function setMode(mode) {
|
||||||
state.selectionMode = mode;
|
state.selectionMode = mode;
|
||||||
document.querySelectorAll('.mode-tabs .tab').forEach((el) => {
|
document.querySelectorAll('.mode-tabs .tab').forEach((el) => {
|
||||||
@ -147,15 +240,25 @@
|
|||||||
? excludeRaw.split(',').map((s) => s.trim()).filter(Boolean)
|
? 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 = {
|
const payload = {
|
||||||
model: $('model').value.trim(),
|
model: $('model').value.trim(),
|
||||||
api_url: $('api_url').value.trim(),
|
api_url: $('api_url').value.trim(),
|
||||||
api_key: $('api_key').value.trim() || 'EMPTY',
|
api_key: $('api_key').value.trim() || 'EMPTY',
|
||||||
thinking: $('thinking').checked,
|
thinking: $('thinking').checked,
|
||||||
selection_mode: state.selectionMode,
|
selection_mode: selectionMode,
|
||||||
suite: state.suite,
|
suite,
|
||||||
datasets,
|
datasets: finalDatasets,
|
||||||
exclude,
|
exclude: selectionMode === 'suite' ? exclude : [],
|
||||||
folder_name: $('folder_name').value.trim() || null,
|
folder_name: $('folder_name').value.trim() || null,
|
||||||
limit: $('limit').value.trim() || null,
|
limit: $('limit').value.trim() || null,
|
||||||
seed: Number($('seed').value || 42),
|
seed: Number($('seed').value || 42),
|
||||||
@ -177,6 +280,19 @@
|
|||||||
return payload;
|
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) {
|
function statusClass(status) {
|
||||||
return status || 'idle';
|
return status || 'idle';
|
||||||
}
|
}
|
||||||
@ -215,10 +331,10 @@
|
|||||||
const thinking = job.payload?.thinking ? 'thinking' : 'no-thinking';
|
const thinking = job.payload?.thinking ? 'thinking' : 'no-thinking';
|
||||||
btn.innerHTML = `
|
btn.innerHTML = `
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<strong>${job.id}</strong>
|
<strong>${escapeHtml(job.id)}</strong>
|
||||||
<span class="badge ${statusClass(job.status)}">${job.status}</span>
|
<span class="badge ${statusClass(job.status)}">${escapeHtml(job.status)}</span>
|
||||||
</div>
|
</div>
|
||||||
<small>${model} · ${thinking}</small>
|
<small>${escapeHtml(model)} · ${thinking}</small>
|
||||||
`;
|
`;
|
||||||
btn.addEventListener('click', () => followJob(job.id, true));
|
btn.addEventListener('click', () => followJob(job.id, true));
|
||||||
if (job.id === activeId) btn.style.borderColor = 'rgba(214,162,74,0.75)';
|
if (job.id === activeId) btn.style.borderColor = 'rgba(214,162,74,0.75)';
|
||||||
@ -302,7 +418,9 @@
|
|||||||
state.meta = await api('/api/meta');
|
state.meta = await api('/api/meta');
|
||||||
fillDefaults(state.meta);
|
fillDefaults(state.meta);
|
||||||
renderSuites();
|
renderSuites();
|
||||||
|
renderCustomSuites();
|
||||||
renderBenchmarks();
|
renderBenchmarks();
|
||||||
|
renderCustomPicker();
|
||||||
setMode('suite');
|
setMode('suite');
|
||||||
|
|
||||||
const jobs = await refreshJobs();
|
const jobs = await refreshJobs();
|
||||||
@ -326,12 +444,73 @@
|
|||||||
});
|
});
|
||||||
$('refreshJobsBtn').addEventListener('click', () => refreshJobs().catch((e) => setMsg(e.message, 'error')));
|
$('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) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setMsg('');
|
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();
|
const payload = collectPayload();
|
||||||
if (payload.selection_mode === 'datasets' && !payload.datasets.length) {
|
if (payload.selection_mode === 'datasets' && !payload.datasets.length) {
|
||||||
setMsg('请至少选择一个 benchmark', 'error');
|
setMsg('请至少选择一个 benchmark / 自定义组合', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$('launchBtn').disabled = true;
|
$('launchBtn').disabled = true;
|
||||||
|
|||||||
@ -34,6 +34,28 @@
|
|||||||
<main class="layout">
|
<main class="layout">
|
||||||
<section class="panel form-panel">
|
<section class="panel form-panel">
|
||||||
<form id="launchForm">
|
<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">
|
<div class="block">
|
||||||
<h2>模型接入</h2>
|
<h2>模型接入</h2>
|
||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
@ -78,14 +100,36 @@
|
|||||||
<div class="block">
|
<div class="block">
|
||||||
<h2>评测范围</h2>
|
<h2>评测范围</h2>
|
||||||
<div class="mode-tabs" role="tablist">
|
<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>
|
<button type="button" class="tab" data-mode="datasets">自选 Benchmark</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="suitePane" class="pane">
|
<div id="suitePane" class="pane">
|
||||||
|
<h3 class="subhead">内置 Suite</h3>
|
||||||
<div id="suiteList" class="suite-list"></div>
|
<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">
|
<label class="mt">
|
||||||
<span>Exclude(逗号分隔,可选)</span>
|
<span>Exclude(逗号分隔,可选,仅对内置 Suite 生效)</span>
|
||||||
<input id="exclude" name="exclude" placeholder="例如: tau2_bench,hle" />
|
<input id="exclude" name="exclude" placeholder="例如: tau2_bench,hle" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -94,6 +138,7 @@
|
|||||||
<div class="bench-actions">
|
<div class="bench-actions">
|
||||||
<button type="button" id="selectAllBtn" class="ghost">全选</button>
|
<button type="button" id="selectAllBtn" class="ghost">全选</button>
|
||||||
<button type="button" id="clearAllBtn" 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>
|
<span id="selectedCount" class="muted">已选 0</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="benchmarkList" class="benchmark-list"></div>
|
<div id="benchmarkList" class="benchmark-list"></div>
|
||||||
@ -101,7 +146,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="block advanced">
|
<details class="block advanced">
|
||||||
<summary>高级参数</summary>
|
<summary>其他参数</summary>
|
||||||
<div class="grid-2 mt">
|
<div class="grid-2 mt">
|
||||||
<label>
|
<label>
|
||||||
<span>Limit(none = 全量)</span>
|
<span>Limit(none = 全量)</span>
|
||||||
@ -122,22 +167,6 @@
|
|||||||
<option value="false">否</option>
|
<option value="false">否</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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>
|
<label>
|
||||||
<span>Judge Model</span>
|
<span>Judge Model</span>
|
||||||
<input id="judge_model" name="judge_model" />
|
<input id="judge_model" name="judge_model" />
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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 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" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
@ -65,6 +65,26 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel charts-panel">
|
<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="chart-block">
|
||||||
<div class="side-head">
|
<div class="side-head">
|
||||||
<h2>得分走势</h2>
|
<h2>得分走势</h2>
|
||||||
@ -117,6 +137,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/results.js"></script>
|
<script src="/static/results.js?v=20260729"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
const state = {
|
const state = {
|
||||||
overview: null,
|
overview: null,
|
||||||
compare: 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);
|
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) {
|
function renderCharts(compare) {
|
||||||
destroyCharts();
|
destroyCharts();
|
||||||
const labels = compare.benchmarks;
|
const labels = compare.benchmarks || [];
|
||||||
const common = {
|
const common = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
labels: { color: '#c9d4cb', boxWidth: 12, font: { family: 'IBM Plex Sans' } },
|
labels: { color: '#c9d4cb', boxWidth: 12, font: { family: 'IBM Plex Sans', size: 13 } },
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
@ -147,60 +198,62 @@
|
|||||||
scales: {},
|
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',
|
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: {
|
options: {
|
||||||
...common,
|
...common,
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: { ...axisOptions(0).x, grid: { display: false } },
|
||||||
ticks: { color: '#92a197', maxRotation: 45, minRotation: 0 },
|
y: axisOptions(0).y,
|
||||||
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' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
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',
|
type: 'bar',
|
||||||
data: { labels, datasets: chartDatasets(compare.series, 'bar') },
|
data: { labels, datasets: chartDatasets(compare.series, 'bar') },
|
||||||
options: {
|
options: {
|
||||||
...common,
|
...common,
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: { ...axisOptions(45).x, grid: { display: false } },
|
||||||
ticks: { color: '#92a197', maxRotation: 45 },
|
y: axisOptions(45).y,
|
||||||
grid: { display: false },
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
ticks: { color: '#92a197', callback: (v) => v + '%' },
|
|
||||||
grid: { color: 'rgba(51,64,56,0.6)' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const radarLabels = labels;
|
const radarLabels = labels;
|
||||||
const canRadar = radarLabels.length >= 3 && compare.series.length > 0;
|
const canRadar = radarLabels.length >= 3 && (compare.series || []).length > 0;
|
||||||
const radarCanvas = $('radarChart');
|
const radarCanvas = $('radarChart');
|
||||||
if (!canRadar) {
|
if (!canRadar) {
|
||||||
radarCanvas.parentElement.classList.add('empty');
|
markEmpty(radarCanvas, '共同 benchmark 不足 3 个,暂不绘制雷达图');
|
||||||
radarCanvas.parentElement.dataset.empty = '共同 benchmark 不足 3 个,暂不绘制雷达图';
|
|
||||||
} else {
|
} else {
|
||||||
radarCanvas.parentElement.classList.remove('empty');
|
makeChart('radar', radarCanvas, {
|
||||||
delete radarCanvas.parentElement.dataset.empty;
|
|
||||||
state.charts.radar = new Chart(radarCanvas, {
|
|
||||||
type: 'radar',
|
type: 'radar',
|
||||||
data: { labels: radarLabels, datasets: chartDatasets(compare.series, 'radar') },
|
data: { labels: radarLabels, datasets: chartDatasets(compare.series, 'radar') },
|
||||||
options: {
|
options: {
|
||||||
@ -216,7 +269,7 @@
|
|||||||
ticks: { color: '#92a197', backdropColor: 'transparent', stepSize: 20 },
|
ticks: { color: '#92a197', backdropColor: 'transparent', stepSize: 20 },
|
||||||
grid: { color: 'rgba(51,64,56,0.7)' },
|
grid: { color: 'rgba(51,64,56,0.7)' },
|
||||||
angleLines: { 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;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block + .block { margin-top: 22px; }
|
.block + .block { margin-top: 18px; }
|
||||||
.block h2, .side-head h2, .log-head h3, .jobs-head h3 {
|
.block h2, .side-head h2, .log-head h3, .jobs-head h3 {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 10px;
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.03em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #c7d2c9;
|
color: #c7d2c9;
|
||||||
}
|
}
|
||||||
|
.subhead {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #c7d2c9;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
color: var(--muted);
|
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 {
|
input, select, button, summary {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@ -119,8 +126,10 @@ input, select {
|
|||||||
background: var(--bg0);
|
background: var(--bg0);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 10px 12px;
|
padding: 7px 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.35;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color .15s, box-shadow .15s;
|
transition: border-color .15s, box-shadow .15s;
|
||||||
}
|
}
|
||||||
@ -170,23 +179,24 @@ input:focus, select:focus {
|
|||||||
transform: translateX(18px);
|
transform: translateX(18px);
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
.switch-text { color: var(--text); font-size: 14px; }
|
.switch-text { color: var(--text); font-size: 15px; }
|
||||||
|
|
||||||
.mode-tabs {
|
.mode-tabs {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
background: var(--bg0);
|
background: var(--bg0);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 4px;
|
padding: 3px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.tab {
|
.tab {
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
padding: 8px 14px;
|
padding: 7px 12px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: rgba(214, 162, 74, 0.18);
|
background: rgba(214, 162, 74, 0.18);
|
||||||
@ -195,40 +205,97 @@ input:focus, select:focus {
|
|||||||
|
|
||||||
.suite-list {
|
.suite-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.suite-card {
|
.suite-card {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: var(--bg0);
|
background: var(--bg0);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 12px;
|
padding: 10px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color .15s, transform .15s;
|
transition: border-color .15s, transform .15s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.suite-card:hover { transform: translateY(-1px); }
|
.suite-card:hover { transform: translateY(-1px); }
|
||||||
.suite-card.active {
|
.suite-card.active {
|
||||||
border-color: rgba(214, 162, 74, 0.75);
|
border-color: rgba(214, 162, 74, 0.75);
|
||||||
background: rgba(214, 162, 74, 0.08);
|
background: rgba(214, 162, 74, 0.08);
|
||||||
}
|
}
|
||||||
.suite-card strong { display: block; margin-bottom: 4px; }
|
.suite-card.custom {
|
||||||
.suite-card small { color: var(--muted); line-height: 1.4; }
|
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 {
|
.benchmark-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
max-height: 420px;
|
max-height: 420px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
.benchmark-list.compact { max-height: 280px; }
|
||||||
.bench-category {
|
.bench-category {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
background: rgba(12, 16, 14, 0.55);
|
background: rgba(12, 16, 14, 0.55);
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
.bench-category.cat-math { border-left: 3px solid #6fbf8a; }
|
.bench-category.cat-math { border-left: 3px solid #6fbf8a; }
|
||||||
.bench-category.cat-code { border-left: 3px solid #6aa8d6; }
|
.bench-category.cat-code { border-left: 3px solid #6aa8d6; }
|
||||||
@ -251,32 +318,35 @@ input:focus, select:focus {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.bench-cat-title strong {
|
.bench-cat-title strong {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.bench-cat-actions { display: flex; gap: 4px; }
|
.bench-cat-actions { display: flex; gap: 4px; }
|
||||||
.bench-cat-grid {
|
.bench-cat-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.bench-item {
|
.bench-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding: 8px 10px;
|
padding: 6px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: var(--bg0);
|
background: var(--bg0);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
.bench-item input { width: auto; }
|
.bench-item input { width: auto; }
|
||||||
.bench-item.multi { border-color: rgba(111, 191, 138, 0.35); }
|
.bench-item.multi { border-color: rgba(111, 191, 138, 0.35); }
|
||||||
.bench-item .run-tag {
|
.bench-item .run-tag {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: #9ee0b2;
|
color: #9ee0b2;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
@ -284,7 +354,7 @@ input:focus, select:focus {
|
|||||||
.bench-actions {
|
.bench-actions {
|
||||||
display: flex; gap: 8px; align-items: center; margin-bottom: 10px;
|
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; }
|
.mt { margin-top: 12px; }
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
@ -307,12 +377,13 @@ input:focus, select:focus {
|
|||||||
margin-top: 22px;
|
margin-top: 22px;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
padding: 10px 16px;
|
padding: 8px 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--bg2);
|
background: var(--bg2);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
button:disabled { opacity: 0.45; cursor: not-allowed; }
|
button:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||||
button.primary {
|
button.primary {
|
||||||
@ -328,10 +399,10 @@ button.danger {
|
|||||||
}
|
}
|
||||||
button.ghost {
|
button.ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 6px 10px;
|
padding: 5px 9px;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.msg { color: var(--muted); font-size: 13px; }
|
.msg { color: var(--muted); font-size: 14px; }
|
||||||
.msg.error { color: var(--danger); }
|
.msg.error { color: var(--danger); }
|
||||||
.msg.ok { color: var(--accent-2); }
|
.msg.ok { color: var(--accent-2); }
|
||||||
|
|
||||||
@ -582,15 +653,29 @@ button.ghost {
|
|||||||
}
|
}
|
||||||
.rank-row {
|
.rank-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 28px 1fr auto;
|
grid-template-columns: 28px minmax(0, 1fr) auto;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 12px;
|
align-items: start;
|
||||||
padding: 4px 0;
|
font-size: 13px;
|
||||||
|
padding: 6px 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.rank-idx { color: var(--accent); font-family: var(--mono); }
|
.rank-idx { color: var(--accent); font-family: var(--mono); }
|
||||||
.rank-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
|
.rank-name {
|
||||||
.rank-score { font-family: var(--mono); color: #c9d4cb; }
|
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) {
|
@media (max-width: 1100px) {
|
||||||
.layout { grid-template-columns: 1fr; }
|
.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