niceeval 0.6.2 → 0.7.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 (218) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +2 -2
  4. package/dist/i18n/zh-CN.d.ts +3 -3
  5. package/dist/report/aggregate.d.ts +32 -26
  6. package/dist/report/aggregate.js +157 -76
  7. package/dist/report/built-in/index.d.ts +2 -0
  8. package/dist/report/built-in/index.js +8 -0
  9. package/dist/report/components.d.ts +91 -164
  10. package/dist/report/components.js +377 -114
  11. package/dist/report/compute.d.ts +86 -73
  12. package/dist/report/compute.js +592 -432
  13. package/dist/report/flag.d.ts +28 -17
  14. package/dist/report/flag.js +86 -16
  15. package/dist/report/format.d.ts +11 -11
  16. package/dist/report/format.js +17 -15
  17. package/dist/report/index.d.ts +16 -17
  18. package/dist/report/index.js +20 -22
  19. package/dist/report/load.js +3 -2
  20. package/dist/report/locale.d.ts +49 -34
  21. package/dist/report/locale.js +106 -58
  22. package/dist/report/metrics.d.ts +10 -3
  23. package/dist/report/metrics.js +46 -12
  24. package/dist/report/primitives.d.ts +42 -15
  25. package/dist/report/primitives.js +135 -26
  26. package/dist/report/react/AttemptList.d.ts +10 -8
  27. package/dist/report/react/AttemptList.js +18 -10
  28. package/dist/report/react/DeltaTable.js +19 -18
  29. package/dist/report/react/EvalList.d.ts +3 -3
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +4 -2
  32. package/dist/report/react/ExperimentComparison.js +5 -4
  33. package/dist/report/react/ExperimentList.d.ts +3 -3
  34. package/dist/report/react/ExperimentList.js +16 -15
  35. package/dist/report/react/MetricBars.js +5 -4
  36. package/dist/report/react/MetricLine.js +12 -5
  37. package/dist/report/react/MetricMatrix.js +1 -1
  38. package/dist/report/react/MetricScatter.js +54 -17
  39. package/dist/report/react/MetricTable.js +2 -12
  40. package/dist/report/react/ScopeSummary.d.ts +10 -0
  41. package/dist/report/react/ScopeSummary.js +17 -0
  42. package/dist/report/react/Scoreboard.js +6 -6
  43. package/dist/report/react/cell.js +2 -2
  44. package/dist/report/react/fixtures.d.ts +5 -9
  45. package/dist/report/react/fixtures.js +105 -149
  46. package/dist/report/react/index.d.ts +15 -5
  47. package/dist/report/react/index.js +18 -7
  48. package/dist/report/report.d.ts +137 -20
  49. package/dist/report/report.js +261 -34
  50. package/dist/report/text/faces.d.ts +17 -19
  51. package/dist/report/text/faces.js +225 -157
  52. package/dist/report/text/plot.js +1 -1
  53. package/dist/report/text/table.js +2 -2
  54. package/dist/report/tree.d.ts +90 -40
  55. package/dist/report/tree.js +252 -94
  56. package/dist/report/types.d.ts +245 -300
  57. package/dist/report/types.js +4 -3
  58. package/dist/report/web.d.ts +21 -5
  59. package/dist/report/web.js +42 -16
  60. package/dist/results/select.d.ts +38 -16
  61. package/dist/results/select.js +73 -25
  62. package/dist/results/types.d.ts +38 -14
  63. package/dist/shared/aggregate.d.ts +3 -2
  64. package/dist/shared/aggregate.js +5 -4
  65. package/docs-site/zh/README.md +44 -0
  66. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  67. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  68. package/docs-site/zh/examples/index.mdx +50 -0
  69. package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
  70. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  71. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  72. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  73. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  74. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  75. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  76. package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
  77. package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
  78. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  79. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
  80. package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
  81. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  82. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
  83. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
  84. package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
  85. package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
  86. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
  87. package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
  88. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
  89. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
  90. package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
  91. package/docs-site/zh/index.mdx +23 -25
  92. package/docs-site/zh/introduction.mdx +8 -8
  93. package/docs-site/zh/reference/builtin-agents.mdx +5 -5
  94. package/docs-site/zh/reference/capabilities.mdx +6 -6
  95. package/docs-site/zh/reference/cli.mdx +9 -7
  96. package/docs-site/zh/reference/define-agent.mdx +1 -1
  97. package/docs-site/zh/reference/events.mdx +3 -3
  98. package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
  99. package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
  100. package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
  101. package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
  102. package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
  103. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  104. package/package.json +6 -2
  105. package/src/agents/index.ts +2 -2
  106. package/src/agents/openai-compat.ts +1 -1
  107. package/src/agents/streaming.ts +2 -2
  108. package/src/agents/types.ts +3 -3
  109. package/src/cli.ts +42 -23
  110. package/src/context/context.ts +1 -1
  111. package/src/context/session.test.ts +1 -1
  112. package/src/context/session.ts +1 -1
  113. package/src/i18n/en.ts +18 -16
  114. package/src/i18n/zh-CN.ts +16 -15
  115. package/src/report/aggregate.ts +175 -87
  116. package/src/report/built-in/index.tsx +9 -0
  117. package/src/report/components.tsx +625 -285
  118. package/src/report/compute.ts +717 -515
  119. package/src/report/dual-render.test.tsx +738 -1148
  120. package/src/report/flag.ts +97 -33
  121. package/src/report/format.ts +18 -22
  122. package/src/report/index.ts +113 -58
  123. package/src/report/load.ts +3 -2
  124. package/src/report/locale.ts +120 -69
  125. package/src/report/metrics.ts +42 -12
  126. package/src/report/primitives.tsx +190 -45
  127. package/src/report/react/AttemptList.tsx +32 -20
  128. package/src/report/react/DeltaTable.tsx +63 -45
  129. package/src/report/react/EvalList.tsx +0 -0
  130. package/src/report/react/ExperimentComparison.tsx +12 -7
  131. package/src/report/react/ExperimentList.tsx +38 -26
  132. package/src/report/react/MetricBars.tsx +5 -4
  133. package/src/report/react/MetricLine.tsx +13 -8
  134. package/src/report/react/MetricMatrix.tsx +2 -2
  135. package/src/report/react/MetricScatter.tsx +74 -20
  136. package/src/report/react/MetricTable.tsx +4 -76
  137. package/src/report/react/ScopeSummary.tsx +86 -0
  138. package/src/report/react/Scoreboard.tsx +28 -10
  139. package/src/report/react/cell.tsx +2 -2
  140. package/src/report/react/enhance.js +57 -5
  141. package/src/report/react/fixtures.ts +109 -156
  142. package/src/report/react/index.tsx +24 -39
  143. package/src/report/react/render.test.tsx +139 -104
  144. package/src/report/react/styles.css +181 -91
  145. package/src/report/report.test.ts +761 -1031
  146. package/src/report/report.ts +425 -47
  147. package/src/report/text/faces.ts +257 -164
  148. package/src/report/text/plot.ts +1 -1
  149. package/src/report/text/table.ts +2 -2
  150. package/src/report/tree.ts +362 -104
  151. package/src/report/types.ts +257 -287
  152. package/src/report/web.ts +63 -20
  153. package/src/results/attempt-evidence.test.ts +4 -4
  154. package/src/results/attempt-evidence.ts +5 -5
  155. package/src/results/copy.ts +6 -6
  156. package/src/results/host-equivalence.test.ts +26 -14
  157. package/src/results/index.ts +10 -4
  158. package/src/results/open.ts +8 -4
  159. package/src/results/results.test.ts +4 -3
  160. package/src/results/select.ts +104 -34
  161. package/src/results/types.ts +36 -14
  162. package/src/runner/feedback/human.test.ts +1 -1
  163. package/src/runner/run.ts +1 -1
  164. package/src/sandbox/cli-commands.ts +2 -2
  165. package/src/scoring/judge.test.ts +1 -1
  166. package/src/shared/aggregate.ts +5 -4
  167. package/src/show/compose.ts +50 -67
  168. package/src/show/index.ts +107 -56
  169. package/src/show/render.ts +43 -27
  170. package/src/show/report-host.test.ts +188 -0
  171. package/src/show/report-host.ts +375 -0
  172. package/src/show/show.test.ts +86 -36
  173. package/src/view/app/App.test.tsx +69 -0
  174. package/src/view/app/App.tsx +144 -48
  175. package/src/view/app/components/AttemptModal.tsx +324 -63
  176. package/src/view/app/components/CodeView.tsx +10 -4
  177. package/src/view/app/i18n.ts +31 -17
  178. package/src/view/app/lib/artifact-url.ts +14 -3
  179. package/src/view/app/main.tsx +13 -8
  180. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  181. package/src/view/app/types.ts +4 -1
  182. package/src/view/artifact-serving.test.ts +21 -1
  183. package/src/view/client-dist/app.css +1 -1
  184. package/src/view/client-dist/app.js +6 -6
  185. package/src/view/data.test.ts +9 -3
  186. package/src/view/data.ts +145 -49
  187. package/src/view/index.ts +48 -44
  188. package/src/view/server.ts +35 -15
  189. package/src/view/shared/types.ts +34 -5
  190. package/src/view/styles.css +224 -0
  191. package/src/view/view-report.test.ts +161 -57
  192. package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
  193. package/dist/report/built-ins/experiment-comparison.js +0 -119
  194. package/dist/report/built-ins/index.d.ts +0 -2
  195. package/dist/report/built-ins/index.js +0 -2
  196. package/dist/report/react/GroupSummary.d.ts +0 -8
  197. package/dist/report/react/GroupSummary.js +0 -8
  198. package/dist/report/react/RunOverview.d.ts +0 -8
  199. package/dist/report/react/RunOverview.js +0 -12
  200. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  201. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  202. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  203. package/docs-site/zh/example/showcase.mdx +0 -39
  204. package/src/report/built-in-user-parity.test.tsx +0 -597
  205. package/src/report/built-ins/experiment-comparison.tsx +0 -179
  206. package/src/report/built-ins/index.ts +0 -7
  207. package/src/report/react/GroupSummary.tsx +0 -66
  208. package/src/report/react/RunOverview.tsx +0 -109
  209. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  210. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  211. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  212. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  213. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  214. /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
  215. /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
  216. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
  217. /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
  218. /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
