niceeval 0.8.1 → 0.9.0
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 +2 -0
- 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 +66 -53
- 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.js +6 -1
- 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 +15 -2
- package/dist/results/select.js +75 -10
- 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/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 +22 -6
- package/docs-site/zh/how-to/publish-report.mdx +5 -4
- package/docs-site/zh/how-to/viewing-results.mdx +3 -3
- 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 +1 -1
- package/docs-site/zh/reference/define-eval.mdx +8 -0
- package/docs-site/zh/reference/official-adapters.mdx +10 -5
- 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 +8 -6
- package/src/define.ts +3 -0
- package/src/i18n/en.ts +5 -2
- package/src/i18n/zh-CN.ts +5 -1
- 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 +139 -12
- 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 +187 -7
- package/src/report/report.ts +97 -62
- 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 +8 -1
- package/src/report/types.ts +51 -0
- package/src/report/web.ts +7 -40
- package/src/results/host-equivalence.test.ts +5 -1
- package/src/results/open.ts +5 -4
- package/src/results/results.test.ts +78 -1
- package/src/results/select.ts +78 -10
- 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/show/index.ts +17 -10
- package/src/show/render.ts +11 -11
- package/src/show/report-host.test.ts +32 -15
- package/src/show/report-host.ts +5 -4
- package/src/show/show.test.ts +140 -3
- package/src/view/app/App.test.tsx +78 -17
- package/src/view/app/App.tsx +17 -78
- 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 +2 -2
- package/src/view/shared/types.ts +11 -6
- package/src/view/styles.css +6 -266
- package/src/view/view-report.test.ts +59 -22
- 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
|
@@ -8,16 +8,32 @@ import {
|
|
|
8
8
|
BUILT_IN_PAGE_TITLE,
|
|
9
9
|
BUILT_IN_REPORT_TITLE,
|
|
10
10
|
HostReportError,
|
|
11
|
+
loadHostReport,
|
|
11
12
|
localizeText,
|
|
12
13
|
localizedTextEquals,
|
|
13
14
|
normalizeHostReport,
|
|
14
15
|
resolveReportTitle,
|
|
15
16
|
showCommand,
|
|
16
17
|
} from "./report-host.ts";
|
|
17
|
-
import {
|
|
18
|
+
import { otherPagesText } from "./render.ts";
|
|
19
|
+
// dist-sourced:裸宿主装载的就是这份预编译产物的默认导出(show 与 view 同一条路),
|
|
20
|
+
// raw-src import 会是另一份模块实例,引用等同断言必须对着 dist。
|
|
21
|
+
import distBuiltInReport from "../../dist/report/built-in/index.js";
|
|
18
22
|
|
|
19
23
|
const tree = { kind: "node" }; // 页 content 对宿主是不透明值,规范化不解析树
|
|
20
24
|
|
|
25
|
+
describe("裸宿主装载内建报告", () => {
|
|
26
|
+
it("缺省(无 --report)装载 niceeval/report/built-in 的默认导出:三页与其 content 同引用", async () => {
|
|
27
|
+
const host = await loadHostReport(process.cwd(), undefined);
|
|
28
|
+
const builtIn = distBuiltInReport as { pages: readonly { id: string; content: unknown }[] };
|
|
29
|
+
expect(host.pages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
|
|
30
|
+
expect(builtIn.pages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
|
|
31
|
+
for (let i = 0; i < host.pages.length; i++) {
|
|
32
|
+
expect(host.pages[i]!.content).toBe(builtIn.pages[i]!.content); // 同一份默认导出,不是复制品
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
21
37
|
describe("装载规范化:外壳 + 非空页列表", () => {
|
|
22
38
|
it("content 缩写展开为唯一页 id `report`,页名是内置页名「报告 / Report」", () => {
|
|
23
39
|
const report = normalizeHostReport({ kind: "report", content: tree }, "reports/frontier.tsx");
|
|
@@ -45,13 +61,13 @@ describe("装载规范化:外壳 + 非空页列表", () => {
|
|
|
45
61
|
expect(report.footer).toBe("Published nightly.");
|
|
46
62
|
});
|
|
47
63
|
|
|
48
|
-
it("content 与 pages 恰好声明一个:同给 / 同缺都报错,文案给出
|
|
64
|
+
it("content 与 pages 恰好声明一个:同给 / 同缺都报错,文案给出 extends: standard 下一步", () => {
|
|
49
65
|
for (const bad of [
|
|
50
66
|
{ kind: "report", content: tree, pages: [{ id: "a", title: "A", content: tree }] },
|
|
51
67
|
{ kind: "report", title: "T" },
|
|
52
68
|
]) {
|
|
53
69
|
expect(() => normalizeHostReport(bad, "reports/site.tsx")).toThrow(HostReportError);
|
|
54
|
-
expect(() => normalizeHostReport(bad, "reports/site.tsx")).toThrow(
|
|
70
|
+
expect(() => normalizeHostReport(bad, "reports/site.tsx")).toThrow(/niceeval\/report\/built-in/);
|
|
55
71
|
}
|
|
56
72
|
});
|
|
57
73
|
|
|
@@ -141,7 +157,7 @@ describe("LocalizedText 回退:locale → en → 键字典序第一个非空值"
|
|
|
141
157
|
});
|
|
142
158
|
});
|
|
143
159
|
|
|
144
|
-
describe("
|
|
160
|
+
describe("其余页索引与索引命令上下文", () => {
|
|
145
161
|
const report = normalizeHostReport(
|
|
146
162
|
{
|
|
147
163
|
kind: "report",
|
|
@@ -155,27 +171,28 @@ describe("页索引与索引命令上下文", () => {
|
|
|
155
171
|
"reports/site.tsx",
|
|
156
172
|
);
|
|
157
173
|
|
|
158
|
-
it("
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
it("只列未渲染的页,索引命令保留当前 --results / --report 与位置参数,复制即可复现下一层视图", () => {
|
|
175
|
+
// 渲染的是 overview,其余页索引只含 exam 一行——与「渲染初始页 + 尾部附其余页索引」
|
|
176
|
+
// 的新行为一致(docs/feature/reports/show/reports.md Case 2)。
|
|
177
|
+
const text = otherPagesText({
|
|
178
|
+
otherPages: report.pages.filter((p) => p.id !== "overview").map((p) => ({ id: p.id, title: p.title })),
|
|
162
179
|
command: { patterns: [], results: "tmp/published-results", report: "reports/site.tsx" },
|
|
163
180
|
locale: "zh-CN",
|
|
164
181
|
});
|
|
165
|
-
expect(text).toContain("
|
|
166
|
-
expect(text).toContain("niceeval show --results tmp/published-results --report reports/site.tsx --page overview");
|
|
182
|
+
expect(text).toContain("其余页:");
|
|
167
183
|
expect(text).toContain("niceeval show --results tmp/published-results --report reports/site.tsx --page exam");
|
|
168
|
-
expect(text).toContain("总览");
|
|
169
184
|
expect(text).toContain("成绩单");
|
|
185
|
+
expect(text).not.toContain("总览");
|
|
186
|
+
expect(text).not.toContain("--page overview");
|
|
170
187
|
});
|
|
171
188
|
|
|
172
|
-
it("show 不消费 links
|
|
173
|
-
const text =
|
|
174
|
-
report,
|
|
175
|
-
title: "Memory Evals",
|
|
189
|
+
it("show 不消费 links:其余页索引不含 icon svg 与 href(icon 是 web 面属性)", () => {
|
|
190
|
+
const text = otherPagesText({
|
|
191
|
+
otherPages: report.pages.filter((p) => p.id !== "overview").map((p) => ({ id: p.id, title: p.title })),
|
|
176
192
|
command: { patterns: [] },
|
|
177
193
|
locale: "en",
|
|
178
194
|
});
|
|
195
|
+
expect(text).toContain("Other pages:");
|
|
179
196
|
expect(text).not.toContain("<svg");
|
|
180
197
|
expect(text).not.toContain("https://example.com");
|
|
181
198
|
});
|
package/src/show/report-host.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// - 装载规范化唯一产物是「外壳 + 非空页列表」:`defineReport(树)` ≡ `{ content: 树 }` ≡
|
|
8
8
|
// `pages: [{ id: "report", title: 内置页名, content: 树 }]`。
|
|
9
9
|
// - 标题回退单点:def.title → Scope 中唯一且相同(LocalizedText 深相等)的非空快照 name →
|
|
10
|
-
// 内置文案「Eval 运行结果 / Eval Results
|
|
11
|
-
//
|
|
10
|
+
// 内置文案「Eval 运行结果 / Eval Results」。宿主落点是浏览器标题与 show 页索引标题行;
|
|
11
|
+
// 页内 hero 标题由 Hero 组件消费同一取值链(ctx.report.title),宿主页头无品牌位。
|
|
12
12
|
// - LocalizedText 回退:当前 locale → en → 按 locale 键字典序的第一个非空值。
|
|
13
13
|
//
|
|
14
14
|
// ⚠ 集成状态:src/report/** 正被并行重写(plan/reports-redesign-implementation.md)。这里把宿主
|
|
@@ -212,11 +212,12 @@ export function normalizeHostReport(definition: unknown, sourceLabel: string): H
|
|
|
212
212
|
const hasContent = def.content !== undefined;
|
|
213
213
|
const hasPages = def.pages !== undefined;
|
|
214
214
|
if (hasContent === hasPages) {
|
|
215
|
-
// content / pages
|
|
215
|
+
// content / pages 同缺或同给:装载期完整用户反馈(defineReport 产物恒已折叠,这里只拦
|
|
216
|
+
// 手搓 kind:"report" 的无类型输入),下一步指向内建视图的 extends 复用。
|
|
216
217
|
throw new HostReportError(
|
|
217
218
|
`${sourceLabel}: a report declares exactly one of "content" or "pages" — ` +
|
|
218
219
|
(hasContent ? "it declares both. " : "it declares neither. ") +
|
|
219
|
-
`To render the built-in report
|
|
220
|
+
`To render the built-in report, write extends: standard (import { standard } from "niceeval/report/built-in").`,
|
|
220
221
|
);
|
|
221
222
|
}
|
|
222
223
|
const pages: HostReportPage[] = hasPages
|
package/src/show/show.test.ts
CHANGED
|
@@ -163,6 +163,57 @@ describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text
|
|
|
163
163
|
expect(out).not.toMatch(/\[[EXD⏱,]+\]/);
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
it("裸 show 是内建报告首页:页首 Hero 标题行 + 最后运行 meta,尾部附 attempts / traces 页索引(命令带 --results 上下文)", async () => {
|
|
167
|
+
// docs/feature/reports/show/default-report.md:Hero 两行在页首,「其余页」两行在尾部。
|
|
168
|
+
const root = await seedComposedRoot();
|
|
169
|
+
const { out, code } = await show(root, []);
|
|
170
|
+
expect(code).toBe(0);
|
|
171
|
+
const lines = out.split("\n");
|
|
172
|
+
expect(lines[0]).toBe("Eval Results"); // 标题回退链终点(快照无 name、无 --report title)
|
|
173
|
+
expect(lines[1]).toMatch(/^Last run \d{4}-\d{2}-\d{2} \d{2}:\d{2}$/); // Hero meta 行
|
|
174
|
+
// 尾部「其余页」索引:只列未渲染的两页,命令携带完整 --results 上下文,复制即可复现。
|
|
175
|
+
const tailAt = out.indexOf("Other pages:");
|
|
176
|
+
expect(tailAt).toBeGreaterThan(-1);
|
|
177
|
+
const tail = out.slice(tailAt);
|
|
178
|
+
expect(tail).toContain(`niceeval show --results ${root} --page attempts`);
|
|
179
|
+
expect(tail).toContain(`niceeval show --results ${root} --page traces`);
|
|
180
|
+
expect(tail).not.toContain("--page report"); // 已渲染的首页不进索引
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("裸 show 的 --page attempts / traces 渲染内建证据页(AttemptList / TraceWaterfall 的 text 面)", async () => {
|
|
184
|
+
const root = await seedComposedRoot();
|
|
185
|
+
const attempts = await show(root, [], { page: "attempts" }, 160);
|
|
186
|
+
expect(attempts.code).toBe(0);
|
|
187
|
+
// AttemptList text 面:范围内每个 attempt 一条,失败行带主失败摘要。
|
|
188
|
+
expect(attempts.out).toContain("fixtures/button");
|
|
189
|
+
expect(attempts.out).toContain("weather/brooklyn");
|
|
190
|
+
expect(attempts.out).toMatch(/@1[0-9a-z]{7}/);
|
|
191
|
+
const traces = await show(root, [], { page: "traces" }, 160);
|
|
192
|
+
expect(traces.code).toBe(0);
|
|
193
|
+
// TraceWaterfall text 面:每 attempt 一行,行尾是可复制的 --timing 下钻命令。
|
|
194
|
+
expect(traces.out).toMatch(/niceeval show @1[0-9a-z]{7} --timing/);
|
|
195
|
+
expect(traces.out).toContain("no trace"); // fixture 无 trace artifact:如实显示缺失
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("裸 show 的选择警告由页内 ScopeWarnings 组件显示(partial-coverage 按动作聚合成组头行)", async () => {
|
|
199
|
+
const root = await makeRoot();
|
|
200
|
+
await writeSnapshot(
|
|
201
|
+
root,
|
|
202
|
+
"2026-07-08T10-00-00-000Z",
|
|
203
|
+
{
|
|
204
|
+
experimentId: "compare/bub",
|
|
205
|
+
startedAt: "2026-07-08T10:00:00.000Z",
|
|
206
|
+
knownEvalIds: ["weather/brooklyn", "weather/queens"], // 少跑一题 → partial-coverage
|
|
207
|
+
},
|
|
208
|
+
[res("weather/brooklyn", "passed")],
|
|
209
|
+
);
|
|
210
|
+
const { out, code } = await show(root, [], {}, 160);
|
|
211
|
+
expect(code).toBe(0);
|
|
212
|
+
// 警告块在 Hero 之后、组内容之前;组头一行含实验 id、徽标与可复制命令。
|
|
213
|
+
expect(out).toMatch(/^! compare\/bub — coverage 1\/2 → niceeval exp compare\/bub$/m);
|
|
214
|
+
expect(out.indexOf("Eval Results")).toBeLessThan(out.indexOf("! compare/bub"));
|
|
215
|
+
});
|
|
216
|
+
|
|
166
217
|
it("裸 show 的默认报告 chrome 跟随 locale", async () => {
|
|
167
218
|
const root = await seedComposedRoot();
|
|
168
219
|
process.env.NICEEVAL_LANG = "zh-CN";
|
|
@@ -172,17 +223,24 @@ describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text
|
|
|
172
223
|
expect(out).toContain("成本 × 端到端成功率 没有可绘制的数据");
|
|
173
224
|
expect(out).toContain("1 通过 / 1 失败");
|
|
174
225
|
expect(out).toMatch(/\bbub\s+默认\s+bub\s+1s\s+50%/);
|
|
226
|
+
// 页首 Hero 与尾部页索引同样跟随 locale(内置文案与页名)。
|
|
227
|
+
expect(out.split("\n")[0]).toBe("Eval 运行结果");
|
|
228
|
+
expect(out).toContain("其余页:");
|
|
229
|
+
expect(out).toContain("追踪");
|
|
175
230
|
} finally {
|
|
176
231
|
process.env.NICEEVAL_LANG = "en";
|
|
177
232
|
}
|
|
178
233
|
});
|
|
179
234
|
|
|
180
|
-
it("窄终端截断长 eval
|
|
235
|
+
it("窄终端截断长 eval 与原因,报告正文每一行都不超过终端显示宽度", async () => {
|
|
181
236
|
const root = await seedComposedRoot();
|
|
182
237
|
const { out, code } = await show(root, [], {}, 60);
|
|
183
238
|
expect(code).toBe(0);
|
|
184
239
|
expect(out).toContain("fixtures/button");
|
|
185
|
-
|
|
240
|
+
// 「其余页」索引里的命令是可复制的完整命令(携带 --results 绝对路径),复制即可执行,
|
|
241
|
+
// 永不为宽度折行——宽度约束只作用于报告正文。
|
|
242
|
+
const body = out.slice(0, out.indexOf("Other pages:"));
|
|
243
|
+
for (const line of body.trimEnd().split("\n")) {
|
|
186
244
|
expect(stringWidth(line), line).toBeLessThanOrEqual(60);
|
|
187
245
|
}
|
|
188
246
|
});
|
|
@@ -438,6 +496,85 @@ describe("--report 装载", () => {
|
|
|
438
496
|
return path;
|
|
439
497
|
}
|
|
440
498
|
|
|
499
|
+
/**
|
|
500
|
+
* 两页文件:overview / exam,各自的 text 面输出一个可断言的标记字符串——够验证
|
|
501
|
+
* 「渲染初始页 + 尾部附其余页索引,不倾倒其余页内容」而不需要引入更多组合语义
|
|
502
|
+
* (docs/feature/reports/show/reports.md Case 2)。
|
|
503
|
+
*/
|
|
504
|
+
async function writeMultiPageReportFile(dir: string): Promise<string> {
|
|
505
|
+
const path = join(dir, "site.mjs");
|
|
506
|
+
await writeFile(
|
|
507
|
+
path,
|
|
508
|
+
[
|
|
509
|
+
'const FACES = Symbol.for("niceeval.report.faces");',
|
|
510
|
+
'const DEFINITION = Symbol.for("niceeval.report.definition");',
|
|
511
|
+
"const Overview = () => null;",
|
|
512
|
+
"Overview[FACES] = { web: () => null, text: () => \"OVERVIEW PAGE CONTENT\" };",
|
|
513
|
+
"const Exam = () => null;",
|
|
514
|
+
"Exam[FACES] = { web: () => null, text: () => \"EXAM PAGE CONTENT\" };",
|
|
515
|
+
"const definition = {",
|
|
516
|
+
' kind: "report",',
|
|
517
|
+
" links: [],",
|
|
518
|
+
" scripts: [],",
|
|
519
|
+
" styles: [],",
|
|
520
|
+
" pages: [",
|
|
521
|
+
' { id: "overview", title: { en: "Overview", "zh-CN": "总览" }, content: { type: Overview, props: {} } },',
|
|
522
|
+
' { id: "exam", title: { en: "Exam", "zh-CN": "成绩单" }, content: { type: Exam, props: {} } },',
|
|
523
|
+
" ],",
|
|
524
|
+
"};",
|
|
525
|
+
"Object.defineProperty(definition, DEFINITION, { value: true });",
|
|
526
|
+
"export default definition;",
|
|
527
|
+
"",
|
|
528
|
+
].join("\n"),
|
|
529
|
+
"utf-8",
|
|
530
|
+
);
|
|
531
|
+
return path;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
it("多页文件:渲染初始页(缺省第一页)+ 尾部附其余页索引,不倾倒其余页内容", async () => {
|
|
535
|
+
// show() 测试助手缺省把 root 当 --results 传下去(见上方 show() 定义),
|
|
536
|
+
// 因此索引命令恒含 --results;这里的默认 locale 是 en(beforeAll 固定)。
|
|
537
|
+
const root = await seedComposedRoot();
|
|
538
|
+
const report = await writeMultiPageReportFile(root);
|
|
539
|
+
const { out, code } = await show(root, [], { report });
|
|
540
|
+
expect(code).toBe(0);
|
|
541
|
+
expect(out).toContain("OVERVIEW PAGE CONTENT");
|
|
542
|
+
expect(out).not.toContain("EXAM PAGE CONTENT");
|
|
543
|
+
expect(out).toContain("Other pages:");
|
|
544
|
+
expect(out).toContain(`niceeval show --results ${root} --report ${report} --page exam`);
|
|
545
|
+
expect(out).toContain("Exam");
|
|
546
|
+
expect(out).not.toContain("--page overview"); // 已渲染的页不进「其余页」索引
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("多页文件:--page 选中的页渲染,尾部索引只列剩下的页", async () => {
|
|
550
|
+
const root = await seedComposedRoot();
|
|
551
|
+
const report = await writeMultiPageReportFile(root);
|
|
552
|
+
const { out, code } = await show(root, [], { report, page: "exam" });
|
|
553
|
+
expect(code).toBe(0);
|
|
554
|
+
expect(out).toContain("EXAM PAGE CONTENT");
|
|
555
|
+
expect(out).not.toContain("OVERVIEW PAGE CONTENT");
|
|
556
|
+
expect(out).toContain("Other pages:");
|
|
557
|
+
expect(out).toContain(`niceeval show --results ${root} --report ${report} --page overview`);
|
|
558
|
+
expect(out).not.toContain("--page exam");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("单页定义直接渲染,无「其余页」段", async () => {
|
|
562
|
+
const root = await seedComposedRoot();
|
|
563
|
+
const report = await writeReportFile(root);
|
|
564
|
+
const { out, code } = await show(root, [], { report });
|
|
565
|
+
expect(code).toBe(0);
|
|
566
|
+
expect(out).not.toContain("其余页");
|
|
567
|
+
expect(out).not.toContain("Other pages");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("其余页索引命令保留当前 --results / --report 与位置参数上下文,复制即可复现下一层视图", async () => {
|
|
571
|
+
const root = await seedComposedRoot();
|
|
572
|
+
const report = await writeMultiPageReportFile(root);
|
|
573
|
+
const { out, code } = await show(root, ["weather"], { report, results: root });
|
|
574
|
+
expect(code).toBe(0);
|
|
575
|
+
expect(out).toContain(`niceeval show weather --results ${root} --report ${report} --page exam`);
|
|
576
|
+
});
|
|
577
|
+
|
|
441
578
|
it("装载 + 注入 Selection + attemptCommand 下钻命令", async () => {
|
|
442
579
|
const root = await seedComposedRoot();
|
|
443
580
|
const report = await writeReportFile(root);
|
|
@@ -521,7 +658,7 @@ describe("--report 装载", () => {
|
|
|
521
658
|
const builtin = await show(root, [], { page: "typo" });
|
|
522
659
|
expect(builtin.code).toBe(1);
|
|
523
660
|
expect(builtin.err).toContain('page "typo" not found in the built-in report');
|
|
524
|
-
expect(builtin.err).toContain("Available pages: report");
|
|
661
|
+
expect(builtin.err).toContain("Available pages: report, attempts, traces");
|
|
525
662
|
});
|
|
526
663
|
});
|
|
527
664
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// cases: docs/engineering/unit-tests/reports/cases.md
|
|
2
2
|
// 「外壳、页面与 Tabs」分区——
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// ReportLink.icon 渲染在 label 前(web 面)。契约:docs/feature/reports/library/shell.md
|
|
3
|
+
// 宿主页头不渲染任何品牌位、宿主无 hero 区(hero / 品牌行是页内组件,见 site-components 测试);
|
|
4
|
+
// view 导航只有报告定义声明的页(声明序),宿主不追加或保留任何导航项;
|
|
5
|
+
// ReportLink.icon 渲染在 label 前(web 面)。契约:docs/feature/reports/library/shell.md「行为约束」、
|
|
6
|
+
// docs/feature/reports/view.md「页面构成」。
|
|
6
7
|
|
|
7
8
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
8
9
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
@@ -14,14 +15,18 @@ beforeAll(() => {
|
|
|
14
15
|
(globalThis as { location?: unknown }).location = { hash: "", search: "", pathname: "/" };
|
|
15
16
|
});
|
|
16
17
|
|
|
17
|
-
const reportPages = {
|
|
18
|
+
const reportPages = {
|
|
19
|
+
report: { en: "<p>REPORT_BODY</p>", "zh-CN": "<p>REPORT_BODY</p>" },
|
|
20
|
+
attempts: { en: "<p>ATTEMPTS_BODY</p>", "zh-CN": "<p>ATTEMPTS_BODY</p>" },
|
|
21
|
+
traces: { en: "<p>TRACES_BODY</p>", "zh-CN": "<p>TRACES_BODY</p>" },
|
|
22
|
+
};
|
|
18
23
|
|
|
19
24
|
function dataWithShell(report: ViewData["report"]): ViewData {
|
|
20
25
|
return { composedRuns: 1, snapshots: [], ...(report !== undefined ? { report } : {}) };
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
describe("
|
|
24
|
-
it("
|
|
28
|
+
describe("外壳:宿主无品牌位与 hero,导航只有报告页", () => {
|
|
29
|
+
it("宿主导航壳 DOM 无品牌节点、无 hero 区:品牌与 hero 是页内组件,不归宿主", () => {
|
|
25
30
|
const html = renderToStaticMarkup(
|
|
26
31
|
<App
|
|
27
32
|
data={dataWithShell({
|
|
@@ -33,19 +38,75 @@ describe("外壳:品牌位、hero 标题与 ReportLink.icon", () => {
|
|
|
33
38
|
reportPages={reportPages}
|
|
34
39
|
/>,
|
|
35
40
|
);
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
expect(
|
|
39
|
-
expect(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
expect(
|
|
41
|
+
// 宿主 DOM 无品牌节点:没有字标、没有 Powered by、没有任何官网品牌链接。
|
|
42
|
+
expect(html).not.toContain('class="brand"');
|
|
43
|
+
expect(html).not.toContain("NiceEval");
|
|
44
|
+
expect(html).not.toContain("Powered by");
|
|
45
|
+
expect(html).not.toContain("niceeval.com");
|
|
46
|
+
// 宿主无 hero 区:标题只落浏览器 <title>(useEffect,静态渲染不执行),不落任何宿主节点。
|
|
47
|
+
expect(html).not.toContain('class="hero"');
|
|
48
|
+
expect(html).not.toContain("<h1");
|
|
49
|
+
expect(html).not.toContain("Memory Evals");
|
|
43
50
|
});
|
|
44
51
|
|
|
45
|
-
it("
|
|
46
|
-
const html = renderToStaticMarkup(
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
it("导航项 = 报告定义声明的页,按声明序;宿主不追加 Attempts / Traces 等任何项", () => {
|
|
53
|
+
const html = renderToStaticMarkup(
|
|
54
|
+
<App
|
|
55
|
+
data={dataWithShell({
|
|
56
|
+
title: "T",
|
|
57
|
+
links: [],
|
|
58
|
+
pages: [
|
|
59
|
+
{ id: "overview", title: { en: "Overview", "zh-CN": "总览" } },
|
|
60
|
+
{ id: "exam", title: { en: "Exam", "zh-CN": "成绩单" } },
|
|
61
|
+
],
|
|
62
|
+
initialPageId: "overview",
|
|
63
|
+
})}
|
|
64
|
+
reportPages={{
|
|
65
|
+
overview: { en: "<p>A</p>", "zh-CN": "<p>A</p>" },
|
|
66
|
+
exam: { en: "<p>B</p>", "zh-CN": "<p>B</p>" },
|
|
67
|
+
}}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
const triggers = html.match(/role="tab"/g) ?? [];
|
|
71
|
+
expect(triggers).toHaveLength(2); // 恰为声明的两页,无宿主追加项
|
|
72
|
+
expect(html.indexOf("Overview")).toBeLessThan(html.indexOf("Exam")); // 声明序
|
|
73
|
+
expect(html).not.toContain("#/attempts");
|
|
74
|
+
expect(html).not.toContain("#/traces");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("裸 view(内建报告三页声明)导航恰为 报告 · Attempts · 追踪,来自页列表而非宿主", () => {
|
|
78
|
+
const html = renderToStaticMarkup(
|
|
79
|
+
<App
|
|
80
|
+
data={dataWithShell({
|
|
81
|
+
title: { en: "Eval Results", "zh-CN": "Eval 运行结果" },
|
|
82
|
+
links: [],
|
|
83
|
+
pages: [
|
|
84
|
+
{ id: "report", title: { en: "Report", "zh-CN": "报告" } },
|
|
85
|
+
{ id: "attempts", title: "Attempts" },
|
|
86
|
+
{ id: "traces", title: { en: "Traces", "zh-CN": "追踪" } },
|
|
87
|
+
],
|
|
88
|
+
initialPageId: "report",
|
|
89
|
+
})}
|
|
90
|
+
reportPages={reportPages}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
expect(html.match(/role="tab"/g)).toHaveLength(3);
|
|
94
|
+
for (const label of ["Report", "Attempts", "Traces"]) expect(html).toContain(label);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("树形态定义(单页 report)导航只有一项", () => {
|
|
98
|
+
const html = renderToStaticMarkup(
|
|
99
|
+
<App
|
|
100
|
+
data={dataWithShell({
|
|
101
|
+
title: "T",
|
|
102
|
+
links: [],
|
|
103
|
+
pages: [{ id: "report", title: { en: "Report", "zh-CN": "报告" } }],
|
|
104
|
+
initialPageId: "report",
|
|
105
|
+
})}
|
|
106
|
+
reportPages={{ report: reportPages.report }}
|
|
107
|
+
/>,
|
|
108
|
+
);
|
|
109
|
+
expect(html.match(/role="tab"/g)).toHaveLength(1);
|
|
49
110
|
});
|
|
50
111
|
|
|
51
112
|
it("ReportLink.icon 的内联 SVG 渲染在 label 前,原样内联", () => {
|
package/src/view/app/App.tsx
CHANGED
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { detectLocale, makeTranslator, persistLocale, setDocumentLocale } from "./i18n.ts";
|
|
3
3
|
import type { Locale, LocalizedText, ReportSlotHtml, Tab, ViewData, ViewReportPageMeta, ViewResult } from "./types.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { resultFromUrl } from "./lib/rows.ts";
|
|
5
5
|
import { parseAttemptHash, resolveAttemptLocator, unresolvedAttemptWarning } from "./lib/attempt-route.ts";
|
|
6
|
-
import { formatDateTime } from "./lib/format.ts";
|
|
7
|
-
import { CopyFixPrompt } from "./components/CopyControls.tsx";
|
|
8
|
-
import { SkippedRunsBanner } from "./components/SkippedRunsBanner.tsx";
|
|
9
6
|
import { AttemptModal } from "./components/AttemptModal.tsx";
|
|
10
7
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs.tsx";
|
|
11
|
-
import { AttemptsView } from "./pages/AttemptsPage.tsx";
|
|
12
|
-
import { TracesView } from "./pages/TracesPage.tsx";
|
|
13
8
|
|
|
14
|
-
//
|
|
15
|
-
// (路由 `#/page/<id>`,`--page <id>` 定初始页)
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{ id: "traces", label: "nav.traces" },
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
/** niceeval 官网;web 面 hero 下方恒含指向它的 `Powered by NiceEval` 一行,无关闭配置。 */
|
|
24
|
-
const NICEEVAL_SITE_URL = "https://niceeval.com";
|
|
9
|
+
// 导航组成只有一条规则(docs/feature/reports/view.md「页面构成」):导航项 = 报告定义声明的页,
|
|
10
|
+
// 按声明顺序排列(路由 `#/page/<id>`,`--page <id>` 定初始页)。宿主不追加、不保留任何导航项——
|
|
11
|
+
// 裸 view 的「报告 / Attempts / 追踪」三个 tab 就是内建报告的三页。页面里的 hero、品牌行、
|
|
12
|
+
// 选择警告都不是宿主渲染的:它们是页内的站点组件(Hero / PoweredBy / ScopeWarnings)。
|
|
13
|
+
// 宿主保留的只有机器:管线与路由、attempt 详情路由、文档单例(<title>)、语言切换
|
|
14
|
+
// (docs/feature/reports/architecture.md「宿主保留的只有机器」)。
|
|
25
15
|
|
|
26
16
|
/**
|
|
27
17
|
* LocalizedText 的确定回退(docs/feature/reports/library/shell.md):当前 locale → en →
|
|
@@ -61,23 +51,20 @@ function ReportSlot({ html }: { html: string }) {
|
|
|
61
51
|
return <div className="report-slot" dangerouslySetInnerHTML={markup} />;
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
/** `#/page/<id>`
|
|
54
|
+
/** `#/page/<id>` → tab 值;认不出返回 null(交给初始页兜底)。 */
|
|
65
55
|
function tabFromHash(hash: string, pages: ViewReportPageMeta[]): Tab | null {
|
|
66
56
|
const pageMatch = /^#\/page\/([a-z0-9-]+)$/.exec(hash);
|
|
67
57
|
if (pageMatch && pages.some((p) => p.id === pageMatch[1])) return `page:${pageMatch[1]}`;
|
|
68
|
-
if (hash === "#/attempts") return "attempts";
|
|
69
|
-
if (hash === "#/traces") return "traces";
|
|
70
58
|
return null;
|
|
71
59
|
}
|
|
72
60
|
|
|
73
|
-
/** tab 值 → hash 路由(报告页 `#/page/<id
|
|
61
|
+
/** tab 值 → hash 路由(报告页 `#/page/<id>`)。 */
|
|
74
62
|
function hashForTab(tab: Tab): string {
|
|
75
|
-
return
|
|
63
|
+
return `#/page/${tab.slice("page:".length)}`;
|
|
76
64
|
}
|
|
77
65
|
|
|
78
66
|
export function App({ data, reportPages }: { data: ViewData; reportPages: Record<string, ReportSlotHtml> }) {
|
|
79
67
|
const snapshots = data.snapshots ?? [];
|
|
80
|
-
const attempts = useMemo(() => flattenAttempts(snapshots), [snapshots]);
|
|
81
68
|
const [locale, setLocale] = useState<Locale>(() => detectLocale());
|
|
82
69
|
const t = useMemo(() => makeTranslator(locale), [locale]);
|
|
83
70
|
|
|
@@ -99,9 +86,9 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
|
|
|
99
86
|
persistLocale(locale);
|
|
100
87
|
}, [locale]);
|
|
101
88
|
|
|
102
|
-
//
|
|
103
|
-
// 内置文案「Eval 运行结果 / Eval Results」);缺声明(旧数据)时按内置文案兜底。
|
|
104
|
-
//
|
|
89
|
+
// 浏览器标题是宿主文档单例:跟随外壳标题(回退链在 server 侧走完:def.title →
|
|
90
|
+
// 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」);缺声明(旧数据)时按内置文案兜底。
|
|
91
|
+
// 页面里的 hero 标题不归宿主——它是页内 Hero 组件,同一取值链经 ctx.report.title 贯通。
|
|
105
92
|
const shellTitle = localizedText(data.report?.title, locale) ?? t("hero.title");
|
|
106
93
|
useEffect(() => {
|
|
107
94
|
document.title = shellTitle;
|
|
@@ -123,7 +110,9 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
|
|
|
123
110
|
}, []);
|
|
124
111
|
|
|
125
112
|
// 浏览器前进/后退、手改 hash、页内链接(attempt 深链与 `#/page/<id>` 页路由)统一从
|
|
126
|
-
// hashchange 分发:attempt hash
|
|
113
|
+
// hashchange 分发:attempt hash 开证据室弹窗,页 hash 切当前 tab。
|
|
114
|
+
// attempt 详情路由对完整结果根解析(viewData.snapshots 全量通道):被位置参数 / --experiment
|
|
115
|
+
// 收窄滤掉的 attempt 仍能经深链打开,报告里的证据引用不因页面过滤失效。
|
|
127
116
|
useEffect(() => {
|
|
128
117
|
const onHashChange = () => {
|
|
129
118
|
const locator = parseAttemptHash(location.hash);
|
|
@@ -162,23 +151,12 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
|
|
|
162
151
|
return (
|
|
163
152
|
<Tabs value={tab} onValueChange={(v) => selectTab(v as Tab)}>
|
|
164
153
|
<header className="topbar">
|
|
165
|
-
{/* 页头左端是恒定的 NiceEval 品牌字标(与 Powered by 行同族的产品品牌位),
|
|
166
|
-
报告定义不能覆盖或移除;报告 title 的落点是下方 hero 与浏览器标题。 */}
|
|
167
|
-
<a className="brand" href={hashForTab(`page:${initialPageId}`)}>
|
|
168
|
-
<span className="mark" />
|
|
169
|
-
<span>NiceEval</span>
|
|
170
|
-
</a>
|
|
171
154
|
<TabsList aria-label={t("nav.label")}>
|
|
172
155
|
{pages.map((page) => (
|
|
173
156
|
<TabsTrigger key={`page:${page.id}`} value={`page:${page.id}`}>
|
|
174
157
|
{localizedText(page.title, locale) ?? page.id}
|
|
175
158
|
</TabsTrigger>
|
|
176
159
|
))}
|
|
177
|
-
{EVIDENCE_TABS.map((item) => (
|
|
178
|
-
<TabsTrigger key={item.id} value={item.id}>
|
|
179
|
-
{t(item.label)}
|
|
180
|
-
</TabsTrigger>
|
|
181
|
-
))}
|
|
182
160
|
</TabsList>
|
|
183
161
|
{data.report?.links?.length ? (
|
|
184
162
|
<nav className="shell-links" aria-label="Links">
|
|
@@ -208,53 +186,14 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
|
|
|
208
186
|
</div>
|
|
209
187
|
</header>
|
|
210
188
|
<main>
|
|
211
|
-
<section className="hero">
|
|
212
|
-
{/* hero 标题 = 走完回退链的报告标题(与浏览器标题同源)。 */}
|
|
213
|
-
<h1>{shellTitle}</h1>
|
|
214
|
-
<div className="meta">
|
|
215
|
-
<span>
|
|
216
|
-
{/* viewData 只带原始值(ISO / number),这里按当前界面 locale 格式化。 */}
|
|
217
|
-
<b>{t("hero.lastRun")}</b> {data.lastRunAt ? formatDateTime(data.lastRunAt, locale) : t("hero.noRuns")}
|
|
218
|
-
</span>
|
|
219
|
-
{data.composedRuns > 0 ? (
|
|
220
|
-
// 报告槽是跨 run 合成的现刻水位,hero 如实标注合成来源(几个 run)。
|
|
221
|
-
<span>{t("hero.composedFrom", { count: data.composedRuns })}</span>
|
|
222
|
-
) : null}
|
|
223
|
-
</div>
|
|
224
|
-
{/* 品牌行:恒在 hero 之下、恒带官网链接,不占 footer 的语义位、没有关闭配置
|
|
225
|
-
(shell.md「行为约束」)。 */}
|
|
226
|
-
<span className="powered-by">
|
|
227
|
-
<a href={NICEEVAL_SITE_URL} target="_blank" rel="noreferrer">
|
|
228
|
-
Powered by NiceEval
|
|
229
|
-
</a>
|
|
230
|
-
</span>
|
|
231
|
-
</section>
|
|
232
|
-
|
|
233
|
-
<SkippedRunsBanner skippedRuns={data.skippedRuns ?? []} t={t} />
|
|
234
|
-
|
|
235
189
|
{pages.map((page) => (
|
|
236
190
|
<TabsContent key={`page:${page.id}`} value={`page:${page.id}`} id={`tab-page-${page.id}`}>
|
|
237
|
-
{/* 壳区:报告槽上方靠右的批量修复 prompt 按钮。失败清单从 viewData.snapshots
|
|
238
|
-
现算(latest 口径),默认报告与 --report 两种填充下都在。 */}
|
|
239
|
-
<div className="section-sub-head">
|
|
240
|
-
<span className="group-detail-label" />
|
|
241
|
-
<div className="controls">
|
|
242
|
-
<CopyFixPrompt snapshots={snapshots} t={t} />
|
|
243
|
-
</div>
|
|
244
|
-
</div>
|
|
245
191
|
{/* 报告槽:server 侧逐页渲染好的静态 HTML(含 <Style> 产物),按当前页与界面语言
|
|
246
|
-
摆放对应块;Scope
|
|
192
|
+
摆放对应块;hero、品牌行、Scope 警告、批量修复 prompt 都是页内组件,壳不再渲染。
|
|
247
193
|
attempt 深链是普通 <a href="#/attempt/…">,经 hashchange 打开证据室弹窗。 */}
|
|
248
194
|
<ReportSlot html={reportPages[page.id]?.[locale] || reportPages[page.id]?.en || ""} />
|
|
249
195
|
</TabsContent>
|
|
250
196
|
))}
|
|
251
|
-
|
|
252
|
-
<TabsContent value="attempts">
|
|
253
|
-
<AttemptsView attempts={attempts} t={t} />
|
|
254
|
-
</TabsContent>
|
|
255
|
-
<TabsContent value="traces">
|
|
256
|
-
<TracesView attempts={attempts} t={t} />
|
|
257
|
-
</TabsContent>
|
|
258
197
|
</main>
|
|
259
198
|
{footerText ? (
|
|
260
199
|
<footer className="site-footer">
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import { Check, Copy } from "lucide-react";
|
|
3
3
|
import type { T } from "../shared.ts";
|
|
4
|
-
import type { ViewResult
|
|
5
|
-
import { snapshotLabel } from "../lib/rows.ts";
|
|
4
|
+
import type { ViewResult } from "../types.ts";
|
|
6
5
|
import { reasonFor } from "../lib/verdict.ts";
|
|
7
6
|
|
|
8
7
|
/** 修复 prompt 的一条失败条目;路径均相对 view 输入根(默认 `.niceeval/`)。 */
|
|
@@ -63,46 +62,9 @@ export function buildFixPrompt(entries: FixPromptEntry[]): string {
|
|
|
63
62
|
].join("\n");
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* 默认报告与 --report 两种填充下按钮都在,不依赖任何统计产物。
|
|
70
|
-
*/
|
|
71
|
-
export function fixPromptEntries(snapshots: ViewSnapshot[]): FixPromptEntry[] {
|
|
72
|
-
return snapshots
|
|
73
|
-
.filter((s) => s.latest)
|
|
74
|
-
.flatMap((snapshot) =>
|
|
75
|
-
snapshot.results
|
|
76
|
-
.filter((r: ViewResult) => r.verdict === "failed" || r.verdict === "errored")
|
|
77
|
-
.map((r: ViewResult) => toFixPromptEntry(r, snapshotLabel(snapshot))),
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function CopyFixPrompt({ snapshots, t }: { snapshots: ViewSnapshot[]; t: T }) {
|
|
82
|
-
const [copied, setCopied] = useState(false);
|
|
83
|
-
|
|
84
|
-
const entries = fixPromptEntries(snapshots);
|
|
85
|
-
|
|
86
|
-
if (!entries.length) return null;
|
|
87
|
-
|
|
88
|
-
const copy = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
89
|
-
event.stopPropagation();
|
|
90
|
-
try {
|
|
91
|
-
await copyText(buildFixPrompt(entries));
|
|
92
|
-
setCopied(true);
|
|
93
|
-
setTimeout(() => setCopied(false), 1500);
|
|
94
|
-
} catch {
|
|
95
|
-
setCopied(false);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<button className={`copy-all-errors${copied ? " is-copied" : ""}`} onClick={copy} title={t("action.copyPrompt")}>
|
|
101
|
-
{copied ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
|
|
102
|
-
<span>{copied ? t("action.copied") : `${t("action.copyPrompt")} (${entries.length})`}</span>
|
|
103
|
-
</button>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
65
|
+
// 批量修复 prompt 不再由壳渲染:它是内建报告首页里的 CopyFixPrompt 组件
|
|
66
|
+
// (niceeval/report 的站点组件,prompt 在 resolve 期烘进静态 HTML)。这里只留
|
|
67
|
+
// attempt 弹窗的单条版。
|
|
106
68
|
|
|
107
69
|
/** attempt 弹窗里的单条版:只打包当前 attempt 的失败,供逐条转交 agent。 */
|
|
108
70
|
export function CopyAttemptPrompt({ result, t }: { result: ViewResult; t: T }) {
|