dsh-livebench-panel 0.1.16 → 0.1.18

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 (2) hide show
  1. package/lib/client.js +501 -508
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -13,6 +13,7 @@ window.__ModuleLoader__.load({
13
13
  .dlb_head h2{margin:0;font-size:16px;font-weight:600}
14
14
  .dlb_sub{color:var(--dsw-alias-label-tertiary);font-size:12px}
15
15
  .dlb_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:12px}
16
+ .dlb_fields{display:flex;flex-direction:column;gap:12px}
16
17
  .dlb_grid{display:grid;grid-template-columns:repeat(12,1fr);gap:10px}
17
18
  @media (max-width:860px){.dlb_grid>.dlb_field{grid-column:1/-1 !important}}
18
19
  .dlb_field{display:flex;flex-direction:column;gap:4px;min-width:0}
@@ -51,10 +52,9 @@ window.__ModuleLoader__.load({
51
52
  .dlb_chip{appearance:none;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:3px 12px;font-size:12px;line-height:18px}
52
53
  .dlb_chip:disabled{opacity:.4;cursor:not-allowed}
53
54
  .dlb_chipOn{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 14%,transparent)}
54
- .dlb_actionWrap{display:flex;gap:12px;align-items:stretch}
55
- .dlb_actionCol{display:flex;flex-direction:column;gap:8px;width:130px;flex-shrink:0}
56
- .dlb_btnBar{flex:1;min-height:44px;font-weight:600;white-space:normal}
57
- @media (max-width:760px){.dlb_actionWrap{flex-direction:column}.dlb_actionCol{flex-direction:row;width:100%}.dlb_btnBar{min-height:40px;flex:1}}
55
+ .dlb_btnRow{display:flex;gap:10px;justify-content:center;margin-top:2px}
56
+ .dlb_btnBar{min-width:190px;min-height:40px;font-weight:600;text-align:center}
57
+ @media (max-width:700px){.dlb_btnRow{flex-wrap:wrap}.dlb_btnBar{max-width:none}}
58
58
  .dlb_drag{cursor:grab;color:var(--dsw-alias-label-tertiary);text-align:center;user-select:none}`;
59
59
  const tagId = "dsh-livebench-panel/panel.css";
60
60
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
@@ -95,537 +95,530 @@ window.__ModuleLoader__.load({
95
95
  }
96
96
 
97
97
  function LiveBenchView() {
98
- const [config, setConfig] = useState(null);
99
- const [configError, setConfigError] = useState(null);
100
- const [sel, setSel] = useState({ models: [], efforts: {}, cats: [], tasks: [], release: "2024-11-25", begin: "", end: "", maxTokens: "32000" });
101
- const [modelDdOpen, setModelDdOpen] = useState(false);
98
+ const [config, setConfig] = useState(null);
99
+ const [configError, setConfigError] = useState(null);
100
+ const [sel, setSel] = useState({ models: [], efforts: {}, cats: [], tasks: [], release: "2024-11-25", begin: "", end: "", maxTokens: "32000" });
101
+ const [modelDdOpen, setModelDdOpen] = useState(false);
102
+ const [busy, setBusy] = useState(false);
103
+ const [running, setRunning] = useState(false);
104
+ const [runsList, setRunsList] = useState([]);
105
+ const [startError, setStartError] = useState(null);
106
+ const [homeInput, setHomeInput] = useState("");
107
+ const [homeBusy, setHomeBusy] = useState(false);
108
+ const [results, setResults] = useState(null);
109
+ const [pickedModels, setPickedModels] = useState(() => new Set());
110
+ const [modelOrder, setModelOrder] = useState(() => {
111
+ try { return JSON.parse(localStorage.getItem("dlb_model_order_v1")) ?? []; } catch { return []; }
112
+ });
113
+ const dragKey = useRef(null);
102
114
  const modelDdRef = useRef(null);
103
- const [busy, setBusy] = useState(false);
104
- const [running, setRunning] = useState(false);
105
- const [runsList, setRunsList] = useState([]);
106
- const [startError, setStartError] = useState(null);
107
- const [homeInput, setHomeInput] = useState("");
108
- const [homeBusy, setHomeBusy] = useState(false);
109
- const [results, setResults] = useState(null);
110
- const [pickedModels, setPickedModels] = useState(() => new Set());
111
- const [modelOrder, setModelOrder] = useState(() => {
112
- try { return JSON.parse(localStorage.getItem("dlb_model_order_v1")) ?? []; } catch { return []; }
113
- });
114
- const dragKey = useRef(null);
115
- const logRef = useRef(null);
116
115
 
117
- // 点击面板外部时关闭模型下拉
118
- useEffect(() => {
119
- if (!modelDdOpen) return;
120
- const onDocDown = (event) => {
121
- if (modelDdRef.current && !modelDdRef.current.contains(event.target)) setModelDdOpen(false);
122
- };
123
- document.addEventListener("mousedown", onDocDown);
124
- return () => document.removeEventListener("mousedown", onDocDown);
125
- }, [modelDdOpen]);
116
+ // 点击面板外部时关闭模型下拉
117
+ useEffect(() => {
118
+ if (!modelDdOpen) return undefined;
119
+ const onDocDown = (event) => {
120
+ if (modelDdRef.current && !modelDdRef.current.contains(event.target)) setModelDdOpen(false);
121
+ };
122
+ document.addEventListener("mousedown", onDocDown);
123
+ return () => document.removeEventListener("mousedown", onDocDown);
124
+ }, [modelDdOpen]);
126
125
 
127
- const loadConfig = useCallback(async () => {
128
- const payload = await api("/config");
129
- if (payload.ok) {
130
- setConfig(payload);
131
- setConfigError(null);
132
- setSel((prev) => {
133
- const next = { ...prev, release: payload.releases.includes(prev.release) ? prev.release : "2024-11-25" };
134
- // 保留仍存在的选择,剔除失效项
135
- next.models = prev.models.filter((v) => {
136
- const [pid] = v.split("::");
137
- const p = payload.providers.find((x) => x.id === pid);
138
- return p ? true : false;
139
- });
140
- return next;
141
- });
142
- } else {
143
- setConfigError(payload.error ?? "config unavailable");
144
- }
145
- }, []);
146
-
147
- const loadResults = useCallback(async () => {
148
- const payload = await api("/results");
149
- if (payload.ok) setResults(payload);
150
- }, []);
126
+ const loadConfig = useCallback(async () => {
127
+ const payload = await api("/config");
128
+ if (payload.ok) {
129
+ setConfig(payload);
130
+ setConfigError(null);
131
+ setSel((prev) => {
132
+ const next = { ...prev, release: payload.releases.includes(prev.release) ? prev.release : "2024-11-25" };
133
+ next.models = prev.models.filter((value) => {
134
+ const pid = value.split("::")[0];
135
+ return payload.providers.some((p) => p.id === pid);
136
+ });
137
+ return next;
138
+ });
139
+ } else {
140
+ setConfigError(payload.error ?? "config unavailable");
141
+ }
142
+ }, []);
151
143
 
152
- const refreshStatus = useCallback(async () => {
153
- const payload = await api("/status");
154
- if (payload.ok) {
155
- setRunsList(payload.runs ?? []);
156
- setRunning(payload.running === true);
157
- return payload.running === true;
158
- }
159
- return false;
160
- }, []);
144
+ const loadResults = useCallback(async () => {
145
+ const payload = await api("/results");
146
+ if (payload.ok) setResults(payload);
147
+ }, []);
161
148
 
162
- useEffect(() => {
163
- loadConfig();
164
- loadResults();
165
- refreshStatus();
166
- }, [loadConfig, loadResults, refreshStatus]);
149
+ const refreshStatus = useCallback(async () => {
150
+ const payload = await api("/status");
151
+ if (payload.ok) {
152
+ setRunsList(payload.runs ?? []);
153
+ setRunning(payload.running === true);
154
+ return payload.running === true;
155
+ }
156
+ return false;
157
+ }, []);
167
158
 
168
- // poll while a run is active; refresh results when it ends
169
- const wasRunning = useRef(false);
170
- useEffect(() => {
171
- const timer = setInterval(async () => {
172
- const active = await refreshStatus();
173
- if (wasRunning.current && !active) loadResults();
174
- wasRunning.current = active;
175
- }, 2500);
176
- return () => clearInterval(timer);
177
- }, [refreshStatus, loadResults]);
159
+ useEffect(() => {
160
+ loadConfig();
161
+ loadResults();
162
+ refreshStatus();
163
+ }, [loadConfig, loadResults, refreshStatus]);
178
164
 
179
- const categories = useMemo(() => (config ? Object.keys(config.tasks) : []), [config]);
180
- // 有效题数:LiveBench 会丢弃「发布晚于所选 release」和「在所选 release
181
- // 前已退役」的题目,这里按题目桶 (发布日, 移除日) 精确复算。
182
- const countFor = useCallback((category, task) => {
183
- if (!config || category.length === 0) return null;
184
- const meta = (config.tasks[category] ?? {})[task];
185
- if (!meta || !Array.isArray(meta.buckets)) return null;
186
- const option = sel.release;
187
- return meta.buckets.reduce((sum, b) => {
188
- const releasedOk = b.r !== "" && b.r <= option;
189
- const notRemoved = b.rm === "" || b.rm > option;
190
- return sum + (releasedOk && notRemoved ? b.n : 0);
191
- }, 0);
192
- }, [config, sel.release]);
193
- const catCount = useCallback((category) => {
194
- if (!config) return 0;
195
- return Object.keys(config.tasks[category] ?? {}).reduce((sum, task) => sum + (countFor(category, task) ?? 0), 0);
196
- }, [config, countFor]);
197
- // 多选任务 chips:未选分类时展示全部分类的任务
198
- const taskChips = useMemo(() => {
199
- const cats = sel.cats.length > 0 ? sel.cats : categories;
200
- return cats.flatMap((cat) => Object.keys(config?.tasks[cat] ?? {}).map((task) => `${cat}/${task}`));
201
- }, [sel.cats, categories, config]);
202
- const toggleList = (key, value) => setSel((prev) => {
203
- const list = prev[key];
204
- const next = list.includes(value) ? list.filter((v) => v !== value) : [...list, value];
205
- return { ...prev, [key]: next };
206
- });
207
- // 已选模型(provider::model 值列表)的解析与 per-model effort
208
- const selectedModelEntries = useMemo(() => sel.models.map((value) => {
209
- const [pid, ...rest] = value.split("::");
210
- const mid = rest.join("::");
211
- const provider = config?.providers.find((p) => p.id === pid) ?? null;
212
- const model = provider?.models.find((m) => m.id === mid) ?? null;
213
- return { providerId: pid, modelId: mid, modelValue: value, efforts: model?.efforts ?? [] };
214
- }).filter((entry) => entry.modelId !== ""), [sel.models, config]);
165
+ // 轮询评测状态;从运行中转为结束后自动刷新成绩
166
+ const wasRunning = useRef(false);
167
+ useEffect(() => {
168
+ const timer = setInterval(async () => {
169
+ const active = await refreshStatus();
170
+ if (wasRunning.current && !active) loadResults();
171
+ wasRunning.current = active;
172
+ }, 2500);
173
+ return () => clearInterval(timer);
174
+ }, [refreshStatus, loadResults]);
215
175
 
216
- const onStart = async () => {
217
- setStartError(null);
218
- if (selectedModelEntries.length === 0) {
219
- setStartError("请先在下拉框中至少选择一个模型。");
220
- return;
221
- }
222
- const benchNames = [];
223
- if (sel.cats.length === 0) {
224
- benchNames.push("live_bench");
225
- } else if (sel.tasks.length === 0) {
226
- for (const cat of sel.cats) benchNames.push(`live_bench/${cat}`);
227
- } else {
228
- for (const t of sel.tasks) benchNames.push(`live_bench/${t}`);
229
- }
230
- const zeroTask = sel.tasks.find((t) => countFor(t.split("/")[0], t.split("/")[1]) === 0);
231
- if (zeroTask) {
232
- setStartError(`任务 ${zeroTask.split("/")[1]} 在 release ${sel.release} 下没有可用题目(该批题目已退役),请取消勾选或换 release。`);
233
- return;
234
- }
235
- setBusy(true);
236
- try {
237
- // 每个 selected 模型并发启动一个评测(服务端有并发上限保护),
238
- // 推理强度按各模型在次级下拉中的选择逐个下发
239
- const failures = [];
240
- const launches = selectedModelEntries.map(async (entry) => {
241
- const payload = await api("/start", {
242
- method: "POST",
243
- headers: { "content-type": "application/json" },
244
- body: JSON.stringify({
245
- provider: entry.providerId,
246
- model: entry.modelId,
247
- reasoningEffort: sel.efforts[entry.modelValue] ?? "default",
248
- benchNames,
249
- release: sel.release,
250
- begin: sel.begin,
251
- end: sel.end,
252
- maxTokens: sel.maxTokens,
253
- }),
254
- });
255
- if (!payload.ok) failures.push(`${entry.modelId}: ${payload.error ?? "启动失败"}`);
176
+ const categories = useMemo(() => (config ? Object.keys(config.tasks) : []), [config]);
177
+ // 有效题数:LiveBench 丢弃「发布晚于所选 release」与「在所选 release 前已退役」的题目
178
+ const countFor = useCallback((category, task) => {
179
+ if (!config || category.length === 0) return null;
180
+ const meta = (config.tasks[category] ?? {})[task];
181
+ if (!meta || !Array.isArray(meta.buckets)) return null;
182
+ const option = sel.release;
183
+ return meta.buckets.reduce((sum, b) => {
184
+ const releasedOk = b.r !== "" && b.r <= option;
185
+ const notRemoved = b.rm === "" || b.rm > option;
186
+ return sum + (releasedOk && notRemoved ? b.n : 0);
187
+ }, 0);
188
+ }, [config, sel.release]);
189
+ const catCount = useCallback((category) => {
190
+ if (!config) return 0;
191
+ return Object.keys(config.tasks[category] ?? {}).reduce((sum, task) => sum + (countFor(category, task) ?? 0), 0);
192
+ }, [config, countFor]);
193
+ const taskChips = useMemo(() => {
194
+ const cats = sel.cats.length > 0 ? sel.cats : categories;
195
+ return cats.flatMap((cat) => Object.keys(config?.tasks[cat] ?? {}).map((task) => `${cat}/${task}`));
196
+ }, [sel.cats, categories, config]);
197
+ const toggleList = (key, value) => setSel((prev) => {
198
+ const list = prev[key];
199
+ const next = list.includes(value) ? list.filter((v) => v !== value) : [...list, value];
200
+ return { ...prev, [key]: next };
256
201
  });
257
- await Promise.all(launches);
258
- if (failures.length > 0) setStartError(failures.join(";"));
259
- await refreshStatus();
260
- } catch (error) {
261
- setStartError(String(error.message ?? error));
262
- } finally {
263
- setBusy(false);
264
- }
265
- };
266
202
 
267
- const onStop = async () => {
268
- await api("/stop", { method: "POST" });
269
- await refreshStatus();
270
- };
203
+ // 已选模型解析(provider::model)
204
+ const selectedModelEntries = useMemo(() => sel.models.map((value) => {
205
+ const sep = value.indexOf("::");
206
+ const pid = value.slice(0, sep);
207
+ const mid = value.slice(sep + 2);
208
+ const provider = config?.providers.find((p) => p.id === pid) ?? null;
209
+ const model = provider?.models.find((m) => m.id === mid) ?? null;
210
+ return { providerId: pid, modelId: mid, modelValue: value, efforts: model?.efforts ?? [] };
211
+ }), [sel.models, config]);
271
212
 
272
- const saveHome = async () => {
273
- setHomeBusy(true);
274
- setStartError(null);
275
- try {
276
- const payload = await api("/home", {
277
- method: "POST",
278
- headers: { "content-type": "application/json" },
279
- body: JSON.stringify({ path: homeInput }),
280
- });
281
- if (!payload.ok) setStartError(payload.error ?? "设置失败");
282
- else { setHomeInput(""); await loadConfig(); }
283
- } catch (error) {
284
- setStartError(String(error.message ?? error));
285
- } finally {
286
- setHomeBusy(false);
287
- }
288
- };
213
+ const onStart = async () => {
214
+ setStartError(null);
215
+ if (selectedModelEntries.length === 0) {
216
+ setStartError("请先在模型下拉中至少勾选一个模型。");
217
+ return;
218
+ }
219
+ const benchNames = [];
220
+ if (sel.cats.length === 0) {
221
+ benchNames.push("live_bench");
222
+ } else if (sel.tasks.length === 0) {
223
+ for (const cat of sel.cats) benchNames.push(`live_bench/${cat}`);
224
+ } else {
225
+ for (const t of sel.tasks) benchNames.push(`live_bench/${t}`);
226
+ }
227
+ const zeroTask = sel.tasks.find((t) => countFor(t.split("/")[0], t.split("/")[1]) === 0);
228
+ if (zeroTask) {
229
+ setStartError(`任务 ${zeroTask.split("/")[1]} 在 release ${sel.release} 下没有可用题目(该批题目已退役),请取消勾选或换 release。`);
230
+ return;
231
+ }
232
+ setBusy(true);
233
+ try {
234
+ // 每个模型并发启动一个评测(服务端并发上限 6)
235
+ const failures = [];
236
+ const launches = selectedModelEntries.map(async (entry) => {
237
+ const payload = await api("/start", {
238
+ method: "POST",
239
+ headers: { "content-type": "application/json" },
240
+ body: JSON.stringify({
241
+ provider: entry.providerId,
242
+ model: entry.modelId,
243
+ reasoningEffort: sel.efforts[entry.modelValue] ?? "default",
244
+ benchNames,
245
+ release: sel.release,
246
+ begin: sel.begin,
247
+ end: sel.end,
248
+ maxTokens: sel.maxTokens,
249
+ }),
250
+ });
251
+ if (!payload.ok) failures.push(`${entry.modelId}: ${payload.error ?? "启动失败"}`);
252
+ });
253
+ await Promise.all(launches);
254
+ if (failures.length > 0) setStartError(failures.join(";"));
255
+ await refreshStatus();
256
+ } catch (error) {
257
+ setStartError(String(error.message ?? error));
258
+ } finally {
259
+ setBusy(false);
260
+ }
261
+ };
289
262
 
290
- const setField = (key) => (event) => {
291
- const value = event.target.value;
292
- setSel((prev) => ({ ...prev, [key]: value }));
293
- };
263
+ const onStop = async () => {
264
+ await api("/stop", { method: "POST" });
265
+ await refreshStatus();
266
+ };
294
267
 
295
- // ---- 成绩矩阵:model 为行标识、category/task 为列标识 ----
296
- // 附带:time 列(该模型评测开始时间 = 行内最早判分时间)、
297
- // 行拖拽排序(localStorage 持久化)、行选择与单行/批量删除。
298
- const taskKey = (category, task) => `${category}/${task}`;
299
- const matrix = useMemo(() => {
300
- if (!results) return null;
301
- const models = [...new Set(results.rows.map((r) => r.model))].sort();
302
- const tasks = [...new Set(results.rows.map((r) => taskKey(r.category, r.task)))].sort();
303
- const cells = new Map(); // `${model}\u0000${taskKey}` -> row
304
- const startTimes = new Map(); // model -> min tstamp
305
- for (const row of results.rows) {
306
- cells.set(`${row.model}\u0000${taskKey(row.category, row.task)}`, row);
307
- const prev = startTimes.get(row.model);
308
- if (row.time && (prev === undefined || row.time < prev)) startTimes.set(row.model, row.time);
309
- }
310
- return { models, tasks, cells, startTimes };
311
- }, [results]);
312
- const sortedModels = useMemo(() => {
313
- if (!matrix) return [];
314
- const indexOf = new Map(modelOrder.map((key, index) => [key, index]));
315
- return [...matrix.models].sort((a, b) => {
316
- const ia = indexOf.has(a) ? indexOf.get(a) : Number.MAX_SAFE_INTEGER;
317
- const ib = indexOf.has(b) ? indexOf.get(b) : Number.MAX_SAFE_INTEGER;
318
- if (ia !== ib) return ia - ib;
319
- return a.localeCompare(b);
320
- });
321
- }, [matrix, modelOrder]);
268
+ const saveHome = async () => {
269
+ setHomeBusy(true);
270
+ setStartError(null);
271
+ try {
272
+ const payload = await api("/home", {
273
+ method: "POST",
274
+ headers: { "content-type": "application/json" },
275
+ body: JSON.stringify({ path: homeInput }),
276
+ });
277
+ if (!payload.ok) setStartError(payload.error ?? "设置失败");
278
+ else { setHomeInput(""); await loadConfig(); }
279
+ } catch (error) {
280
+ setStartError(String(error.message ?? error));
281
+ } finally {
282
+ setHomeBusy(false);
283
+ }
284
+ };
322
285
 
323
- const persistModelOrder = (keys) => {
324
- setModelOrder(keys);
325
- try { localStorage.setItem("dlb_model_order_v1", JSON.stringify(keys)); } catch { /* private mode */ }
326
- };
286
+ const setField = (key) => (event) => {
287
+ const value = event.target.value;
288
+ setSel((prev) => ({ ...prev, [key]: value }));
289
+ };
290
+ const toggleModel = (value) => setSel((prev) => {
291
+ const set = new Set(prev.models);
292
+ const nextEfforts = { ...prev.efforts };
293
+ if (set.has(value)) { set.delete(value); delete nextEfforts[value]; }
294
+ else set.add(value);
295
+ return { ...prev, models: [...set], efforts: nextEfforts };
296
+ });
297
+ const setModelEffort = (value) => (event) => {
298
+ const v = event.target.value;
299
+ setSel((prev) => ({ ...prev, efforts: { ...prev.efforts, [value]: v } }));
300
+ };
327
301
 
328
- const onRowDrop = (targetModel) => {
329
- const sourceModel = dragKey.current;
330
- dragKey.current = null;
331
- if (!sourceModel || sourceModel === targetModel) return;
332
- const keys = sortedModels.slice();
333
- const from = keys.indexOf(sourceModel);
334
- const to = keys.indexOf(targetModel);
335
- if (from < 0 || to < 0) return;
336
- keys.splice(to, 0, keys.splice(from, 1)[0]);
337
- persistModelOrder(keys);
338
- };
302
+ // ---- 成绩矩阵:model 为行标识、category/task 为列标识 ----
303
+ // time = 该模型评测开始时间(行内最早判分时间)
304
+ const taskKey = (category, task) => `${category}/${task}`;
305
+ const matrix = useMemo(() => {
306
+ if (!results) return null;
307
+ const models = [...new Set(results.rows.map((r) => r.model))].sort();
308
+ const tasks = [...new Set(results.rows.map((r) => taskKey(r.category, r.task)))].sort();
309
+ const cells = new Map();
310
+ const startTimes = new Map();
311
+ for (const row of results.rows) {
312
+ cells.set(`${row.model}__@__${taskKey(row.category, row.task)}`, row);
313
+ const prev = startTimes.get(row.model);
314
+ if (row.time && (prev === undefined || row.time < prev)) startTimes.set(row.model, row.time);
315
+ }
316
+ return { models, tasks, cells, startTimes };
317
+ }, [results]);
318
+ const cellOf = (model, key) => matrix.cells.get(`${model}__@__${key}`);
319
+ const sortedModels = useMemo(() => {
320
+ if (!matrix) return [];
321
+ const indexOf = new Map(modelOrder.map((key, index) => [key, index]));
322
+ return [...matrix.models].sort((a, b) => {
323
+ const ia = indexOf.has(a) ? indexOf.get(a) : Number.MAX_SAFE_INTEGER;
324
+ const ib = indexOf.has(b) ? indexOf.get(b) : Number.MAX_SAFE_INTEGER;
325
+ if (ia !== ib) return ia - ib;
326
+ return a.localeCompare(b);
327
+ });
328
+ }, [matrix, modelOrder]);
339
329
 
340
- const togglePicked = (model) => setPickedModels((prev) => {
341
- const next = new Set(prev);
342
- if (next.has(model)) next.delete(model); else next.add(model);
343
- return next;
344
- });
330
+ const persistModelOrder = (keys) => {
331
+ setModelOrder(keys);
332
+ try { localStorage.setItem("dlb_model_order_v1", JSON.stringify(keys)); } catch { /* ignore */ }
333
+ };
345
334
 
346
- // 删除一个模型行 = 删除该模型在所有任务下的判分与答案记录
347
- const deleteModels = async (modelsToDelete) => {
348
- if (!results || modelsToDelete.length === 0) return;
349
- const serverRows = [];
350
- for (const row of results.rows) {
351
- if (modelsToDelete.includes(row.model)) {
352
- serverRows.push({ model: row.model, category: row.category, task: row.task });
353
- }
354
- }
355
- if (serverRows.length === 0) return;
356
- if (!window.confirm(`确认删除所选 ${modelsToDelete.length} 个模型的全部评测成绩(共 ${serverRows.length} 条记录,含答案)?不可恢复。`)) return;
357
- await api("/delete", {
358
- method: "POST",
359
- headers: { "content-type": "application/json" },
360
- body: JSON.stringify({ rows: serverRows }),
361
- });
362
- setPickedModels(new Set());
363
- persistModelOrder(modelOrder.filter((model) => !modelsToDelete.includes(model)));
364
- await loadResults();
365
- };
335
+ const onRowDrop = (targetModel) => {
336
+ const sourceModel = dragKey.current;
337
+ dragKey.current = null;
338
+ if (!sourceModel || sourceModel === targetModel) return;
339
+ const keys = sortedModels.slice();
340
+ const from = keys.indexOf(sourceModel);
341
+ const to = keys.indexOf(targetModel);
342
+ if (from < 0 || to < 0) return;
343
+ keys.splice(to, 0, keys.splice(from, 1)[0]);
344
+ persistModelOrder(keys);
345
+ };
366
346
 
367
- const fmtTime = (t) => {
368
- if (!t) return "—";
369
- const dt = new Date(t * 1000);
370
- const pad = (n) => String(n).padStart(2, "0");
371
- return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
372
- };
347
+ const togglePicked = (model) => setPickedModels((prev) => {
348
+ const next = new Set(prev);
349
+ if (next.has(model)) next.delete(model); else next.add(model);
350
+ return next;
351
+ });
373
352
 
374
- return h("div", { className: c("root") },
375
- h("div", { className: c("head") },
376
- h("h2", null, "LiveBench"),
377
- h("span", { className: c("sub") }, config?.root ? `LiveBench 评测面板 · ${config.root}` : "LiveBench 评测面板"),
378
- h("span", { className: c("badge"), "data-ok": config?.available ? "1" : "0" },
379
- config === null ? "检测中…" : config.available ? "LiveBench 就绪" : "未找到 LiveBench"),
380
- ),
381
- h("div", { className: c("row") },
382
- h("span", { className: c("label") }, "LiveBench 路径"),
383
- h("input", {
384
- className: c("input"), style: { flex: "1", minWidth: "240px" },
385
- placeholder: config?.root ?? "V:\\…\\LiveBench 或 ~/LiveBench",
386
- value: homeInput, onChange: (event) => setHomeInput(event.target.value),
387
- }),
388
- h("button", { className: c("btnGhost") + " " + c("btn"), disabled: homeBusy || homeInput.trim().length === 0, onClick: saveHome },
389
- homeBusy ? "保存中…" : "设置路径"),
390
- h("span", { className: c("hint") }, "需含 livebench\\run_livebench.py 与 .venv;也可用环境变量 DSH_LIVEBENCH_HOME"),
391
- ),
392
- configError !== null && h("p", { className: c("error") }, `加载配置失败:${configError}`),
393
- h("div", { className: c("card") },
394
- h("div", { className: c("actionWrap") },
395
- h("div", { className: c("grid"), style: { flex: 1, minWidth: 0 } },
396
- h("div", { className: c("field"), ref: modelDdRef, style: { gridColumn: "span 6" } },
397
- h("label", { className: c("label") }, "模型(harness 全部模型,可多选;勾选后可在行内单独设定推理强度)"),
398
- h("button", {
399
- type: "button", className: c("select"), style: { textAlign: "left", width: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
400
- onClick: () => setModelDdOpen((v) => !v),
401
- }, sel.models.length === 0
402
- ? "点击选择模型…"
403
- : sel.models.length === 1
404
- ? sel.models[0].split("::")[1]
405
- : `已选 ${sel.models.length} 个模型`),
406
- modelDdOpen && h("div", {
407
- style: {
408
- marginTop: "6px", maxHeight: "340px", overflowY: "auto", border: "1px solid var(--dsw-alias-border-l2)",
409
- borderRadius: "8px", background: "var(--dsw-alias-bg-layer-1)", padding: "6px",
410
- },
411
- },
412
- (config?.providers ?? []).map((p) => h("div", { key: p.id, style: { marginBottom: "4px" } },
413
- h("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary)", padding: "3px 4px" } },
414
- p.name + (p.routable ? "" : " ·未接LiveBench")),
415
- p.models.map((m) => {
416
- const value = p.id + "::" + m.id;
417
- const checked = sel.models.includes(value);
418
- return h("div", { key: value, style: { borderBottom: "1px solid var(--dsw-alias-border-l2)", padding: "3px 4px" } },
419
- h("div", {
420
- style: { display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" },
421
- onClick: () => {
422
- setSel((prev) => {
423
- const set = new Set(prev.models);
424
- const nextEfforts = { ...prev.efforts };
425
- if (set.has(value)) { set.delete(value); delete nextEfforts[value]; }
426
- else set.add(value);
427
- return { ...prev, models: [...set], efforts: nextEfforts };
428
- });
429
- },
430
- },
431
- h("input", { type: "checkbox", checked, readOnly: true, style: { cursor: "pointer" } }),
432
- h("span", { style: { fontSize: "12.5px", flex: 1 } }, m.name)),
433
- checked && m.efforts.length > 0 && h("div", { style: { display: "flex", alignItems: "center", gap: "6px", padding: "4px 0 2px 26px" } },
434
- h("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary)", whiteSpace: "nowrap" } }, "推理强度"),
435
- h("select", {
436
- className: c("select"), style: { height: "28px", fontSize: "12px", flex: 1 },
437
- value: sel.efforts[value] ?? "default",
438
- onClick: (event) => event.stopPropagation(),
439
- onChange: (event) => {
440
- const v = event.target.value;
441
- setSel((prev) => ({ ...prev, efforts: { ...prev.efforts, [value]: v } }));
442
- },
443
- },
444
- h("option", { value: "default" }, "(模型默认)"),
445
- m.efforts.map((e) => h("option", { key: e, value: e }, e === "off" ? "off(关闭思考)" : e)))));
446
- }))),
353
+ // 删除一个模型行 = 删除该模型在所有任务下的判分与答案记录
354
+ const deleteModels = async (modelsToDelete) => {
355
+ if (!results || modelsToDelete.length === 0) return;
356
+ const serverRows = [];
357
+ for (const row of results.rows) {
358
+ if (modelsToDelete.includes(row.model)) {
359
+ serverRows.push({ model: row.model, category: row.category, task: row.task });
360
+ }
361
+ }
362
+ if (serverRows.length === 0) return;
363
+ if (!window.confirm(`确认删除所选 ${modelsToDelete.length} 个模型的全部评测成绩(共 ${serverRows.length} 条记录,含答案)?不可恢复。`)) return;
364
+ await api("/delete", {
365
+ method: "POST",
366
+ headers: { "content-type": "application/json" },
367
+ body: JSON.stringify({ rows: serverRows }),
368
+ });
369
+ setPickedModels(new Set());
370
+ persistModelOrder(modelOrder.filter((model) => !modelsToDelete.includes(model)));
371
+ await loadResults();
372
+ };
373
+
374
+ const fmtTime = (t) => {
375
+ if (!t) return "";
376
+ const dt = new Date(t * 1000);
377
+ const pad = (n) => String(n).padStart(2, "0");
378
+ return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
379
+ };
380
+
381
+ return h("div", { className: c("root") },
382
+ h("div", { className: c("head") },
383
+ h("h2", null, "LiveBench"),
384
+ h("span", { className: c("sub") }, config?.root ? `LiveBench 评测面板 · ${config.root}` : "LiveBench 评测面板"),
385
+ h("span", { className: c("badge"), "data-ok": config?.available ? "1" : "0" },
386
+ config === null ? "检测中…" : config.available ? "LiveBench 就绪" : "未找到 LiveBench"),
447
387
  ),
448
- h("div", { className: c("field"), style: { gridColumn: "span 2" } },
449
- h("label", { className: c("label") }, "题集 release"),
450
- h("select", { className: c("select"), value: sel.release, onChange: setField("release") },
451
- (config?.releases ?? ["2024-11-25"]).map((r) => h("option", { key: r, value: r }, r))),
388
+ h("div", { className: c("row") },
389
+ h("span", { className: c("label") }, "LiveBench 路径"),
390
+ h("input", {
391
+ className: c("input"), style: { flex: "1", minWidth: "240px" },
392
+ placeholder: config?.root ?? "选择或输入 LiveBench 目录",
393
+ value: homeInput, onChange: (event) => setHomeInput(event.target.value),
394
+ }),
395
+ h("button", { className: c("btnGhost") + " " + c("btn"), disabled: homeBusy || homeInput.trim().length === 0, onClick: saveHome },
396
+ homeBusy ? "保存中…" : "设置路径"),
397
+ h("span", { className: c("hint") }, "需含 livebench\\run_livebench.py 与 .venv;也可用环境变量 DSH_LIVEBENCH_HOME"),
452
398
  ),
453
- h("div", { className: c("field"), style: { gridColumn: "span 2" } },
454
- h("label", { className: c("label") }, "题目序号范围(可选)"),
455
- h("div", { className: c("row") },
456
- h("input", { className: c("input"), type: "number", min: 0, placeholder: "起", value: sel.begin, onChange: setField("begin"), style: { width: "50%" } }),
457
- h("input", { className: c("input"), type: "number", min: 0, placeholder: "止", value: sel.end, onChange: setField("end"), style: { width: "50%" } }),
399
+ configError !== null && h("p", { className: c("error") }, `加载配置失败:${configError}`),
400
+ h("div", { className: c("card") },
401
+ h("div", { className: c("fields") },
402
+ h("div", { className: c("field"), ref: modelDdRef },
403
+ h("label", { className: c("label") }, "模型(harness 全部模型,可多选;勾选后可在行内单独设定推理强度)"),
404
+ h("button", {
405
+ type: "button", className: c("select"), style: { textAlign: "left", width: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
406
+ onClick: () => setModelDdOpen((v) => !v),
407
+ }, sel.models.length === 0
408
+ ? "点击选择模型…"
409
+ : sel.models.length === 1
410
+ ? sel.models[0].split("::")[1]
411
+ : `已选 ${sel.models.length} 个模型`),
412
+ modelDdOpen && h("div", {
413
+ style: {
414
+ marginTop: "6px", maxHeight: "340px", overflowY: "auto", border: "1px solid var(--dsw-alias-border-l2)",
415
+ borderRadius: "8px", background: "var(--dsw-alias-bg-layer-1)", padding: "6px",
416
+ },
417
+ },
418
+ (config?.providers ?? []).map((p) => h("div", { key: p.id, style: { marginBottom: "4px" } },
419
+ h("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary)", padding: "3px 4px" } },
420
+ p.name + (p.routable ? "" : " ·未接LiveBench")),
421
+ p.models.map((m) => {
422
+ const value = p.id + "::" + m.id;
423
+ const checked = sel.models.includes(value);
424
+ return h("div", { key: value, style: { borderBottom: "1px solid var(--dsw-alias-border-l2)", padding: "3px 4px" } },
425
+ h("div", {
426
+ style: { display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" },
427
+ onClick: () => toggleModel(value),
428
+ },
429
+ h("input", { type: "checkbox", checked, readOnly: true, style: { cursor: "pointer" } }),
430
+ h("span", { style: { fontSize: "12.5px", flex: 1 } }, m.name)),
431
+ checked && m.efforts.length > 0 && h("div", { style: { display: "flex", alignItems: "center", gap: "6px", padding: "4px 0 2px 26px" } },
432
+ h("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary)", whiteSpace: "nowrap" } }, "推理强度"),
433
+ h("select", {
434
+ className: c("select"), style: { height: "28px", fontSize: "12px", flex: 1 },
435
+ value: sel.efforts[value] ?? "default",
436
+ onClick: (event) => event.stopPropagation(),
437
+ onChange: setModelEffort(value),
438
+ },
439
+ h("option", { value: "default" }, "(模型默认)"),
440
+ m.efforts.map((e) => h("option", { key: e, value: e }, e === "off" ? "off(关闭思考)" : e)))));
441
+ }))),
442
+ ),
443
+ h("div", { className: c("field") },
444
+ h("label", { className: c("label") }, "题集 release"),
445
+ h("select", { className: c("select"), value: sel.release, onChange: setField("release") },
446
+ (config?.releases ?? ["2024-11-25"]).map((r) => h("option", { key: r, value: r }, r))),
447
+ ),
448
+ ),
449
+ h("div", { className: c("field") },
450
+ h("label", { className: c("label") }, "max-tokens(默认 32000)"),
451
+ h("input", { className: c("input"), type: "number", min: 256, max: 32768, value: sel.maxTokens, onChange: setField("maxTokens") }),
452
+ ),
453
+ h("div", { className: c("field") },
454
+ h("label", { className: c("label") }, "题目序号范围(可选)"),
455
+ h("div", { className: c("row") },
456
+ h("input", { className: c("input"), type: "number", min: 0, placeholder: "起", value: sel.begin, onChange: setField("begin"), style: { width: "50%" } }),
457
+ h("input", { className: c("input"), type: "number", min: 0, placeholder: "止", value: sel.end, onChange: setField("end"), style: { width: "50%" } }),
458
+ ),
459
+ h("div", { className: c("field") },
460
+ h("label", { className: c("label") }, "分类(可多选,不选 = 全部分类;括号内为该 release 下有效题数)"),
461
+ h("div", { className: c("chips") },
462
+ categories.map((cat) => {
463
+ const n = catCount(cat);
464
+ const active = sel.cats.includes(cat);
465
+ return h("button", {
466
+ key: cat, type: "button", className: c("chip") + (active ? " " + c("chipOn") : ""),
467
+ onClick: () => toggleList("cats", cat), disabled: n === 0,
468
+ }, `${cat}(${n})`);
469
+ })),
470
+ ),
471
+ sel.cats.length > 0 && h("div", { className: c("field") },
472
+ h("label", { className: c("label") }, "任务(可多选,不选 = 所选分类的全部任务)"),
473
+ h("div", { className: c("chips") },
474
+ taskChips.map((key) => {
475
+ const [cat, task] = key.split("/");
476
+ const n = countFor(cat, task) ?? 0;
477
+ const active = sel.tasks.includes(key);
478
+ return h("button", {
479
+ key, type: "button", className: c("chip") + (active ? " " + c("chipOn") : ""),
480
+ onClick: () => toggleList("tasks", key), disabled: n === 0,
481
+ title: n === 0 ? "该任务在此 release 下已无可用题目" : `${n} 题有效`,
482
+ }, `${task}(${n})`);
483
+ })),
484
+ ),
458
485
  ),
486
+ h("div", { className: c("btnRow") },
487
+ h("button", {
488
+ className: c("btn") + " " + c("btnBar"), onClick: onStart,
489
+ disabled: busy || !config?.available || sel.models.length === 0,
490
+ }, `开始评测(${sel.models.length} 个模型并发)`),
491
+ running && h("button", { className: c("btn") + " " + c("btnDanger") + " " + c("btnBar"), onClick: onStop }, "停止全部"),
492
+ h("button", { className: c("btnGhost") + " " + c("btn") + " " + c("btnBar"), onClick: () => { loadConfig(); loadResults(); } }, "刷新"),
493
+ ),
494
+ startError !== null && h("p", { className: c("error") }, startError),
459
495
  ),
460
- h("div", { className: c("field"), style: { gridColumn: "span 2" } },
461
- h("label", { className: c("label") }, "max-tokens(默认 32000)"),
462
- h("input", { className: c("input"), type: "number", min: 256, max: 32768, value: sel.maxTokens, onChange: setField("maxTokens") }),
463
- ),
464
- h("div", { className: c("field"), style: { gridColumn: "1 / -1" } },
465
- h("label", { className: c("label") }, "分类(可多选,不选 = 全部分类;括号内为该 release 下有效题数)"),
466
- h("div", { className: c("chips") },
467
- categories.map((cat) => {
468
- const n = catCount(cat);
469
- const active = sel.cats.includes(cat);
470
- return h("button", {
471
- key: cat, type: "button", className: c("chip") + (active ? " " + c("chipOn") : ""),
472
- onClick: () => toggleList("cats", cat), disabled: n === 0,
473
- }, `${cat}(${n})`);
474
- })),
475
- ),
476
- sel.cats.length > 0 && h("div", { className: c("field"), style: { gridColumn: "1 / -1" } },
477
- h("label", { className: c("label") }, "任务(可多选,不选 = 所选分类的全部任务)"),
478
- h("div", { className: c("chips") },
479
- taskChips.map((key) => {
480
- const [cat, task] = key.split("/");
481
- const n = countFor(cat, task) ?? 0;
482
- const active = sel.tasks.includes(key);
483
- return h("button", {
484
- key, type: "button", className: c("chip") + (active ? " " + c("chipOn") : ""),
485
- onClick: () => toggleList("tasks", key), disabled: n === 0,
486
- title: n === 0 ? "该任务在此 release 下已无可用题目" : `${n} 题有效`,
487
- }, `${task}(${n})`);
488
- })),
489
- ),
490
- ),
491
- h("div", { className: c("actionCol") },
492
- h("button", {
493
- className: c("btn") + " " + c("btnBar"), onClick: onStart,
494
- disabled: busy || !config?.available || sel.models.length === 0,
495
- }, `开始评测(${sel.models.length})`),
496
- running && h("button", { className: c("btn") + " " + c("btnDanger") + " " + c("btnBar"), onClick: onStop }, "停止全部"),
497
- h("button", { className: c("btnGhost") + " " + c("btn") + " " + c("btnBar"), onClick: () => { loadConfig(); loadResults(); } }, "刷新"),
496
+ runsList.length > 0 && h("div", { className: c("card") },
497
+ h("div", { className: c("row") },
498
+ h("span", { className: c("badge"), "data-ok": running ? "1" : "0" },
499
+ running ? "有评测运行中" : "无运行中的评测"),
500
+ ),
501
+ runsList.map((r) => h("details", { key: r.runId, open: r.running },
502
+ h("summary", { style: { cursor: "pointer", fontSize: "12px", color: "var(--dsw-alias-label-secondary)" } },
503
+ h("span", { className: c("badge"), "data-ok": r.running ? "1" : "0" }, r.running ? "运行中" : r.exitCode === 0 ? "完成" : `退出(${r.exitCode})`),
504
+ " ",
505
+ r.displayName),
506
+ h("pre", { className: c("log") }, r.log || "(暂无输出)"),
507
+ ))),
498
508
  ),
499
- ),
500
- startError !== null && h("p", { className: c("error") }, startError),
501
- ),
502
- runsList.length > 0 && h("div", { className: c("card") },
503
- h("div", { className: c("row") },
504
- h("span", { className: c("badge"), "data-ok": running ? "1" : "0" },
505
- running ? "有评测运行中" : "无运行中的评测"),
506
- ),
507
- runsList.map((r) => h("details", { key: r.runId, open: r.running },
508
- h("summary", { style: { cursor: "pointer", fontSize: "12px", color: "var(--dsw-alias-label-secondary)" } },
509
- h("span", { className: c("badge"), "data-ok": r.running ? "1" : "0" }, r.running ? "运行中" : r.exitCode === 0 ? "完成" : `退出(${r.exitCode})`),
510
- " ",
511
- r.displayName),
512
- h("pre", { className: c("log"), ref: r.running ? logRef : null }, r.log || "(暂无输出)"),
513
- )))),
514
- sortedModels.length > 0 && h("div", { className: c("card") },
515
- h("div", { className: c("row") },
516
- h("span", { className: c("label") }, "评测成绩(model 为行;分数 = 平均分 ×100;行首可拖动排序,顺序保存在本机)"),
517
- h("button", {
518
- className: c("btn") + " " + c("btnDanger"),
519
- disabled: pickedModels.size === 0,
520
- onClick: () => deleteModels([...pickedModels]),
521
- }, `删除所选(${pickedModels.size})`),
522
- ),
523
- h("table", { className: c("table") },
524
- h("thead", null, h("tr", null,
525
- h("th", { style: { width: "26px" } }, "⠿"),
526
- h("th", { style: { width: "30px" } },
527
- h("input", { type: "checkbox", checked: pickedModels.size > 0 && pickedModels.size === sortedModels.length,
528
- onChange: (event) => setPickedModels(event.target.checked ? new Set(sortedModels) : new Set()) })),
529
- h("th", null, "model"),
530
- matrix.tasks.map((taskKeyCol) => {
531
- const [cat, task] = taskKeyCol.split("/");
532
- return h("th", { key: taskKeyCol, title: `${cat} / ${task}` }, task);
533
- }),
534
- h("th", null, "删除"),
535
- h("th", null, "time"),
536
- )),
537
- h("tbody", null,
538
- sortedModels.map((model) => {
539
- const checked = pickedModels.has(model);
540
- const startTime = matrix.startTimes.get(model);
541
- return h("tr", {
542
- key: model,
543
- draggable: true,
544
- onDragStart: () => { dragKey.current = model; },
545
- onDragOver: (event) => event.preventDefault(),
546
- onDrop: () => onRowDrop(model),
547
- },
548
- h("td", { className: c("drag"), title: "拖动排序" }, "⠿"),
549
- h("td", null, h("input", { type: "checkbox", checked, onChange: () => togglePicked(model) })),
550
- h("td", null, model),
509
+ sortedModels.length > 0 && h("div", { className: c("card") },
510
+ h("div", { className: c("row") },
511
+ h("span", { className: c("label") }, "评测成绩(model 为行;分数 = 平均分 ×100;行首可拖动排序,顺序保存在本机)"),
512
+ h("button", {
513
+ className: c("btn") + " " + c("btnDanger"),
514
+ disabled: pickedModels.size === 0,
515
+ onClick: () => deleteModels([...pickedModels]),
516
+ }, `删除所选(${pickedModels.size})`),
517
+ ),
518
+ h("table", { className: c("table") },
519
+ h("thead", null, h("tr", null,
520
+ h("th", { style: { width: "26px" } }, "⠿"),
521
+ h("th", { style: { width: "30px" } },
522
+ h("input", { type: "checkbox", checked: pickedModels.size > 0 && pickedModels.size === sortedModels.length,
523
+ onChange: (event) => setPickedModels(event.target.checked ? new Set(sortedModels) : new Set()) })),
524
+ h("th", null, "model"),
551
525
  matrix.tasks.map((taskKeyCol) => {
552
- const row = matrix.cells.get(`${model}\u0000${taskKeyCol}`);
553
- // ×= 该模型未做过此任务;E = 做了但全部 API 失败
554
- let cellText = "×";
555
- let title = "该模型未执行此任务";
556
- if (row) {
557
- if (row.answered > 0 && row.errors >= row.answered) {
558
- cellText = `E (${row.errors})`;
559
- title = "该任务全部回答均 API 失败($ERROR$),计 0 分";
560
- } else if (row.judged === 0) {
561
- cellText = "未判分";
562
- title = "有答案但尚未判分";
563
- } else {
564
- cellText = `${row.score.toFixed(1)} (${row.judged}/${row.total})`;
565
- if (row.errors > 0) title += ` · 其中 ${row.errors} 题 API 失败计 0 分`;
566
- }
567
- }
568
- return h("td", { key: taskKeyCol, className: c("score"), title }, cellText);
526
+ const [cat, task] = taskKeyCol.split("/");
527
+ return h("th", { key: taskKeyCol, title: `${cat} / ${task}` }, task);
569
528
  }),
570
- h("td", null, h("button", {
571
- className: c("btnGhost") + " " + c("btn"), title: "删除该模型的所有评测成绩",
572
- onClick: () => deleteModels([model]),
573
- }, "删除")),
574
- h("td", null, fmtTime(startTime)),
575
- );
576
- })),
577
- ),
578
- ),
579
- config !== null && config.available === false && h("div", { className: c("card") + " " + c("notice") },
580
- h("p", { className: c("noticeTitle") }, "⚠ 本面板需要本地安装 LiveBench(当前未检测到)"),
581
- h("p", { className: c("hint") },
582
- "dsh-livebench-panel 只是控制台,真正的评测由 LiveBench 项目完成。面板会在以下位置查找它:环境变量 DSH_LIVEBENCH_HOME,或默认路径 V:\\PythonProject\\C_UtilizeSpace\\LiveBench(需含 .venv 与 livebench\\run_livebench.py)。按下面步骤安装:"),
583
- h("ol", { className: c("steps") },
584
- h("li", null, "克隆 LiveBench 仓库(放到默认路径可免配置环境变量):",
585
- h("code", { className: c("code") }, "git clone https://github.com/LiveBench/LiveBench V:\\PythonProject\\C_UtilizeSpace\\LiveBench")),
586
- h("li", null, "用 Python 3.11 建虚拟环境并安装(3.10 会因 litellm 报 NotRequired 错误):",
587
- h("code", { className: c("code") }, "cd /d V:\\PythonProject\\C_UtilizeSpace\\LiveBench && py -3.11 -m venv .venv"),
588
- h("code", { className: c("code") }, ".venv\\Scripts\\python -m pip install -e .")),
589
- h("li", null, "(评测 coding 类才需要)安装评分依赖:",
590
- h("code", { className: c("code") }, ".venv\\Scripts\\python -m pip install -r livebench\\code_runner\\requirements_eval.txt")),
591
- h("li", null, "下载题目数据:",
592
- h("code", { className: c("code") }, "cd livebench && ..\\.venv\\Scripts\\python download_questions.py")),
593
- h("li", null, "重启 dsh web,回到本页点「刷新」。",
594
- h("br", null),
595
- "若装在其它目录:设置系统环境变量 ", h("code", { className: c("code") }, "DSH_LIVEBENCH_HOME=你的LiveBench目录"), " 后再重启 dsh web。"),
596
- ),
597
- h("p", { className: c("hint") },
598
- "除 LiveBench 外无需其它配置:评测所需的 API Key 会自动从 harness 凭据库读取并注入;npm 安装本插件时可一并执行 dsh plugin --profile web add dsh-livebench-panel。完整说明见插件目录内 README:~\\.dsh\\plugins\\dsh-livebench-panel\\README.md"),
599
- ),
600
- h("details", { className: c("details") },
601
- h("summary", null, "❓ 使用说明 · 选项含义与选择建议(点击展开)"),
602
- h("p", { className: c("helpP") },
603
- "操作流程:选择参数 点「开始评测」→ 日志区实时显示运行进度 结束后成绩表自动刷新(运行中可「停止」)。全部判分均为客观比对(文本/符号执行/测试用例),不使用 AI 评分。"),
604
- h("table", { className: c("helpTable") },
605
- h("thead", null, h("tr", null, h("th", null, "选项"), h("th", null, "含义与选择建议"))),
606
- h("tbody", null,
607
- h("tr", null, h("th", null, "模型"), h("td", null,
608
- "harness 全部 provider 的全部模型(含内置 DeepSeek 官方)。API Key 自动从 harness 凭据库读取并注入,无需手工配置;名字带「·未接LiveBench」的 provider 无法自动路由,评测会按模型名原生尝试。")),
609
- h("tr", null, h("th", null, "推理强度"), h("td", null,
610
- "模型的思考深度(off=关闭思考)。强度会编码进条目名(如 glm-5.3@max),不同强度在成绩表中是独立条目,方便对比。想省成本选 low,追求质量选 high/max。")),
611
- h("tr", null, h("th", null, "题集 release"), h("td", null,
612
- "题目发布批次。推荐 2024-11-25(公开题目最全)。LiveBench 每月换题:新批次下老任务会陆续退役,任务下拉括号内就是该批次下的有效题数。")),
613
- h("tr", null, h("th", null, "分类 / 任务"), h("td", null,
614
- "六大类共 18 个任务:coding(代码生成/补全)、math(竞赛数学等)、reasoning(空间推理/逻辑谜题)、language(拼写/连线/语义)、data_analysis(表格操作)、instruction_following(指令遵循)。首次验证推荐 language → typos。")),
615
- h("tr", null, h("th", null, "题目序号范围"), h("td", null,
616
- "从 0 起。冒烟测试填 0 到 2(只跑 3 题);2 题得分噪声很大(对一题就是 0↔100 的波动),想看真实水平建议 20 题以上。")),
617
- h("tr", null, h("th", null, "max-tokens"), h("td", null,
618
- "单题回答的 token 上限,默认 32000。推理模型思考也占 token,不要低于 8192,否则思考被截断、答案为空会记 0 分。")),
529
+ h("th", null, "删除"),
530
+ h("th", null, "time"),
531
+ )),
532
+ h("tbody", null,
533
+ sortedModels.map((model) => {
534
+ const checked = pickedModels.has(model);
535
+ const startTime = matrix.startTimes.get(model);
536
+ return h("tr", {
537
+ key: model,
538
+ draggable: true,
539
+ onDragStart: () => { dragKey.current = model; },
540
+ onDragOver: (event) => event.preventDefault(),
541
+ onDrop: () => onRowDrop(model),
542
+ },
543
+ h("td", { className: c("drag"), title: "拖动排序" }, "⠿"),
544
+ h("td", null, h("input", { type: "checkbox", checked, onChange: () => togglePicked(model) })),
545
+ h("td", null, model),
546
+ matrix.tasks.map((taskKeyCol) => {
547
+ const row = matrix.cells.get(`${model}__@__${taskKeyCol}`);
548
+ let cellText = "×";
549
+ let title = "该模型未执行此任务";
550
+ if (row) {
551
+ if (row.answered > 0 && row.errors >= row.answered) {
552
+ cellText = `E (${row.errors})`;
553
+ title = "该任务全部回答均 API 失败($ERROR$),计 0 分";
554
+ } else if (row.judged === 0) {
555
+ cellText = "未判分";
556
+ title = "有答案但尚未判分";
557
+ } else {
558
+ cellText = `${row.score.toFixed(1)} (${row.judged}/${row.total})`;
559
+ if (row.errors > 0) title += ` · 其中 ${row.errors} API 失败计 0 分`;
560
+ }
561
+ }
562
+ return h("td", { key: taskKeyCol, className: c("score"), title }, cellText);
563
+ }),
564
+ h("td", null, h("button", {
565
+ className: c("btnGhost") + " " + c("btn"), title: "删除该模型的所有评测成绩",
566
+ onClick: () => deleteModels([model]),
567
+ }, "删除")),
568
+ h("td", null, fmtTime(startTime)),
569
+ );
570
+ })),
571
+ ),
619
572
  ),
620
- ),
621
- h("p", { className: c("helpP"), style: { marginTop: "10px" } },
622
- "提示:回答为 $ERROR$ 表示该题 API 调用失败(网络/鉴权/参数问题)计 0 分,可在上方日志区查看具体错误;zebra_puzzle(逻辑谜题)是公认最难的任务,低分属正常现象。"),
623
- ),
624
- h("p", { className: c("hint") },
625
- "评分读取 LiveBench ground_truth_judgment.jsonl;正式榜单可用 release 2024-11-25。多选题集请在「分类/任务」中缩小范围,避免长时间运行。"),
626
- );
627
- }
628
-
573
+ config !== null && config.available === false && h("div", { className: c("card") + " " + c("notice") },
574
+ h("p", { className: c("noticeTitle") }, "⚠ 本面板需要本地安装 LiveBench(当前未检测到)"),
575
+ h("p", { className: c("hint") },
576
+ "dsh-livebench-panel 只是控制台,真正的评测由 LiveBench 项目完成。面板会依次查找:本页「LiveBench 路径」输入框保存的目录、环境变量 DSH_LIVEBENCH_HOME、用户目录 ~/LiveBench(需含 .venv 与 livebench\\run_livebench.py)。按下面步骤安装:"),
577
+ h("ol", { className: c("steps") },
578
+ h("li", null, "克隆 LiveBench 仓库:",
579
+ h("code", { className: c("code") }, "git clone https://github.com/LiveBench/LiveBench V:\\PythonProject\\C_UtilizeSpace\\LiveBench")),
580
+ h("li", null, "用 Python 3.11 建虚拟环境并安装(3.10 会因 litellm 报 NotRequired 错误):",
581
+ h("code", { className: c("code") }, "cd /d V:\\PythonProject\\C_UtilizeSpace\\LiveBench && py -3.11 -m venv .venv"),
582
+ h("code", { className: c("code") }, ".venv\\Scripts\\python -m pip install -e .")),
583
+ h("li", null, "(评测 coding 类才需要)安装评分依赖:",
584
+ h("code", { className: c("code") }, ".venv\\Scripts\\python -m pip install -r livebench\\code_runner\\requirements_eval.txt")),
585
+ h("li", null, "下载题目数据:",
586
+ h("code", { className: c("code") }, "cd livebench && ..\\.venv\\Scripts\\python download_questions.py")),
587
+ h("li", null, "重启 dsh web,回到本页点「刷新」。",
588
+ h("br", null),
589
+ "若装在其它目录:在本页顶部「LiveBench 路径」输入框填入目录并点「设置路径」即可,也可用环境变量 DSH_LIVEBENCH_HOME。"),
590
+ ),
591
+ h("p", { className: c("hint") },
592
+ "除 LiveBench 外无需其它配置:评测所需的 API Key 会自动从 harness 凭据库读取并注入;npm 安装本插件时可一并执行 dsh plugin --profile web add dsh-livebench-panel。完整说明见插件目录内 README:~\\.dsh\\plugins\\dsh-livebench-panel\\README.md"),
593
+ ),
594
+ h("details", { className: c("details") },
595
+ h("summary", null, "❓ 使用说明 · 选项含义与选择建议(点击展开)"),
596
+ h("p", { className: c("helpP") },
597
+ "操作流程:选择参数 → 点「开始评测」→ 日志区实时显示运行进度 → 结束后成绩表自动刷新(运行中可「停止」)。全部判分均为客观比对(文本/符号执行/测试用例),不使用 AI 评分。"),
598
+ h("table", { className: c("helpTable") },
599
+ h("thead", null, h("tr", null, h("th", null, "选项"), h("th", null, "含义与选择建议"))),
600
+ h("tbody", null,
601
+ h("tr", null, h("th", null, "模型"), h("td", null,
602
+ "harness 全部 provider 的全部模型(含内置 DeepSeek 官方)。API Key 自动从 harness 凭据库读取并注入,无需手工配置;名字带「·未接LiveBench」的 provider 无法自动路由,评测会按模型名原生尝试。")),
603
+ h("tr", null, h("th", null, "推理强度"), h("td", null,
604
+ "在模型下拉中勾选模型后,于该行内的次级下拉单独设定;强度会编码进条目名(如 glm-5.3@max),不同强度在成绩表中是独立条目,方便对比。")),
605
+ h("tr", null, h("th", null, "题集 release"), h("td", null,
606
+ "题目发布批次。推荐 2024-11-25(公开题目最全)。LiveBench 每月换题:新批次下老任务会陆续退役,任务下拉括号内就是该批次下的有效题数。")),
607
+ h("tr", null, h("th", null, "分类 / 任务"), h("td", null,
608
+ "六大类共 18 个任务:coding(代码生成/补全)、math(竞赛数学等)、reasoning(空间推理/逻辑谜题)、language(拼写/连线/语义)、data_analysis(表格操作)、instruction_following(指令遵循)。首次验证推荐 language → typos。")),
609
+ h("tr", null, h("th", null, "题目序号范围"), h("td", null,
610
+ "从 0 起。冒烟测试填 0 到 2(只跑 3 题);2 题得分噪声很大(对一题就是 0↔100 的波动),想看真实水平建议 20 题以上。")),
611
+ h("tr", null, h("th", null, "max-tokens"), h("td", null,
612
+ "单题回答的 token 上限,默认 32000。推理模型思考也占 token,不要低于 8192,否则思考被截断、答案为空会记 0 分。")),
613
+ ),
614
+ ),
615
+ h("p", { className: c("helpP"), style: { marginTop: "10px" } },
616
+ "提示:回答为 $ERROR$ 表示该题 API 调用失败(网络/鉴权/参数问题)计 0 分,可在上方日志区查看具体错误;zebra_puzzle(逻辑谜题)是公认最难的任务,低分属正常现象。"),
617
+ ),
618
+ h("p", { className: c("hint") },
619
+ "评分读取 LiveBench 的 ground_truth_judgment.jsonl;正式榜单可用 release 2024-11-25。多选题集请在「分类/任务」中缩小范围,避免长时间运行。"),
620
+ );
621
+ }
629
622
  /**
630
623
  * Mount the LiveBench tab into the Trajectory view.
631
624
  * @param ctx - the browser plugin context.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-livebench-panel",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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",