niceeval 0.6.1 → 0.7.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 (378) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +69 -7
  4. package/dist/context/types.d.ts +32 -12
  5. package/dist/i18n/en.d.ts +54 -0
  6. package/dist/i18n/zh-CN.d.ts +57 -3
  7. package/dist/o11y/types.d.ts +16 -2
  8. package/dist/report/aggregate.d.ts +32 -24
  9. package/dist/report/aggregate.js +158 -50
  10. package/dist/report/built-in/index.d.ts +2 -0
  11. package/dist/report/built-in/index.js +8 -0
  12. package/dist/report/components.d.ts +93 -160
  13. package/dist/report/components.js +377 -114
  14. package/dist/report/compute.d.ts +87 -81
  15. package/dist/report/compute.js +597 -417
  16. package/dist/report/flag.d.ts +32 -6
  17. package/dist/report/flag.js +92 -4
  18. package/dist/report/format.d.ts +19 -11
  19. package/dist/report/format.js +30 -13
  20. package/dist/report/index.d.ts +16 -16
  21. package/dist/report/index.js +20 -21
  22. package/dist/report/load.js +3 -2
  23. package/dist/report/locale.d.ts +57 -33
  24. package/dist/report/locale.js +122 -56
  25. package/dist/report/metrics.d.ts +23 -4
  26. package/dist/report/metrics.js +110 -25
  27. package/dist/report/primitives.d.ts +48 -15
  28. package/dist/report/primitives.js +135 -26
  29. package/dist/report/react/AttemptList.d.ts +9 -7
  30. package/dist/report/react/AttemptList.js +17 -10
  31. package/dist/report/react/DeltaTable.js +19 -18
  32. package/dist/report/react/EvalList.d.ts +4 -4
  33. package/dist/report/react/EvalList.js +0 -0
  34. package/dist/report/react/ExperimentComparison.d.ts +10 -0
  35. package/dist/report/react/ExperimentComparison.js +12 -0
  36. package/dist/report/react/ExperimentList.d.ts +4 -3
  37. package/dist/report/react/ExperimentList.js +17 -18
  38. package/dist/report/react/MetricBars.js +5 -4
  39. package/dist/report/react/MetricLine.js +12 -5
  40. package/dist/report/react/MetricMatrix.js +1 -1
  41. package/dist/report/react/MetricScatter.js +59 -28
  42. package/dist/report/react/MetricTable.js +2 -12
  43. package/dist/report/react/ScopeSummary.d.ts +10 -0
  44. package/dist/report/react/ScopeSummary.js +17 -0
  45. package/dist/report/react/Scoreboard.js +6 -6
  46. package/dist/report/react/cell.js +2 -2
  47. package/dist/report/react/chart-math.d.ts +23 -6
  48. package/dist/report/react/chart-math.js +71 -19
  49. package/dist/report/react/fixtures.d.ts +5 -9
  50. package/dist/report/react/fixtures.js +110 -147
  51. package/dist/report/react/index.d.ts +15 -5
  52. package/dist/report/react/index.js +18 -7
  53. package/dist/report/report.d.ts +137 -16
  54. package/dist/report/report.js +259 -28
  55. package/dist/report/text/faces.d.ts +17 -19
  56. package/dist/report/text/faces.js +253 -184
  57. package/dist/report/text/plot.js +1 -1
  58. package/dist/report/text/table.js +38 -7
  59. package/dist/report/tree.d.ts +90 -40
  60. package/dist/report/tree.js +252 -94
  61. package/dist/report/types.d.ts +247 -284
  62. package/dist/report/types.js +4 -3
  63. package/dist/report/web.d.ts +21 -5
  64. package/dist/report/web.js +42 -16
  65. package/dist/results/select.d.ts +38 -16
  66. package/dist/results/select.js +73 -25
  67. package/dist/results/types.d.ts +49 -14
  68. package/dist/runner/feedback/sink.d.ts +110 -0
  69. package/dist/runner/types.d.ts +513 -22
  70. package/dist/sandbox/docker.d.ts +23 -2
  71. package/dist/sandbox/e2b.d.ts +15 -1
  72. package/dist/sandbox/errors.d.ts +30 -3
  73. package/dist/sandbox/io-retry.d.ts +17 -0
  74. package/dist/sandbox/registry.d.ts +2 -0
  75. package/dist/sandbox/resolve.d.ts +18 -5
  76. package/dist/sandbox/retry.d.ts +11 -1
  77. package/dist/sandbox/types.d.ts +39 -5
  78. package/dist/sandbox/vercel.d.ts +7 -1
  79. package/dist/scoring/coverage.d.ts +30 -0
  80. package/dist/scoring/display.d.ts +21 -0
  81. package/dist/scoring/display.js +120 -0
  82. package/dist/scoring/types.d.ts +103 -20
  83. package/dist/shared/aggregate.d.ts +4 -2
  84. package/dist/shared/aggregate.js +8 -7
  85. package/dist/shared/types.d.ts +28 -0
  86. package/dist/tty-line.d.ts +0 -4
  87. package/dist/util.d.ts +23 -0
  88. package/docs-site/zh/README.md +44 -0
  89. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  90. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  91. package/docs-site/zh/examples/index.mdx +50 -0
  92. package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
  93. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  94. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  95. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  96. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  97. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  98. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  99. package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
  100. package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
  101. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  102. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
  103. package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
  104. package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
  105. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  106. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
  107. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
  108. package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
  109. package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
  110. package/docs-site/zh/how-to/publish-report.mdx +105 -0
  111. package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
  112. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
  113. package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
  114. package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
  115. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
  116. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
  117. package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
  118. package/docs-site/zh/index.mdx +24 -26
  119. package/docs-site/zh/introduction.mdx +8 -8
  120. package/docs-site/zh/reference/builtin-agents.mdx +32 -5
  121. package/docs-site/zh/reference/capabilities.mdx +8 -8
  122. package/docs-site/zh/reference/cli.mdx +40 -12
  123. package/docs-site/zh/reference/define-agent.mdx +58 -5
  124. package/docs-site/zh/reference/define-config.mdx +1 -1
  125. package/docs-site/zh/reference/define-eval.mdx +42 -9
  126. package/docs-site/zh/reference/events.mdx +3 -3
  127. package/docs-site/zh/reference/expect.mdx +26 -1
  128. package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
  129. package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
  130. package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
  131. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
  132. package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
  133. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  134. package/package.json +10 -2
  135. package/src/agents/ai-sdk-otel.test.ts +1 -0
  136. package/src/agents/ai-sdk.test.ts +3 -0
  137. package/src/agents/ai-sdk.ts +3 -0
  138. package/src/agents/bub-install-spec.test.ts +34 -0
  139. package/src/agents/bub-install-spec.ts +32 -0
  140. package/src/agents/bub.ts +31 -32
  141. package/src/agents/claude-code.test.ts +130 -9
  142. package/src/agents/claude-code.ts +76 -4
  143. package/src/agents/codex.test.ts +189 -40
  144. package/src/agents/codex.ts +155 -14
  145. package/src/agents/coding-cli-versions.test.ts +15 -0
  146. package/src/agents/coding-cli-versions.ts +3 -0
  147. package/src/agents/index.ts +13 -2
  148. package/src/agents/langgraph.test.ts +204 -0
  149. package/src/agents/langgraph.ts +495 -0
  150. package/src/agents/marketplace.ts +85 -0
  151. package/src/agents/native-config.test.ts +179 -0
  152. package/src/agents/native-config.ts +267 -0
  153. package/src/agents/openai-compat.test.ts +1 -0
  154. package/src/agents/openai-compat.ts +1 -1
  155. package/src/agents/openclaw.test.ts +31 -0
  156. package/src/agents/openclaw.ts +171 -0
  157. package/src/agents/plugin-config.test.ts +1 -0
  158. package/src/agents/sdk-streams.test.ts +79 -0
  159. package/src/agents/sdk-streams.ts +55 -10
  160. package/src/agents/skills.test.ts +1 -0
  161. package/src/agents/streaming.test.ts +3 -9
  162. package/src/agents/streaming.ts +2 -2
  163. package/src/agents/types.ts +71 -8
  164. package/src/agents/ui-message-stream.test.ts +3 -0
  165. package/src/cli.ts +446 -124
  166. package/src/context/context.test.ts +51 -12
  167. package/src/context/context.ts +162 -30
  168. package/src/context/session.test.ts +2 -1
  169. package/src/context/session.ts +115 -7
  170. package/src/context/types.ts +30 -12
  171. package/src/define.test.ts +13 -8
  172. package/src/define.ts +25 -4
  173. package/src/expect/index.ts +53 -23
  174. package/src/i18n/en.ts +81 -17
  175. package/src/i18n/zh-CN.ts +80 -17
  176. package/src/o11y/cost.test.ts +1 -0
  177. package/src/o11y/execution-tree.test.ts +1 -20
  178. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  179. package/src/o11y/otlp/parse.test.ts +1 -0
  180. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  181. package/src/o11y/parsers/bub.test.ts +1 -0
  182. package/src/o11y/parsers/claude-code.test.ts +1 -34
  183. package/src/o11y/parsers/openclaw.test.ts +154 -0
  184. package/src/o11y/parsers/openclaw.ts +310 -0
  185. package/src/o11y/prices.json +746 -311
  186. package/src/o11y/tool-names.test.ts +1 -0
  187. package/src/o11y/types.ts +16 -2
  188. package/src/report/aggregate.ts +178 -61
  189. package/src/report/built-in/index.tsx +9 -0
  190. package/src/report/components.tsx +625 -279
  191. package/src/report/compute.ts +723 -491
  192. package/src/report/dual-render.test.tsx +741 -1024
  193. package/src/report/flag.ts +104 -12
  194. package/src/report/format.ts +32 -12
  195. package/src/report/index.ts +119 -46
  196. package/src/report/load.ts +3 -2
  197. package/src/report/locale.ts +136 -65
  198. package/src/report/metrics.ts +108 -25
  199. package/src/report/primitives.tsx +196 -45
  200. package/src/report/react/AttemptList.tsx +30 -43
  201. package/src/report/react/DeltaTable.tsx +63 -45
  202. package/src/report/react/EvalList.tsx +0 -0
  203. package/src/report/react/ExperimentComparison.tsx +73 -0
  204. package/src/report/react/ExperimentList.tsx +50 -32
  205. package/src/report/react/MetricBars.tsx +5 -4
  206. package/src/report/react/MetricLine.tsx +13 -8
  207. package/src/report/react/MetricMatrix.tsx +2 -2
  208. package/src/report/react/MetricScatter.tsx +86 -34
  209. package/src/report/react/MetricTable.tsx +4 -76
  210. package/src/report/react/ScopeSummary.tsx +86 -0
  211. package/src/report/react/Scoreboard.tsx +28 -10
  212. package/src/report/react/cell.tsx +2 -2
  213. package/src/report/react/chart-math.test.ts +85 -0
  214. package/src/report/react/chart-math.ts +101 -22
  215. package/src/report/react/enhance.js +89 -5
  216. package/src/report/react/fixtures.ts +114 -154
  217. package/src/report/react/index.tsx +24 -39
  218. package/src/report/react/render.test.tsx +138 -158
  219. package/src/report/react/styles.css +243 -82
  220. package/src/report/report.test.ts +779 -841
  221. package/src/report/report.ts +423 -41
  222. package/src/report/text/faces.ts +290 -193
  223. package/src/report/text/plot.ts +1 -1
  224. package/src/report/text/table.ts +44 -7
  225. package/src/report/tree.ts +362 -104
  226. package/src/report/types.ts +261 -271
  227. package/src/report/web.ts +63 -20
  228. package/src/results/annotated-source.test.ts +62 -9
  229. package/src/results/annotated-source.ts +64 -6
  230. package/src/results/attempt-evidence.test.ts +13 -11
  231. package/src/results/attempt-evidence.ts +20 -13
  232. package/src/results/attempt-source.ts +6 -3
  233. package/src/results/copy.ts +150 -60
  234. package/src/results/host-equivalence.test.ts +34 -20
  235. package/src/results/index.ts +12 -4
  236. package/src/results/locator.test.ts +1 -22
  237. package/src/results/open.ts +15 -5
  238. package/src/results/publish.ts +149 -0
  239. package/src/results/results.test.ts +89 -54
  240. package/src/results/select.ts +104 -34
  241. package/src/results/truncate.ts +90 -0
  242. package/src/results/types.ts +43 -14
  243. package/src/results/writer.ts +31 -13
  244. package/src/runner/attempt.test.ts +138 -7
  245. package/src/runner/attempt.ts +603 -104
  246. package/src/runner/discover.test.ts +47 -0
  247. package/src/runner/discover.ts +36 -2
  248. package/src/runner/eval-source.test.ts +1 -27
  249. package/src/runner/feedback/agent.test.ts +504 -0
  250. package/src/runner/feedback/agent.ts +409 -0
  251. package/src/runner/feedback/ci.test.ts +562 -0
  252. package/src/runner/feedback/ci.ts +401 -0
  253. package/src/runner/feedback/coordinator.test.ts +317 -0
  254. package/src/runner/feedback/coordinator.ts +397 -0
  255. package/src/runner/feedback/failure.ts +40 -0
  256. package/src/runner/feedback/human.test.ts +616 -0
  257. package/src/runner/feedback/human.ts +535 -0
  258. package/src/runner/feedback/index.ts +66 -0
  259. package/src/runner/feedback/io.ts +78 -0
  260. package/src/runner/feedback/profile.test.ts +50 -0
  261. package/src/runner/feedback/profile.ts +58 -0
  262. package/src/runner/feedback/reducer.test.ts +395 -0
  263. package/src/runner/feedback/reducer.ts +260 -0
  264. package/src/runner/feedback/renderer.ts +82 -0
  265. package/src/runner/feedback/sink.ts +203 -0
  266. package/src/runner/feedback/testing.ts +106 -0
  267. package/src/runner/ledger.test.ts +230 -0
  268. package/src/runner/ledger.ts +329 -0
  269. package/src/runner/report.test.ts +128 -3
  270. package/src/runner/report.ts +33 -9
  271. package/src/runner/reporters/artifacts.ts +8 -2
  272. package/src/runner/reporters/braintrust.test.ts +8 -7
  273. package/src/runner/reporters/braintrust.ts +9 -2
  274. package/src/runner/reporters/index.ts +2 -2
  275. package/src/runner/reporters/json.test.ts +162 -0
  276. package/src/runner/reporters/json.ts +35 -8
  277. package/src/runner/reporters/shared.ts +1 -5
  278. package/src/runner/run.test.ts +760 -3
  279. package/src/runner/run.ts +243 -37
  280. package/src/runner/sandbox-prep.ts +3 -42
  281. package/src/runner/timing.ts +158 -0
  282. package/src/runner/types.ts +518 -22
  283. package/src/sandbox/checkpoint.test.ts +55 -0
  284. package/src/sandbox/checkpoint.ts +29 -8
  285. package/src/sandbox/cli-commands.ts +407 -0
  286. package/src/sandbox/docker.ts +115 -16
  287. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  288. package/src/sandbox/e2b-agent-template.ts +94 -0
  289. package/src/sandbox/e2b.ts +74 -9
  290. package/src/sandbox/errors.ts +111 -4
  291. package/src/sandbox/index.ts +2 -0
  292. package/src/sandbox/io-retry.test.ts +58 -0
  293. package/src/sandbox/io-retry.ts +45 -0
  294. package/src/sandbox/keep-registry.test.ts +86 -0
  295. package/src/sandbox/keep-registry.ts +142 -0
  296. package/src/sandbox/keep.ts +178 -0
  297. package/src/sandbox/paths.test.ts +1 -0
  298. package/src/sandbox/paths.ts +19 -8
  299. package/src/sandbox/registry.ts +20 -3
  300. package/src/sandbox/resolve.ts +76 -11
  301. package/src/sandbox/retry.test.ts +70 -0
  302. package/src/sandbox/retry.ts +46 -4
  303. package/src/sandbox/types.ts +44 -6
  304. package/src/sandbox/vercel.ts +43 -20
  305. package/src/scoring/collector.ts +60 -17
  306. package/src/scoring/coverage.ts +95 -0
  307. package/src/scoring/diff.ts +81 -0
  308. package/src/scoring/display.test.ts +121 -0
  309. package/src/scoring/display.ts +133 -0
  310. package/src/scoring/evidence.test.ts +189 -0
  311. package/src/scoring/judge.test.ts +142 -0
  312. package/src/scoring/judge.ts +15 -18
  313. package/src/scoring/scoped.ts +217 -50
  314. package/src/scoring/types.ts +117 -20
  315. package/src/scoring/verdict.ts +16 -4
  316. package/src/shared/aggregate.ts +8 -6
  317. package/src/shared/types.ts +31 -0
  318. package/src/show/compose.ts +50 -67
  319. package/src/show/index.ts +127 -56
  320. package/src/show/render.ts +662 -131
  321. package/src/show/report-host.test.ts +188 -0
  322. package/src/show/report-host.ts +375 -0
  323. package/src/show/show.test.ts +320 -54
  324. package/src/tty-line.ts +8 -26
  325. package/src/util.test.ts +1 -0
  326. package/src/util.ts +41 -0
  327. package/src/view/app/App.test.tsx +69 -0
  328. package/src/view/app/App.tsx +144 -48
  329. package/src/view/app/components/AttemptModal.tsx +423 -11
  330. package/src/view/app/components/CodeView.tsx +41 -14
  331. package/src/view/app/components/CopyControls.tsx +2 -2
  332. package/src/view/app/i18n.ts +37 -17
  333. package/src/view/app/lib/attempt-route.test.ts +1 -0
  334. package/src/view/app/lib/verdict.ts +7 -9
  335. package/src/view/app/main.tsx +13 -8
  336. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  337. package/src/view/app/types.ts +4 -1
  338. package/src/view/artifact-serving.test.ts +2 -1
  339. package/src/view/client-dist/app.css +1 -1
  340. package/src/view/client-dist/app.js +17 -17
  341. package/src/view/data.test.ts +10 -3
  342. package/src/view/data.ts +155 -49
  343. package/src/view/index.ts +56 -41
  344. package/src/view/server.ts +37 -15
  345. package/src/view/shared/types.ts +34 -5
  346. package/src/view/styles.css +227 -0
  347. package/src/view/view-report.test.ts +167 -62
  348. package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
  349. package/dist/report/built-ins/experiment-comparison.js +0 -13
  350. package/dist/report/built-ins/index.d.ts +0 -1
  351. package/dist/report/built-ins/index.js +0 -2
  352. package/dist/report/react/GroupSummary.d.ts +0 -8
  353. package/dist/report/react/GroupSummary.js +0 -8
  354. package/dist/report/react/RunOverview.d.ts +0 -8
  355. package/dist/report/react/RunOverview.js +0 -12
  356. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  357. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  358. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  359. package/docs-site/zh/example/showcase.mdx +0 -39
  360. package/docs-site/zh/guides/publish-report.mdx +0 -91
  361. package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
  362. package/src/report/built-in-user-parity.test.tsx +0 -640
  363. package/src/report/built-ins/experiment-comparison.tsx +0 -19
  364. package/src/report/built-ins/index.ts +0 -2
  365. package/src/report/react/GroupSummary.tsx +0 -66
  366. package/src/report/react/RunOverview.tsx +0 -109
  367. package/src/runner/reporters/console.ts +0 -70
  368. package/src/runner/reporters/live.test.ts +0 -56
  369. package/src/runner/reporters/live.ts +0 -247
  370. package/src/runner/reporters/quiet.test.ts +0 -66
  371. package/src/runner/reporters/quiet.ts +0 -49
  372. package/src/runner/reporters/table.ts +0 -277
  373. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  374. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  375. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  376. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  377. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  378. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
