niceeval 0.5.3 → 0.6.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.
Files changed (344) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +317 -0
  3. package/dist/context/types.d.ts +335 -0
  4. package/dist/i18n/core.d.ts +11 -0
  5. package/dist/i18n/en.d.ts +181 -0
  6. package/dist/i18n/index.d.ts +5 -0
  7. package/dist/i18n/zh-CN.d.ts +183 -0
  8. package/dist/o11y/execution-tree.d.ts +103 -0
  9. package/dist/o11y/otlp/parse.d.ts +3 -0
  10. package/dist/o11y/otlp/receiver.d.ts +21 -0
  11. package/dist/o11y/otlp/select.d.ts +22 -0
  12. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  13. package/dist/o11y/types.d.ts +190 -0
  14. package/dist/report/aggregate.d.ts +57 -0
  15. package/dist/report/aggregate.js +212 -0
  16. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
  17. package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
  18. package/dist/report/built-ins/index.d.ts +1 -0
  19. package/dist/report/built-ins/index.js +2 -0
  20. package/dist/report/components.d.ts +178 -0
  21. package/dist/report/components.js +132 -0
  22. package/dist/report/compute.d.ts +106 -0
  23. package/dist/report/compute.js +618 -0
  24. package/dist/report/flag.d.ts +6 -0
  25. package/dist/report/flag.js +8 -0
  26. package/dist/report/format.d.ts +32 -0
  27. package/dist/report/format.js +126 -0
  28. package/dist/report/index.d.ts +19 -0
  29. package/dist/report/index.js +30 -0
  30. package/dist/report/load.d.ts +12 -0
  31. package/dist/report/load.js +56 -0
  32. package/dist/report/locale.d.ts +83 -0
  33. package/dist/report/locale.js +160 -0
  34. package/dist/report/metrics.d.ts +21 -0
  35. package/dist/report/metrics.js +115 -0
  36. package/dist/report/primitives.d.ts +64 -0
  37. package/dist/report/primitives.js +113 -0
  38. package/dist/report/react/AttemptList.d.ts +23 -0
  39. package/dist/report/react/AttemptList.js +20 -0
  40. package/dist/report/react/DeltaTable.d.ts +8 -0
  41. package/dist/report/react/DeltaTable.js +24 -0
  42. package/dist/report/react/EvalList.d.ts +10 -0
  43. package/dist/report/react/EvalList.js +0 -0
  44. package/dist/report/react/ExperimentList.d.ts +10 -0
  45. package/dist/report/react/ExperimentList.js +15 -0
  46. package/dist/report/react/GroupSummary.d.ts +8 -0
  47. package/dist/report/react/GroupSummary.js +8 -0
  48. package/dist/report/react/MetricBars.d.ts +10 -0
  49. package/dist/report/react/MetricBars.js +48 -0
  50. package/dist/report/react/MetricLine.d.ts +9 -0
  51. package/dist/report/react/MetricLine.js +68 -0
  52. package/dist/report/react/MetricMatrix.d.ts +10 -0
  53. package/dist/report/react/MetricMatrix.js +23 -0
  54. package/dist/report/react/MetricScatter.d.ts +9 -0
  55. package/dist/report/react/MetricScatter.js +100 -0
  56. package/dist/report/react/MetricTable.d.ts +11 -0
  57. package/dist/report/react/MetricTable.js +26 -0
  58. package/dist/report/react/RunOverview.d.ts +8 -0
  59. package/dist/report/react/RunOverview.js +12 -0
  60. package/dist/report/react/Scoreboard.d.ts +8 -0
  61. package/dist/report/react/Scoreboard.js +26 -0
  62. package/dist/report/react/cell.d.ts +9 -0
  63. package/dist/report/react/cell.js +9 -0
  64. package/dist/report/react/chart-math.d.ts +14 -0
  65. package/dist/report/react/chart-math.js +68 -0
  66. package/dist/report/react/colors.d.ts +14 -0
  67. package/dist/report/react/colors.js +45 -0
  68. package/dist/report/react/fixtures.d.ts +19 -0
  69. package/dist/report/react/fixtures.js +393 -0
  70. package/dist/report/react/format.d.ts +3 -0
  71. package/dist/report/react/format.js +7 -0
  72. package/dist/report/react/index.d.ts +6 -0
  73. package/dist/report/react/index.js +14 -0
  74. package/dist/report/report.d.ts +24 -0
  75. package/dist/report/report.js +49 -0
  76. package/dist/report/text/faces.d.ts +29 -0
  77. package/dist/report/text/faces.js +503 -0
  78. package/dist/report/text/layout.d.ts +75 -0
  79. package/dist/report/text/layout.js +173 -0
  80. package/dist/report/text/plot.d.ts +37 -0
  81. package/dist/report/text/plot.js +81 -0
  82. package/dist/report/text/table.d.ts +8 -0
  83. package/dist/report/text/table.js +86 -0
  84. package/dist/report/tree.d.ts +99 -0
  85. package/dist/report/tree.js +228 -0
  86. package/dist/report/types.d.ts +420 -0
  87. package/dist/report/types.js +4 -0
  88. package/dist/report/web.d.ts +15 -0
  89. package/dist/report/web.js +37 -0
  90. package/dist/results/annotated-source.d.ts +61 -0
  91. package/dist/results/attempt-evidence.d.ts +69 -0
  92. package/dist/results/attempt-source.d.ts +15 -0
  93. package/dist/results/locator.d.ts +75 -0
  94. package/dist/results/locator.js +0 -0
  95. package/dist/results/select.d.ts +45 -0
  96. package/dist/results/select.js +230 -0
  97. package/dist/results/source-hash.d.ts +9 -0
  98. package/dist/results/types.d.ts +204 -0
  99. package/dist/runner/eval-source.d.ts +22 -0
  100. package/dist/runner/fingerprint.d.ts +23 -0
  101. package/dist/runner/types.d.ts +332 -0
  102. package/dist/sandbox/docker-stream.d.ts +24 -0
  103. package/dist/sandbox/docker.d.ts +84 -0
  104. package/dist/sandbox/e2b.d.ts +31 -0
  105. package/dist/sandbox/errors.d.ts +4 -0
  106. package/dist/sandbox/local-files.d.ts +6 -0
  107. package/dist/sandbox/paths.d.ts +4 -0
  108. package/dist/sandbox/registry.d.ts +13 -0
  109. package/dist/sandbox/resolve.d.ts +40 -0
  110. package/dist/sandbox/retry.d.ts +10 -0
  111. package/dist/sandbox/shell.d.ts +11 -0
  112. package/dist/sandbox/source-files.d.ts +13 -0
  113. package/dist/sandbox/types.d.ts +203 -0
  114. package/dist/sandbox/vercel.d.ts +37 -0
  115. package/dist/scoring/types.d.ts +70 -0
  116. package/dist/shared/aggregate.d.ts +29 -0
  117. package/dist/shared/aggregate.js +55 -0
  118. package/dist/shared/types.d.ts +27 -0
  119. package/dist/shared/verdict.d.ts +27 -0
  120. package/dist/shared/verdict.js +34 -0
  121. package/dist/tty-line.d.ts +6 -0
  122. package/dist/types.d.ts +7 -0
  123. package/dist/util.d.ts +22 -0
  124. package/docs-site/zh/concepts/adapter.mdx +9 -4
  125. package/docs-site/zh/concepts/assert.mdx +8 -8
  126. package/docs-site/zh/concepts/evals.mdx +5 -5
  127. package/docs-site/zh/concepts/experiment.mdx +3 -1
  128. package/docs-site/zh/concepts/judge.mdx +9 -9
  129. package/docs-site/zh/concepts/overview.mdx +2 -2
  130. package/docs-site/zh/concepts/tier.mdx +6 -6
  131. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  132. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  133. package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
  134. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  135. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  136. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  137. package/docs-site/zh/guides/experiments.mdx +7 -0
  138. package/docs-site/zh/guides/fixtures.mdx +1 -1
  139. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  140. package/docs-site/zh/guides/publish-report.mdx +4 -4
  141. package/docs-site/zh/guides/report-components.mdx +194 -55
  142. package/docs-site/zh/guides/reporters.mdx +2 -2
  143. package/docs-site/zh/guides/results-data.mdx +35 -34
  144. package/docs-site/zh/guides/runner.mdx +2 -2
  145. package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
  146. package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
  147. package/docs-site/zh/guides/viewing-results.mdx +199 -79
  148. package/docs-site/zh/guides/write-experiment.mdx +23 -1
  149. package/docs-site/zh/guides/write-send.mdx +2 -2
  150. package/docs-site/zh/index.mdx +4 -3
  151. package/docs-site/zh/introduction.mdx +2 -1
  152. package/docs-site/zh/quickstart.mdx +5 -3
  153. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  154. package/docs-site/zh/reference/capabilities.mdx +1 -1
  155. package/docs-site/zh/reference/cli.mdx +36 -13
  156. package/docs-site/zh/reference/define-agent.mdx +24 -12
  157. package/docs-site/zh/reference/define-config.mdx +1 -1
  158. package/docs-site/zh/reference/define-eval.mdx +3 -3
  159. package/docs-site/zh/reference/events.mdx +7 -1
  160. package/docs-site/zh/reference/expect.mdx +3 -3
  161. package/package.json +10 -6
  162. package/src/agents/bub.ts +134 -43
  163. package/src/agents/claude-code.test.ts +152 -0
  164. package/src/agents/claude-code.ts +127 -13
  165. package/src/agents/codex.test.ts +192 -0
  166. package/src/agents/codex.ts +151 -12
  167. package/src/agents/index.ts +11 -3
  168. package/src/agents/manifest.ts +41 -0
  169. package/src/agents/plugin-config.test.ts +94 -0
  170. package/src/agents/shared.ts +33 -2
  171. package/src/agents/skills.test.ts +214 -0
  172. package/src/agents/skills.ts +304 -0
  173. package/src/agents/streaming.ts +5 -5
  174. package/src/agents/types.ts +62 -2
  175. package/src/cli.ts +74 -47
  176. package/src/context/context.test.ts +20 -2
  177. package/src/context/context.ts +3 -0
  178. package/src/context/control-flow.ts +1 -1
  179. package/src/context/session.test.ts +92 -1
  180. package/src/context/session.ts +26 -1
  181. package/src/context/types.ts +4 -4
  182. package/src/define.test.ts +82 -0
  183. package/src/define.ts +72 -11
  184. package/src/expect/index.ts +5 -5
  185. package/src/i18n/en.ts +36 -15
  186. package/src/i18n/zh-CN.ts +39 -21
  187. package/src/index.ts +1 -1
  188. package/src/o11y/execution-tree.test.ts +452 -0
  189. package/src/o11y/execution-tree.ts +367 -0
  190. package/src/o11y/otlp/receiver.ts +1 -1
  191. package/src/o11y/otlp/select.ts +7 -3
  192. package/src/o11y/parsers/claude-code.test.ts +175 -0
  193. package/src/o11y/parsers/claude-code.ts +32 -0
  194. package/src/o11y/types.ts +6 -1
  195. package/src/report/aggregate.ts +26 -20
  196. package/src/report/built-in-user-parity.test.tsx +643 -0
  197. package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
  198. package/src/report/built-ins/index.ts +2 -0
  199. package/src/report/components.tsx +223 -50
  200. package/src/report/compute.ts +358 -53
  201. package/src/report/dual-render.test.tsx +1125 -0
  202. package/src/report/flag.ts +1 -1
  203. package/src/report/format.ts +50 -2
  204. package/src/report/index.ts +48 -14
  205. package/src/report/load.ts +1 -1
  206. package/src/report/locale.ts +206 -0
  207. package/src/report/metrics.ts +39 -15
  208. package/src/report/primitives.tsx +100 -2
  209. package/src/report/react/AttemptList.tsx +115 -0
  210. package/src/report/react/DeltaTable.tsx +9 -6
  211. package/src/report/react/EvalList.tsx +0 -0
  212. package/src/report/react/ExperimentList.tsx +108 -0
  213. package/src/report/react/GroupSummary.tsx +66 -0
  214. package/src/report/react/MetricBars.tsx +24 -13
  215. package/src/report/react/MetricLine.tsx +21 -21
  216. package/src/report/react/MetricMatrix.tsx +8 -4
  217. package/src/report/react/MetricScatter.tsx +136 -96
  218. package/src/report/react/MetricTable.tsx +115 -15
  219. package/src/report/react/RunOverview.tsx +48 -26
  220. package/src/report/react/Scoreboard.tsx +15 -7
  221. package/src/report/react/cell.tsx +18 -9
  222. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  223. package/src/report/react/colors.ts +24 -15
  224. package/src/report/react/enhance.js +129 -0
  225. package/src/report/react/fixtures.ts +163 -34
  226. package/src/report/react/format.ts +1 -1
  227. package/src/report/react/index.tsx +22 -6
  228. package/src/report/react/render.test.tsx +164 -31
  229. package/src/report/react/styles.css +405 -213
  230. package/src/report/report.test.ts +403 -94
  231. package/src/report/report.ts +29 -11
  232. package/src/report/text/faces.ts +303 -95
  233. package/src/report/text/layout.ts +76 -11
  234. package/src/report/text/table.ts +98 -0
  235. package/src/report/tree.ts +103 -20
  236. package/src/report/types.ts +196 -25
  237. package/src/report/web.ts +46 -19
  238. package/src/results/annotated-source.test.ts +146 -0
  239. package/src/results/annotated-source.ts +128 -0
  240. package/src/results/attempt-evidence.test.ts +158 -0
  241. package/src/results/attempt-evidence.ts +158 -0
  242. package/src/results/attempt-source.ts +48 -0
  243. package/src/results/copy.ts +154 -139
  244. package/src/results/format.ts +54 -47
  245. package/src/results/host-equivalence.test.ts +527 -0
  246. package/src/results/index.ts +56 -14
  247. package/src/results/locator.test.ts +198 -0
  248. package/src/results/locator.ts +0 -0
  249. package/src/results/open.ts +392 -175
  250. package/src/results/results.test.ts +943 -494
  251. package/src/results/select.ts +129 -23
  252. package/src/results/skipped-notice.ts +0 -0
  253. package/src/results/source-hash.ts +28 -0
  254. package/src/results/types.ts +98 -62
  255. package/src/results/writer.ts +304 -176
  256. package/src/runner/attempt.test.ts +170 -0
  257. package/src/runner/attempt.ts +97 -15
  258. package/src/runner/discover.test.ts +65 -0
  259. package/src/runner/discover.ts +6 -2
  260. package/src/runner/eval-source.test.ts +121 -0
  261. package/src/runner/eval-source.ts +45 -0
  262. package/src/runner/fingerprint.ts +54 -1
  263. package/src/runner/remote-sandbox.ts +1 -1
  264. package/src/runner/report.test.ts +6 -8
  265. package/src/runner/report.ts +5 -4
  266. package/src/runner/reporters/artifacts.ts +27 -41
  267. package/src/runner/reporters/braintrust.test.ts +3 -3
  268. package/src/runner/reporters/braintrust.ts +1 -1
  269. package/src/runner/reporters/console.ts +6 -6
  270. package/src/runner/reporters/json.ts +4 -4
  271. package/src/runner/reporters/live.test.ts +56 -0
  272. package/src/runner/reporters/live.ts +36 -10
  273. package/src/runner/reporters/quiet.test.ts +66 -0
  274. package/src/runner/reporters/quiet.ts +49 -0
  275. package/src/runner/reporters/shared.ts +5 -5
  276. package/src/runner/reporters/table.ts +17 -17
  277. package/src/runner/run.ts +61 -102
  278. package/src/runner/types.ts +43 -32
  279. package/src/sandbox/checkpoint.ts +1 -1
  280. package/src/sandbox/docker.ts +15 -2
  281. package/src/sandbox/e2b.ts +11 -5
  282. package/src/sandbox/errors.ts +13 -0
  283. package/src/sandbox/index.ts +3 -3
  284. package/src/sandbox/local-files.ts +1 -1
  285. package/src/sandbox/registry.ts +6 -5
  286. package/src/sandbox/resolve.ts +38 -23
  287. package/src/sandbox/retry.test.ts +68 -0
  288. package/src/sandbox/retry.ts +44 -0
  289. package/src/sandbox/types.ts +69 -28
  290. package/src/sandbox/vercel.ts +17 -3
  291. package/src/scoring/collector.ts +3 -3
  292. package/src/scoring/judge.ts +2 -2
  293. package/src/scoring/scoped.ts +13 -1
  294. package/src/scoring/types.ts +7 -7
  295. package/src/scoring/verdict.ts +4 -4
  296. package/src/shared/aggregate.ts +23 -3
  297. package/src/shared/types.ts +2 -2
  298. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  299. package/src/show/compose.ts +12 -132
  300. package/src/show/index.ts +125 -94
  301. package/src/show/render.ts +660 -160
  302. package/src/show/show.test.ts +472 -253
  303. package/src/source-loc.ts +1 -1
  304. package/src/tty-line.ts +32 -0
  305. package/src/types.ts +2 -2
  306. package/src/util.test.ts +21 -1
  307. package/src/util.ts +5 -1
  308. package/src/view/app/App.tsx +33 -198
  309. package/src/view/app/components/AttemptModal.tsx +6 -6
  310. package/src/view/app/components/CodeView.tsx +2 -2
  311. package/src/view/app/components/CopyControls.tsx +32 -36
  312. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  313. package/src/view/app/components/ui/badge.tsx +1 -1
  314. package/src/view/app/components/ui/dialog.tsx +2 -2
  315. package/src/view/app/i18n.ts +40 -31
  316. package/src/view/app/lib/artifact-url.ts +1 -1
  317. package/src/view/app/lib/attempt-route.test.ts +37 -47
  318. package/src/view/app/lib/attempt-route.ts +26 -26
  319. package/src/view/app/lib/rows.ts +9 -112
  320. package/src/view/app/lib/verdict.ts +25 -0
  321. package/src/view/app/main.tsx +9 -12
  322. package/src/view/app/pages/RunsPage.tsx +4 -4
  323. package/src/view/app/pages/TracesPage.tsx +3 -3
  324. package/src/view/app/shared.ts +0 -1
  325. package/src/view/app/types.ts +6 -51
  326. package/src/view/artifact-serving.test.ts +136 -0
  327. package/src/view/client-dist/app.css +1 -1
  328. package/src/view/client-dist/app.js +21 -21
  329. package/src/view/data.test.ts +214 -143
  330. package/src/view/data.ts +166 -120
  331. package/src/view/index.ts +23 -11
  332. package/src/view/server.ts +52 -17
  333. package/src/view/shared/types.ts +22 -31
  334. package/src/view/styles.css +117 -327
  335. package/src/view/view-report.test.ts +146 -98
  336. package/docs-site/zh/guides/sandbox-backends.mdx +0 -75
  337. package/src/report/default-report.tsx +0 -222
  338. package/src/report/dual-face.test.tsx +0 -527
  339. package/src/report/react/CaseList.tsx +0 -70
  340. package/src/view/app/components/CostScoreChart.tsx +0 -147
  341. package/src/view/app/components/ExperimentTable.tsx +0 -284
  342. package/src/view/app/components/GroupSelector.tsx +0 -61
  343. package/src/view/app/components/primitives.tsx +0 -43
  344. package/src/view/app/lib/outcome.ts +0 -82
