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.
- package/INDEX.md +77 -45
- package/dist/agents/types.d.ts +28 -6
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/zh-CN.d.ts +3 -1
- package/dist/report/built-in/index.d.ts +3 -2
- package/dist/report/built-in/index.js +7 -8
- package/dist/report/built-in/standard.d.ts +1 -0
- package/dist/report/built-in/standard.js +30 -0
- package/dist/report/components.d.ts +69 -2
- package/dist/report/components.js +152 -3
- package/dist/report/compute.d.ts +28 -1
- package/dist/report/compute.js +123 -0
- package/dist/report/index.d.ts +4 -4
- package/dist/report/index.js +3 -2
- package/dist/report/locale.d.ts +39 -1
- package/dist/report/locale.js +69 -0
- package/dist/report/react/AttemptList.d.ts +3 -1
- package/dist/report/react/AttemptList.js +3 -3
- package/dist/report/react/CopyFixPrompt.d.ts +12 -0
- package/dist/report/react/CopyFixPrompt.js +12 -0
- package/dist/report/react/HeroCard.d.ts +13 -0
- package/dist/report/react/HeroCard.js +35 -0
- package/dist/report/react/PoweredBy.d.ts +5 -0
- package/dist/report/react/PoweredBy.js +7 -0
- package/dist/report/react/ScopeWarnings.d.ts +12 -0
- package/dist/report/react/ScopeWarnings.js +18 -0
- package/dist/report/react/TraceWaterfall.d.ts +14 -0
- package/dist/report/react/TraceWaterfall.js +22 -0
- package/dist/report/react/index.d.ts +6 -1
- package/dist/report/react/index.js +6 -0
- package/dist/report/report.d.ts +22 -6
- package/dist/report/report.js +68 -55
- package/dist/report/scope-warnings.d.ts +28 -0
- package/dist/report/scope-warnings.js +101 -0
- package/dist/report/text/faces.d.ts +19 -1
- package/dist/report/text/faces.js +61 -0
- package/dist/report/tree.d.ts +1 -1
- package/dist/report/tree.js +7 -2
- package/dist/report/types.d.ts +45 -0
- package/dist/report/web.d.ts +5 -4
- package/dist/report/web.js +7 -22
- package/dist/results/select.d.ts +17 -4
- package/dist/results/select.js +76 -11
- package/dist/results/types.d.ts +26 -0
- package/dist/runner/fingerprint.d.ts +3 -3
- package/dist/runner/sandbox-selection.d.ts +12 -0
- package/dist/runner/types.d.ts +18 -6
- package/dist/sandbox/types.d.ts +12 -0
- package/dist/shared/aggregate.d.ts +1 -1
- package/dist/shared/aggregate.js +1 -1
- package/docs-site/zh/explanation/evals.mdx +2 -1
- package/docs-site/zh/explanation/experiment.mdx +2 -0
- package/docs-site/zh/how-to/custom-reports.mdx +23 -7
- package/docs-site/zh/how-to/experiments.mdx +2 -2
- package/docs-site/zh/how-to/publish-report.mdx +11 -4
- package/docs-site/zh/how-to/viewing-results.mdx +17 -8
- package/docs-site/zh/how-to/write-experiment.mdx +40 -1
- package/docs-site/zh/reference/builtin-agents.mdx +40 -3
- package/docs-site/zh/reference/cli.mdx +3 -3
- package/docs-site/zh/reference/define-eval.mdx +8 -0
- package/docs-site/zh/reference/official-adapters.mdx +10 -5
- package/docs-site/zh/troubleshooting/debugging.mdx +3 -3
- package/package.json +2 -1
- package/src/agents/bub.ts +13 -1
- package/src/agents/claude-code.test.ts +43 -1
- package/src/agents/claude-code.ts +32 -14
- package/src/agents/codex.test.ts +168 -1
- package/src/agents/codex.ts +51 -15
- package/src/agents/mcp.ts +31 -0
- package/src/agents/post-setup.ts +33 -0
- package/src/agents/types.ts +28 -7
- package/src/cli.ts +15 -14
- package/src/define.ts +3 -0
- package/src/i18n/en.ts +8 -5
- package/src/i18n/zh-CN.ts +8 -4
- package/src/index.ts +1 -0
- package/src/report/built-in/index.tsx +8 -7
- package/src/report/built-in/standard.tsx +59 -0
- package/src/report/components.tsx +218 -2
- package/src/report/compute.ts +138 -1
- package/src/report/dual-render.test.tsx +141 -14
- package/src/report/index.ts +20 -0
- package/src/report/locale.ts +83 -1
- package/src/report/react/AttemptList.tsx +13 -1
- package/src/report/react/CopyFixPrompt.tsx +37 -0
- package/src/report/react/HeroCard.tsx +59 -0
- package/src/report/react/PoweredBy.tsx +20 -0
- package/src/report/react/ScopeWarnings.tsx +74 -0
- package/src/report/react/TraceWaterfall.tsx +78 -0
- package/src/report/react/enhance.js +14 -0
- package/src/report/react/index.tsx +11 -0
- package/src/report/react/styles.css +193 -7
- package/src/report/report.ts +99 -64
- package/src/report/scope-warnings.ts +155 -0
- package/src/report/site-components.test.tsx +526 -0
- package/src/report/text/faces.ts +66 -0
- package/src/report/tree.ts +10 -3
- package/src/report/types.ts +51 -0
- package/src/report/web.ts +7 -40
- package/src/results/host-equivalence.test.ts +6 -2
- package/src/results/open.ts +5 -4
- package/src/results/results.test.ts +78 -1
- package/src/results/select.ts +80 -12
- package/src/results/types.ts +27 -0
- package/src/runner/attempt.ts +10 -9
- package/src/runner/discover.test.ts +9 -1
- package/src/runner/discover.ts +3 -3
- package/src/runner/fingerprint.ts +9 -4
- package/src/runner/ledger.test.ts +30 -1
- package/src/runner/ledger.ts +26 -4
- package/src/runner/run.ts +5 -1
- package/src/runner/sandbox-selection.test.ts +131 -0
- package/src/runner/sandbox-selection.ts +110 -0
- package/src/runner/types.ts +19 -2
- package/src/sandbox/types.ts +6 -0
- package/src/shared/aggregate.ts +1 -1
- package/src/show/index.ts +19 -12
- package/src/show/render.ts +12 -12
- package/src/show/report-host.test.ts +34 -17
- package/src/show/report-host.ts +6 -5
- package/src/show/show.test.ts +141 -4
- package/src/view/app/App.test.tsx +79 -17
- package/src/view/app/App.tsx +25 -74
- package/src/view/app/components/CopyControls.tsx +4 -42
- package/src/view/app/i18n.ts +5 -227
- package/src/view/app/lib/rows.ts +3 -21
- package/src/view/app/shared.ts +1 -3
- package/src/view/app/types.ts +2 -2
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.ts +21 -10
- package/src/view/index.ts +2 -12
- package/src/view/server.ts +4 -4
- package/src/view/shared/types.ts +11 -6
- package/src/view/styles.css +9 -252
- package/src/view/view-report.test.ts +99 -33
- package/src/view/app/components/LazyArtifact.tsx +0 -51
- package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
- package/src/view/app/pages/AttemptsPage.tsx +0 -80
- 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、--
|
|
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
|
-
/** 单页报告(
|
|
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("
|
|
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 =
|
|
157
|
-
//
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
expect(
|
|
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("--
|
|
194
|
+
it("--exp 过滤:报告槽 Selection 只留该实验", async () => {
|
|
167
195
|
const root = await seedRoot();
|
|
168
|
-
const reportHtml =
|
|
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
|
|
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
|
|
229
|
+
it("裸跑产出的报告槽 HTML 与 --report <re-export 内建报告的文件> 逐页完全一致(双语)", async () => {
|
|
194
230
|
const root = await seedRoot();
|
|
195
|
-
const bare =
|
|
196
|
-
const viaReport =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
|
|
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 =
|
|
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("
|
|
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(
|
|
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&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
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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
|
-
}
|