@@ -0,0 +1,73 @@
1
+ // ExperimentComparison 的 web 面:完整持有所有可比组,组选择只切换已经独立计算好的 panel。
2
+ // 静态 HTML 用原生 <details> 保留每组完整内容(第一组默认展开、无 JS 完整可读);
3
+ // enhance.js 再把上方摘要卡变成单选切换。切组是纯浏览状态,不重新计算任何数字。
4
+
5
+ import type { ReactElement } from "react";
6
+ import type { ExperimentComparisonData } from "../types.ts";
7
+ import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
8
+ import { ScopeSummary } from "./ScopeSummary.tsx";
9
+ import { MetricScatter } from "./MetricScatter.tsx";
10
+ import { ExperimentList } from "./ExperimentList.tsx";
11
+ import type { AttemptLocator } from "../../results/locator.ts";
12
+ import { cx } from "./format.ts";
13
+
14
+ export function ExperimentComparisonView({
15
+ data,
16
+ className,
17
+ locale = DEFAULT_REPORT_LOCALE,
18
+ attemptHref,
19
+ }: {
20
+ data: ExperimentComparisonData;
21
+ className?: string;
22
+ locale?: ReportLocale;
23
+ attemptHref?: (locator: AttemptLocator) => string;
24
+ }): ReactElement {
25
+ if (data.groups.length === 0) {
26
+ return (
27
+ <div className={cx("nre", "nre-experiment-comparison", className)}>
28
+ <p className="nre-experiment-groups-empty">{localeText(locale, "experimentComparison.empty")}</p>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <div className={cx("nre", "nre-experiment-comparison", className)} data-nre-experiment-groups>
35
+ <div
36
+ className="nre-experiment-group-tabs"
37
+ role="tablist"
38
+ aria-label={localeText(locale, "experimentComparison.groups")}
39
+ >
40
+ {data.groups.map((group, index) => (
41
+ <div
42
+ key={group.key}
43
+ className="nre-experiment-group-tab"
44
+ role="tab"
45
+ tabIndex={index === 0 ? 0 : -1}
46
+ aria-selected={index === 0 ? "true" : "false"}
47
+ aria-label={localeText(locale, "experimentComparison.selectGroup", { group: group.key })}
48
+ data-nre-experiment-group-select={index}
49
+ >
50
+ <strong className="nre-experiment-group-name">{group.key}</strong>
51
+ <ScopeSummary data={group.summary} locale={locale} />
52
+ </div>
53
+ ))}
54
+ </div>
55
+
56
+ <div className="nre-experiment-group-panels">
57
+ {data.groups.map((group, index) => (
58
+ <details
59
+ key={group.key}
60
+ className="nre-experiment-group-panel"
61
+ data-nre-experiment-group-panel={index}
62
+ role="tabpanel"
63
+ open={index === 0}
64
+ >
65
+ <summary>{group.key}</summary>
66
+ <MetricScatter data={group.scatter} locale={locale} />
67
+ <ExperimentList data={group.experiments} filter locale={locale} relativeTo={group.key} attemptHref={attemptHref} />
68
+ </details>
69
+ ))}
70
+ </div>
71
+ </div>
72
+ );
73
+ }
@@ -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 } 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,18 +79,19 @@ function EvalAttempts({
73
79
  attemptHref: (locator: AttemptLocator) => string;
74
80
  locale: ReportLocale;
75
81
  }): ReactElement {
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);
76
84
  return (
77
85
  <li className="nre-experiment-eval">
86
+ {/* Eval 父行只显示折叠判定、Attempt 数与题级平均;失败原因只在 Attempt 子行 */}
78
87
  <div className={cx("nre-experiment-eval-header", `nre-eval-${row.verdict}`)}>
79
88
  <span className={cx("nre-eval-verdict", `nre-verdict-${row.verdict}`)}>{verdictMark(row.verdict)}</span>
80
89
  <span className="nre-eval-id">{row.evalId}</span>
81
90
  <span className="nre-eval-attempt-count">{localeText(locale, "overview.attemptsCount", { n: row.attempts.length })}</span>
82
- <span className="nre-eval-summary">
83
- {row.verdict === "passed"
84
- ? [formatDurationMs(row.duration.value ?? 0), row.cost.value === null ? undefined : formatUSD(row.cost.value)]
85
- .filter((value): value is string => value !== undefined)
86
- .join(" · ")
87
- : row.reason}
91
+ <span className="nre-eval-rollup">
92
+ {localeText(locale, "entityList.average", { value: duration })}
93
+ {" · "}
94
+ {localeText(locale, "entityList.average", { value: cost })}
88
95
  </span>
89
96
  </div>
90
97
  <ul className="nre-experiment-attempts">
@@ -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
  );