@@ -1,54 +1,68 @@
1
- // MetricScatter:质量 × 成本 frontier 的积木,内联 SVG、零图表库。
1
+ // MetricScatter:质量 × 成本 frontier 的积木,内联 SVG、零图表库、零 hooks。
2
2
  // 轴向随 better:"lower" 的轴反向画,「好」的角落恒在右上(成本轴 $20 → $0 就是这么来的);
3
- // 同系列的点按 x 值排序连线,系列名标在线旁;x y 为 null 的点不画,
4
- // 底部注脚如实报「n 个点缺数据」;hover 信息退化为 SVG <title>,不 hydrate 也在。
3
+ // niceTicks 刻度 + 网格线;每个点直接标注(防撞布局,被挤开时补 leader line);
4
+ // 同系列的点按 x 值排序连线,系列图例列在图下。x y null 的点不画,
5
+ // 底部注脚如实报「n 个点缺数据」;hover 信息退化为 SVG <title>,不 hydrate 也在
6
+ // (enhance.js 在场时升级为样式化 tooltip)。配色走类名(nre-series-cN)由 CSS 上色,
7
+ // 深色主题下图表随令牌切换,不留内联 hex。
5
8
 
6
9
  import type { ReactElement } from "react";
7
10
  import type { MetricColumn, ScatterData } from "../types.ts";
