dsh-livebench-panel 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +36 -4
- package/lib/index.js +58 -39
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -107,6 +107,8 @@ window.__ModuleLoader__.load({
|
|
|
107
107
|
const [homeBusy, setHomeBusy] = useState(false);
|
|
108
108
|
const [results, setResults] = useState(null);
|
|
109
109
|
const [pickedModels, setPickedModels] = useState(() => new Set());
|
|
110
|
+
const [sortBy, setSortBy] = useState(null);
|
|
111
|
+
const [sortDir, setSortDir] = useState("asc");
|
|
110
112
|
const [modelOrder, setModelOrder] = useState(() => {
|
|
111
113
|
try { return JSON.parse(localStorage.getItem("dlb_model_order_v1")) ?? []; } catch { return []; }
|
|
112
114
|
});
|
|
@@ -323,16 +325,37 @@ window.__ModuleLoader__.load({
|
|
|
323
325
|
return { models, tasks, cells, startTimes };
|
|
324
326
|
}, [results]);
|
|
325
327
|
const cellOf = (model, key) => matrix.cells.get(`${model}__@__${key}`);
|
|
328
|
+
// 排序:sortBy=null 时用拖拽自定义顺序;点击表头在 正序/倒序 间切换
|
|
326
329
|
const sortedModels = useMemo(() => {
|
|
327
330
|
if (!matrix) return [];
|
|
331
|
+
const base = [...matrix.models];
|
|
332
|
+
if (sortBy === "model") {
|
|
333
|
+
base.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
|
334
|
+
return base;
|
|
335
|
+
}
|
|
336
|
+
if (sortBy === "time") {
|
|
337
|
+
base.sort((a, b) => {
|
|
338
|
+
const ta = matrix.startTimes.get(a) ?? 0;
|
|
339
|
+
const tb = matrix.startTimes.get(b) ?? 0;
|
|
340
|
+
return sortDir === "asc" ? ta - tb : tb - ta;
|
|
341
|
+
});
|
|
342
|
+
return base;
|
|
343
|
+
}
|
|
328
344
|
const indexOf = new Map(modelOrder.map((key, index) => [key, index]));
|
|
329
|
-
return
|
|
345
|
+
return base.sort((a, b) => {
|
|
330
346
|
const ia = indexOf.has(a) ? indexOf.get(a) : Number.MAX_SAFE_INTEGER;
|
|
331
347
|
const ib = indexOf.has(b) ? indexOf.get(b) : Number.MAX_SAFE_INTEGER;
|
|
332
348
|
if (ia !== ib) return ia - ib;
|
|
333
349
|
return a.localeCompare(b);
|
|
334
350
|
});
|
|
335
|
-
}, [matrix, modelOrder]);
|
|
351
|
+
}, [matrix, modelOrder, sortBy, sortDir]);
|
|
352
|
+
|
|
353
|
+
// 点击表头:切换排序字段(再次点击同一列则反转方向)
|
|
354
|
+
const toggleSort = (field) => {
|
|
355
|
+
if (sortBy === field) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
|
356
|
+
else { setSortBy(field); setSortDir("asc"); }
|
|
357
|
+
};
|
|
358
|
+
const sortMark = (field) => (sortBy === field ? (sortDir === "asc" ? " ↑" : " ↓") : "");
|
|
336
359
|
|
|
337
360
|
const persistModelOrder = (keys) => {
|
|
338
361
|
setModelOrder(keys);
|
|
@@ -343,6 +366,7 @@ window.__ModuleLoader__.load({
|
|
|
343
366
|
const sourceModel = dragKey.current;
|
|
344
367
|
dragKey.current = null;
|
|
345
368
|
if (!sourceModel || sourceModel === targetModel) return;
|
|
369
|
+
setSortBy(null); // 拖拽即回到自定义顺序
|
|
346
370
|
const keys = sortedModels.slice();
|
|
347
371
|
const from = keys.indexOf(sourceModel);
|
|
348
372
|
const to = keys.indexOf(targetModel);
|
|
@@ -531,13 +555,21 @@ window.__ModuleLoader__.load({
|
|
|
531
555
|
h("th", { style: { width: "30px" } },
|
|
532
556
|
h("input", { type: "checkbox", checked: pickedModels.size > 0 && pickedModels.size === sortedModels.length,
|
|
533
557
|
onChange: (event) => setPickedModels(event.target.checked ? new Set(sortedModels) : new Set()) })),
|
|
534
|
-
h("th",
|
|
558
|
+
h("th", {
|
|
559
|
+
style: { cursor: "pointer", userSelect: "none" },
|
|
560
|
+
title: "点击按模型名排序(再次点击切换正序/倒序)",
|
|
561
|
+
onClick: () => toggleSort("model"),
|
|
562
|
+
}, "model" + sortMark("model")),
|
|
535
563
|
matrix.tasks.map((taskKeyCol) => {
|
|
536
564
|
const [cat, task] = taskKeyCol.split("/");
|
|
537
565
|
return h("th", { key: taskKeyCol, title: `${cat} / ${task}` }, task);
|
|
538
566
|
}),
|
|
539
567
|
h("th", null, "删除"),
|
|
540
|
-
h("th",
|
|
568
|
+
h("th", {
|
|
569
|
+
style: { cursor: "pointer", userSelect: "none" },
|
|
570
|
+
title: "点击按时间排序(再次点击切换正序/倒序)",
|
|
571
|
+
onClick: () => toggleSort("time"),
|
|
572
|
+
}, "time" + sortMark("time")),
|
|
541
573
|
)),
|
|
542
574
|
h("tbody", null,
|
|
543
575
|
sortedModels.map((model) => {
|
package/lib/index.js
CHANGED
|
@@ -485,56 +485,73 @@ function parseAnswerLineLight(line) {
|
|
|
485
485
|
|
|
486
486
|
/**
|
|
487
487
|
* Compute mean scores per (model, category, task) from ground-truth judgment
|
|
488
|
-
* files, plus
|
|
489
|
-
*
|
|
490
|
-
*
|
|
488
|
+
* files, plus judged/total question counts per task.
|
|
489
|
+
*
|
|
490
|
+
* 性能:数据目录可能上百 MB(每次评测生成独立答案文件)。这里:
|
|
491
|
+
* - answer 文件用【文件名】当模型名,只做行数与 "$ERROR$" 子串统计 —— 不解析大 JSON 行;
|
|
492
|
+
* - question.jsonl 只数行数;
|
|
493
|
+
* - 结果缓存 5 秒,避免前端频繁刷新时重复全量扫描。
|
|
491
494
|
*/
|
|
495
|
+
let resultsCache = { at: 0, data: null };
|
|
496
|
+
|
|
497
|
+
async function readTextFileAsync(path) {
|
|
498
|
+
const fsPromises = await import("node:fs/promises");
|
|
499
|
+
try {
|
|
500
|
+
return await fsPromises.readFile(path, "utf8");
|
|
501
|
+
} catch {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
492
506
|
async function computeResults(dataDir) {
|
|
493
|
-
const
|
|
507
|
+
const now = Date.now();
|
|
508
|
+
if (resultsCache.data !== null && now - resultsCache.at < 5000) return resultsCache.data;
|
|
509
|
+
const judged = new Map(); // `${model}${task}` -> {model, category, task, sum, n, time}
|
|
494
510
|
const totals = new Map(); // task -> question count
|
|
495
|
-
const answers = new Map(); // `${model}
|
|
496
|
-
const catByTask = new Map();
|
|
511
|
+
const answers = new Map(); // `${model}${task}` -> {answered, errors}
|
|
512
|
+
const catByTask = new Map();
|
|
497
513
|
if (!existsSync(dataDir)) return { rows: [], taskTotals: {} };
|
|
498
514
|
for (const category of readDirSafe(dataDir)) {
|
|
499
515
|
const catDir = join(dataDir, category);
|
|
500
516
|
for (const task of readDirSafe(catDir)) {
|
|
501
517
|
const taskDir = join(catDir, task);
|
|
502
518
|
catByTask.set(task, category);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
totals.set(task, questionCount);
|
|
506
|
-
// answer 侧:各模型产出了多少答案、其中多少 $ERROR$(API 失败)
|
|
519
|
+
const qText = await readTextFileAsync(join(taskDir, "question.jsonl"));
|
|
520
|
+
totals.set(task, qText === null ? 0 : qText.split("\n").reduce((n, l) => n + (l.trim().length > 0 ? 1 : 0), 0));
|
|
507
521
|
const answerDir = join(taskDir, "model_answer");
|
|
508
522
|
for (const file of readDirSafe(answerDir)) {
|
|
509
523
|
if (!file.endsWith(".jsonl")) continue;
|
|
510
|
-
const
|
|
511
|
-
await
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
524
|
+
const model = file.slice(0, -".jsonl".length);
|
|
525
|
+
const text = await readTextFileAsync(join(answerDir, file));
|
|
526
|
+
if (text === null) continue;
|
|
527
|
+
const key = `${model}${task}`;
|
|
528
|
+
const stat = answers.get(key) ?? { answered: 0, errors: 0 };
|
|
529
|
+
for (const line of text.split("\n")) {
|
|
530
|
+
if (line.trim().length === 0) continue;
|
|
517
531
|
stat.answered += 1;
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
532
|
+
if (line.includes('"$ERROR$"')) stat.errors += 1;
|
|
533
|
+
}
|
|
534
|
+
answers.set(key, stat);
|
|
521
535
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
536
|
+
const jText = await readTextFileAsync(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"));
|
|
537
|
+
if (jText !== null) {
|
|
538
|
+
for (const line of jText.split("\n")) {
|
|
539
|
+
if (line.trim().length === 0) continue;
|
|
540
|
+
let row;
|
|
541
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
542
|
+
const model = typeof row.model === "string" ? row.model : null;
|
|
543
|
+
const score = typeof row.score === "number" ? row.score : Number(row.score);
|
|
544
|
+
if (model === null || !Number.isFinite(score) || score < 0) continue;
|
|
545
|
+
const key = `${model}${task}`;
|
|
546
|
+
const entry = judged.get(key) ?? { model, category, task, sum: 0, n: 0, time: 0 };
|
|
547
|
+
entry.sum += score;
|
|
548
|
+
entry.n += 1;
|
|
549
|
+
const tstamp = Number(row.tstamp);
|
|
550
|
+
if (Number.isFinite(tstamp) && tstamp > entry.time) entry.time = tstamp;
|
|
551
|
+
judged.set(key, entry);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// 让出事件循环,保证 HTTP/websocket 实时响应
|
|
538
555
|
await new Promise((resolve) => setImmediate(resolve));
|
|
539
556
|
}
|
|
540
557
|
}
|
|
@@ -542,7 +559,7 @@ async function computeResults(dataDir) {
|
|
|
542
559
|
const rows = [...keys].map((key) => {
|
|
543
560
|
const entry = judged.get(key);
|
|
544
561
|
const stat = answers.get(key) ?? { answered: 0, errors: 0 };
|
|
545
|
-
const [model, task] = key.split("
|
|
562
|
+
const [model, task] = key.split("");
|
|
546
563
|
const category = entry?.category ?? catByTask.get(task) ?? "";
|
|
547
564
|
return {
|
|
548
565
|
model,
|
|
@@ -556,8 +573,9 @@ async function computeResults(dataDir) {
|
|
|
556
573
|
errors: stat.errors,
|
|
557
574
|
};
|
|
558
575
|
});
|
|
559
|
-
const
|
|
560
|
-
|
|
576
|
+
const result = { rows, taskTotals: Object.fromEntries(totals) };
|
|
577
|
+
resultsCache = { at: Date.now(), data: result };
|
|
578
|
+
return result;
|
|
561
579
|
}
|
|
562
580
|
|
|
563
581
|
/**
|
|
@@ -1101,6 +1119,7 @@ function apply(ctx) {
|
|
|
1101
1119
|
removed += filterJsonlByModel(join(taskDir, "model_answer", `${model}.jsonl`), "model_id", model);
|
|
1102
1120
|
removed += filterJsonlByModel(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"), "model", model);
|
|
1103
1121
|
}
|
|
1122
|
+
resultsCache = { at: 0, data: null };
|
|
1104
1123
|
sendJson(res, 200, { ok: true, removed });
|
|
1105
1124
|
},
|
|
1106
1125
|
}), `${name}: delete route`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-livebench-panel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "DSH web plugin: a LiveBench tab in the Trajectory view (right of 对话/轨迹). Run LiveBench evaluations against every model configured in the DeepSeek Harness — pick provider/model, category, task, release and question range from dropdowns, watch progress, and read scores in place.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|