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
@@ -1,17 +1,20 @@
1
- // ExperimentList:实体列表的第一级。web 面是一行一个 experiment 的固定列比较表;
2
- // 每行用原生 <details> 展开到 Eval Attempt locator。数据仍完全来自
3
- // ExperimentList.data(),不恢复旧 ExperimentTable 的混合实体计算层。
1
+ // ExperimentList:实体列表的第一级。web 面是一行一个 experiment 的固定八列比较表
2
+ // (Experiment / Model / Agent / Avg duration / End-to-end pass rate / Tokens / Cost / Result),
3
+ // 每行用原生 <details> 展开到 Eval 与 Attempt locator。数据完全来自 experimentListData(),
4
+ // 组件不重算、不推断组边界。
4
5
 
5
6
  import type { ReactElement } from "react";
6
7
  import type { AttemptLocator } from "../../results/locator.ts";
7
8
  import type { AttemptListItem, ExperimentListEvalRow, ExperimentListItem } from "../types.ts";
8
- import { attemptItemReason, experimentDisplayName } from "../format.ts";
9
+ import { experimentDisplayName } from "../format.ts";
9
10
  import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
10
- import { AttemptLocatorBadge } from "./AttemptList.tsx";
11
+ import { AttemptLocatorBadge, failureSummaryText } from "./AttemptList.tsx";
11
12
  import { MetricCellView } from "./cell.tsx";
12
13
  import { colorClassForKey } from "./colors.ts";
13
14
  import { cx, formatDurationMs, formatUSD, verdictMark } from "./format.ts";
14
15
 
16
+ const DEFAULT_ATTEMPT_HREF = (locator: AttemptLocator): string => `#/attempt/${locator}`;
17
+
15
18
  const verdictOrder = ["passed", "failed", "errored", "skipped"] as const;
16
19
 