@@ -0,0 +1,69 @@
1
+ // cases: docs/engineering/unit-tests/reports/cases.md
2
+ // 「外壳、页面与 Tabs」分区——
3
+ // 品牌位恒为 NiceEval 字标(声明 title 也不覆盖)、hero 走标题回退链(与浏览器标题同源;
4
+ // document.title 由 useEffect 设置,静态渲染不执行,这里断言 hero 即断言同一个 shellTitle)、
5
+ // ReportLink.icon 渲染在 label 前(web 面)。契约:docs/feature/reports/library/shell.md「行为约束」。
6
+
7
+ import { renderToStaticMarkup } from "react-dom/server";
8
+ import { beforeAll, describe, expect, it } from "vitest";
9
+ import { App } from "./App.tsx";
10
+ import type { ViewData } from "./types.ts";
11
+
12
+ beforeAll(() => {
13
+ // App 的初始状态读 location(hash 路由 / 旧版 ?modal=);node 下静态渲染补最小 stub。
14
+ (globalThis as { location?: unknown }).location = { hash: "", search: "", pathname: "/" };
15
+ });
16
+
17
+ const reportPages = { report: { en: "<p>REPORT_BODY</p>", "zh-CN": "<p>REPORT_BODY</p>" } };
18
+
19
+ function dataWithShell(report: ViewData["report"]): ViewData {
20
+ return { composedRuns: 1, snapshots: [], ...(report !== undefined ? { report } : {}) };
21
+ }
22
+
23
+ describe("外壳:品牌位、hero 标题与 ReportLink.icon", () => {
24
+ it("声明 title 后品牌位仍是 NiceEval 字标;hero 显示走完回退链的报告标题", () => {
25
+ const html = renderToStaticMarkup(
26
+ <App
27
+ data={dataWithShell({
28
+ title: { en: "Memory Evals", "zh-CN": "记忆能力评测" },
29
+ links: [],
30
+ pages: [{ id: "report", title: "Report" }],
31
+ initialPageId: "report",
32
+ })}
33
+ reportPages={reportPages}
34
+ />,
35
+ );
36
+ // 品牌位:恒定的 NiceEval 字标,不吃报告 title。
37
+ const brand = html.match(/class="brand"[\s\S]*?<\/a>/)![0];
38
+ expect(brand).toContain(">NiceEval</span>");
39
+ expect(brand).not.toContain("Memory Evals");
40
+ // hero:报告 title(node 环境 locale 回退 en)。
41
+ const hero = html.match(/<h1>[\s\S]*?<\/h1>/)![0];
42
+ expect(hero).toContain("Memory Evals");
43
+ });
44
+
45
+ it("缺外壳声明(旧数据)时 hero 落内置文案 Eval Results,品牌位不变", () => {
46
+ const html = renderToStaticMarkup(<App data={dataWithShell(undefined)} reportPages={reportPages} />);
47
+ expect(html.match(/<h1>[\s\S]*?<\/h1>/)![0]).toContain("Eval Results");
48
+ expect(html.match(/class="brand"[\s\S]*?<\/a>/)![0]).toContain(">NiceEval</span>");
49
+ });
50
+
51
+ it("ReportLink.icon 的内联 SVG 渲染在 label 前,原样内联", () => {
52
+ const html = renderToStaticMarkup(
53
+ <App
54
+ data={dataWithShell({
55
+ title: "T",
56
+ links: [{ label: "GitHub", href: "https://example.com", icon: { svg: '<svg data-mark="gh"></svg>' } }],
57
+ pages: [{ id: "report", title: "Report" }],
58
+ initialPageId: "report",
59
+ })}
60
+ reportPages={reportPages}
61
+ />,
62
+ );
63
+ const link = html.match(/<a[^>]*href="https:\/\/example\.com"[\s\S]*?<\/a>/)![0];
64
+ const iconAt = link.indexOf('<svg data-mark="gh"></svg>');
65
+ const labelAt = link.indexOf("GitHub");
66
+ expect(iconAt).toBeGreaterThan(-1);
67
+ expect(labelAt).toBeGreaterThan(iconAt);
68
+ });
69
+ });
@@ -1,7 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { detectLocale, makeTranslator, persistLocale, setDocumentLocale } from "./i18n.ts";
3
- import type { MessageKey } from "./i18n.ts";
4
- import type { Locale, LocalizedText, ReportSlotHtml, Tab, ViewData, ViewResult } from "./types.ts";
3
+ import type { Locale, LocalizedText, ReportSlotHtml, Tab, ViewData, ViewReportPageMeta, ViewResult } from "./types.ts";
5
4
  import { flattenAttempts, resultFromUrl } from "./lib/rows.ts";
