niceeval 0.8.1 → 0.9.1

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 (140) hide show
  1. package/INDEX.md +77 -45
  2. package/dist/agents/types.d.ts +28 -6
  3. package/dist/i18n/en.d.ts +2 -0
  4. package/dist/i18n/zh-CN.d.ts +3 -1
  5. package/dist/report/built-in/index.d.ts +3 -2
  6. package/dist/report/built-in/index.js +7 -8
  7. package/dist/report/built-in/standard.d.ts +1 -0
  8. package/dist/report/built-in/standard.js +30 -0
  9. package/dist/report/components.d.ts +69 -2
  10. package/dist/report/components.js +152 -3
  11. package/dist/report/compute.d.ts +28 -1
  12. package/dist/report/compute.js +123 -0
  13. package/dist/report/index.d.ts +4 -4
  14. package/dist/report/index.js +3 -2
  15. package/dist/report/locale.d.ts +39 -1
  16. package/dist/report/locale.js +69 -0
  17. package/dist/report/react/AttemptList.d.ts +3 -1
  18. package/dist/report/react/AttemptList.js +3 -3
  19. package/dist/report/react/CopyFixPrompt.d.ts +12 -0
  20. package/dist/report/react/CopyFixPrompt.js +12 -0
  21. package/dist/report/react/HeroCard.d.ts +13 -0
  22. package/dist/report/react/HeroCard.js +35 -0
  23. package/dist/report/react/PoweredBy.d.ts +5 -0
  24. package/dist/report/react/PoweredBy.js +7 -0
  25. package/dist/report/react/ScopeWarnings.d.ts +12 -0
  26. package/dist/report/react/ScopeWarnings.js +18 -0
  27. package/dist/report/react/TraceWaterfall.d.ts +14 -0
  28. package/dist/report/react/TraceWaterfall.js +22 -0
  29. package/dist/report/react/index.d.ts +6 -1
  30. package/dist/report/react/index.js +6 -0
  31. package/dist/report/report.d.ts +22 -6
  32. package/dist/report/report.js +68 -55
  33. package/dist/report/scope-warnings.d.ts +28 -0
  34. package/dist/report/scope-warnings.js +101 -0
  35. package/dist/report/text/faces.d.ts +19 -1
  36. package/dist/report/text/faces.js +61 -0
  37. package/dist/report/tree.d.ts +1 -1
  38. package/dist/report/tree.js +7 -2
  39. package/dist/report/types.d.ts +45 -0
  40. package/dist/report/web.d.ts +5 -4
  41. package/dist/report/web.js +7 -22
  42. package/dist/results/select.d.ts +17 -4
  43. package/dist/results/select.js +76 -11
  44. package/dist/results/types.d.ts +26 -0
  45. package/dist/runner/fingerprint.d.ts +3 -3
  46. package/dist/runner/sandbox-selection.d.ts +12 -0
  47. package/dist/runner/types.d.ts +18 -6
  48. package/dist/sandbox/types.d.ts +12 -0
  49. package/dist/shared/aggregate.d.ts +1 -1
  50. package/dist/shared/aggregate.js +1 -1
  51. package/docs-site/zh/explanation/evals.mdx +2 -1
  52. package/docs-site/zh/explanation/experiment.mdx +2 -0
  53. package/docs-site/zh/how-to/custom-reports.mdx +23 -7
  54. package/docs-site/zh/how-to/experiments.mdx +2 -2
  55. package/docs-site/zh/how-to/publish-report.mdx +11 -4
  56. package/docs-site/zh/how-to/viewing-results.mdx +17 -8
  57. package/docs-site/zh/how-to/write-experiment.mdx +40 -1
  58. package/docs-site/zh/reference/builtin-agents.mdx +40 -3
  59. package/docs-site/zh/reference/cli.mdx +3 -3
  60. package/docs-site/zh/reference/define-eval.mdx +8 -0
  61. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  62. package/docs-site/zh/troubleshooting/debugging.mdx +3 -3
  63. package/package.json +2 -1
  64. package/src/agents/bub.ts +13 -1
  65. package/src/agents/claude-code.test.ts +43 -1
  66. package/src/agents/claude-code.ts +32 -14
  67. package/src/agents/codex.test.ts +168 -1
  68. package/src/agents/codex.ts +51 -15
  69. package/src/agents/mcp.ts +31 -0
  70. package/src/agents/post-setup.ts +33 -0
  71. package/src/agents/types.ts +28 -7
  72. package/src/cli.ts +15 -14
  73. package/src/define.ts +3 -0
  74. package/src/i18n/en.ts +8 -5
  75. package/src/i18n/zh-CN.ts +8 -4
  76. package/src/index.ts +1 -0
  77. package/src/report/built-in/index.tsx +8 -7
  78. package/src/report/built-in/standard.tsx +59 -0
  79. package/src/report/components.tsx +218 -2
  80. package/src/report/compute.ts +138 -1
  81. package/src/report/dual-render.test.tsx +141 -14
  82. package/src/report/index.ts +20 -0
  83. package/src/report/locale.ts +83 -1
  84. package/src/report/react/AttemptList.tsx +13 -1
  85. package/src/report/react/CopyFixPrompt.tsx +37 -0
  86. package/src/report/react/HeroCard.tsx +59 -0
  87. package/src/report/react/PoweredBy.tsx +20 -0
  88. package/src/report/react/ScopeWarnings.tsx +74 -0
  89. package/src/report/react/TraceWaterfall.tsx +78 -0
  90. package/src/report/react/enhance.js +14 -0
  91. package/src/report/react/index.tsx +11 -0
  92. package/src/report/react/styles.css +193 -7
  93. package/src/report/report.ts +99 -64
  94. package/src/report/scope-warnings.ts +155 -0
  95. package/src/report/site-components.test.tsx +526 -0
  96. package/src/report/text/faces.ts +66 -0
  97. package/src/report/tree.ts +10 -3
  98. package/src/report/types.ts +51 -0
  99. package/src/report/web.ts +7 -40
  100. package/src/results/host-equivalence.test.ts +6 -2
  101. package/src/results/open.ts +5 -4
  102. package/src/results/results.test.ts +78 -1
  103. package/src/results/select.ts +80 -12
  104. package/src/results/types.ts +27 -0
  105. package/src/runner/attempt.ts +10 -9
  106. package/src/runner/discover.test.ts +9 -1
  107. package/src/runner/discover.ts +3 -3
  108. package/src/runner/fingerprint.ts +9 -4
  109. package/src/runner/ledger.test.ts +30 -1
  110. package/src/runner/ledger.ts +26 -4
  111. package/src/runner/run.ts +5 -1
  112. package/src/runner/sandbox-selection.test.ts +131 -0
  113. package/src/runner/sandbox-selection.ts +110 -0
  114. package/src/runner/types.ts +19 -2
  115. package/src/sandbox/types.ts +6 -0
  116. package/src/shared/aggregate.ts +1 -1
  117. package/src/show/index.ts +19 -12
  118. package/src/show/render.ts +12 -12
  119. package/src/show/report-host.test.ts +34 -17
  120. package/src/show/report-host.ts +6 -5
  121. package/src/show/show.test.ts +141 -4
  122. package/src/view/app/App.test.tsx +79 -17
  123. package/src/view/app/App.tsx +25 -74
  124. package/src/view/app/components/CopyControls.tsx +4 -42
  125. package/src/view/app/i18n.ts +5 -227
  126. package/src/view/app/lib/rows.ts +3 -21
  127. package/src/view/app/shared.ts +1 -3
  128. package/src/view/app/types.ts +2 -2
  129. package/src/view/client-dist/app.css +1 -1
  130. package/src/view/client-dist/app.js +20 -20
  131. package/src/view/data.ts +21 -10
  132. package/src/view/index.ts +2 -12
  133. package/src/view/server.ts +4 -4
  134. package/src/view/shared/types.ts +11 -6
  135. package/src/view/styles.css +9 -252
  136. package/src/view/view-report.test.ts +99 -33
  137. package/src/view/app/components/LazyArtifact.tsx +0 -51
  138. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  139. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  140. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -2,7 +2,7 @@
