dsh-livebench-panel 0.1.6 → 0.1.8

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.
Files changed (3) hide show
  1. package/lib/client.js +116 -59
  2. package/lib/index.js +102 -16
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -98,10 +98,12 @@ window.__ModuleLoader__.load({
98
98
  const [log, setLog] = useState("");
99
99
  const [exitCode, setExitCode] = useState(null);
100
100
  const [startError, setStartError] = useState(null);
101
+ const [homeInput, setHomeInput] = useState("");
102
+ const [homeBusy, setHomeBusy] = useState(false);
101
103
  const [results, setResults] = useState(null);
102
- const [pickedRows, setPickedRows] = useState(() => new Set());
103
- const [order, setOrder] = useState(() => {
104
- try { return JSON.parse(localStorage.getItem("dlb_result_order_v1")) ?? []; } catch { return []; }
104
+ const [pickedModels, setPickedModels] = useState(() => new Set());
105
+ const [modelOrder, setModelOrder] = useState(() => {
106
+ try { return JSON.parse(localStorage.getItem("dlb_model_order_v1")) ?? []; } catch { return []; }
105
107
  });
106
108
  const dragKey = useRef(null);
107
109
  const logRef = useRef(null);
@@ -253,6 +255,24 @@ window.__ModuleLoader__.load({
253
255
  await refreshStatus();
254
256
  };
255
257
 
258
+ const saveHome = async () => {
259
+ setHomeBusy(true);
260
+ setStartError(null);
261
+ try {
262
+ const payload = await api("/home", {
263
+ method: "POST",
264
+ headers: { "content-type": "application/json" },
265
+ body: JSON.stringify({ path: homeInput }),
266
+ });
267
+ if (!payload.ok) setStartError(payload.error ?? "设置失败");
268
+ else { setHomeInput(""); await loadConfig(); }
269
+ } catch (error) {
270
+ setStartError(String(error.message ?? error));
271
+ } finally {
272
+ setHomeBusy(false);
273
+ }
274
+ };
275
+
256
276
  const setField = (key) => (event) => {
257
277
  const value = event.target.value;
258
278
  setSel((prev) => {
@@ -265,52 +285,75 @@ window.__ModuleLoader__.load({
265
285
  });
266
286
  };
267
287
 
268
- // ---- 成绩行:排序(拖拽,localStorage 持久化)/ 选择 / 删除 ----
269
- const rowKey = (row) => `${row.model}|${row.category}|${row.task}`;
270
- const sortedRows = useMemo(() => {
271
- if (!results) return [];
272
- const indexOf = new Map(order.map((key, index) => [key, index]));
273
- return [...results.rows].sort((a, b) => {
274
- const ia = indexOf.has(rowKey(a)) ? indexOf.get(rowKey(a)) : Number.MAX_SAFE_INTEGER;
275
- const ib = indexOf.has(rowKey(b)) ? indexOf.get(rowKey(b)) : Number.MAX_SAFE_INTEGER;
288
+ // ---- 成绩矩阵:model 为行标识、category/task 为列标识 ----
289
+ // 附带:time 列(该模型评测开始时间 = 行内最早判分时间)、
290
+ // 行拖拽排序(localStorage 持久化)、行选择与单行/批量删除。
291
+ const taskKey = (category, task) => `${category}/${task}`;
292
+ const matrix = useMemo(() => {
293
+ if (!results) return null;
294
+ const models = [...new Set(results.rows.map((r) => r.model))].sort();
295
+ const tasks = [...new Set(results.rows.map((r) => taskKey(r.category, r.task)))].sort();
296
+ const cells = new Map(); // `${model}\u0000${taskKey}` -> row
297
+ const startTimes = new Map(); // model -> min tstamp
298
+ for (const row of results.rows) {
299
+ cells.set(`${row.model}\u0000${taskKey(row.category, row.task)}`, row);
300
+ const prev = startTimes.get(row.model);
301
+ if (row.time && (prev === undefined || row.time < prev)) startTimes.set(row.model, row.time);
302
+ }
303
+ return { models, tasks, cells, startTimes };
304
+ }, [results]);
305
+ const sortedModels = useMemo(() => {
306
+ if (!matrix) return [];
307
+ const indexOf = new Map(modelOrder.map((key, index) => [key, index]));
308
+ return [...matrix.models].sort((a, b) => {
309
+ const ia = indexOf.has(a) ? indexOf.get(a) : Number.MAX_SAFE_INTEGER;
310
+ const ib = indexOf.has(b) ? indexOf.get(b) : Number.MAX_SAFE_INTEGER;
276
311
  if (ia !== ib) return ia - ib;
277
- return (b.time ?? 0) - (a.time ?? 0);
312
+ return a.localeCompare(b);
278
313
  });
279
- }, [results, order]);
314
+ }, [matrix, modelOrder]);
280
315
 
281
- const persistOrder = (keys) => {
282
- setOrder(keys);
283
- try { localStorage.setItem("dlb_result_order_v1", JSON.stringify(keys)); } catch { /* private mode */ }
316
+ const persistModelOrder = (keys) => {
317
+ setModelOrder(keys);
318
+ try { localStorage.setItem("dlb_model_order_v1", JSON.stringify(keys)); } catch { /* private mode */ }
284
319
  };
285
320
 
286
- const onRowDrop = (targetKey) => {
287
- const sourceKey = dragKey.current;
321
+ const onRowDrop = (targetModel) => {
322
+ const sourceModel = dragKey.current;
288
323
  dragKey.current = null;
289
- if (!sourceKey || sourceKey === targetKey) return;
290
- const keys = sortedRows.map(rowKey);
291
- const from = keys.indexOf(sourceKey);
292
- const to = keys.indexOf(targetKey);
324
+ if (!sourceModel || sourceModel === targetModel) return;
325
+ const keys = sortedModels.slice();
326
+ const from = keys.indexOf(sourceModel);
327
+ const to = keys.indexOf(targetModel);
293
328
  if (from < 0 || to < 0) return;
294
329
  keys.splice(to, 0, keys.splice(from, 1)[0]);
295
- persistOrder(keys);
330
+ persistModelOrder(keys);
296
331
  };
297
332
 
298
- const togglePicked = (key) => setPickedRows((prev) => {
333
+ const togglePicked = (model) => setPickedModels((prev) => {
299
334
  const next = new Set(prev);
300
- if (next.has(key)) next.delete(key); else next.add(key);
335
+ if (next.has(model)) next.delete(model); else next.add(model);
301
336
  return next;
302
337
  });
303
338
 
304
- const deleteRows = async (rows) => {
305
- if (rows.length === 0) return;
306
- if (!window.confirm(`确认删除 ${rows.length} 条评测成绩?将同时删除对应的模型答案记录,不可恢复。`)) return;
339
+ // 删除一个模型行 = 删除该模型在所有任务下的判分与答案记录
340
+ const deleteModels = async (modelsToDelete) => {
341
+ if (!results || modelsToDelete.length === 0) return;
342
+ const serverRows = [];
343
+ for (const row of results.rows) {
344
+ if (modelsToDelete.includes(row.model)) {
345
+ serverRows.push({ model: row.model, category: row.category, task: row.task });
346
+ }
347
+ }
348
+ if (serverRows.length === 0) return;
349
+ if (!window.confirm(`确认删除所选 ${modelsToDelete.length} 个模型的全部评测成绩(共 ${serverRows.length} 条记录,含答案)?不可恢复。`)) return;
307
350
  await api("/delete", {
308
351
  method: "POST",
309
352
  headers: { "content-type": "application/json" },
310
- body: JSON.stringify({ rows: rows.map((r) => ({ model: r.model, category: r.category, task: r.task })) }),
353
+ body: JSON.stringify({ rows: serverRows }),
311
354
  });
312
- setPickedRows(new Set());
313
- persistOrder([...order].filter((key) => !rows.some((r) => rowKey(r) === key)));
355
+ setPickedModels(new Set());
356
+ persistModelOrder(modelOrder.filter((model) => !modelsToDelete.includes(model)));
314
357
  await loadResults();
315
358
  };
316
359
 
@@ -324,10 +367,21 @@ window.__ModuleLoader__.load({
324
367
  return h("div", { className: c("root") },
325
368
  h("div", { className: c("head") },
326
369
  h("h2", null, "LiveBench"),
327
- h("span", { className: c("sub") }, "LiveBench 评测面板 · 基于 V:\\PythonProject\\C_UtilizeSpace\\LiveBench"),
370
+ h("span", { className: c("sub") }, config?.root ? `LiveBench 评测面板 · ${config.root}` : "LiveBench 评测面板"),
328
371
  h("span", { className: c("badge"), "data-ok": config?.available ? "1" : "0" },
329
372
  config === null ? "检测中…" : config.available ? "LiveBench 就绪" : "未找到 LiveBench"),
330
373
  ),
374
+ h("div", { className: c("row") },
375
+ h("span", { className: c("label") }, "LiveBench 路径"),
376
+ h("input", {
377
+ className: c("input"), style: { flex: "1", minWidth: "240px" },
378
+ placeholder: config?.root ?? "V:\\…\\LiveBench 或 ~/LiveBench",
379
+ value: homeInput, onChange: (event) => setHomeInput(event.target.value),
380
+ }),
381
+ h("button", { className: c("btnGhost") + " " + c("btn"), disabled: homeBusy || homeInput.trim().length === 0, onClick: saveHome },
382
+ homeBusy ? "保存中…" : "设置路径"),
383
+ h("span", { className: c("hint") }, "需含 livebench\\run_livebench.py 与 .venv;也可用环境变量 DSH_LIVEBENCH_HOME"),
384
+ ),
331
385
  configError !== null && h("p", { className: c("error") }, `加载配置失败:${configError}`),
332
386
  h("div", { className: c("card") },
333
387
  h("div", { className: c("grid") },
@@ -405,50 +459,53 @@ window.__ModuleLoader__.load({
405
459
  ),
406
460
  h("pre", { className: c("log"), ref: logRef }, log || "(暂无输出)"),
407
461
  ),
408
- sortedRows.length > 0 && h("div", { className: c("card") },
462
+ sortedModels.length > 0 && h("div", { className: c("card") },
409
463
  h("div", { className: c("row") },
410
- h("span", { className: c("label") }, "评测成绩(分数 = 平均分 ×100;行首可拖动排序,顺序保存在本机)"),
464
+ h("span", { className: c("label") }, "评测成绩(model 为行;分数 = 平均分 ×100;行首可拖动排序,顺序保存在本机)"),
411
465
  h("button", {
412
466
  className: c("btn") + " " + c("btnDanger"),
413
- disabled: pickedRows.size === 0,
414
- onClick: () => deleteRows(sortedRows.filter((r) => pickedRows.has(rowKey(r)))),
415
- }, `删除所选(${pickedRows.size})`),
467
+ disabled: pickedModels.size === 0,
468
+ onClick: () => deleteModels([...pickedModels]),
469
+ }, `删除所选(${pickedModels.size})`),
416
470
  ),
417
471
  h("table", { className: c("table") },
418
472
  h("thead", null, h("tr", null,
419
- h("th", { style: { width: "30px" } }, "⠿"),
473
+ h("th", { style: { width: "26px" } }, "⠿"),
420
474
  h("th", { style: { width: "30px" } },
421
- h("input", { type: "checkbox", checked: pickedRows.size > 0 && pickedRows.size === sortedRows.length,
422
- onChange: (event) => setPickedRows(event.target.checked ? new Set(sortedRows.map(rowKey)) : new Set()) })),
423
- h("th", null, "time"),
475
+ h("input", { type: "checkbox", checked: pickedModels.size > 0 && pickedModels.size === sortedModels.length,
476
+ onChange: (event) => setPickedModels(event.target.checked ? new Set(sortedModels) : new Set()) })),
424
477
  h("th", null, "model"),
425
- h("th", null, "category"),
426
- h("th", null, "task"),
427
- h("th", null, "score"),
428
- h("th", null, "操作"),
478
+ matrix.tasks.map((taskKeyCol) => {
479
+ const [cat, task] = taskKeyCol.split("/");
480
+ return h("th", { key: taskKeyCol, title: `${cat} / ${task}` }, task);
481
+ }),
482
+ h("th", null, "删除"),
483
+ h("th", null, "time"),
429
484
  )),
430
485
  h("tbody", null,
431
- sortedRows.map((row) => {
432
- const key = rowKey(row);
433
- const checked = pickedRows.has(key);
486
+ sortedModels.map((model) => {
487
+ const checked = pickedModels.has(model);
488
+ const startTime = matrix.startTimes.get(model);
434
489
  return h("tr", {
435
- key,
490
+ key: model,
436
491
  draggable: true,
437
- onDragStart: () => { dragKey.current = key; },
492
+ onDragStart: () => { dragKey.current = model; },
438
493
  onDragOver: (event) => event.preventDefault(),
439
- onDrop: () => onRowDrop(key),
494
+ onDrop: () => onRowDrop(model),
440
495
  },
441
496
  h("td", { className: c("drag"), title: "拖动排序" }, "⠿"),
442
- h("td", null, h("input", { type: "checkbox", checked, onChange: () => togglePicked(key) })),
443
- h("td", null, fmtTime(row.time)),
444
- h("td", null, row.model),
445
- h("td", null, row.category),
446
- h("td", null, row.task),
447
- h("td", { className: c("score") }, `${row.score.toFixed(1)} (${row.judged}/${row.total})`),
497
+ h("td", null, h("input", { type: "checkbox", checked, onChange: () => togglePicked(model) })),
498
+ h("td", null, model),
499
+ matrix.tasks.map((taskKeyCol) => {
500
+ const row = matrix.cells.get(`${model}\u0000${taskKeyCol}`);
501
+ return h("td", { key: taskKeyCol, className: c("score") },
502
+ row ? `${row.score.toFixed(1)} (${row.judged}/${row.total})` : "—");
503
+ }),
448
504
  h("td", null, h("button", {
449
- className: c("btnGhost") + " " + c("btn"), title: "删除此条成绩",
450
- onClick: () => deleteRows([row]),
505
+ className: c("btnGhost") + " " + c("btn"), title: "删除该模型的所有评测成绩",
506
+ onClick: () => deleteModels([model]),
451
507
  }, "删除")),
508
+ h("td", null, fmtTime(startTime)),
452
509
  );
453
510
  })),
454
511
  ),
package/lib/index.js CHANGED
@@ -39,8 +39,61 @@ const inject = ["webServer"];
39
39
  const API = "/dsh-livebench-panel/api";
40
40
  /** Profile whose settings.yaml holds the harness provider config. */
41
41
  const PROFILE = "web";
42
- /** LiveBench checkout used unless DSH_LIVEBENCH_HOME overrides it. */
43
- const DEFAULT_LIVEBENCH_HOME = "V:\\PythonProject\\C_UtilizeSpace\\LiveBench";
42
+ /** LiveBench location resolution: saved user path > env var > user home > legacy. */
43
+ const CONFIG_FILE = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "livebench-panel.json");
44
+ const LEGACY_LIVEBENCH_HOME = "V:\\PythonProject\\C_UtilizeSpace\\LiveBench";
45
+
46
+ /** Read the user-saved LiveBench path (set from the panel UI). */
47
+ function savedLivebenchHome() {
48
+ try {
49
+ const path = JSON.parse(readFileSync(CONFIG_FILE, "utf8")).livebenchHome;
50
+ return typeof path === "string" && path.trim().length > 0 ? path.trim() : null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /** Candidate roots, most explicit first. Windows and Linux both covered. */
57
+ function livebenchCandidates() {
58
+ return [
59
+ savedLivebenchHome(),
60
+ process.env.DSH_LIVEBENCH_HOME ?? null,
61
+ join(homedir(), "LiveBench"),
62
+ LEGACY_LIVEBENCH_HOME,
63
+ ].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
64
+ }
65
+
66
+ /** A root counts as a LiveBench checkout when the venv python and entry script exist. */
67
+ function isLivebenchRoot(root) {
68
+ return existsSync(join(root, ".venv", "Scripts", "python.exe")) || existsSync(join(root, ".venv", "bin", "python"))
69
+ ? existsSync(join(root, "livebench", "run_livebench.py"))
70
+ : false;
71
+ }
72
+
73
+ /** Resolve the LiveBench layout from the first valid candidate. */
74
+ function livebenchLayout() {
75
+ const candidates = livebenchCandidates();
76
+ for (const root of candidates) {
77
+ const pythonExe = join(root, ".venv", "Scripts", process.platform === "win32" ? "python.exe" : "python");
78
+ if (existsSync(pythonExe) && existsSync(join(root, "livebench", "run_livebench.py"))) {
79
+ return {
80
+ root,
81
+ livebenchDir: join(root, "livebench"),
82
+ dataDir: join(root, "livebench", "data", "live_bench"),
83
+ pythonExe,
84
+ available: true,
85
+ };
86
+ }
87
+ }
88
+ const root = candidates[0] ?? join(homedir(), "LiveBench");
89
+ return {
90
+ root,
91
+ livebenchDir: join(root, "livebench"),
92
+ dataDir: join(root, "livebench", "data", "live_bench"),
93
+ pythonExe: join(root, ".venv", process.platform === "win32" ? join("Scripts", "python.exe") : join("bin", "python")),
94
+ available: false,
95
+ };
96
+ }
44
97
  /** Question-set releases LiveBench accepts (mirrors livebench/common.py). */
45
98
  const RELEASES = [
46
99
  "2024-06-24", "2024-07-26", "2024-08-31", "2024-11-25",
@@ -210,19 +263,6 @@ function effortsOfModel(model) {
210
263
  return efforts;
211
264
  }
212
265
 
213
- /** LiveBench layout on disk. */
214
- function livebenchLayout() {
215
- const root = process.env.DSH_LIVEBENCH_HOME ?? DEFAULT_LIVEBENCH_HOME;
216
- const venvPython = join(root, ".venv", "Scripts", "python.exe");
217
- return {
218
- root,
219
- livebenchDir: join(root, "livebench"),
220
- dataDir: join(root, "livebench", "data", "live_bench"),
221
- pythonExe: venvPython,
222
- available: existsSync(venvPython) && existsSync(join(root, "livebench", "run_livebench.py")),
223
- };
224
- }
225
-
226
266
  /** Scan data/live_bench/<category>/<task>/question.jsonl into a category→tasks map. */
227
267
  function scanCategoryTasks(dataDir) {
228
268
  const tasks = {};
@@ -695,7 +735,14 @@ function apply(ctx) {
695
735
  return;
696
736
  }
697
737
  try {
698
- run.proc.kill();
738
+ if (process.platform === "win32") {
739
+ // proc.kill() only terminates run_livebench.py itself; its child
740
+ // (cmd → gen_api_answer.py → …) would survive. taskkill /T /F
741
+ // takes down the whole tree.
742
+ spawn("taskkill", ["/PID", String(run.proc.pid), "/T", "/F"], { windowsHide: true });
743
+ } else {
744
+ run.proc.kill();
745
+ }
699
746
  } catch (error) {
700
747
  sendJson(res, 500, { ok: false, error: String(error.message ?? error) });
701
748
  return;
@@ -718,6 +765,45 @@ function apply(ctx) {
718
765
  },
719
766
  }), `${name}: results route`);
720
767
 
768
+ ctx.effect(() => ctx.webServer.register({
769
+ kind: "exact",
770
+ path: `${API}/home`,
771
+ handler: async (req, res) => {
772
+ if (req.method !== "POST") {
773
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
774
+ return;
775
+ }
776
+ if (!originAllowed(req)) {
777
+ sendJson(res, 403, { ok: false, error: "forbidden origin" });
778
+ return;
779
+ }
780
+ let body;
781
+ try {
782
+ body = JSON.parse(await readBody(req));
783
+ } catch {
784
+ sendJson(res, 400, { ok: false, error: "invalid request body" });
785
+ return;
786
+ }
787
+ const path = typeof body.path === "string" ? body.path.trim() : "";
788
+ if (path.length === 0 || path.length > 260 || /[<>:"|?*\0]/.test(path.replace(/^[A-Za-z]:/, ""))) {
789
+ sendJson(res, 400, { ok: false, error: "invalid path" });
790
+ return;
791
+ }
792
+ if (!existsSync(join(path, "livebench", "run_livebench.py"))) {
793
+ sendJson(res, 400, { ok: false, error: `该目录下没有 livebench\\run_livebench.py:${path}` });
794
+ return;
795
+ }
796
+ try {
797
+ writeFileSync(CONFIG_FILE, JSON.stringify({ livebenchHome: path }, null, 2), "utf8");
798
+ } catch (error) {
799
+ sendJson(res, 500, { ok: false, error: `cannot save config: ${error.message}` });
800
+ return;
801
+ }
802
+ const layout = livebenchLayout();
803
+ sendJson(res, 200, { ok: true, root: layout.root, available: layout.available });
804
+ },
805
+ }), `${name}: home route`);
806
+
721
807
  ctx.effect(() => ctx.webServer.register({
722
808
  kind: "exact",
723
809
  path: `${API}/delete`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-livebench-panel",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",