8
- import { colorHexForKey } from "./colors.ts";
9
- import { MISSING_TEXT, cx } from "./format.ts";
11
+ import { formatMetricValue } from "../format.ts";
12
+ import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
13
+ import { layoutLabelOffsets, niceTicks } from "./chart-math.ts";
14
+ import { colorClassForKey, seriesClassForKey } from "./colors.ts";
15
+ import { cx } from "./format.ts";
10
16
 
11
- // 画布与边距:右侧留白给系列名,底部给 x 轴标签
12
17
  const WIDTH = 640;
13
- const HEIGHT = 400;
14
- const PLOT = { left: 64, right: WIDTH - 140, top: 24, bottom: HEIGHT - 56 };
18
+ const HEIGHT = 360;
19
+ const MARGIN = { top: 26, right: 24, bottom: 46, left: 62 };
20
+ const PLOT_W = WIDTH - MARGIN.left - MARGIN.right;
21
+ const PLOT_H = HEIGHT - MARGIN.top - MARGIN.bottom;
15
22
 
16
23
  /** 可画的点:x/y 都有值。组件内部的整理结果,不改数据。 */
17
24
  interface DrawablePoint {
18
25
  key: string;
19
26
  series?: string;
27
+ label: string;
20
28
  xValue: number;
21
29
  yValue: number;
22
- xDisplay: string;
23
- yDisplay: string;
24
30
  title: string;
25
31
  px: number;
26
32
  py: number;
27
33
  }