17
20
  function passRateTone(value: number | null): string | undefined {
@@ -34,8 +37,8 @@ function formatDate(value: string, locale: ReportLocale): string {
34
37
 
35
38
  function VerdictSummary({ item, locale }: { item: ExperimentListItem; locale: ReportLocale }): ReactElement {
36
39
  const parts = verdictOrder
37
- .filter((verdict) => item.verdicts[verdict] > 0)
38
- .map((verdict) => `${item.verdicts[verdict]} ${localeText(locale, `verdict.${verdict}`)}`);
40
+ .filter((verdict) => item.evalVerdicts[verdict] > 0)
41
+ .map((verdict) => `${item.evalVerdicts[verdict]} ${localeText(locale, `verdict.${verdict}`)}`);
39
42
  return <span className="nre-experiment-pill">{parts.join(" / ") || "—"}</span>;
40
43
  }
41
44
 
@@ -43,12 +46,14 @@ function ExperimentAttemptRow({
43
46
  attempt,
44
47
  last,
45
48
  attemptHref,
49
+ locale,
46
50
  }: {
47
51
  attempt: AttemptListItem;
48
52
  last: boolean;
49
53
  attemptHref: (locator: AttemptLocator) => string;
54
+ locale: ReportLocale;
50
55
  }): ReactElement {
51
- const reason = attemptItemReason(attempt);
56
+ const reason = failureSummaryText(attempt, locale);
52
57
  return (
53
58
  <li className={cx("nre-experiment-attempt-row", `nre-eval-${attempt.verdict}`)}>
54
59
  <span className="nre-attempt-branch" aria-hidden="true">{last ? "└─" : "├─"}</span>
@@ -57,8 +62,9 @@ function ExperimentAttemptRow({
57
62
  </span>
58
63
  <span className="nre-eval-attempt-metrics">
59
64
  {formatDurationMs(attempt.durationMs)}
60
- {attempt.costUSD !== undefined && <> · {formatUSD(attempt.costUSD)}</>}
65
+ {attempt.costUSD !== null && <> · {formatUSD(attempt.costUSD)}</>}
61
66
  </span>
67
+ {/* passed attempt 的 Result 是 —,不罗列通过的 assertions */}
62
68
  <span className="nre-eval-reason">{reason ?? "—"}</span>
63
69
  </li>
64
70
  );
@@ -73,10 +79,11 @@ function EvalAttempts({
73
79
  attemptHref: (locator: AttemptLocator) => string;
74
80
  locale: ReportLocale;
75
81
  }): ReactElement {
76
- const duration = row.duration.value === null ? localeText(locale, "cell.missing") : formatDurationMs(row.duration.value);
77
- const cost = row.cost.value === null ? localeText(locale, "cell.missing") : formatUSD(row.cost.value);
82
+ const duration = row.durationMs.value === null ? localeText(locale, "cell.missing") : formatDurationMs(row.durationMs.value);
83
+ const cost = row.costUSD.value === null ? localeText(locale, "cell.missing") : formatUSD(row.costUSD.value);
78
84
  return (
79
85
  <li className="nre-experiment-eval">
86
+ {/* Eval 父行只显示折叠判定、Attempt 数与题级平均;失败原因只在 Attempt 子行 */}
80
87
  <div className={cx("nre-experiment-eval-header", `nre-eval-${row.verdict}`)}>
81
88
  <span className={cx("nre-eval-verdict", `nre-verdict-${row.verdict}`)}>{verdictMark(row.verdict)}</span>
82
89
  <span className="nre-eval-id">{row.evalId}</span>
@@ -94,6 +101,7 @@ function EvalAttempts({
94
101
  attempt={attempt}
95
102
  last={index === row.attempts.length - 1}
96
103
  attemptHref={attemptHref}
104
+ locale={locale}
97
105
  />
98
106
  ))}
99
107
  </ul>
@@ -107,7 +115,7 @@ function Flags({ flags, locale }: { flags: Record<string, unknown> | undefined;
107
115
  <div className="nre-experiment-flags">
108
116
  <span>{localeText(locale, "experimentList.flags")}</span>
109
117
  {Object.entries(flags).map(([key, value]) => (
110
- <b key={key}>{key}={String(value)}</b>
118
+ <b key={key}>{key}={typeof value === "string" ? value : JSON.stringify(value)}</b>
111
119
  ))}
112
120
  </div>
113
121
  );
@@ -127,6 +135,7 @@ function ExperimentRow({
127
135
  return (
128
136
  <details className="nre-experiment-entry">
129
137
  <summary className="nre-experiment-summary">
138
+ {/* relativeTo 只影响显示末段;完整 id 仍是排序 / 着色 / 过滤 / 折叠的键 */}
130
139
  <span className="nre-experiment-name" data-sort-value={item.experimentId}>
131
140
  <b className={cx("nre-experiment-id", "nre-key", colorClassForKey(item.experimentId))}>
132
141
  {experimentDisplayName(item.experimentId, relativeTo)}
@@ -139,19 +148,22 @@ function ExperimentRow({
139
148
  </span>
140
149
  <span data-sort-value={item.model ?? ""}>{item.model ?? localeText(locale, "experimentList.defaultModel")}</span>
141
150
  <span data-sort-value={item.agent}>{item.agent}</span>
142
- <span className="nre-num" data-sort-value={item.duration.value ?? ""}>
143
- <MetricCellView cell={item.duration} locale={locale} />
151
+ <span className="nre-num" data-sort-value={item.durationMs.value ?? ""}>
152
+ <MetricCellView cell={item.durationMs} locale={locale} />
144
153
  </span>
145
- <span className={cx("nre-num", passRateTone(item.passRate.value))} data-sort-value={item.passRate.value ?? ""}>
146
- <MetricCellView cell={item.passRate} locale={locale} />
154
+ <span
155
+ className={cx("nre-num", passRateTone(item.endToEndPassRate.value))}
156
+ data-sort-value={item.endToEndPassRate.value ?? ""}
157
+ >
158
+ <MetricCellView cell={item.endToEndPassRate} locale={locale} />
147
159
  </span>
148
160
  <span className="nre-num" data-sort-value={item.tokens.value ?? ""}>
149
161
  <MetricCellView cell={item.tokens} locale={locale} />
150
162
  </span>
151
- <span className="nre-num" data-sort-value={item.cost.value ?? ""}>
152
- <MetricCellView cell={item.cost} locale={locale} />
163
+ <span className="nre-num" data-sort-value={item.costUSD.value ?? ""}>
164
+ <MetricCellView cell={item.costUSD} locale={locale} />
153
165
  </span>
154
- <span data-sort-value={item.verdicts.passed}><VerdictSummary item={item} locale={locale} /></span>
166
+ <span data-sort-value={item.evalVerdicts.passed}><VerdictSummary item={item} locale={locale} /></span>
155
167
  </summary>
156
168
  <div className="nre-experiment-detail">
157
169
  <Flags flags={item.flags} locale={locale} />
@@ -166,15 +178,15 @@ function ExperimentRow({
166
178
  }
167
179
 
168
180
  export function ExperimentList({
169
- items,
170
- attemptHref,
181
+ data,
182
+ attemptHref = DEFAULT_ATTEMPT_HREF,
171
183
  filter = false,
172
184
  className,
173
185
  locale = DEFAULT_REPORT_LOCALE,
174
186
  relativeTo,
175
187
  }: {
176
- items: ExperimentListItem[];
177
- attemptHref: (locator: AttemptLocator) => string;
188
+ data: readonly ExperimentListItem[];
189
+ attemptHref?: (locator: AttemptLocator) => string;
178
190
  filter?: boolean;
179
191
  className?: string;
180
192
  locale?: ReportLocale;
@@ -187,7 +199,7 @@ export function ExperimentList({
187
199
  localeText(locale, "experimentList.avgDuration"),
188
200
  localeText(locale, "experimentList.passRate"),
189
201
  localeText(locale, "experimentList.tokens"),
190
- localeText(locale, "experimentList.estimatedCost"),
202
+ localeText(locale, "experimentList.cost"),
191
203
  localeText(locale, "experimentList.result"),
192
204
  ];
193
205
  const board = (
@@ -204,8 +216,8 @@ export function ExperimentList({
204
216
  </button>
205
217
  ))}
206
218
  </div>
207
- {items.length === 0 && <p className="nre-experiment-list-empty">{localeText(locale, "attemptList.empty")}</p>}
208
- {items.map((item) => (
219
+ {data.length === 0 && <p className="nre-experiment-list-empty">{localeText(locale, "attemptList.empty")}</p>}
220
+ {data.map((item) => (
209
221
  <ExperimentRow key={item.experimentId} item={item} attemptHref={attemptHref} locale={locale} relativeTo={relativeTo} />
210
222
  ))}
211
223
  </div>
@@ -6,7 +6,7 @@
6
6
  import type { ReactElement } from "react";
7
7
  import type { MatrixData, MetricCell } from "../types.ts";
8
8
  import type { AttemptLocator } from "../../results/locator.ts";
9
- import { DEFAULT_REPORT_LOCALE, resolveMetricLabel, type ReportLocale } from "../locale.ts";
9
+ import { DEFAULT_REPORT_LOCALE, resolveLocalizedText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
10
10
  import { colorClassForKey, seriesClassForKey } from "./colors.ts";
11
11
  import { cx } from "./format.ts";
12
12
 
@@ -52,7 +52,7 @@ export function MetricBars({
52
52
  className="nre-bars-svg"
53
53
  viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
54
54
  role="img"
55
- aria-label={`${metricLabel} by ${data.rows} × ${data.columns}`}
55
+ aria-label={`${metricLabel} by ${data.rowDimension} × ${data.columnDimension}`}
56
56
  >
57
57
  {groupKeys.map((group, gi) => {
58
58
  const x0 = PLOT.left + gi * groupWidth;
@@ -71,7 +71,8 @@ export function MetricBars({
71
71
  const h = Math.max(1, ratioOf(cell.value as number) * plotHeight);
72
72
  const x = startX + si * barWidth;
73
73
  const y = PLOT.bottom - h;
74
- const title = `${group} · ${series}: ${cell.display}(${cell.samples}/${cell.total})`;
74
+ const display = resolveLocalizedText(cell.display, locale);
75
+ const title = `${group} · ${series}: ${display}(${cell.samples}/${cell.total})`;
75
76
  const rect = (
76
77
  <rect
77
78
  className={cx("nre-bar", seriesClassForKey(series))}
@@ -89,7 +90,7 @@ export function MetricBars({
89
90
  {attemptHref && locator ? <a href={attemptHref(locator)}>{rect}</a> : rect}
90
91
  {/* 柱顶标数值;覆盖不全时把 samples/total 一并标出,不藏 */}
91
92
  <text className="nre-bar-value" x={x + barWidth / 2} y={y - 4} textAnchor="middle">
92
- {cell.samples < cell.total ? `${cell.display} ${cell.samples}/${cell.total}` : cell.display}
93
+ {cell.samples < cell.total ? `${display} ${cell.samples}/${cell.total}` : display}
93
94
  </text>
94
95
  </g>
95
96
  );
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ReactElement } from "react";
9
9
  import type { LineData } from "../types.ts";
10
- import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
10
+ import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveLocalizedText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
11
11
  import { seriesClassForKey } from "./colors.ts";
12
12
  import { cx } from "./format.ts";
13
13
 
@@ -52,6 +52,7 @@ export function MetricLine({
52
52
  const missing = data.rows.filter((r) => r.x === null || r.y.value === null);
53
53
  const drawableRows = data.rows.filter((r) => r.x !== null && r.y.value !== null);
54
54
  const yLabel = resolveMetricLabel(data.y.label, locale, data.y.key);
55
+ const xLabel = resolveMetricLabel(data.x.label, locale, data.x.key);
55
56
 
56
57
  const missingNote =
57
58
  missing.length > 0 ? (
@@ -83,7 +84,7 @@ export function MetricLine({
83
84
  series: r.series,
84
85
  xValue: r.x as number,
85
86
  yValue: r.y.value as number,
86
- title: `${r.key}\n${data.x.label}: ${r.xDisplay}\n${yLabel}: ${r.y.display}(${r.y.samples}/${r.y.total})`,
87
+ title: `${r.key}\n${xLabel}: ${resolveLocalizedText(r.xDisplay, locale)}\n${yLabel}: ${resolveLocalizedText(r.y.display, locale)}(${r.y.samples}/${r.y.total})`,
87
88
  px: xScale.scale(r.x as number),
88
89
  py: yScale.scale(r.y.value as number),
89
90
  }));
@@ -103,10 +104,14 @@ export function MetricLine({
103
104
 
104
105
  const xTicks = xScale.lo === xScale.hi ? [xScale.lo] : [xScale.lo, xScale.hi];
105
106
  const yTicks = yScale.lo === yScale.hi ? [yScale.lo] : [yScale.lo, yScale.hi];
106
- const xDisplayFor = (value: number) =>
107
- drawableRows.find((r) => r.x === value)?.xDisplay ?? String(value);
108
- const yDisplayFor = (value: number) =>
109
- drawableRows.find((r) => r.y.value === value)?.y.display ?? String(value);
107
+ const xDisplayFor = (value: number) => {
108
+ const row = drawableRows.find((r) => r.x === value);
109
+ return row === undefined ? String(value) : resolveLocalizedText(row.xDisplay, locale);
110
+ };
111
+ const yDisplayFor = (value: number) => {
112
+ const row = drawableRows.find((r) => r.y.value === value);
113
+ return row === undefined ? String(value) : resolveLocalizedText(row.y.display, locale);
114
+ };
110
115
 
111
116
  return (
112
117
  <figure className={cx("nre", "nre-metric-line", className)}>
@@ -114,7 +119,7 @@ export function MetricLine({
114
119
  className="nre-line-svg"
115
120
  viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
116
121
  role="img"
117
- aria-label={`${yLabel} by ${data.x.label}`}
122
+ aria-label={`${yLabel} by ${xLabel}`}
118
123
  >
119
124
  <rect
120
125
  className="nre-line-plot"
@@ -125,7 +130,7 @@ export function MetricLine({
125
130
  fill="none"
126
131
  />
127
132
  <text className="nre-line-xlabel" x={(PLOT.left + PLOT.right) / 2} y={HEIGHT - 8} textAnchor="middle">
128
- {data.x.label}
133
+ {xLabel}
129
134
  {data.x.unit ? `(${data.x.unit})` : ""}
130
135
  </text>
131
136
  <text
@@ -40,13 +40,13 @@ export function MetricMatrix({
40
40
  {resolveMetricLabel(data.metric.label, locale, data.metric.key)}
41
41
  {data.metric.unit && <span className="nre-unit">({data.metric.unit})</span>}
42
42
  <span className="nre-matrix-axes">
43
- {data.rows} × {data.columns}
43
+ {data.rowDimension} × {data.columnDimension}
44
44
  </span>
45
45
  </caption>
46
46
  <thead>
47
47
  <tr>
48
48
  <th scope="col" className="nre-dimension">
49
- {data.rows}
49
+ {data.rowDimension}
50
50
  </th>
51
51
  {columnKeys.map((column) => (
52
52
  // 列键 = 维度键(如 agent):稳定散列上色,与 scatter 的线、DeltaTable 的行同色
@@ -1,5 +1,6 @@
1
1
  // MetricScatter:质量 × 成本 frontier 的积木,内联 SVG、零图表库、零 hooks。
2
- // 轴向随 better:"lower" 的轴反向画,「好」的角落恒在右上(成本轴 $20 → $0 就是这么来的);
2
+ // 轴向随 better:"lower" 的轴反向画(值大在左/下),「更好」恒指向右上;方向提示恒为
3
+ // 「越靠右上越好」且仅当两轴都声明 better 时显示;刻度显示真实值。
3
4
  // niceTicks 刻度 + 网格线;每个点直接标注(placePointLabels 候选位择优:避开其它标签、
4
5
  // 数据点与画布边界,离开左右紧邻位时补 leader line);
5
6
  // 同系列的点按 x 值排序连线,系列图例列在图下。x 或 y 为 null 的点不画,
@@ -10,7 +11,7 @@
10
11
  import type { ReactElement } from "react";
11
12
  import type { MetricColumn, ScatterData } from "../types.ts";
12
13
  import { formatMetricValue } from "../format.ts";
13
- import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
14
+ import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveLocalizedText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
14
15
  import { niceTicks, placePointLabels } from "./chart-math.ts";
15
16
  import { colorClassForKey, seriesClassForKey } from "./colors.ts";
16
17
  import { cx } from "./format.ts";
@@ -33,22 +34,54 @@ interface DrawablePoint {
33
34
  py: number;
34
35
  }
35
36
 
36
- /** 点的直接标签:experiment id 的末段(完整 id 在 <title> 里)。 */
37
- function pointLabel(key: string): string {
38
- return key.split("/").filter(Boolean).at(-1) ?? key;
37
+ /**
38
+ * 点的直接标签:末段在当前 data 中唯一才缩成末段;重名时逐步加长为能区分它们的最短
39
+ * 路径后缀(完整 id 与两轴值仍进 <title>)。
40
+ */
41
+ function pointLabels(keys: readonly string[]): Map<string, string> {
42
+ const segsOf = (key: string) => key.split("/").filter(Boolean);
43
+ const depth = new Map<string, number>(keys.map((key) => [key, 1]));
44
+ for (;;) {
45
+ const byLabel = new Map<string, string[]>();
46
+ for (const key of keys) {
47
+ const segs = segsOf(key);
48
+ const label = segs.slice(-Math.min(depth.get(key)!, segs.length)).join("/") || key;
49
+ byLabel.set(label, [...(byLabel.get(label) ?? []), key]);
50
+ }
51
+ let grew = false;
52
+ for (const group of byLabel.values()) {
53
+ if (group.length < 2) continue;
54
+ for (const key of group) {
55
+ const segs = segsOf(key);
56
+ if (depth.get(key)! < segs.length) {
57
+ depth.set(key, depth.get(key)! + 1);
58
+ grew = true;
59
+ }
60
+ }
61
+ }
62
+ if (!grew) {
63
+ const out = new Map<string, string>();
64
+ for (const key of keys) {
65
+ const segs = segsOf(key);
66
+ out.set(key, segs.slice(-Math.min(depth.get(key)!, segs.length)).join("/") || key);
67
+ }
68
+ return out;
69
+ }
70
+ }
39
71
  }
40
72
 
41
73
  /**
42
- * 一根轴:niceTicks 撑出整齐的值域,值 → 像素做线性映射;
43
- * better: "lower" 时反向,好的一端固定在右 / 上。
74
+ * 一根轴:niceTicks 撑出整齐的值域,值 → 像素做线性映射。轴方向跟随指标的 `better`:
75
+ * `better: "lower"` 的轴反向渲染(值大的一端在左 / 下),「更好」因此恒指向右与上;
76
+ * 刻度标签始终显示真实值,反向只改方向不改数字(docs/feature/reports/library/metric-views.md)。
44
77
  */
45
- function axisScale(values: number[], better: MetricColumn["better"], pixelLo: number, pixelHi: number) {
78
+ function axisScale(values: number[], pixelLo: number, pixelHi: number, invert: boolean) {
46
79
  const ticks = niceTicks(Math.min(...values), Math.max(...values), 5);
47
80
  const lo = ticks[0];
48
81
  const hi = ticks[ticks.length - 1];
49
82
  const scale = (v: number) => {
50
83
  let t = (v - lo) / (hi - lo || 1);
51
- if (better === "lower") t = 1 - t; // 反向:值越低越靠「好」的一端
84
+ if (invert) t = 1 - t;
52
85
  return pixelLo + t * (pixelHi - pixelLo);
53
86
  };
54
87
  return { ticks, scale };
@@ -91,9 +124,24 @@ export function MetricScatter({
91
124
 
92
125
  const axisLabel = (label: string, col: MetricColumn) => `${label}${col.unit ? `(${col.unit})` : ""}`;
93
126
 
94
- const xScale = axisScale(drawableRows.map((r) => r.x.value as number), data.x.better, MARGIN.left, MARGIN.left + PLOT_W);
95
- // y 像素轴向下增长:better:"higher" 高值在上 → 映射到 [bottom, top];"lower" 由 axisScale 反向后同样落到上方
96
- const yScale = axisScale(drawableRows.map((r) => r.y.value as number), data.y.better, MARGIN.top + PLOT_H, MARGIN.top);
127
+ // 轴方向跟随 better:lower 反向(值大在左 / ),higher 与未声明正向。
128
+ const xScale = axisScale(
129
+ drawableRows.map((r) => r.x.value as number),
130
+ MARGIN.left,
131
+ MARGIN.left + PLOT_W,
132
+ data.x.better === "lower",
133
+ );
134
+ // y 像素轴向下增长:正向 = 高值在上 → 映射到 [bottom, top];lower 反向 = 高值在下。
135
+ const yScale = axisScale(
136
+ drawableRows.map((r) => r.y.value as number),
137
+ MARGIN.top + PLOT_H,
138
+ MARGIN.top,
139
+ data.y.better === "lower",
140
+ );
141
+ const labelByKey = pointLabels(drawableRows.map((r) => r.key));
142
+ // 方向提示恒为「越靠右上越好」,仅当两轴都声明 better 时显示——任一轴未声明,
143
+ // 组件不猜「更好」朝哪边,整图无提示。
144
+ const showBetterHint = data.x.better !== undefined && data.y.better !== undefined;
97
145
 
98
146
  const points: DrawablePoint[] = drawableRows.map((r) => {
99
147
  const xValue = r.x.value as number;
@@ -101,12 +149,11 @@ export function MetricScatter({
101
149
  return {
102
150
  key: r.key,
103
151
  series: r.series,
104
- label: pointLabel(r.key),
152
+ label: labelByKey.get(r.key) ?? r.key,
105
153
  xValue,
106
154
  yValue,
107
- // hover 内容:experiment(点键)+ 系列(series,ExperimentComparison 传的是 agent 维度,
108
- // 有则加一行;无系列的散点没有这行)+ 两轴 display samples/total(docs/feature/reports/library.md「MetricScatter」行为清单)
109
- title: `${r.key}${r.series !== undefined ? `\n${r.series}` : ""}\n${xLabel}: ${r.x.display}(${r.x.samples}/${r.x.total})\n${yLabel}: ${r.y.display}(${r.y.samples}/${r.y.total})`,
155
+ // hover 内容:experiment(点键)+ 系列(有则加一行)+ 两轴 display 与 samples/total
156
+ title: `${r.key}${r.series !== undefined ? `\n${r.series}` : ""}\n${xLabel}: ${resolveLocalizedText(r.x.display, locale)}(${r.x.samples}/${r.x.total})\n${yLabel}: ${resolveLocalizedText(r.y.display, locale)}(${r.y.samples}/${r.y.total})`,
110
157
  px: xScale.scale(xValue),
111
158
  py: yScale.scale(yValue),
112
159
  };
@@ -149,10 +196,17 @@ export function MetricScatter({
149
196
  ))}
150
197
  </g>
151
198
 
152
- {/* 「好」的角落恒在右上:轴向已按 better 反转,这里只是把这句契约写在图上 */}
153
- <text className="nre-scatter-better-hint" x={MARGIN.left + PLOT_W - 6} y={MARGIN.top + 14} textAnchor="end">
154
- {localeText(locale, "scatter.betterHint")}
155
- </text>
199
+ {/* 轴向已随 better 反正,「更好」恒指向右上;提示只在两轴都声明 better 时出现 */}
200
+ {showBetterHint && (
201
+ <text
202
+ className="nre-scatter-better-hint"
203
+ x={MARGIN.left + PLOT_W - 6}
204
+ y={MARGIN.top + 14}
205
+ textAnchor="end"
206
+ >
207
+ {localeText(locale, "scatter.betterUpperRight")}
208
+ </text>
209
+ )}
156
210
 
157
211
  {/* 刻度:已格式化的整齐值(formatMetricValue 与计算侧同一套) */}
158
212
  <g className="nre-scatter-axis nre-scatter-axis-y">
@@ -1,43 +1,20 @@
1
1
  // MetricTable:行维度 × 指标列。没有实体下钻——要展开到 experiment 的 Eval 或 Eval 的
2
2
  // Attempt,用 ExperimentList / EvalList,这个组件只表达任意维度 × 任意指标。
3
- // 行按传入顺序渲染——排序发生在计算侧(tableData 的 sort 参数),组件不重排;
3
+ // 行按传入顺序渲染——排序发生在计算侧(metricTableData 的 sort 参数),组件不重排;
4
4
  // 静态 HTML 以数据侧预排的顺序呈现即完整。web 面额外输出渐进增强的 data 属性:
5
5
  // 所有表头带 data-nre-sort、格子带 data-sort-value,enhance.js 在场时点表头
6
6
  // 可就地重排(纯展示态交互,不改口径);filter 开时在表格前渲染过滤输入框。
7
- // meta 在场(rows: "experiment")时补 Model / Agent / Verdicts 列,列序对齐 view
8
- // 原生榜单:experiment、model、agent、指标列…、verdicts。
9
7
  // 列头以箭头标注 better 方向;samples < total 的格子带覆盖率角标;
10
8
  // 一组全 null 渲染成「缺数据」,绝不画 0(逻辑在 MetricCellView)。
11
9
 
12
10
  import type { ReactElement } from "react";
13
- import type { TableData, TableRowMeta } from "../types.ts";
11
+ import type { TableData } from "../types.ts";
14
12
  import type { AttemptLocator } from "../../results/locator.ts";
15
13
  import { DEFAULT_REPORT_LOCALE, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
16
14
  import { MetricCellView } from "./cell.tsx";
17
15
  import { colorClassForKey } from "./colors.ts";
18
16
  import { cx } from "./format.ts";
19
17
 
20
- /** verdict 计票 pill(「3 passed / 1 failed」):非零判定各一枚,全零如实空。 */
21
- function VerdictTally({
22
- verdicts,
23
- locale,
24
- }: {
25
- verdicts: NonNullable<TableRowMeta["verdicts"]>;
26
- locale: ReportLocale;
27
- }): ReactElement {
28
- const kinds = (["passed", "failed", "errored", "skipped"] as const).filter((k) => verdicts[k] > 0);
29
- return (
30
- <span className="nre-verdict-tally">
31
- {kinds.map((kind) => (
32
- <span key={kind} className={cx("nre-verdict-pill", `nre-verdict-${kind}`)}>
33
- {verdicts[kind]} {localeText(locale, `verdict.${kind}`)}
34
- </span>
35
- ))}
36
- {kinds.length === 0 && <span className="nre-missing">—</span>}
37
- </span>
38
- );
39
- }
40
-
41
18
  export function MetricTable({
42
19
  data,
43
20
  attemptHref,
@@ -51,27 +28,13 @@ export function MetricTable({
51
28
  className?: string;
52
29
  locale?: ReportLocale;
53
30
  }): ReactElement {
54
- const hasMeta = data.rows.some((row) => row.meta !== undefined);
55
- const hasModel = data.rows.some((row) => row.meta?.model !== undefined);
56
- const hasVerdicts = data.rows.some((row) => row.meta?.verdicts !== undefined);
57
-
58
31
  const table = (
59
32
  <table className={cx("nre", "nre-metric-table", !filter && className)}>
60
33
  <thead>
61
34
  <tr>
62
35
  <th scope="col" className="nre-dimension" data-nre-sort="">
63
- {data.dimension}
36
+ {data.rowDimension}
64
37
  </th>
65
- {hasMeta && hasModel && (
66
- <th scope="col" className="nre-meta-col" data-nre-sort="">
67
- {localeText(locale, "table.model")}
68
- </th>
69
- )}
70
- {hasMeta && (
71
- <th scope="col" className="nre-meta-col" data-nre-sort="">
72
- {localeText(locale, "table.agent")}
73
- </th>
74
- )}
75
38
  {data.columns.map((col) => (
76
39
  <th scope="col" key={col.key} className="nre-metric-col" data-nre-sort="">
77
40
  {resolveMetricLabel(col.label, locale, col.key)}
@@ -87,11 +50,6 @@ export function MetricTable({
87
50
  )}
88
51
  </th>
89
52
  ))}
90
- {hasVerdicts && (
91
- <th scope="col" className="nre-verdicts-col" data-nre-sort="">
92
- {localeText(locale, "table.verdicts")}
93
- </th>
94
- )}
95
53
  </tr>
96
54
  </thead>
97
55
  <tbody>
@@ -104,28 +62,7 @@ export function MetricTable({
104
62
  data-sort-value={row.key}
105
63
  >
106
64
  {row.key}
107
- {/* rows: "experiment" 专属:eval/attempt 数 + 最后运行时间,行键下的一行紧凑摘要
108
- (旧 ExperimentRow 的 "N eval results · N runs · 最后运行时间") */}
109
- {row.meta?.evals !== undefined && (
110
- <div className="nre-row-meta-sub">
111
- {localeText(locale, "overview.evalsCount", { n: row.meta.evals })}
112
- {row.meta.attempts !== undefined && row.meta.attempts > row.meta.evals
113
- ? ` · ${localeText(locale, "overview.attemptsCount", { n: row.meta.attempts })}`
114
- : ""}
115
- {row.meta.lastRunAt ? ` · ${localeText(locale, "latestRun", { run: row.meta.lastRunAt })}` : ""}
116
- </div>
117
- )}
118
65
  </th>
119
- {hasMeta && hasModel && (
120
- <td className="nre-td nre-meta-cell" data-sort-value={row.meta?.model ?? ""}>
121
- {row.meta?.model ?? <span className="nre-missing">—</span>}
122
- </td>
123
- )}
124
- {hasMeta && (
125
- <td className="nre-td nre-meta-cell" data-sort-value={row.meta?.agent ?? ""}>
126
- {row.meta?.agent ?? <span className="nre-missing">—</span>}
127
- </td>
128
- )}
129
66
  {data.columns.map((col) => {
130
67
  const cell = row.cells[col.key];
131
68
  return (
@@ -133,21 +70,12 @@ export function MetricTable({
133
70
  {cell ? (
134
71
  <MetricCellView cell={cell} attemptHref={attemptHref} locale={locale} />
135
72
  ) : (
136
- // 数据侧没给这个格子(理论上 tableData 不会缺列)——按空处理,不编数
73
+ // 数据侧没给这个格子(理论上 metricTableData 不会缺列)——按空处理,不编数
137
74
  <span className="nre-empty" />
138
75
  )}
139
76
  </td>
140
77
  );
141
78
  })}
142
- {hasVerdicts && (
143
- <td className="nre-td nre-verdicts-cell" data-sort-value={row.meta?.verdicts?.passed ?? ""}>
144
- {row.meta?.verdicts ? (
145
- <VerdictTally verdicts={row.meta.verdicts} locale={locale} />
146
- ) : (
147
- <span className="nre-missing">—</span>
148
- )}
149
- </td>
150
- )}
151
79
  </tr>
152
80
  ))}
153
81
  </tbody>
@@ -0,0 +1,86 @@
1
+ // ScopeSummary:一个范围的摘要卡——快照时间窗、experiment / eval / attempt 数、判定计票、
2
+ // 端到端成功率与总成本。data 恒携带 eval 级与 attempt 级两份计票;呈现 prop `votes`
3
+ // 只选择显示哪一级(默认 "eval"),不改变 data。成功率与总成本只渲染算好的 MetricCell,
4
+ // 不现场重算(docs/feature/reports/library/summaries.md「ScopeSummary」)。
5
+
6
+ import type { ReactElement } from "react";
7
+ import type { ScopeSummaryData, VerdictTally } from "../types.ts";
8
+ import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
9
+ import { MetricCellView } from "./cell.tsx";
10
+ import { cx } from "./format.ts";
11
+
12
+ function VerdictTallyView({ tally, locale }: { tally: VerdictTally; locale: ReportLocale }): ReactElement {
13
+ const kinds = (["passed", "failed", "errored", "skipped"] as const).filter((k) => tally[k] > 0);
14
+ return (
15
+ <span className="nre-verdict-tally">
16
+ {kinds.map((kind) => (
17
+ <span key={kind} className={cx("nre-verdict-pill", `nre-verdict-${kind}`)}>
18
+ {tally[kind]} {localeText(locale, `verdict.${kind}`)}
19
+ </span>
20
+ ))}
21
+ {kinds.length === 0 && <span className="nre-missing">—</span>}
22
+ </span>
23
+ );
24
+ }
25
+
26
+ export function ScopeSummary({
27
+ data,
28
+ votes = "eval",
29
+ className,
30
+ locale = DEFAULT_REPORT_LOCALE,
31
+ }: {
32
+ data: ScopeSummaryData;
33
+ /** 显示哪一级计票;默认 "eval"。data 恒携带两级,votes 只选择呈现。 */
34
+ votes?: "eval" | "attempt";
35
+ className?: string;
36
+ locale?: ReportLocale;
37
+ }): ReactElement {
38
+ const tally = votes === "attempt" ? data.attemptVerdicts : data.evalVerdicts;
39
+ return (
40
+ <div className={cx("nre", "nre-scope-summary", className)} data-votes={votes}>
41
+ <dl className="nre-scope-kpis">
42
+ <div className="nre-scope-kpi nre-scope-kpi-rate">
43
+ <dt>{localeText(locale, "scopeSummary.passRate")}</dt>
44
+ <dd>
45
+ <MetricCellView cell={data.endToEndPassRate} locale={locale} />
46
+ </dd>
47
+ </div>
48
+ <div className="nre-scope-kpi">
49
+ <dt>{localeText(locale, "scopeSummary.experiments")}</dt>
50
+ <dd>{data.experiments}</dd>
51
+ </div>
52
+ <div className="nre-scope-kpi">
53
+ <dt>{localeText(locale, "scopeSummary.evals")}</dt>
54
+ <dd>{data.evals}</dd>
55
+ </div>
56
+ <div className="nre-scope-kpi">
57
+ <dt>{localeText(locale, "scopeSummary.attempts")}</dt>
58
+ <dd>{data.attempts}</dd>
59
+ </div>
60
+ <div className="nre-scope-kpi nre-scope-kpi-verdicts">
61
+ <dt>{localeText(locale, votes === "attempt" ? "scopeSummary.votesAttempt" : "scopeSummary.votesEval")}</dt>
62
+ <dd>
63
+ <VerdictTallyView tally={tally} locale={locale} />
64
+ </dd>
65
+ </div>
66
+ <div className="nre-scope-kpi">
67
+ <dt>{localeText(locale, "scopeSummary.totalCost")}</dt>
68
+ <dd>
69
+ <MetricCellView cell={data.totalCostUSD} locale={locale} />
70
+ </dd>
71
+ </div>
72
+ </dl>
73
+ {/* 时间窗:贡献当前数据的快照 startedAt 范围;空范围不编造当前时间 */}
74
+ {data.range.latestStartedAt !== null && (
75
+ <p className="nre-scope-summary-range">
76
+ {data.range.earliestStartedAt !== null && data.range.earliestStartedAt !== data.range.latestStartedAt
77
+ ? localeText(locale, "scopeSummary.range", {
78
+ from: data.range.earliestStartedAt,
79
+ to: data.range.latestStartedAt,
80
+ })
81
+ : data.range.latestStartedAt}
82
+ </p>
83
+ )}
84
+ </div>
85
+ );
86
+ }