6
5
  import { parseAttemptHash, resolveAttemptLocator, unresolvedAttemptWarning } from "./lib/attempt-route.ts";
7
6
  import { formatDateTime } from "./lib/format.ts";
@@ -9,22 +8,34 @@ import { CopyFixPrompt } from "./components/CopyControls.tsx";
9
8
  import { SkippedRunsBanner } from "./components/SkippedRunsBanner.tsx";
10
9
  import { AttemptModal } from "./components/AttemptModal.tsx";
11
10
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs.tsx";
12
- import { RunsView } from "./pages/RunsPage.tsx";
11
+ import { AttemptsView } from "./pages/AttemptsPage.tsx";
13
12
  import { TracesView } from "./pages/TracesPage.tsx";
14
13
 
15
- // 首页 = 报告槽:恒在恒默认,裸跑填充内置默认报告,--report 整槽替换(两种填充走同一个 tab)
16
- // 证据室(Runs / Traces / AttemptModal) view 本体,任何填充下原样保留。
17
- export const navItems: { id: Tab; label: MessageKey }[] = [
18
- { id: "report", label: "nav.report" },
19
- { id: "runs", label: "nav.runs" },
14
+ // 导航组成固定(docs/feature/reports/view.md「页面构成」):报告页按声明顺序在前
15
+ // (路由 `#/page/<id>`,`--page <id>` 定初始页),内置的 Attempts、Traces 证据页恒排在
16
+ // 报告页之后——证据页由宿主拥有,报告定义不能移除或重排它们。
17
+ // 报告页的 tab 值带 `page:` 前缀,避免与证据页 id(attempts / traces)撞名。
18
+ const EVIDENCE_TABS: { id: Tab; label: "nav.attempts" | "nav.traces" }[] = [
19
+ { id: "attempts", label: "nav.attempts" },
20
20
  { id: "traces", label: "nav.traces" },
21
21
  ];
22
22
 
23
- /** config.name 解析成当前界面语言的一条文案:字符串原样返回,多语言映射按 locale 挑,挑不到回退 en / 第一条。 */
23
+ /** niceeval 官网;web hero 下方恒含指向它的 `Powered by NiceEval` 一行,无关闭配置。 */
24
+ const NICEEVAL_SITE_URL = "https://niceeval.com";
25
+
26
+ /**
27
+ * LocalizedText 的确定回退(docs/feature/reports/library/shell.md):当前 locale → en →
28
+ * 按 locale 键字典序的第一个非空值。字符串原样返回。
29
+ */
24
30
  export function localizedText(text: LocalizedText | undefined, locale: Locale): string | undefined {
25
31
  if (!text) return undefined;
26
32
  if (typeof text === "string") return text;
27
- return text[locale] ?? text.en ?? Object.values(text)[0];
33
+ if (text[locale]) return text[locale];
34
+ if (text.en) return text.en;
35
+ for (const key of Object.keys(text).sort()) {
36
+ if (text[key]) return text[key];
37
+ }
38
+ return undefined;
28
39
  }
29
40
 
30
41
  /** 初始 URL → 直接打开的 attempt:先认 #/attempt/@<locator> 深链,回退旧版 ?modal= 参数。 */
@@ -40,12 +51,43 @@ function modalResultFromLocation(snapshots: ViewData["snapshots"]): ViewResult |
40
51
  return resultFromUrl(snapshots);
41
52
  }