28
34
 
29
- /** 一根轴的线性映射:值 像素;better:"lower" 时反向,好的一端固定在右/上。 */
35
+ /** 点的直接标签:experiment id 的末段(完整 id 在 <title> 里)。 */
36
+ function pointLabel(key: string): string {
37
+ return key.split("/").filter(Boolean).at(-1) ?? key;
38
+ }
39
+
40
+ /**
41
+ * 一根轴:niceTicks 撑出整齐的值域,值 → 像素做线性映射;
42
+ * better: "lower" 时反向,好的一端固定在右 / 上。
43
+ */
30
44
  function axisScale(values: number[], better: MetricColumn["better"], pixelLo: number, pixelHi: number) {
31
- const lo = Math.min(...values);
32
- const hi = Math.max(...values);
33
- // 单值域:铺 ±1 让唯一的点落在正中,而不是除零
34
- const span = hi - lo || 2;
35
- const padded = { lo: lo - (hi - lo ? span * 0.08 : 1), hi: hi + (hi - lo ? span * 0.08 : 1) };
45
+ const ticks = niceTicks(Math.min(...values), Math.max(...values), 5);
46
+ const lo = ticks[0];
47
+ const hi = ticks[ticks.length - 1];
36
48
  const scale = (v: number) => {
37
- let t = (v - padded.lo) / (padded.hi - padded.lo);
49
+ let t = (v - lo) / (hi - lo || 1);
38
50
  if (better === "lower") t = 1 - t; // 反向:值越低越靠「好」的一端
39
51
  return pixelLo + t * (pixelHi - pixelLo);
40
52
  };
41
- return { lo, hi, scale };
53
+ return { ticks, scale };
42
54
  }
43
55
 
