dsh-livebench-panel 0.1.7 → 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 +98 -66
  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 [pickedTasks, setPickedTasks] = useState(() => new Set());
103
- const [taskOrder, setTaskOrder] = useState(() => {
104
- try { return JSON.parse(localStorage.getItem("dlb_task_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,78 +285,75 @@ window.__ModuleLoader__.load({
265
285
  });
266
286
  };
267
287
 
268
- // ---- 成绩矩阵:task 为行(含 category 行标)、model 为列 ----
269
- // 附带:time 列(该任务评测开始时间 = 行内最早判分时间)、
288
+ // ---- 成绩矩阵:model 为行标识、category/task 为列标识 ----
289
+ // 附带:time 列(该模型评测开始时间 = 行内最早判分时间)、
270
290
  // 行拖拽排序(localStorage 持久化)、行选择与单行/批量删除。
271
291
  const taskKey = (category, task) => `${category}/${task}`;
272
292
  const matrix = useMemo(() => {
273
293
  if (!results) return null;
274
294
  const models = [...new Set(results.rows.map((r) => r.model))].sort();
275
- const byTask = new Map(); // `${cat}/${task}` -> {category, task, cells: Map(model->row), startTime}
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
276
298
  for (const row of results.rows) {
277
- const key = taskKey(row.category, row.task);
278
- const entry = byTask.get(key) ?? { category: row.category, task: row.task, cells: new Map(), startTime: Infinity };
279
- entry.cells.set(row.model, row);
280
- if (row.time && row.time < entry.startTime) entry.startTime = row.time;
281
- byTask.set(key, entry);
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);
282
302
  }
283
- return { models, tasks: [...byTask.entries()] };
303
+ return { models, tasks, cells, startTimes };
284
304
  }, [results]);
285
- const sortedTasks = useMemo(() => {
305
+ const sortedModels = useMemo(() => {
286
306
  if (!matrix) return [];
287
- const indexOf = new Map(taskOrder.map((key, index) => [key, index]));
288
- return [...matrix.tasks].sort((a, b) => {
289
- const ia = indexOf.has(a[0]) ? indexOf.get(a[0]) : Number.MAX_SAFE_INTEGER;
290
- const ib = indexOf.has(b[0]) ? indexOf.get(b[0]) : Number.MAX_SAFE_INTEGER;
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;
291
311
  if (ia !== ib) return ia - ib;
292
- return a[0].localeCompare(b[0]);
312
+ return a.localeCompare(b);
293
313
  });
294
- }, [matrix, taskOrder]);
314
+ }, [matrix, modelOrder]);
295
315
 
296
- const persistTaskOrder = (keys) => {
297
- setTaskOrder(keys);
298
- try { localStorage.setItem("dlb_task_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 */ }
299
319
  };
300
320
 
301
- const onRowDrop = (targetKey) => {
302
- const sourceKey = dragKey.current;
321
+ const onRowDrop = (targetModel) => {
322
+ const sourceModel = dragKey.current;
303
323
  dragKey.current = null;
304
- if (!sourceKey || sourceKey === targetKey) return;
305
- const keys = sortedTasks.map(([key]) => key);
306
- const from = keys.indexOf(sourceKey);
307
- 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);
308
328
  if (from < 0 || to < 0) return;
309
329
  keys.splice(to, 0, keys.splice(from, 1)[0]);
310
- persistTaskOrder(keys);
330
+ persistModelOrder(keys);
311
331
  };
312
332
 
313
- const togglePicked = (key) => setPickedTasks((prev) => {
333
+ const togglePicked = (model) => setPickedModels((prev) => {
314
334
  const next = new Set(prev);
315
- if (next.has(key)) next.delete(key); else next.add(key);
335
+ if (next.has(model)) next.delete(model); else next.add(model);
316
336
  return next;
317
337
  });
318
338
 
319
- // 删除一个任务行 = 删除该任务下所有模型的判分与答案记录
320
- const deleteTasks = async (keys) => {
321
- if (!results || keys.length === 0) return;
339
+ // 删除一个模型行 = 删除该模型在所有任务下的判分与答案记录
340
+ const deleteModels = async (modelsToDelete) => {
341
+ if (!results || modelsToDelete.length === 0) return;
322
342
  const serverRows = [];
323
- for (const key of keys) {
324
- const [category, task] = key.split("/");
325
- for (const row of results.rows) {
326
- if (row.category === category && row.task === task) {
327
- serverRows.push({ model: row.model, category, task });
328
- }
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 });
329
346
  }
330
347
  }
331
348
  if (serverRows.length === 0) return;
332
- if (!window.confirm(`确认删除所选 ${keys.length} 个任务的全部评测成绩(共 ${serverRows.length} 条模型记录,含答案)?不可恢复。`)) return;
349
+ if (!window.confirm(`确认删除所选 ${modelsToDelete.length} 个模型的全部评测成绩(共 ${serverRows.length} 条记录,含答案)?不可恢复。`)) return;
333
350
  await api("/delete", {
334
351
  method: "POST",
335
352
  headers: { "content-type": "application/json" },
336
353
  body: JSON.stringify({ rows: serverRows }),
337
354
  });
338
- setPickedTasks(new Set());
339
- persistTaskOrder(taskOrder.filter((key) => !keys.includes(key)));
355
+ setPickedModels(new Set());
356
+ persistModelOrder(modelOrder.filter((model) => !modelsToDelete.includes(model)));
340
357
  await loadResults();
341
358
  };
342
359
 
@@ -350,10 +367,21 @@ window.__ModuleLoader__.load({
350
367
  return h("div", { className: c("root") },
351
368
  h("div", { className: c("head") },
352
369
  h("h2", null, "LiveBench"),
353
- h("span", { className: c("sub") }, "LiveBench 评测面板 · 基于 V:\\PythonProject\\C_UtilizeSpace\\LiveBench"),
370
+ h("span", { className: c("sub") }, config?.root ? `LiveBench 评测面板 · ${config.root}` : "LiveBench 评测面板"),
354
371
  h("span", { className: c("badge"), "data-ok": config?.available ? "1" : "0" },
355
372
  config === null ? "检测中…" : config.available ? "LiveBench 就绪" : "未找到 LiveBench"),
356
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
+ ),
357
385
  configError !== null && h("p", { className: c("error") }, `加载配置失败:${configError}`),
358
386
  h("div", { className: c("card") },
359
387
  h("div", { className: c("grid") },
@@ -431,49 +459,53 @@ window.__ModuleLoader__.load({
431
459
  ),
432
460
  h("pre", { className: c("log"), ref: logRef }, log || "(暂无输出)"),
433
461
  ),
434
- sortedTasks.length > 0 && h("div", { className: c("card") },
462
+ sortedModels.length > 0 && h("div", { className: c("card") },
435
463
  h("div", { className: c("row") },
436
- h("span", { className: c("label") }, "评测成绩(分数 = 平均分 ×100,括号内为 已判/总题数;行首可拖动排序,顺序保存在本机)"),
464
+ h("span", { className: c("label") }, "评测成绩(model 为行;分数 = 平均分 ×100;行首可拖动排序,顺序保存在本机)"),
437
465
  h("button", {
438
466
  className: c("btn") + " " + c("btnDanger"),
439
- disabled: pickedTasks.size === 0,
440
- onClick: () => deleteTasks([...pickedTasks]),
441
- }, `删除所选(${pickedTasks.size})`),
467
+ disabled: pickedModels.size === 0,
468
+ onClick: () => deleteModels([...pickedModels]),
469
+ }, `删除所选(${pickedModels.size})`),
442
470
  ),
443
471
  h("table", { className: c("table") },
444
472
  h("thead", null, h("tr", null,
445
473
  h("th", { style: { width: "26px" } }, "⠿"),
446
474
  h("th", { style: { width: "30px" } },
447
- h("input", { type: "checkbox", checked: pickedTasks.size > 0 && pickedTasks.size === sortedTasks.length,
448
- onChange: (event) => setPickedTasks(event.target.checked ? new Set(sortedTasks.map(([key]) => key)) : new Set()) })),
449
- h("th", null, "task"),
450
- matrix.models.map((model) => h("th", { key: model }, model)),
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()) })),
477
+ h("th", null, "model"),
478
+ matrix.tasks.map((taskKeyCol) => {
479
+ const [cat, task] = taskKeyCol.split("/");
480
+ return h("th", { key: taskKeyCol, title: `${cat} / ${task}` }, task);
481
+ }),
451
482
  h("th", null, "删除"),
452
483
  h("th", null, "time"),
453
484
  )),
454
485
  h("tbody", null,
455
- sortedTasks.map(([key, entry]) => {
456
- const checked = pickedTasks.has(key);
486
+ sortedModels.map((model) => {
487
+ const checked = pickedModels.has(model);
488
+ const startTime = matrix.startTimes.get(model);
457
489
  return h("tr", {
458
- key,
490
+ key: model,
459
491
  draggable: true,
460
- onDragStart: () => { dragKey.current = key; },
492
+ onDragStart: () => { dragKey.current = model; },
461
493
  onDragOver: (event) => event.preventDefault(),
462
- onDrop: () => onRowDrop(key),
494
+ onDrop: () => onRowDrop(model),
463
495
  },
464
496
  h("td", { className: c("drag"), title: "拖动排序" }, "⠿"),
465
- h("td", null, h("input", { type: "checkbox", checked, onChange: () => togglePicked(key) })),
466
- h("td", null, h("span", { title: `${entry.category} / ${entry.task}` }, `${entry.category} / ${entry.task}`)),
467
- matrix.models.map((model) => {
468
- const row = entry.cells.get(model);
469
- return h("td", { key: model, className: c("score") },
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") },
470
502
  row ? `${row.score.toFixed(1)} (${row.judged}/${row.total})` : "—");
471
503
  }),
472
504
  h("td", null, h("button", {
473
- className: c("btnGhost") + " " + c("btn"), title: "删除该任务下所有模型的评测成绩",
474
- onClick: () => deleteTasks([key]),
505
+ className: c("btnGhost") + " " + c("btn"), title: "删除该模型的所有评测成绩",
506
+ onClick: () => deleteModels([model]),
475
507
  }, "删除")),
476
- h("td", null, fmtTime(entry.startTime === Infinity ? null : entry.startTime)),
508
+ h("td", null, fmtTime(startTime)),
477
509
  );
478
510
  })),
479
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.7",
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",