@@ -117,16 +125,21 @@ function ExperimentRow({
117
125
  item,
118
126
  attemptHref,
119
127
  locale,
128
+ relativeTo,
120
129
  }: {
121
130
  item: ExperimentListItem;
122
131
  attemptHref: (locator: AttemptLocator) => string;
123
132
  locale: ReportLocale;
133
+ relativeTo?: string;
124
134
  }): ReactElement {
125
135
  return (
126
136
  <details className="nre-experiment-entry">
127
137
  <summary className="nre-experiment-summary">
138
+ {/* relativeTo 只影响显示末段;完整 id 仍是排序 / 着色 / 过滤 / 折叠的键 */}
128
139
  <span className="nre-experiment-name" data-sort-value={item.experimentId}>
129
- <b className={cx("nre-experiment-id", "nre-key", colorClassForKey(item.experimentId))}>{item.experimentId}</b>
140
+ <b className={cx("nre-experiment-id", "nre-key", colorClassForKey(item.experimentId))}>
141
+ {experimentDisplayName(item.experimentId, relativeTo)}
142
+ </b>
130
143
  <small>
131
144
  {localeText(locale, "overview.evalsCount", { n: item.evals })}
132
145
  {item.attempts > item.evals ? ` · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })}` : ""}
@@ -135,19 +148,22 @@ function ExperimentRow({
135
148
  </span>
136
149
  <span data-sort-value={item.model ?? ""}>{item.model ?? localeText(locale, "experimentList.defaultModel")}</span>
137
150
  <span data-sort-value={item.agent}>{item.agent}</span>
138
- <span className="nre-num" data-sort-value={item.duration.value ?? ""}>
139
- <MetricCellView cell={item.duration} locale={locale} />
151
+ <span className="nre-num" data-sort-value={item.durationMs.value ?? ""}>
152
+ <MetricCellView cell={item.durationMs} locale={locale} />
140
153
  </span>
141
- <span className={cx("nre-num", passRateTone(item.passRate.value))} data-sort-value={item.passRate.value ?? ""}>
142
- <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} />
143
159
  </span>
144
160
  <span className="nre-num" data-sort-value={item.tokens.value ?? ""}>
145
161
  <MetricCellView cell={item.tokens} locale={locale} />
146
162
  </span>
147
- <span className="nre-num" data-sort-value={item.cost.value ?? ""}>
148
- <MetricCellView cell={item.cost} locale={locale} />
163
+ <span className="nre-num" data-sort-value={item.costUSD.value ?? ""}>
164
+ <MetricCellView cell={item.costUSD} locale={locale} />
149
165
  </span>
150
- <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>
151
167
  </summary>
152
168
  <div className="nre-experiment-detail">
153
169
  <Flags flags={item.flags} locale={locale} />
@@ -162,17 +178,19 @@ function ExperimentRow({
162
178
  }
163
179
 
164
180
  export function ExperimentList({
165
- items,
166
- attemptHref,
181
+ data,
182
+ attemptHref = DEFAULT_ATTEMPT_HREF,
167
183
  filter = false,
168
184
  className,
169
185
  locale = DEFAULT_REPORT_LOCALE,
186
+ relativeTo,
170
187
  }: {
171
- items: ExperimentListItem[];
172
- attemptHref: (locator: AttemptLocator) => string;
188
+ data: readonly ExperimentListItem[];
189
+ attemptHref?: (locator: AttemptLocator) => string;
173
190
  filter?: boolean;
174
191
  className?: string;
175
192
  locale?: ReportLocale;
193
+ relativeTo?: string;
176
194
  }): ReactElement {
177
195
  const labels = [
178
196
  localeText(locale, "experimentList.experiment"),
@@ -181,7 +199,7 @@ export function ExperimentList({
181
199
  localeText(locale, "experimentList.avgDuration"),
182
200
  localeText(locale, "experimentList.passRate"),
183
201
  localeText(locale, "experimentList.tokens"),
184
- localeText(locale, "experimentList.estimatedCost"),
202
+ localeText(locale, "experimentList.cost"),
185
203
  localeText(locale, "experimentList.result"),
186
204
  ];
187
205
  const board = (
@@ -198,9 +216,9 @@ export function ExperimentList({
198
216
  </button>
199
217
  ))}
200
218
  </div>
201
- {items.length === 0 && <p className="nre-experiment-list-empty">{localeText(locale, "attemptList.empty")}</p>}
202
- {items.map((item) => (
203
- <ExperimentRow key={item.experimentId} item={item} attemptHref={attemptHref} locale={locale} />
219
+ {data.length === 0 && <p className="nre-experiment-list-empty">{localeText(locale, "attemptList.empty")}</p>}
220
+ {data.map((item) => (
221
+ <ExperimentRow key={item.experimentId} item={item} attemptHref={attemptHref} locale={locale} relativeTo={relativeTo} />
204
222
  ))}
205
223
  </div>
206
224
  );
@@ -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,6 +1,8 @@
1
1
  // MetricScatter:质量 × 成本 frontier 的积木,内联 SVG、零图表库、零 hooks。
2
- // 轴向随 better:"lower" 的轴反向画,「好」的角落恒在右上(成本轴 $20 → $0 就是这么来的);
3
- // niceTicks 刻度 + 网格线;每个点直接标注(防撞布局,被挤开时补 leader line);
2
+ // 轴向随 better:"lower" 的轴反向画(值大在左/下),「更好」恒指向右上;方向提示恒为
3
+ // 「越靠右上越好」且仅当两轴都声明 better 时显示;刻度显示真实值。
4
+ // niceTicks 刻度 + 网格线;每个点直接标注(placePointLabels 候选位择优:避开其它标签、
5
+ // 数据点与画布边界,离开左右紧邻位时补 leader line);
4
6
  // 同系列的点按 x 值排序连线,系列图例列在图下。x 或 y 为 null 的点不画,
5
7
  // 底部注脚如实报「n 个点缺数据」;hover 信息退化为 SVG <title>,不 hydrate 也在
6
8
  // (enhance.js 在场时升级为样式化 tooltip)。配色走类名(nre-series-cN)由 CSS 上色,
@@ -9,8 +11,8 @@
9
11
  import type { ReactElement } from "react";
10
12
  import type { MetricColumn, ScatterData } from "../types.ts";
11
13
  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 { DEFAULT_REPORT_LOCALE, countText, localeText, resolveLocalizedText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
15
+ import { niceTicks, placePointLabels } from "./chart-math.ts";
14
16
  import { colorClassForKey, seriesClassForKey } from "./colors.ts";
15
17
  import { cx } from "./format.ts";
16
18
 
@@ -32,22 +34,54 @@ interface DrawablePoint {
32
34
  py: number;
33
35
  }
34
36
 
35
- /** 点的直接标签:experiment id 的末段(完整 id 在 <title> 里)。 */
36
- function pointLabel(key: string): string {
37
- 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
+ }
38
71
  }
39
72
 
40
73
  /**
41
- * 一根轴:niceTicks 撑出整齐的值域,值 → 像素做线性映射;
42
- * better: "lower" 时反向,好的一端固定在右 / 上。
74
+ * 一根轴:niceTicks 撑出整齐的值域,值 → 像素做线性映射。轴方向跟随指标的 `better`:
75
+ * `better: "lower"` 的轴反向渲染(值大的一端在左 / 下),「更好」因此恒指向右与上;
76
+ * 刻度标签始终显示真实值,反向只改方向不改数字(docs/feature/reports/library/metric-views.md)。
43
77
  */
44
- function axisScale(values: number[], better: MetricColumn["better"], pixelLo: number, pixelHi: number) {
78
+ function axisScale(values: number[], pixelLo: number, pixelHi: number, invert: boolean) {
45
79
  const ticks = niceTicks(Math.min(...values), Math.max(...values), 5);
46
80
  const lo = ticks[0];
47
81
  const hi = ticks[ticks.length - 1];
48
82
  const scale = (v: number) => {
49
83
  let t = (v - lo) / (hi - lo || 1);
50
- if (better === "lower") t = 1 - t; // 反向:值越低越靠「好」的一端
84
+ if (invert) t = 1 - t;
51
85
  return pixelLo + t * (pixelHi - pixelLo);
52
86
  };
53
87
  return { ticks, scale };
@@ -90,9 +124,24 @@ export function MetricScatter({
90
124
 
91
125
  const axisLabel = (label: string, col: MetricColumn) => `${label}${col.unit ? `(${col.unit})` : ""}`;
92
126
 
93
- const xScale = axisScale(drawableRows.map((r) => r.x.value as number), data.x.better, MARGIN.left, MARGIN.left + PLOT_W);
94
- // y 像素轴向下增长:better:"higher" 高值在上 → 映射到 [bottom, top];"lower" 由 axisScale 反向后同样落到上方
95
- 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;
96
145
 
97
146
  const points: DrawablePoint[] = drawableRows.map((r) => {
98
147
  const xValue = r.x.value as number;
@@ -100,12 +149,11 @@ export function MetricScatter({
100
149
  return {
101
150
  key: r.key,
102
151
  series: r.series,
103
- label: pointLabel(r.key),
152
+ label: labelByKey.get(r.key) ?? r.key,
104
153
  xValue,
105
154
  yValue,
106
- // hover 内容:experiment(点键)+ 系列(series,ExperimentComparison 传的是 agent 维度,
107
- // 有则加一行;无系列的散点没有这行)+ 两轴 display samples/total(docs/feature/reports/library.md「MetricScatter」行为清单)
108
- 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})`,
109
157
  px: xScale.scale(xValue),
110
158
  py: yScale.scale(yValue),
111
159
  };
@@ -124,12 +172,11 @@ export function MetricScatter({
124
172
  }
125
173
  for (const list of bySeries.values()) list.sort((a, b) => a.xValue - b.xValue);
126
174
 
127
- // 直接标签的防撞布局:靠右的点把标签锚到左侧,重叠的往下推、补 leader line
128
- const positions = points.map((p) => {
129
- const anchorLeft = p.px >= MARGIN.left + PLOT_W * 0.72;
130
- return { cx: p.px, cy: p.py, anchorLeft, width: p.label.length * 6.4 + 10 };
131
- });
132
- const labelOffsets = layoutLabelOffsets(positions);
175
+ // 直接标签的候选位择优布局:锚向、避让方向都由布局按空间决定,画布边界含边距
176
+ const labels = placePointLabels(
177
+ points.map((p) => ({ cx: p.px, cy: p.py, width: p.label.length * 6.4 + 10 })),
178
+ { x0: 2, y0: 2, x1: WIDTH - 2, y1: HEIGHT - 2 },
179
+ );
133
180
 
134
181
  return (
135
182
  <figure className={cx("nre", "nre-metric-scatter", className)}>
@@ -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">
@@ -202,20 +256,18 @@ export function MetricScatter({
202
256
  {/* 点:g 内带 <title>(无 JS 的原生 hover)、直接标签与 leader line;
203
257
  pointHref 时包普通 <a>,静态导出也能下钻 */}
204
258
  {points.map((p, i) => {
205
- const { anchorLeft } = positions[i];
206
- const labelX = p.px + (anchorLeft ? -10 : 10);
207
- const labelY = p.py + 4 + labelOffsets[i];
259
+ const label = labels[i];
208
260
  const group = (
209
261
  <g
210
262
  className={cx("nre-scatter-point", p.series !== undefined ? seriesClassForKey(p.series) : "nre-series-none")}
211
263
  data-key={p.key}
212
264
  >
213
265
  <title>{p.title}</title>
214
- {/* 标签被挤开时补一条 leader line,避免脱离原点看不出对应关系 */}
215
- {labelOffsets[i] !== 0 && <line className="nre-leader" x1={p.px} y1={p.py} x2={labelX} y2={labelY - 4} />}
266
+ {/* 标签不在紧邻位时补一条 leader line,避免脱离原点看不出对应关系 */}
267
+ {label.leader && <line className="nre-leader" x1={p.px} y1={p.py} x2={label.x} y2={label.y - 4} />}
216
268
  <circle className="nre-scatter-hit" cx={p.px} cy={p.py} r={12} />
217
269
  <circle className="nre-scatter-dot" cx={p.px} cy={p.py} r={4.5} />
218
- <text className="nre-scatter-point-label" x={labelX} y={labelY} textAnchor={anchorLeft ? "end" : "start"}>
270
+ <text className="nre-scatter-point-label" x={label.x} y={label.y} textAnchor={label.anchor}>
219
271
  {p.label}
220
272
  </text>
221
273
  </g>