44
56
  export function MetricScatter({
45
57
  data,
46
58
  pointHref,
47
59
  className,
60
+ locale = DEFAULT_REPORT_LOCALE,
48
61
  }: {
49
62
  data: ScatterData;
50
63
  pointHref?: (row: ScatterData["rows"][number]) => string;
51
64
  className?: string;
65
+ locale?: ReportLocale;
52
66
  }): ReactElement {
53
67
  const missing = data.rows.filter((r) => r.x.value === null || r.y.value === null);
54
68
  const drawableRows = data.rows.filter((r) => r.x.value !== null && r.y.value !== null);
@@ -56,23 +70,39 @@ export function MetricScatter({
56
70
  const missingNote =
57
71
  missing.length > 0 ? (
58
72
  <p className="nre-scatter-missing" title={missing.map((r) => r.key).join(", ")}>
59
- {missing.length} {missing.length === 1 ? "point" : "points"} missing data
73
+ {countText(locale, "pointsMissing", missing.length)}
60
74
  </p>
61
75
  ) : null;
62
76
 
63
- // 一张全缺的图不画空坐标系:缺数据文案 + 注脚,与表格的「绝不画 0」同一态度
77
+ const xLabel = resolveMetricLabel(data.x.label, locale, data.x.key);
78
+ const yLabel = resolveMetricLabel(data.y.label, locale, data.y.key);
79
+
80
+ // 0 个可画点:x/y 指标没有可用数据 —— 明确说缺哪两个指标,不画一张空坐标系
81
+ // (与表格的「绝不画 0」同态度)。
64
82
  if (drawableRows.length === 0) {
65
83
  return (
66
84
  <figure className={cx("nre", "nre-metric-scatter", className)}>
67
- <p className="nre-missing">{MISSING_TEXT}</p>
85
+ <p className="nre-scatter-empty nre-missing">{localeText(locale, "scatter.noData", { x: xLabel, y: yLabel })}</p>
86
+ {missingNote}
87
+ </figure>
88
+ );
89
+ }
90
+
91
+ // 恰好 1 个可画点:成本 × 通过率的对比至少要两个实验,单点不成图 —— 显式说清而不是画个孤点。
92
+ if (drawableRows.length === 1) {
93
+ return (
94
+ <figure className={cx("nre", "nre-metric-scatter", className)}>
95
+ <p className="nre-scatter-empty">{localeText(locale, "scatter.needTwo", { x: xLabel, y: yLabel })}</p>
68
96
  {missingNote}
69
97
  </figure>
70
98
  );
71
99
  }
72
100
 
73
- const xScale = axisScale(drawableRows.map((r) => r.x.value as number), data.x.better, PLOT.left, PLOT.right);
101
+ const axisLabel = (label: string, col: MetricColumn) => `${label}${col.unit ? `(${col.unit})` : ""}`;
102
+
103
+ const xScale = axisScale(drawableRows.map((r) => r.x.value as number), data.x.better, MARGIN.left, MARGIN.left + PLOT_W);
74
104
  // y 像素轴向下增长:better:"higher" 高值在上 → 映射到 [bottom, top];"lower" 由 axisScale 反向后同样落到上方
75
- const yScale = axisScale(drawableRows.map((r) => r.y.value as number), data.y.better, PLOT.bottom, PLOT.top);
105
+ const yScale = axisScale(drawableRows.map((r) => r.y.value as number), data.y.better, MARGIN.top + PLOT_H, MARGIN.top);
76
106
 
77
107
  const points: DrawablePoint[] = drawableRows.map((r) => {
78
108
  const xValue = r.x.value as number;
@@ -80,12 +110,12 @@ export function MetricScatter({
80
110
  return {
81
111
  key: r.key,
82
112
  series: r.series,
113
+ label: pointLabel(r.key),
83
114
  xValue,
84
115
  yValue,
85
- xDisplay: r.x.display,
86
- yDisplay: r.y.display,
87
- // hover 内容:display samples/total(docs/reports.md 行为清单)
88
- title: `${r.key}\n${data.x.label}: ${r.x.display}(${r.x.samples}/${r.x.total})\n${data.y.label}: ${r.y.display}(${r.y.samples}/${r.y.total})`,
116
+ // hover 内容:experiment(点键)+ 系列(series,CostPassRateComparison 传的是 agent 维度,
117
+ // 有则加一行;无系列的散点没有这行)+ 两轴 display 与 samples/total(docs/feature/reports/library.md「MetricScatter」行为清单)
118
+ 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})`,
89
119
  px: xScale.scale(xValue),
90
120
  py: yScale.scale(yValue),
91
121
  };
@@ -104,14 +134,12 @@ export function MetricScatter({
104
134
  }
105
135
  for (const list of bySeries.values()) list.sort((a, b) => a.xValue - b.xValue);
106
136
 
107
- // 轴端刻度打在真实极值点的位置上(值域有 padding,角落不等于极值)
108
- const xTicks = xScale.lo === xScale.hi ? [xScale.lo] : [xScale.lo, xScale.hi];
109
- const yTicks = yScale.lo === yScale.hi ? [yScale.lo] : [yScale.lo, yScale.hi];
110
- const displayFor = (axis: "x" | "y", value: number) =>
111
- points.find((p) => (axis === "x" ? p.xValue : p.yValue) === value)?.[axis === "x" ? "xDisplay" : "yDisplay"] ??
112
- String(value);
113
-
114
- const axisLabel = (col: MetricColumn) => `${col.label}${col.unit ? `(${col.unit})` : ""}`;
137
+ // 直接标签的防撞布局:靠右的点把标签锚到左侧,重叠的往下推、补 leader line
138
+ const positions = points.map((p) => {
139
+ const anchorLeft = p.px >= MARGIN.left + PLOT_W * 0.72;
140
+ return { cx: p.px, cy: p.py, anchorLeft, width: p.label.length * 6.4 + 10 };
141
+ });
142
+ const labelOffsets = layoutLabelOffsets(positions);
115
143
 
116
144
  return (
117
145
  <figure className={cx("nre", "nre-metric-scatter", className)}>
@@ -119,98 +147,110 @@ export function MetricScatter({
119
147
  className="nre-scatter-svg"
120
148
  viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
121
149
  role="img"
122
- aria-label={`${axisLabel(data.x)} × ${axisLabel(data.y)}`}
150
+ aria-label={`${axisLabel(xLabel, data.x)} × ${axisLabel(yLabel, data.y)}`}
123
151
  >
124
- {/* 坐标框 */}
125
- <rect
126
- className="nre-scatter-plot"
127
- x={PLOT.left}
128
- y={PLOT.top}
129
- width={PLOT.right - PLOT.left}
130
- height={PLOT.bottom - PLOT.top}
131
- fill="none"
132
- stroke="#d4d4d4"
133
- />
152
+ {/* 网格:niceTicks 的整齐刻度线,颜色走 CSS(var(--line)) */}
153
+ <g className="nre-scatter-grid">
154
+ {yScale.ticks.map((tick) => (
155
+ <line key={`gy${tick}`} x1={MARGIN.left} x2={MARGIN.left + PLOT_W} y1={yScale.scale(tick)} y2={yScale.scale(tick)} />
156
+ ))}
157
+ {xScale.ticks.map((tick) => (
158
+ <line key={`gx${tick}`} y1={MARGIN.top} y2={MARGIN.top + PLOT_H} x1={xScale.scale(tick)} x2={xScale.scale(tick)} />
159
+ ))}
160
+ </g>
161
+
134
162
  {/* 「好」的角落恒在右上:轴向已按 better 反转,这里只是把这句契约写在图上 */}
135
- <text className="nre-scatter-better-hint" x={PLOT.right - 6} y={PLOT.top + 14} textAnchor="end" fontSize={11} fill="#9ca3af">
136
- better
163
+ <text className="nre-scatter-better-hint" x={MARGIN.left + PLOT_W - 6} y={MARGIN.top + 14} textAnchor="end">
164
+ {localeText(locale, "scatter.betterHint")}
137
165
  </text>
138
166
 
167
+ {/* 刻度:已格式化的整齐值(formatMetricValue 与计算侧同一套) */}
168
+ <g className="nre-scatter-axis nre-scatter-axis-y">
169
+ {yScale.ticks.map((tick) => (
170
+ <text key={`ay${tick}`} className="nre-scatter-tick" x={MARGIN.left - 8} y={yScale.scale(tick) + 3} textAnchor="end">
171
+ {formatMetricValue(tick, data.y.unit)}
172
+ </text>
173
+ ))}
174
+ </g>
175
+ <g className="nre-scatter-axis nre-scatter-axis-x">
176
+ {xScale.ticks.map((tick) => (
177
+ <text key={`ax${tick}`} className="nre-scatter-tick" x={xScale.scale(tick)} y={MARGIN.top + PLOT_H + 16} textAnchor="middle">
178
+ {formatMetricValue(tick, data.x.unit)}
179
+ </text>
180
+ ))}
181
+ </g>
182
+
139
183
  {/* 轴标签 */}
140
- <text className="nre-scatter-xlabel" x={(PLOT.left + PLOT.right) / 2} y={HEIGHT - 8} textAnchor="middle" fontSize={12} fill="#525252">
141
- {axisLabel(data.x)}
184
+ <text className="nre-scatter-xlabel" x={MARGIN.left + PLOT_W / 2} y={HEIGHT - 8} textAnchor="middle">
185
+ {axisLabel(xLabel, data.x)}
142
186
  </text>
143
187
  <text
144
188
  className="nre-scatter-ylabel"
145
- x={16}
146
- y={(PLOT.top + PLOT.bottom) / 2}
189
+ x={14}
190
+ y={MARGIN.top + PLOT_H / 2}
147
191
  textAnchor="middle"
148
- fontSize={12}
149
- fill="#525252"
150
- transform={`rotate(-90 16 ${(PLOT.top + PLOT.bottom) / 2})`}
192
+ transform={`rotate(-90 14 ${MARGIN.top + PLOT_H / 2})`}
151
193
  >
152
- {axisLabel(data.y)}
194
+ {axisLabel(yLabel, data.y)}
153
195
  </text>
154
196
 
155
- {/* 轴端刻度:标在真实极值的位置上,文案用该点已格式化的 display */}
156
- {xTicks.map((v) => (
157
- <text key={`x${v}`} className="nre-scatter-tick" x={xScale.scale(v)} y={PLOT.bottom + 16} textAnchor="middle" fontSize={11} fill="#737373">
158
- {displayFor("x", v)}
159
- </text>
160
- ))}
161
- {yTicks.map((v) => (
162
- <text key={`y${v}`} className="nre-scatter-tick" x={PLOT.left - 6} y={yScale.scale(v) + 4} textAnchor="end" fontSize={11} fill="#737373">
163
- {displayFor("y", v)}
164
- </text>
165
- ))}
166
-
167
- {/* 系列连线 + 线旁的系列名(标在视觉上最靠右的点旁) */}
197
+ {/* 系列连线:类名上色(nre-series-cN),深色主题跟随 */}
168
198
  {seriesOrder.map((series) => {
169
199
  const list = bySeries.get(series)!;
170
- const color = colorHexForKey(series);
171
- const labelAt = list.reduce((a, b) => (b.px > a.px ? b : a));
200
+ if (list.length < 2) return null;
172
201
  return (
173
- <g key={series} className="nre-scatter-series" data-series={series}>
174
- {list.length > 1 && (
175
- <polyline
176
- className="nre-scatter-line"
177
- points={list.map((p) => `${p.px},${p.py}`).join(" ")}
178
- fill="none"
179
- stroke={color}
180
- strokeWidth={1.5}
181
- />
182
- )}
183
- <text className="nre-scatter-series-label" x={labelAt.px + 8} y={labelAt.py + 4} fontSize={12} fill={color}>
184
- {series}
185
- </text>
186
- </g>
202
+ <polyline
203
+ key={series}
204
+ className={cx("nre-scatter-line", seriesClassForKey(series))}
205
+ data-series={series}
206
+ points={list.map((p) => `${p.px},${p.py}`).join(" ")}
207
+ fill="none"
208
+ />
187
209
  );
188
210
  })}
189
211
 
190
- {/* 点(带 <title> hover;pointHref 时包普通 <a>,静态导出也能下钻) */}
212
+ {/* 点:g 内带 <title>(无 JS 的原生 hover)、直接标签与 leader line;
213
+ pointHref 时包普通 <a>,静态导出也能下钻 */}
191
214
  {points.map((p, i) => {
192
- const circle = (
193
- <circle
194
- className="nre-scatter-point"
215
+ const { anchorLeft } = positions[i];
216
+ const labelX = p.px + (anchorLeft ? -10 : 10);
217
+ const labelY = p.py + 4 + labelOffsets[i];
218
+ const group = (
219
+ <g
220
+ className={cx("nre-scatter-point", p.series !== undefined ? seriesClassForKey(p.series) : "nre-series-none")}
195
221
  data-key={p.key}
196
- cx={p.px}
197
- cy={p.py}
198
- r={4.5}
199
- fill={p.series !== undefined ? colorHexForKey(p.series) : "#525252"}
200
222
  >
201
223
  <title>{p.title}</title>
202
- </circle>
224
+ {/* 标签被挤开时补一条 leader line,避免脱离原点看不出对应关系 */}
225
+ {labelOffsets[i] !== 0 && <line className="nre-leader" x1={p.px} y1={p.py} x2={labelX} y2={labelY - 4} />}
226
+ <circle className="nre-scatter-hit" cx={p.px} cy={p.py} r={12} />
227
+ <circle className="nre-scatter-dot" cx={p.px} cy={p.py} r={4.5} />
228
+ <text className="nre-scatter-point-label" x={labelX} y={labelY} textAnchor={anchorLeft ? "end" : "start"}>
229
+ {p.label}
230
+ </text>
231
+ </g>
203
232
  );
204
233
  const row = drawableRows[i];
205
234
  return pointHref ? (
206
235
  <a key={p.key} className="nre-scatter-point-link" href={pointHref(row)}>
207
- {circle}
236
+ {group}
208
237
  </a>
209
238
  ) : (
210
- <g key={p.key}>{circle}</g>
239
+ <g key={p.key}>{group}</g>
211
240
  );
212
241
  })}
213
242
  </svg>
243
+
244
+ {/* 系列图例:同键同色(与其它块的稳定散列一致) */}
245
+ {seriesOrder.length > 0 && (
246
+ <figcaption className="nre-scatter-legend">
247
+ {seriesOrder.map((series) => (
248
+ <span key={series} className={cx("nre-legend-key", "nre-key", colorClassForKey(series))}>
249
+ {series}
250
+ </span>
251
+ ))}
252
+ </figcaption>
253
+ )}
214
254
  {missingNote}
215
255
  </figure>
216
256
  );
@@ -1,71 +1,171 @@
1
- // MetricTable:行维度 × 指标列。
2
- // 行按传入顺序渲染——排序发生在计算侧(table() sort 参数),组件不重排;
3
- // 「点列头重排」是后续的渐进增强,不 hydrate 时以数据侧预排的顺序呈现即完整。
1
+ // MetricTable:行维度 × 指标列。没有实体下钻——要展开到 experiment 的 Eval 或 Eval 的
2
+ // Attempt,用 ExperimentList / EvalList,这个组件只表达任意维度 × 任意指标。
3
+ // 行按传入顺序渲染——排序发生在计算侧(tableData sort 参数),组件不重排;
4
+ // 静态 HTML 以数据侧预排的顺序呈现即完整。web 面额外输出渐进增强的 data 属性:
5
+ // 所有表头带 data-nre-sort、格子带 data-sort-value,enhance.js 在场时点表头
6
+ // 可就地重排(纯展示态交互,不改口径);filter 开时在表格前渲染过滤输入框。
7
+ // meta 在场(rows: "experiment")时补 Model / Agent / Verdicts 列,列序对齐 view
8
+ // 原生榜单:experiment、model、agent、指标列…、verdicts。
4
9
  // 列头以箭头标注 better 方向;samples < total 的格子带覆盖率角标;
5
10
  // 一组全 null 渲染成「缺数据」,绝不画 0(逻辑在 MetricCellView)。
6
11
 
7
12
  import type { ReactElement } from "react";
8
- import type { AttemptRef, TableData } from "../types.ts";
13
+ import type { TableData, TableRowMeta } from "../types.ts";
14
+ import type { AttemptLocator } from "../../results/locator.ts";
15
+ import { DEFAULT_REPORT_LOCALE, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
9
16
  import { MetricCellView } from "./cell.tsx";
10
17
  import { colorClassForKey } from "./colors.ts";
11
18
  import { cx } from "./format.ts";
12
19
 
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
+
13
41
  export function MetricTable({
14
42
  data,
15
43
  attemptHref,
44
+ filter,
16
45
  className,
46
+ locale = DEFAULT_REPORT_LOCALE,
17
47
  }: {
18
48
  data: TableData;
19
- attemptHref?: (ref: AttemptRef) => string;
49
+ attemptHref?: (locator: AttemptLocator) => string;
50
+ filter?: boolean;
20
51
  className?: string;
52
+ locale?: ReportLocale;
21
53
  }): ReactElement {
22
- return (
23
- <table className={cx("nre", "nre-metric-table", className)}>
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
+ const table = (
59
+ <table className={cx("nre", "nre-metric-table", !filter && className)}>
24
60
  <thead>
25
61
  <tr>
26
- <th scope="col" className="nre-dimension">
62
+ <th scope="col" className="nre-dimension" data-nre-sort="">
27
63
  {data.dimension}
28
64
  </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
+ )}
29
75
  {data.columns.map((col) => (
30
- <th scope="col" key={col.key} className="nre-metric-col">
31
- {col.label}
76
+ <th scope="col" key={col.key} className="nre-metric-col" data-nre-sort="">
77
+ {resolveMetricLabel(col.label, locale, col.key)}
32
78
  {col.unit && <span className="nre-unit">({col.unit})</span>}
33
79
  {/* better 方向提示:↑ 越高越好 / ↓ 越低越好 */}
34
80
  {col.better && (
35
81
  <span
36
82
  className="nre-better"
37
- title={col.better === "higher" ? "higher is better" : "lower is better"}
83
+ title={localeText(locale, col.better === "higher" ? "table.higherBetter" : "table.lowerBetter")}
38
84
  >
39
85
  {col.better === "higher" ? "↑" : "↓"}
40
86
  </span>
41
87
  )}
42
88
  </th>
43
89
  ))}
90
+ {hasVerdicts && (
91
+ <th scope="col" className="nre-verdicts-col" data-nre-sort="">
92
+ {localeText(locale, "table.verdicts")}
93
+ </th>
94
+ )}
44
95
  </tr>
45
96
  </thead>
46
97
  <tbody>
47
98
  {data.rows.map((row) => (
48
99
  <tr key={row.key}>
49
100
  {/* 行键 = 维度键(如 agent):稳定散列上色,跨块同键同色 */}
50
- <th scope="row" className={cx("nre-row-key", "nre-key", colorClassForKey(row.key))}>
101
+ <th
102
+ scope="row"
103
+ className={cx("nre-row-key", "nre-key", colorClassForKey(row.key))}
104
+ data-sort-value={row.key}
105
+ >
51
106
  {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
+ )}
52
118
  </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
+ )}
53
129
  {data.columns.map((col) => {
54
130
  const cell = row.cells[col.key];
55
131
  return (
56
- <td key={col.key} className="nre-td">
132
+ <td key={col.key} className="nre-td" data-sort-value={cell?.value ?? ""}>
57
133
  {cell ? (
58
- <MetricCellView cell={cell} attemptHref={attemptHref} />
134
+ <MetricCellView cell={cell} attemptHref={attemptHref} locale={locale} />
59
135
  ) : (
60
- // 数据侧没给这个格子(理论上 table() 不会缺列)——按空处理,不编数
136
+ // 数据侧没给这个格子(理论上 tableData 不会缺列)——按空处理,不编数
61
137
  <span className="nre-empty" />
62
138
  )}
63
139
  </td>
64
140
  );
65
141
  })}
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
+ )}
66
151
  </tr>
67
152
  ))}
68
153
  </tbody>
69
154
  </table>
70
155
  );
156
+
157
+ if (!filter) return table;
158
+ // 过滤输入框渲染在表格前(同一个 wrap 里),enhance.js 经 data-nre-filter 接管;
159
+ // 无 JS 时静默无功能,表格内容依旧完整。
160
+ return (
161
+ <div className={cx("nre", "nre-metric-table-wrap", className)}>
162
+ <input
163
+ className="nre-filter"
164
+ data-nre-filter=""
165
+ type="search"
166
+ placeholder={localeText(locale, "table.filterPlaceholder")}
167
+ />
168
+ {table}
169
+ </div>
170
+ );
71
171
  }
@@ -1,69 +1,91 @@
1
1
  // RunOverview:页头 KPI 条——「这批数据是什么」。
2
2
  // 数字下面标注数据来源(几个快照、何时跑的);warnings 有内容时直接显示在条内,
3
- // 诚实不靠使用者记得渲染(docs/reports.md「第一档」行为清单)。
3
+ // 诚实不靠使用者记得渲染(docs/feature/reports/library.md「RunOverview」行为清单)。
4
4
 
5
5
  import type { ReactElement } from "react";
6
6
  import type { OverviewData } from "../types.ts";
7
- import { MISSING_TEXT, cx, formatDurationMs, formatPercent, formatUSD } from "./format.ts";
7
+ import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
8
+ import { cx, formatDurationMs, formatUSD } from "./format.ts";
8
9
 
9
10
  export function RunOverview({
10
11
  data,
11
12
  className,
13
+ locale = DEFAULT_REPORT_LOCALE,
12
14
  }: {
13
15
  data: OverviewData;
14
16
  className?: string;
17
+ locale?: ReportLocale;
15
18
  }): ReactElement {
16
19
  const { totals } = data;
17
- // 通过率口径与内置 passRate 指标一致:skipped null 不进分母,errored/failed 计 0
18
- const judged = totals.passed + totals.failed + totals.errored;
19
- const passRate = judged > 0 ? formatPercent(totals.passed / judged) : null;
20
+ const missing = <span className="nre-missing">{localeText(locale, "cell.missing")}</span>;
21
+ // 通过率只渲染 computeCell 算好的 MetricCell,不现场重算——覆盖率角标复用
22
+ // MetricCellView 的缺数据/coverage 语义(缺数据 = 显式文案,绝不画 0%)
23
+ const { passRate } = totals;
24
+ const passRateNode =
25
+ passRate.value === null ? (
26
+ missing
27
+ ) : (
28
+ <>
29
+ {passRate.display}
30
+ {passRate.samples < passRate.total && (
31
+ <sup
32
+ className="nre-coverage"
33
+ title={localeText(locale, "cell.coverageTitle", { samples: passRate.samples, total: passRate.total })}
34
+ >
35
+ {passRate.samples}/{passRate.total}
36
+ </sup>
37
+ )}
38
+ </>
39
+ );
20
40
 
21
41
  return (
22
42
  <header className={cx("nre", "nre-overview", className)}>
23
43
  <dl className="nre-kpis">
24
44
  <div className="nre-kpi">
25
- <dt>Snapshots</dt>
45
+ <dt>{localeText(locale, "overview.snapshots")}</dt>
26
46
  <dd>{data.snapshots.length}</dd>
27
47
  </div>
28
48
  <div className="nre-kpi">
29
- <dt>Evals</dt>
49
+ <dt>{localeText(locale, "overview.evals")}</dt>
30
50
  <dd>{totals.evals}</dd>
31
51
  </div>
32
52
  <div className="nre-kpi">
33
- <dt>attempts</dt>
53
+ <dt>{localeText(locale, "overview.attempts")}</dt>
34
54
  <dd>{totals.attempts}</dd>
35
55
  </div>
36
56
  <div className="nre-kpi">
37
- <dt>Pass rate</dt>
38
- <dd>{passRate ?? <span className="nre-missing">{MISSING_TEXT}</span>}</dd>
57
+ <dt>{localeText(locale, "overview.passRate")}</dt>
58
+ <dd>{passRateNode}</dd>
39
59
  </div>
40
60
  <div className="nre-kpi">
41
- <dt>Total cost</dt>
61
+ <dt>{localeText(locale, "overview.totalCost")}</dt>
42
62
  {/* costUSD 全缺 = null:显示缺数据,不编 $0 */}
43
- <dd>
44
- {totals.costUSD === null ? (
45
- <span className="nre-missing">{MISSING_TEXT}</span>
46
- ) : (
47
- formatUSD(totals.costUSD)
48
- )}
49
- </dd>
63
+ <dd>{totals.costUSD === null ? missing : formatUSD(totals.costUSD)}</dd>
50
64
  </div>
51
65
  <div className="nre-kpi">
52
- <dt>Total duration</dt>
66
+ <dt>{localeText(locale, "overview.totalDuration")}</dt>
53
67
  <dd>{formatDurationMs(totals.durationMs)}</dd>
54
68
  </div>
55
69
  </dl>
56
70
 
57
- <p className="nre-outcomes">
58
- <span className="nre-outcome nre-outcome-passed">passed {totals.passed}</span>
59
- <span className="nre-outcome nre-outcome-failed">failed {totals.failed}</span>
60
- <span className="nre-outcome nre-outcome-errored">errored {totals.errored}</span>
61
- <span className="nre-outcome nre-outcome-skipped">skipped {totals.skipped}</span>
71
+ <p className="nre-verdicts">
72
+ <span className="nre-verdict nre-verdict-passed">
73
+ {localeText(locale, "verdict.passed")} {totals.passed}
74
+ </span>
75
+ <span className="nre-verdict nre-verdict-failed">
76
+ {localeText(locale, "verdict.failed")} {totals.failed}
77
+ </span>
78
+ <span className="nre-verdict nre-verdict-errored">
79
+ {localeText(locale, "verdict.errored")} {totals.errored}
80
+ </span>
81
+ <span className="nre-verdict nre-verdict-skipped">
82
+ {localeText(locale, "verdict.skipped")} {totals.skipped}
83
+ </span>
62
84
  </p>
63
85
 
64
86
  {/* 数据来源:哪些快照、何时跑的——报告的数字都从这里来 */}
65
87
  <p className="nre-source">
66
- Source: {data.snapshots.length} snapshots
88
+ {localeText(locale, "overview.source", { n: data.snapshots.length })}
67
89
  {data.snapshots.map((s) => (
68
90
  <span key={`${s.experimentId}@${s.startedAt}`} className="nre-source-snapshot">
69
91
  {s.experimentId}({s.agent}
@@ -72,7 +94,7 @@ export function RunOverview({
72
94
  ))}
73
95
  </p>
74
96
 
75
- {/* 选集的警告(残缺快照等)直接渲染在条内,不静默;结构化字段供程序判断,这里打 message */}
97
+ {/* Selection 的警告(残缺快照等)直接渲染在条内,不静默;结构化字段供程序判断,这里打 message */}
76
98
  {data.warnings.length > 0 && (
77
99
  <ul className="nre-warnings">
78
100
  {data.warnings.map((w, i) => (