2
2
  // niceeval view 的报告槽与宿主组合语义(docs/feature/reports/architecture.md「Selection 是计算入口」
3
3
  // 与裁决记录 6;公开行为准绳 docs-site/zh/how-to/viewing-results.mdx / custom-reports.mdx)。
4
4
  // 覆盖:
5
- // - 组合语义与 show 对齐:位置前缀收窄报告槽 Scope、--experiment 过滤、匹配不到直说;
5
+ // - 组合语义与 show 对齐:位置前缀收窄报告槽 Scope、--exp 过滤、匹配不到直说;
6
6
  // - 输入语义:位置参数只表示 eval id 前缀(不随文件系统状态改变),结果根走 --results,
7
7
  // 单开一份快照走 --snapshot(文件不可读时失败);
8
8
  // - 报告槽恒在:裸跑填充内建报告,--report 整槽替换;en / zh-CN 双语各渲染一遍;
@@ -139,33 +139,61 @@ describe("resolveViewInput · 输入语义", () => {
139
139
  });
140
140
 
141
141
 
142
- /** 单页报告(裸跑 / 树形态 --report)的报告槽 HTML:规范化后唯一页 id 恒为 `report`。 */
142
+ /** 单页报告(树形态 --report)的报告槽 HTML:规范化后唯一页 id 恒为 `report`。 */
143
143
  function slotHtml(scan: ViewScan): { en: string; "zh-CN": string } {
144
144
  expect(scan.reportPages).toHaveLength(1);
145
145
  expect(scan.reportPages[0]!.id).toBe("report");
146
146
  return scan.reportPages[0]!.html as { en: string; "zh-CN": string };
147
147
  }