42
53
 
43
- export function App({ data, reportHtml }: { data: ViewData; reportHtml: ReportSlotHtml }) {
54
+ /**
55
+ * 报告槽:React 19 对 dangerouslySetInnerHTML 只比较对象身份,身份一变就无条件重设
56
+ * innerHTML(不再比对 __html 字符串值)。{__html} 必须 memo 住,否则 App 任意一次重渲染
57
+ * (开关 attempt 弹窗、切语言)都会重建槽内 DOM,丢掉用户展开的 <details>、排序和过滤状态。
58
+ */
59
+ function ReportSlot({ html }: { html: string }) {
60
+ const markup = useMemo(() => ({ __html: html }), [html]);
61
+ return <div className="report-slot" dangerouslySetInnerHTML={markup} />;
62
+ }
63
+
64
+ /** `#/page/<id>` / `#/attempts` / `#/traces` → tab 值;认不出返回 null(交给初始页兜底)。 */
65
+ function tabFromHash(hash: string, pages: ViewReportPageMeta[]): Tab | null {
66
+ const pageMatch = /^#\/page\/([a-z0-9-]+)$/.exec(hash);
67
+ 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
+ return null;
71
+ }
72
+
73
+ /** tab 值 → hash 路由(报告页 `#/page/<id>`,证据页 `#/attempts` / `#/traces`)。 */
74
+ function hashForTab(tab: Tab): string {
75
+ return tab.startsWith("page:") ? `#/page/${tab.slice("page:".length)}` : `#/${tab}`;
76
+ }
77
+
78
+ export function App({ data, reportPages }: { data: ViewData; reportPages: Record<string, ReportSlotHtml> }) {
44
79
  const snapshots = data.snapshots ?? [];
45
80
  const attempts = useMemo(() => flattenAttempts(snapshots), [snapshots]);
46
81
  const [locale, setLocale] = useState<Locale>(() => detectLocale());
47
82
  const t = useMemo(() => makeTranslator(locale), [locale]);
48
- const [tab, setTab] = useState<Tab>("report");
83
+
84
+ // 报告声明缺省(旧数据 / 空烘焙)时按单页 `report` 兜底,页名用内置页名。
85
+ const pages: ViewReportPageMeta[] = data.report?.pages?.length
86
+ ? data.report.pages
87
+ : [{ id: "report", title: { en: "Report", "zh-CN": "报告" } }];
88
+ const initialPageId = data.report?.initialPageId ?? pages[0]!.id;
89
+
90
+ const [tab, setTab] = useState<Tab>(() => tabFromHash(location.hash, pages) ?? `page:${initialPageId}`);
49
91
  const [modalResult, setModalResult] = useState<ViewResult | null>(() => modalResultFromLocation(snapshots));
50
92
  // 当前 modal 的 hash 历史条目前面是否还有本页条目(本页 push 的 / 前进键回到的):
51
93
  // true → UI 关闭走 history.back(),前进键还能重新打开;false(深链直接落地)→ 原地抹 hash,
@@ -57,6 +99,14 @@ export function App({ data, reportHtml }: { data: ViewData; reportHtml: ReportSl
57
99
  persistLocale(locale);
58
100
  }, [locale]);
59
101
 
102
+ // 首页 hero 与浏览器标题跟随外壳标题(回退链在 server 侧走完:def.title → 唯一快照 name →
103
+ // 内置文案「Eval 运行结果 / Eval Results」);缺声明(旧数据)时按内置文案兜底。
104
+ // 页头品牌位不归它——那里是恒定的 NiceEval 字标。
105
+ const shellTitle = localizedText(data.report?.title, locale) ?? t("hero.title");
106
+ useEffect(() => {
107
+ document.title = shellTitle;
108
+ }, [shellTitle]);
109
+
60
110
  const closeModal = useCallback(() => {
61
111
  setModalResult(null);
62
112
  if (modalOwnsHistory.current) {
@@ -72,43 +122,77 @@ export function App({ data, reportHtml }: { data: ViewData; reportHtml: ReportSl
72
122
  }
73
123
  }, []);
74
124
 
75
- // 浏览器前进/后退、手改 hash、页内 attempt 链接(含报告槽里的深链)统一从 hashchange 开合 modal。
125
+ // 浏览器前进/后退、手改 hash、页内链接(attempt 深链与 `#/page/<id>` 页路由)统一从
126
+ // hashchange 分发:attempt hash 开证据室弹窗,页/证据室 hash 切当前 tab。
76
127
  useEffect(() => {
77
128
  const onHashChange = () => {
78
129
  const locator = parseAttemptHash(location.hash);
79
- if (!locator) {
80
- modalOwnsHistory.current = false;
81
- setModalResult(null);
130
+ if (locator) {
131
+ const found = resolveAttemptLocator(snapshots, locator);
132
+ if (!found) {
133
+ console.warn(unresolvedAttemptWarning(location.hash));
134
+ setModalResult(null);
135
+ return;
136
+ }
137
+ // 经浏览器导航打开:前一条历史仍是本页,UI 关闭可以安全 back()。
138
+ modalOwnsHistory.current = true;
139
+ setModalResult(found);
82
140
  return;
83
141
  }
84
- const found = resolveAttemptLocator(snapshots, locator);
85
- if (!found) {
86
- console.warn(unresolvedAttemptWarning(location.hash));
87
- setModalResult(null);
88
- return;
89
- }
90
- // 经浏览器导航打开:前一条历史仍是本页,UI 关闭可以安全 back()。
91
- modalOwnsHistory.current = true;
92
- setModalResult(found);
142
+ modalOwnsHistory.current = false;
143
+ setModalResult(null);
144
+ const routed = tabFromHash(location.hash, pages);
145
+ if (routed) setTab(routed);
93
146
  };
94
147
  window.addEventListener("hashchange", onHashChange);
95
148
  return () => window.removeEventListener("hashchange", onHashChange);
96
- }, [snapshots]);
149
+ }, [snapshots, pages]);
150
+
151
+ const selectTab = useCallback((value: Tab) => {
152
+ setTab(value);
153
+ try {
154
+ history.replaceState(null, "", hashForTab(value));
155
+ } catch {
156
+ // 更新路由失败不影响切页。
157
+ }
158
+ }, []);
159
+
160
+ const footerText = localizedText(data.report?.footer, locale);
97
161
 
98
162
  return (
99
- <Tabs value={tab} onValueChange={(v) => setTab(v as Tab)}>
163
+ <Tabs value={tab} onValueChange={(v) => selectTab(v as Tab)}>
100
164
  <header className="topbar">
101
- <a className="brand" href="https://github.com/CorrectRoadH/niceeval" target="_blank" rel="noreferrer">
165
+ {/* 页头左端是恒定的 NiceEval 品牌字标(与 Powered by 行同族的产品品牌位),
166
+ 报告定义不能覆盖或移除;报告 title 的落点是下方 hero 与浏览器标题。 */}
167
+ <a className="brand" href={hashForTab(`page:${initialPageId}`)}>
102
168
  <span className="mark" />
103
169
  <span>NiceEval</span>
104
170
  </a>
105
171
  <TabsList aria-label={t("nav.label")}>
106
- {navItems.map((item) => (
172
+ {pages.map((page) => (
173
+ <TabsTrigger key={`page:${page.id}`} value={`page:${page.id}`}>
174
+ {localizedText(page.title, locale) ?? page.id}
175
+ </TabsTrigger>
176
+ ))}
177
+ {EVIDENCE_TABS.map((item) => (
107
178
  <TabsTrigger key={item.id} value={item.id}>
108
179
  {t(item.label)}
109
180
  </TabsTrigger>
110
181
  ))}
111
182
  </TabsList>
183
+ {data.report?.links?.length ? (
184
+ <nav className="shell-links" aria-label="Links">
185
+ {data.report.links.map((link, i) => (
186
+ <a key={i} href={link.href} target="_blank" rel="noreferrer">
187
+ {/* 内联 SVG 字标(可选)渲染在 label 前,原样内联;内容是作者义务,宿主不校验形状之外的东西。 */}
188
+ {link.icon ? (
189
+ <span className="shell-link-icon" aria-hidden="true" dangerouslySetInnerHTML={{ __html: link.icon.svg }} />
190
+ ) : null}
191
+ {localizedText(link.label, locale) ?? link.href}
192
+ </a>
193
+ ))}
194
+ </nav>
195
+ ) : null}
112
196
  <div className="lang-switch" aria-label="Language">
113
197
  {(["en", "zh-CN"] satisfies Locale[]).map((item) => (
114
198
  <button
@@ -125,7 +209,8 @@ export function App({ data, reportHtml }: { data: ViewData; reportHtml: ReportSl
125
209
  </header>
126
210
  <main>
127
211
  <section className="hero">
128
- <h1>{localizedText(data.name, locale) || t("hero.title")}</h1>
212
+ {/* hero 标题 = 走完回退链的报告标题(与浏览器标题同源)。 */}
213
+ <h1>{shellTitle}</h1>
129
214
  <div className="meta">
130
215
  <span>
131
216
  {/* viewData 只带原始值(ISO / number),这里按当前界面 locale 格式化。 */}
@@ -136,35 +221,46 @@ export function App({ data, reportHtml }: { data: ViewData; reportHtml: ReportSl
136
221
  <span>{t("hero.composedFrom", { count: data.composedRuns })}</span>
137
222
  ) : null}
138
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>
139
231
  </section>
140
232
 
141
233
  <SkippedRunsBanner skippedRuns={data.skippedRuns ?? []} t={t} />
142
234
 
143
- <TabsContent value="report" id="tab-report">
144
- {/* 壳区:报告槽上方靠右的批量修复 prompt 按钮。失败清单从 viewData.snapshots
145
- 现算(latest 口径),默认报告与 --report 两种填充下都在。 */}
146
- <div className="section-sub-head">
147
- <span className="group-detail-label" />
148
- <div className="controls">
149
- <CopyFixPrompt snapshots={snapshots} t={t} />
235
+ {pages.map((page) => (
236
+ <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>
150
244
  </div>
151
- </div>
152
- {/* 报告槽:server 侧渲染好的静态 HTML(含 <Style> 产物),按当前界面语言摆放
153
- 对应语言的块;Selection 警告由报告槽内的 RunOverview 呈现,壳不设第二条通道。
154
- attempt 深链是普通 <a href="#/attempt/…">,经 hashchange 打开证据室弹窗。 */}
155
- <div
156
- className="report-slot"
157
- dangerouslySetInnerHTML={{ __html: reportHtml[locale] || reportHtml.en }}
158
- />
159
- </TabsContent>
245
+ {/* 报告槽:server 侧逐页渲染好的静态 HTML(含 <Style> 产物),按当前页与界面语言
246
+ 摆放对应块;Scope 警告由报告页内呈现,壳不设第二条通道。
247
+ attempt 深链是普通 <a href="#/attempt/…">,经 hashchange 打开证据室弹窗。 */}
248
+ <ReportSlot html={reportPages[page.id]?.[locale] || reportPages[page.id]?.en || ""} />
249
+ </TabsContent>
250
+ ))}
160
251
 
161
- <TabsContent value="runs">
162
- <RunsView attempts={attempts} t={t} />
252
+ <TabsContent value="attempts">
253
+ <AttemptsView attempts={attempts} t={t} />
163
254
  </TabsContent>
164
255
  <TabsContent value="traces">
165
256
  <TracesView attempts={attempts} t={t} />
166
257
  </TabsContent>
167
258
  </main>
259
+ {footerText ? (
260
+ <footer className="site-footer">
261
+ <span className="site-footer-text">{footerText}</span>
262
+ </footer>
263
+ ) : null}
168
264
  {modalResult && <AttemptModal result={modalResult} onClose={closeModal} t={t} />}
169
265
  </Tabs>
170
266
  );