148
148
 
149
+ /** 多页定义(裸跑装载的内建报告是三页)按页 id 取报告槽 HTML。 */
150
+ function pageHtml(scan: ViewScan, id: string): { en: string; "zh-CN": string } {
151
+ const page = scan.reportPages.find((p) => p.id === id);
152
+ expect(page, `page ${id}`).toBeTruthy();
153
+ return page!.html as { en: string; "zh-CN": string };
154
+ }
155
+
156
+ /** 裸跑的首页(内建报告的 report 页):先断言内建三页齐全,再取首页 HTML。 */
157
+ function bareReportHtml(scan: ViewScan): { en: string; "zh-CN": string } {
158
+ expect(scan.reportPages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
159
+ return pageHtml(scan, "report");
160
+ }
161
+
149
162
  // ───────────────────────── 组合语义(与 show 对齐) ─────────────────────────
150
163
 
151
164
  describe("loadViewScan · 组合语义", () => {
152
- it("位置前缀收窄报告槽 Selection;证据室快照不收窄,深链恒可达", async () => {
165
+ it("位置前缀收窄作用在有效根上:全部页与证据室一致缩小,被滤掉的 attempt 不烘进页面数据", async () => {
153
166
  const root = await seedRoot();
154
167
  const scan = await loadViewScan(root, { patterns: ["weather"] });
155
168
  const { viewData } = scan;
156
- const reportHtml = slotHtml(scan);
157
- // 报告槽:两实验都只剩 weather/brooklyn 一题,范围外的失败不再出现。
169
+ const reportHtml = bareReportHtml(scan);
170
+ // 报告页:两实验都只剩 weather/brooklyn 一题,范围外的失败不再出现。
158
171
  expect(reportHtml.en).toContain("compare/bub");
159
172
  expect(reportHtml.en).toContain("compare/codex");
160
173
  expect(reportHtml.en).not.toContain("fixtures/button");
161
- // 证据室:快照明细仍含 fixtures/button(attempt 深链在收窄下也能解析)。
162
- const allIds = viewData.snapshots.flatMap((s) => s.results.map((r) => r.id));
163
- expect(allIds).toContain("fixtures/button");
174
+ // Attempts / Traces 是普通页,共享同一收窄后 Scope:行集同样缩小。
175
+ expect(pageHtml(scan, "attempts").en).not.toContain("fixtures/button");
176
+ expect(pageHtml(scan, "traces").en).not.toContain("fixtures/button");
177
+ const bare = await loadViewScan(root);
178
+ expect(pageHtml(bare, "attempts").en).toContain("fixtures/button"); // 不收窄时在场,对照
179
+ // 有效根即证据室:被滤掉的 attempt 不在 viewData.snapshots 里,深链两宿主一致不可达。
180
+ const filteredOut = viewData.snapshots
181
+ .flatMap((s) => s.results)
182
+ .find((r) => r.id === "fixtures/button");
183
+ expect(filteredOut).toBeUndefined();
184
+ expect(scan.artifactDirs.size).toBe(
185
+ [...scan.artifactDirs.keys()].filter((base) => base.includes("weather/brooklyn")).length,
186
+ );
187
+ // 收窄之内、不在现刻水位的口径差异由 --exp 深链测试覆盖;不收窄时深链照常可达,对照:
188
+ const inBare = bare.viewData.snapshots.flatMap((s) => s.results).find((r) => r.id === "fixtures/button");
189
+ expect(inBare?.locator).toBeTruthy();
190
+ const { resolveAttemptLocator } = await import("./app/lib/attempt-route.ts");
191
+ expect(resolveAttemptLocator(bare.viewData.snapshots, inBare!.locator!)).toBe(inBare);
164
192
  });
165
193
 
166
- it("--experiment 过滤:报告槽 Selection 只留该实验", async () => {
194
+ it("--exp 过滤:报告槽 Selection 只留该实验", async () => {
167
195
  const root = await seedRoot();
168
- const reportHtml = slotHtml(await loadViewScan(root, { experiment: "compare/codex" }));
196
+ const reportHtml = bareReportHtml(await loadViewScan(root, { experiment: "compare/codex" }));
169
197
  expect(reportHtml.en).toContain("compare/codex");
170
198
  expect(reportHtml.en).not.toContain("compare/bub");
171
199
  });
@@ -177,34 +205,43 @@ describe("loadViewScan · 组合语义", () => {
177
205
  await expect(loadViewScan(root, { experiment: "nosuch" })).rejects.toBeInstanceOf(ViewInputError);
178
206
  });
179
207
 
180
- it("全部缺省:报告槽使用现刻水位口径(与 show 相同的合成规则)", async () => {
208
+ it("全部缺省:报告槽使用现刻水位口径(与 show 相同的合成规则);导航页即内建三页", async () => {
181
209
  const root = await seedRoot();
182
- const reportHtml = slotHtml(await loadViewScan(root));
210
+ const scan = await loadViewScan(root);
211
+ const reportHtml = bareReportHtml(scan);
183
212
  // 默认报告的榜单含两个实验;官方组件的稳定类名在场。
184
213
  expect(reportHtml.en).toContain("compare/bub");
185
214
  expect(reportHtml.en).toContain("compare/codex");
186
215
  expect(reportHtml.en).toContain("nre-");
216
+ // 导航 = 报告定义声明的页(声明序),宿主不追加:裸 view 的三个 tab 就是内建三页。
217
+ expect(scan.viewData.report?.pages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
218
+ expect(scan.viewData.report?.pages.map((p) => p.title)).toEqual([
219
+ { en: "Report", "zh-CN": "报告" },
220
+ "Attempts",
221
+ { en: "Traces", "zh-CN": "追踪" },
222
+ ]);
187
223
  });
188
224
  });
189
225
 
190
226
  // ─────────────────── 报告槽恒在:裸跑 ≡ --report <ExperimentComparison> ───────────────────
191
227
 
192
228
  describe("loadViewScan · 默认报告槽(裸跑)", () => {
193
- it("裸跑产出的报告槽 HTML 与 --report <re-export ExperimentComparison 的文件> 完全一致(双语)", async () => {
229
+ it("裸跑产出的报告槽 HTML 与 --report <re-export 内建报告的文件> 逐页完全一致(双语)", async () => {
194
230
  const root = await seedRoot();
195
- const bare = slotHtml(await loadViewScan(root));
196
- const viaReport = slotHtml(
197
- await loadViewScan(root, {
198
- report: { path: DEFAULT_REPORT_REEXPORT, cwd: root },
199
- }),
200
- );
201
- expect(bare.en).toBe(viaReport.en);
202
- expect(bare["zh-CN"]).toBe(viaReport["zh-CN"]);
231
+ const bare = await loadViewScan(root);
232
+ const viaReport = await loadViewScan(root, {
233
+ report: { path: DEFAULT_REPORT_REEXPORT, cwd: root },
234
+ });
235
+ expect(viaReport.reportPages.map((p) => p.id)).toEqual(bare.reportPages.map((p) => p.id));
236
+ for (const pageId of ["report", "attempts", "traces"]) {
237
+ expect(pageHtml(viaReport, pageId).en).toBe(pageHtml(bare, pageId).en);
238
+ expect(pageHtml(viaReport, pageId)["zh-CN"]).toBe(pageHtml(bare, pageId)["zh-CN"]);
239
+ }
203
240
  });
204
241
 
205
242
  it("报告槽双语渲染:同一棵树按 locale 渲染两遍,chrome 文案分语言、数据不分语言", async () => {
206
243
  const root = await seedRoot();
207
- const reportHtml = slotHtml(await loadViewScan(root));
244
+ const reportHtml = bareReportHtml(await loadViewScan(root));
208
245
  expect(reportHtml.en).toContain("End-to-end pass rate"); // ExperimentList 主行(en)
209
246
  expect(reportHtml["zh-CN"]).toContain("端到端成功率"); // ExperimentList 主行(zh-CN)
210
247
  for (const html of [reportHtml.en, reportHtml["zh-CN"]]) {
@@ -216,11 +253,17 @@ describe("loadViewScan · 默认报告槽(裸跑)", () => {
216
253
  }
217
254
  });
218
255
 
219
- it("失败清单与警告住在报告槽里:ExperimentList 列出失败,壳的 viewData 不携带统计产物", async () => {
256
+ it("失败清单与警告住在报告页里:ExperimentList 列出失败,壳的 viewData 不携带统计产物", async () => {
220
257
  const root = await seedRoot();
221
258
  const scan = await loadViewScan(root);
222
259
  const { viewData } = scan;
223
- expect(slotHtml(scan).en).toContain("fixtures/button");
260
+ expect(bareReportHtml(scan).en).toContain("fixtures/button");
261
+ // 品牌行是页内组件(Hero 恒含),不是宿主 chrome:每页 web 面都带官网品牌行。
262
+ for (const pageId of ["report", "attempts", "traces"]) {
263
+ const html = pageHtml(scan, pageId).en;
264
+ expect(html).toContain("nre-powered-by");
265
+ expect(html).toContain("https://niceeval.com/?utm_source=report&amp;utm_medium=powered-by");
266
+ }
224
267
  // viewData 只有证据室数据:快照 + skipped + 壳元信息。
225
268
  expect(viewData).not.toHaveProperty("overview");
226
269
  expect(viewData).not.toHaveProperty("table");
@@ -431,18 +474,41 @@ describe("buildView · --out 与 --report", () => {
431
474
  expect(existsSync(join(exported, "o11y.json"))).toBe(false);
432
475
  });
433
476
 
434
- it("--out 与位置参数 / --experiment 互斥:报错含 copySnapshots + filter 下一步", async () => {
477
+ it("--out 接受收窄,出站的就是收窄到的:被滤掉实验的证据文件与页面数据都不出站", async () => {
435
478
  const root = await seedRoot();
479
+ // 两个实验各写一份 events.json:导出后只有收窄内的那份在站里。
480
+ const bubDir = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "weather", "brooklyn", "a0");
481
+ const codexDir = join(root, "compare_codex", "2026-07-09T10-00-00-000Z", "weather", "brooklyn", "a0");
482
+ for (const dir of [bubDir, codexDir]) {
483
+ await mkdir(dir, { recursive: true });
484
+ await writeFile(join(dir, "events.json"), "[]", "utf-8");
485
+ }
486
+
436
487
  const out = join(root, "site");
437
- for (const scan of [{ patterns: ["weather"] }, { experiment: "compare/bub" }]) {
438
- const attempt = buildView({ input: root, out, scan });
439
- await expect(attempt).rejects.toBeInstanceOf(ViewInputError);
440
- await expect(
441
- buildView({ input: root, out, scan }),
442
- ).rejects.toThrow(/copySnapshots/);
488
+ await buildView({ input: root, out, scan: { patterns: [], experiment: "compare/codex" } });
489
+ expect(existsSync(join(out, "artifact", "compare_codex/2026-07-09T10-00-00-000Z/weather/brooklyn/a0", "events.json"))).toBe(true);
490
+ // 被滤掉的实验:证据树整目录不出站,页面数据(烘进 HTML 的 viewData 与报告块)也不含它。
491
+ expect(existsSync(join(out, "artifact", "compare_bub"))).toBe(false);
492
+ const html = await readFile(join(out, "index.html"), "utf-8");
493
+ expect(html).not.toContain("compare/bub");
494
+ expect(html).toContain("compare/codex");
495
+ });
496
+
497
+ it("eval 前缀收窄导出:不匹配 attempt 的证据不出站", async () => {
498
+ const root = await seedRoot();
499
+ const weatherDir = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "weather", "brooklyn", "a0");
500
+ const buttonDir = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "fixtures", "button", "a0");
501
+ for (const dir of [weatherDir, buttonDir]) {
502
+ await mkdir(dir, { recursive: true });
503
+ await writeFile(join(dir, "events.json"), "[]", "utf-8");
443
504
  }
444
- // 同参数不带 --out 时照常收窄报告槽(不报错)。
445
- await expect(loadViewScan(root, { patterns: ["weather"] })).resolves.toBeTruthy();
505
+
506
+ const out = join(root, "site");
507
+ await buildView({ input: root, out, scan: { patterns: ["weather"] } });
508
+ expect(existsSync(join(out, "artifact", "compare_bub/2026-07-08T10-00-00-000Z/weather/brooklyn/a0", "events.json"))).toBe(true);
509
+ expect(existsSync(join(out, "artifact", "compare_bub/2026-07-08T10-00-00-000Z/fixtures"))).toBe(false);
510
+ const html = await readFile(join(out, "index.html"), "utf-8");
511
+ expect(html).not.toContain("fixtures/button");
446
512
  });
447
513
 
448
514
  it("默认导出(无 --report):报告槽填充 ExperimentComparison,双语块与增强 runtime 恒内联", async () => {
@@ -1,51 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import type { LazyArtifactType, T } from "../shared.ts";
3
- import { artifactUrl } from "../lib/artifact-url.ts";
4
- import { asEvents, asSpans } from "../lib/guards.ts";
5
- import { Trace } from "./Trace.tsx";
6
- import { Transcript } from "./Transcript.tsx";
7
-
8
- export function LazyArtifact({ type, src, autoLoad = false, t }: { type: LazyArtifactType; src: string; autoLoad?: boolean; t: T }) {
9
- const [open, setOpen] = useState(autoLoad);
10
- const [loaded, setLoaded] = useState(false);
11
- const [content, setContent] = useState<unknown>(null);
12
- const [error, setError] = useState("");
13
-
14
- const load = async () => {
15
- if (loaded) return;
16
- setLoaded(true);
17
- try {
18
- const resp = await fetch(artifactUrl(src));
19
- if (!resp.ok) throw new Error("HTTP " + resp.status);
20
- const body = await resp.json();
21
- setContent(body);
22
- setError("");
23
- } catch (e) {
24
- setLoaded(false);
25
- setError(`${t("trace.loadFailed")} ${String(e)}`);
26
- }
27
- };
28
-
29
- useEffect(() => {
30
- if (autoLoad) void load();
31
- }, []);
32
-
33
- return (
34
- <details
35
- className="trace-details"
36
- open={open}
37
- onToggle={(e) => {
38
- const isOpen = e.currentTarget.open;
39
- setOpen(isOpen);
40
- if (isOpen) void load();
41
- }}
42
- >
43
- <summary>{type === "transcript" ? t("trace.transcript") : t("trace.timing")}</summary>
44
- <div className="trace-slot">
45
- {error ? <div className="trace-span-meta">{error}</div> : !content ? <div className="trace-span-meta">{t("trace.loading")}</div> : null}
46
- {content && type === "transcript" ? <Transcript events={asEvents(content) ?? []} t={t} /> : null}
47
- {content && type === "trace" ? <Trace spans={asSpans(content) ?? []} t={t} /> : null}
48
- </div>
49
- </details>
50
- );
51
- }
@@ -1,140 +0,0 @@
1
- import { useState } from "react";
2
- import { AlertCircle, ChevronRight, Copy, Check } from "lucide-react";
3
- import type { T } from "../shared.ts";
4
- import type { MessageKey } from "../i18n.ts";
5
- import type { SkippedRunNotice } from "../types.ts";
6
- import { copyText } from "./CopyControls.tsx";
7
-
8
- // 折叠前默认可见的条目数(跨分组累计);超出的部分收进「展开其余」。
9
- const DEFAULT_VISIBLE = 3;
10
-
11
- type GroupKind = "incompatible" | "incompatibleForeign" | "malformed" | "incomplete";
12
-
13
- interface SkippedGroup {
14
- key: string;
15
- kind: GroupKind;
16
- messageKey: MessageKey;
17
- vars: Record<string, string | number>;
18
- items: SkippedRunNotice[];
19
- }
20
-
21
- /** 同一原因(且描述文案完全相同)的失败 run 归成一组,组内条目只保留目录名 + 命令,
22
- * 避免把完整原因句子重复 N 遍——这正是原实现把首屏挤满的根因。 */
23
- function groupSkippedRuns(runs: SkippedRunNotice[]): SkippedGroup[] {
24
- const groups = new Map<string, SkippedGroup>();
25
- for (const run of runs) {
26
- let kind: GroupKind;
27
- let key: string;
28
- let messageKey: MessageKey;
29
- let vars: Record<string, string | number>;
30
-
31
- if (run.reason === "malformed") {
32
- kind = "malformed";
33
- vars = { detail: run.detail ?? "?" };
34
- key = `malformed:${vars.detail}`;
35
- messageKey = "banner.skipped.malformed";
36
- } else if (run.reason === "incomplete") {
37
- kind = "incomplete";
38
- vars = {};
39
- key = "incomplete";
40
- messageKey = "banner.skipped.incomplete";
41
- } else if (run.producerName && run.producerName !== "niceeval") {
42
- kind = "incompatibleForeign";
43
- vars = { name: run.producerName, version: run.producerVersion ?? "?", schemaVersion: run.schemaVersion ?? "?" };
44
- key = `foreign:${vars.name}:${vars.version}:${vars.schemaVersion}`;
45
- messageKey = "banner.skipped.incompatibleForeign";
46
- } else {
47
- kind = "incompatible";
48
- vars = { producer: run.producerVersion ?? "?", schemaVersion: run.schemaVersion ?? "?" };
49
- key = `incompatible:${vars.producer}:${vars.schemaVersion}`;
50
- messageKey = "banner.skipped.incompatible";
51
- }
52
-
53
- const existing = groups.get(key);
54
- if (existing) {
55
- existing.items.push(run);
56
- } else {
57
- groups.set(key, { key, kind, messageKey, vars, items: [run] });
58
- }
59
- }
60
- return [...groups.values()];
61
- }
62
-
63
- function CopyCommandButton({ command, t }: { command: string; t: T }) {
64
- const [copied, setCopied] = useState(false);
65
- return (
66
- <button
67
- type="button"
68
- className={`ib-copy${copied ? " is-copied" : ""}`}
69
- title={t("banner.copyCommand")}
70
- onClick={async () => {
71
- await copyText(command);
72
- setCopied(true);
73
- setTimeout(() => setCopied(false), 1200);
74
- }}
75
- >
76
- {copied ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
77
- </button>
78
- );
79
- }
80
-
81
- export function SkippedRunsBanner({ skippedRuns, t }: { skippedRuns: SkippedRunNotice[]; t: T }) {
82
- const [expanded, setExpanded] = useState(false);
83
- if (skippedRuns.length === 0) return null;
84
-
85
- const groups = groupSkippedRuns(skippedRuns);
86
- const total = skippedRuns.length;
87
- let budget = expanded ? total : DEFAULT_VISIBLE;
88
- const hiddenCount = expanded ? 0 : Math.max(0, total - DEFAULT_VISIBLE);
89
-
90
- return (
91
- <section className="incompatible-banner" role="alert">
92
- <div className="ib-head">
93
- <AlertCircle className="ib-icon" aria-hidden="true" />
94
- <b>{t("banner.skippedTitle", { count: total })}</b>
95
- </div>
96
- <div className="ib-groups">
97
- {groups.map((group) => {
98
- const visibleItems = group.items.slice(0, Math.max(0, budget));
99
- budget -= visibleItems.length;
100
- const hiddenInGroup = group.items.length - visibleItems.length;
101
- return (
102
- <div className="ib-group" key={group.key}>
103
- <div className="ib-group-head">
104
- <span className="ib-group-count">{group.items.length}</span>
105
- <span className="ib-group-desc">{t(group.messageKey, group.vars)}</span>
106
- </div>
107
- {visibleItems.length > 0 && (
108
- <ul>
109
- {visibleItems.map((item) => (
110
- <li key={item.dir}>
111
- <span className="ib-dir">{item.dir}</span>
112
- {item.command && <CopyCommandButton command={item.command} t={t} />}
113
- </li>
114
- ))}
115
- {hiddenInGroup > 0 && !expanded && (
116
- <li className="ib-more" aria-hidden="true">
117
- +{hiddenInGroup}
118
- </li>
119
- )}
120
- </ul>
121
- )}
122
- </div>
123
- );
124
- })}
125
- </div>
126
- {hiddenCount > 0 && (
127
- <button type="button" className="ib-toggle" onClick={() => setExpanded(true)}>
128
- <ChevronRight aria-hidden="true" />
129
- {t("banner.expandRest", { count: hiddenCount })}
130
- </button>
131
- )}
132
- {expanded && total > DEFAULT_VISIBLE && (
133
- <button type="button" className="ib-toggle" onClick={() => setExpanded(false)}>
134
- <ChevronRight className="is-open" aria-hidden="true" />
135
- {t("banner.collapse")}
136
- </button>
137
- )}
138
- </section>
139
- );
140
- }
@@ -1,80 +0,0 @@
1
- import { useState } from "react";
2
- import type { RowRun, T } from "../shared.ts";
3
- import { verdictClass, verdictLabel } from "../lib/verdict.ts";
4
- import { formatCost, formatDateTime, formatDuration, formatTokens, totalTokens } from "../lib/format.ts";
5
-
6
- /** 全部历史 attempt 打平成一张表;列表在 App 里由 flattenAttempts(全部快照,已跨快照去重)算好。 */
7
- export function AttemptsView({ attempts, t }: { attempts: RowRun[]; t: T }) {
8
- const [query, setQuery] = useState("");
9
- const allRuns = attempts;
10
- const filtered = allRuns.filter((r: RowRun) => {
11
- const q = query.trim().toLowerCase();
12
- return !q || `${r.id} ${r.rowLabel} ${r.rowAgent} ${r.rowModel || ""}`.toLowerCase().includes(q);
13
- });
14
- return (
15
- <section id="tab-attempts">
16
- <div className="section-head">
17
- <h2>{t("section.attempts")}</h2>
18
- <div className="controls">
19
- <input
20
- className="search"
21
- type="search"
22
- placeholder={t("search.attempts")}
23
- autoComplete="off"
24
- value={query}
25
- onChange={(e) => setQuery(e.target.value)}
26
- />
27
- </div>
28
- </div>
29
- {!allRuns.length ? (
30
- <div className="empty">{t("empty.attempts")}</div>
31
- ) : (
32
- <div className="table-wrap">
33
- <table>
34
- <thead>
35
- <tr>
36
- <th>{t("table.evalId")}</th>
37
- <th>{t("table.experiment")}</th>
38
- <th>{t("table.verdict")}</th>
39
- <th>{t("table.agent")}</th>
40
- <th>{t("table.model")}</th>
41
- <th>{t("metric.duration")}</th>
42
- <th>{t("table.tokens")}</th>
43
- <th>{t("table.estCost")}</th>
44
- <th>{t("table.ranAt")}</th>
45
- </tr>
46
- </thead>
47
- <tbody>
48
- {filtered.length ? (
49
- filtered.map((r: RowRun) => {
50
- const verdict = r.verdict;
51
- return (
52
- <tr key={`${r.id}-${r.rowLabel}-${r.attempt}`}>
53
- <td>
54
- <span className="name">{r.id}</span>
55
- </td>
56
- <td>{r.rowLabel}</td>
57
- <td className={verdictClass(verdict)}>{verdictLabel(verdict, t)}</td>
58
- <td>{r.rowAgent}</td>
59
- <td>{r.rowModel || t("config.default")}</td>
60
- <td className="num">{formatDuration(r.durationMs)}</td>
61
- <td className="num">{formatTokens(totalTokens(r.usage))}</td>
62
- <td className="num">{formatCost(r.estimatedCostUSD)}</td>
63
- <td className="num">{r.startedAt ? formatDateTime(r.startedAt) : "-"}</td>
64
- </tr>
65
- );
66
- })
67
- ) : (
68
- <tr>
69
- <td colSpan={9} style={{ textAlign: "center", color: "var(--muted)" }}>
70
- {t("empty.attemptsFilter")}
71
- </td>
72
- </tr>
73
- )}
74
- </tbody>
75
- </table>
76
- </div>
77
- )}
78
- </section>
79
- );
80
- }
@@ -1,35 +0,0 @@
1
- import type { RowRun, T } from "../shared.ts";
2
- import { verdictClass, verdictLabel } from "../lib/verdict.ts";
3
- import { formatDuration } from "../lib/format.ts";
4
- import { LazyArtifact } from "../components/LazyArtifact.tsx";
5
-
6
- /** 全部历史 attempt 的 trace/transcript 视图;列表在 App 里由 flattenAttempts 算好。 */
7
- export function TracesView({ attempts, t }: { attempts: RowRun[]; t: T }) {
8
- const traceable = attempts.filter((r: RowRun) => r.hasEvents || r.hasTrace);
9
- return (
10
- <section id="tab-traces">
11
- <div className="section-head">
12
- <h2>{t("section.traces")}</h2>
13
- </div>
14
- {!traceable.length ? (
15
- <div className="empty">{t("empty.traces")}</div>
16
- ) : (
17
- traceable.map((r: RowRun) => {
18
- const verdict = r.verdict;
19
- return (
20
- <div className="traces-entry" key={`${r.id}-${r.rowLabel}-${r.attempt}`}>
21
- <div className="traces-entry-head">
22
- <span className={`${verdictClass(verdict)} traces-verdict`}>{verdictLabel(verdict, t)}</span>
23
- <span className="eval-id">{r.id}</span>
24
- <span className="traces-exp">{r.rowLabel}</span>
25
- <span className="num traces-dur">{formatDuration(r.durationMs)}</span>
26
- </div>
27
- {r.hasEvents && r.artifactBase ? <LazyArtifact type="transcript" src={`${r.artifactBase}/events.json`} t={t} /> : null}
28
- {r.hasTrace && r.artifactBase ? <LazyArtifact type="trace" src={`${r.artifactBase}/trace.json`} t={t} /> : null}
29
- </div>
30
- );
31
- })
32
- )}
33
- </section>
34
- );
35
- }