niceeval 0.9.1 → 0.10.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 (428) hide show
  1. package/INDEX.md +31 -33
  2. package/dist/agents/types.d.ts +20 -17
  3. package/dist/context/types.d.ts +21 -21
  4. package/dist/i18n/en.d.ts +10 -2
  5. package/dist/i18n/zh-CN.d.ts +14 -6
  6. package/dist/o11y/execution-tree.d.ts +103 -0
  7. package/dist/o11y/otlp/select.d.ts +22 -0
  8. package/dist/report/{react → assets}/colors.d.ts +8 -0
  9. package/dist/report/{react → assets}/colors.js +23 -0
  10. package/dist/report/built-in/index.d.ts +2 -2
  11. package/dist/report/built-in/index.js +2 -2
  12. package/dist/report/built-in/standard.d.ts +8 -1
  13. package/dist/report/built-in/standard.js +17 -7
  14. package/dist/report/components/attempt-detail/AttemptAssertions.d.ts +6 -0
  15. package/dist/report/components/attempt-detail/AttemptAssertions.js +17 -0
  16. package/dist/report/components/attempt-detail/AttemptConversation.d.ts +6 -0
  17. package/dist/report/components/attempt-detail/AttemptConversation.js +34 -0
  18. package/dist/report/components/attempt-detail/AttemptDiagnostics.d.ts +6 -0
  19. package/dist/report/components/attempt-detail/AttemptDiagnostics.js +7 -0
  20. package/dist/report/components/attempt-detail/AttemptDiff.d.ts +6 -0
  21. package/dist/report/components/attempt-detail/AttemptDiff.js +11 -0
  22. package/dist/report/components/attempt-detail/AttemptError.d.ts +6 -0
  23. package/dist/report/components/attempt-detail/AttemptError.js +16 -0
  24. package/dist/report/components/attempt-detail/AttemptFixPrompt.d.ts +6 -0
  25. package/dist/report/components/attempt-detail/AttemptFixPrompt.js +7 -0
  26. package/dist/report/components/attempt-detail/AttemptSource.d.ts +6 -0
  27. package/dist/report/components/attempt-detail/AttemptSource.js +27 -0
  28. package/dist/report/components/attempt-detail/AttemptSummary.d.ts +8 -0
  29. package/dist/report/components/attempt-detail/AttemptSummary.js +20 -0
  30. package/dist/report/components/attempt-detail/AttemptTimeline.d.ts +6 -0
  31. package/dist/report/components/attempt-detail/AttemptTimeline.js +28 -0
  32. package/dist/report/components/attempt-detail/AttemptTrace.d.ts +6 -0
  33. package/dist/report/components/attempt-detail/AttemptTrace.js +29 -0
  34. package/dist/report/components/attempt-detail/AttemptUsage.d.ts +6 -0
  35. package/dist/report/components/attempt-detail/AttemptUsage.js +20 -0
  36. package/dist/report/components/attempt-detail/compute.d.ts +24 -0
  37. package/dist/report/components/attempt-detail/compute.js +254 -0
  38. package/dist/report/components/attempt-detail/faces.d.ts +14 -0
  39. package/dist/report/components/attempt-detail/faces.js +235 -0
  40. package/dist/report/components/attempt-detail/index.d.ts +38 -0
  41. package/dist/report/components/attempt-detail/index.js +527 -0
  42. package/dist/report/{react → components}/cell.d.ts +5 -3
  43. package/dist/report/{react → components}/cell.js +3 -3
  44. package/dist/report/{react → components/entity-lists}/AttemptList.d.ts +8 -4
  45. package/dist/report/{react → components/entity-lists}/AttemptList.js +16 -10
  46. package/dist/report/{react → components/entity-lists}/EvalList.d.ts +3 -3
  47. package/dist/report/{react → components/entity-lists}/EvalList.js +0 -0
  48. package/dist/report/{react → components/entity-lists}/ExperimentList.d.ts +3 -3
  49. package/dist/report/{react → components/entity-lists}/ExperimentList.js +10 -10
  50. package/dist/report/components/entity-lists/compute.d.ts +23 -0
  51. package/dist/report/components/entity-lists/compute.js +171 -0
  52. package/dist/report/components/entity-lists/faces.d.ts +5 -0
  53. package/dist/report/components/entity-lists/faces.js +174 -0
  54. package/dist/report/components/entity-lists/index.d.ts +51 -0
  55. package/dist/report/components/entity-lists/index.js +165 -0
  56. package/dist/report/{react → components}/fixtures.d.ts +2 -2
  57. package/dist/report/{react → components}/fixtures.js +1 -1
  58. package/dist/report/{react → components/metric-views}/DeltaTable.d.ts +2 -2
  59. package/dist/report/{react → components/metric-views}/DeltaTable.js +4 -4
  60. package/dist/report/{react → components/metric-views}/MetricBars.d.ts +3 -3
  61. package/dist/report/{react → components/metric-views}/MetricBars.js +3 -3
  62. package/dist/report/{react → components/metric-views}/MetricLine.d.ts +2 -2
  63. package/dist/report/{react → components/metric-views}/MetricLine.js +8 -5
  64. package/dist/report/{react → components/metric-views}/MetricMatrix.d.ts +3 -3
  65. package/dist/report/{react → components/metric-views}/MetricMatrix.js +4 -4
  66. package/dist/report/components/metric-views/MetricScatter.d.ts +11 -0
  67. package/dist/report/{react → components/metric-views}/MetricScatter.js +26 -23
  68. package/dist/report/{react → components/metric-views}/MetricTable.d.ts +3 -3
  69. package/dist/report/{react → components/metric-views}/MetricTable.js +4 -4
  70. package/dist/report/{react → components/metric-views}/Scoreboard.d.ts +2 -2
  71. package/dist/report/{react → components/metric-views}/Scoreboard.js +3 -3
  72. package/dist/report/components/metric-views/compute.d.ts +89 -0
  73. package/dist/report/{compute.js → components/metric-views/compute.js} +32 -415
  74. package/dist/report/components/metric-views/faces.d.ts +13 -0
  75. package/dist/report/components/metric-views/faces.js +381 -0
  76. package/dist/report/components/metric-views/index.d.ts +50 -0
  77. package/dist/report/components/metric-views/index.js +273 -0
  78. package/dist/report/{text → components/metric-views}/plot.js +1 -1
  79. package/dist/report/components/shared-compute.d.ts +22 -0
  80. package/dist/report/components/shared-compute.js +47 -0
  81. package/dist/report/components/shared-faces.d.ts +9 -0
  82. package/dist/report/components/shared-faces.js +26 -0
  83. package/dist/report/components/shared.d.ts +68 -0
  84. package/dist/report/components/shared.js +125 -0
  85. package/dist/report/{react → components/site-components}/CopyFixPrompt.d.ts +2 -2
  86. package/dist/report/{react → components/site-components}/CopyFixPrompt.js +2 -2
  87. package/dist/report/{react → components/site-components}/HeroCard.d.ts +2 -2
  88. package/dist/report/{react → components/site-components}/HeroCard.js +2 -2
  89. package/dist/report/{react → components/site-components}/ScopeWarnings.d.ts +2 -2
  90. package/dist/report/{react → components/site-components}/ScopeWarnings.js +3 -3
  91. package/dist/report/{react → components/site-components}/TraceWaterfall.d.ts +3 -3
  92. package/dist/report/{react → components/site-components}/TraceWaterfall.js +5 -5
  93. package/dist/report/components/site-components/compute.d.ts +28 -0
  94. package/dist/report/components/site-components/compute.js +132 -0
  95. package/dist/report/components/site-components/faces.d.ts +21 -0
  96. package/dist/report/components/site-components/faces.js +75 -0
  97. package/dist/report/components/site-components/index.d.ts +74 -0
  98. package/dist/report/components/site-components/index.js +220 -0
  99. package/dist/report/{scope-warnings.d.ts → components/site-components/scope-warnings.d.ts} +2 -2
  100. package/dist/report/{scope-warnings.js → components/site-components/scope-warnings.js} +2 -2
  101. package/dist/report/{react → components/summaries}/ScopeSummary.d.ts +2 -2
  102. package/dist/report/{react → components/summaries}/ScopeSummary.js +17 -8
  103. package/dist/report/components/summaries/compute.d.ts +7 -0
  104. package/dist/report/components/summaries/compute.js +51 -0
  105. package/dist/report/components/summaries/faces.d.ts +7 -0
  106. package/dist/report/components/summaries/faces.js +38 -0
  107. package/dist/report/components/summaries/index.d.ts +27 -0
  108. package/dist/report/components/summaries/index.js +78 -0
  109. package/dist/report/definition/grid-layout.d.ts +50 -0
  110. package/dist/report/definition/grid-layout.js +89 -0
  111. package/dist/report/{primitives.d.ts → definition/primitives.d.ts} +34 -4
  112. package/dist/report/{primitives.js → definition/primitives.js} +101 -10
  113. package/dist/report/{report.d.ts → definition/report.d.ts} +36 -65
  114. package/dist/report/{report.js → definition/report.js} +58 -90
  115. package/dist/report/{text/table.d.ts → definition/table-text.d.ts} +2 -2
  116. package/dist/report/{text/table.js → definition/table-text.js} +2 -2
  117. package/dist/report/{tree.d.ts → definition/tree.d.ts} +43 -11
  118. package/dist/report/{tree.js → definition/tree.js} +9 -11
  119. package/dist/report/index.d.ts +33 -18
  120. package/dist/report/index.js +23 -14
  121. package/dist/report/{aggregate.d.ts → model/aggregate.d.ts} +12 -4
  122. package/dist/report/{aggregate.js → model/aggregate.js} +28 -8
  123. package/dist/report/{flag.d.ts → model/flag.d.ts} +14 -1
  124. package/dist/report/{flag.js → model/flag.js} +35 -0
  125. package/dist/report/{format.d.ts → model/format.d.ts} +11 -5
  126. package/dist/report/{format.js → model/format.js} +48 -4
  127. package/dist/report/{locale.d.ts → model/locale.d.ts} +17 -17
  128. package/dist/report/{locale.js → model/locale.js} +31 -33
  129. package/dist/report/{metrics.d.ts → model/metrics.d.ts} +2 -2
  130. package/dist/report/{metrics.js → model/metrics.js} +2 -2
  131. package/dist/report/{types.d.ts → model/types.d.ts} +140 -20
  132. package/dist/report/react/index.d.ts +32 -21
  133. package/dist/report/react/index.js +33 -21
  134. package/dist/report/{load.d.ts → runtime/load.d.ts} +1 -1
  135. package/dist/report/{load.js → runtime/load.js} +1 -1
  136. package/dist/report/runtime/text.d.ts +67 -0
  137. package/dist/report/runtime/text.js +114 -0
  138. package/dist/report/runtime/web.d.ts +35 -0
  139. package/dist/report/runtime/web.js +70 -0
  140. package/dist/results/annotated-source.d.ts +87 -0
  141. package/dist/results/attempt-evidence.d.ts +74 -0
  142. package/dist/results/attempt-source.d.ts +15 -0
  143. package/dist/results/select.d.ts +11 -1
  144. package/dist/results/select.js +27 -5
  145. package/dist/runner/eval-selection.d.ts +26 -0
  146. package/dist/runner/feedback/sink.d.ts +26 -1
  147. package/dist/runner/types.d.ts +149 -23
  148. package/dist/sandbox/docker.d.ts +6 -0
  149. package/dist/sandbox/types.d.ts +23 -21
  150. package/dist/scoring/display.d.ts +7 -2
  151. package/dist/scoring/display.js +42 -4
  152. package/dist/scoring/types.d.ts +3 -3
  153. package/dist/shared/aggregate.d.ts +14 -0
  154. package/dist/shared/aggregate.js +31 -0
  155. package/dist/shared/types.d.ts +6 -1
  156. package/docs-site/images/logo-dark.svg +7 -0
  157. package/docs-site/images/logo.svg +6 -5
  158. package/docs-site/zh/README.md +8 -7
  159. package/docs-site/zh/examples/ai-agent-application.mdx +5 -5
  160. package/docs-site/zh/examples/coding-agent-extensions.mdx +4 -4
  161. package/docs-site/zh/examples/index.mdx +3 -3
  162. package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +1 -1
  163. package/docs-site/zh/examples/integrations/claude-sdk.mdx +3 -3
  164. package/docs-site/zh/examples/integrations/codex-sdk.mdx +3 -3
  165. package/docs-site/zh/examples/integrations/langgraph.mdx +3 -3
  166. package/docs-site/zh/examples/integrations/pi-sdk.mdx +2 -2
  167. package/docs-site/zh/explanation/adapter.mdx +19 -19
  168. package/docs-site/zh/explanation/assert.mdx +9 -9
  169. package/docs-site/zh/explanation/drive.mdx +4 -4
  170. package/docs-site/zh/explanation/evals.mdx +8 -8
  171. package/docs-site/zh/explanation/experiment.mdx +8 -8
  172. package/docs-site/zh/explanation/hitl.mdx +13 -13
  173. package/docs-site/zh/explanation/judge.mdx +3 -3
  174. package/docs-site/zh/explanation/overview.mdx +7 -7
  175. package/docs-site/zh/explanation/runner.mdx +11 -11
  176. package/docs-site/zh/explanation/tier.mdx +6 -6
  177. package/docs-site/zh/index.mdx +14 -14
  178. package/docs-site/zh/introduction.mdx +13 -16
  179. package/docs-site/zh/reference/builtin-agents.mdx +61 -28
  180. package/docs-site/zh/reference/capabilities.mdx +7 -27
  181. package/docs-site/zh/reference/cli.mdx +35 -32
  182. package/docs-site/zh/reference/define-agent.mdx +32 -31
  183. package/docs-site/zh/reference/define-config.mdx +6 -6
  184. package/docs-site/zh/reference/define-eval.mdx +33 -20
  185. package/docs-site/zh/reference/events.mdx +4 -4
  186. package/docs-site/zh/reference/expect.mdx +3 -3
  187. package/docs-site/zh/reference/official-adapters.mdx +17 -17
  188. package/docs-site/zh/reference/report-components.mdx +193 -121
  189. package/docs-site/zh/reference/results-data.mdx +13 -13
  190. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +11 -11
  191. package/docs-site/zh/troubleshooting/debugging.mdx +40 -16
  192. package/docs-site/zh/{how-to → tutorials}/agent-feedback-loop.mdx +38 -38
  193. package/docs-site/zh/tutorials/agent-onboarding.mdx +100 -0
  194. package/docs-site/zh/tutorials/authoring.mdx +211 -0
  195. package/docs-site/zh/{how-to → tutorials}/ci-integration.mdx +8 -8
  196. package/docs-site/zh/{how-to → tutorials}/connect-otel.mdx +22 -22
  197. package/docs-site/zh/{how-to → tutorials}/connect-your-agent.mdx +58 -63
  198. package/docs-site/zh/tutorials/custom-reports.mdx +453 -0
  199. package/docs-site/zh/{how-to → tutorials}/dataset-fanout.mdx +4 -4
  200. package/docs-site/zh/tutorials/experiments.mdx +103 -0
  201. package/docs-site/zh/{how-to → tutorials}/fixtures.mdx +9 -9
  202. package/docs-site/zh/{how-to → tutorials}/publish-report.mdx +6 -6
  203. package/docs-site/zh/tutorials/quickstart.mdx +18 -18
  204. package/docs-site/zh/{how-to → tutorials}/reporters.mdx +6 -6
  205. package/docs-site/zh/{how-to → tutorials}/sandbox-agent.mdx +6 -6
  206. package/docs-site/zh/{how-to → tutorials}/sandbox-providers.mdx +41 -19
  207. package/docs-site/zh/{how-to → tutorials}/scoring-guide.mdx +4 -4
  208. package/docs-site/zh/{how-to → tutorials}/viewing-results.mdx +49 -49
  209. package/docs-site/zh/tutorials/write-experiment.mdx +355 -0
  210. package/docs-site/zh/{how-to → tutorials}/write-send.mdx +40 -40
  211. package/package.json +8 -4
  212. package/src/agents/bub.ts +21 -8
  213. package/src/agents/claude-code.ts +23 -10
  214. package/src/agents/codex.test.ts +22 -7
  215. package/src/agents/codex.ts +22 -9
  216. package/src/agents/index.ts +2 -2
  217. package/src/agents/openai-compat.ts +1 -1
  218. package/src/agents/post-setup.ts +45 -22
  219. package/src/agents/streaming.ts +1 -1
  220. package/src/agents/types.ts +20 -17
  221. package/src/agents/ui-message-stream.test.ts +10 -0
  222. package/src/agents/ui-message-stream.ts +12 -1
  223. package/src/cli.ts +71 -57
  224. package/src/context/types.ts +21 -21
  225. package/src/define.ts +13 -0
  226. package/src/i18n/en.ts +27 -12
  227. package/src/i18n/zh-CN.ts +27 -12
  228. package/src/index.ts +2 -0
  229. package/src/report/{react → assets}/colors.ts +22 -0
  230. package/src/report/{react → assets}/enhance.js +1 -33
  231. package/src/report/{react → assets}/styles.css +124 -106
  232. package/src/report/built-in/index.tsx +2 -2
  233. package/src/report/built-in/standard.tsx +18 -6
  234. package/src/report/components/attempt-detail/AttemptAssertions.tsx +67 -0
  235. package/src/report/components/attempt-detail/AttemptConversation.tsx +110 -0
  236. package/src/report/components/attempt-detail/AttemptDiagnostics.tsx +38 -0
  237. package/src/report/components/attempt-detail/AttemptDiff.tsx +35 -0
  238. package/src/report/components/attempt-detail/AttemptError.tsx +31 -0
  239. package/src/report/components/attempt-detail/AttemptFixPrompt.tsx +26 -0
  240. package/src/report/components/attempt-detail/AttemptSource.tsx +80 -0
  241. package/src/report/components/attempt-detail/AttemptSummary.tsx +74 -0
  242. package/src/report/components/attempt-detail/AttemptTimeline.tsx +108 -0
  243. package/src/report/components/attempt-detail/AttemptTrace.tsx +63 -0
  244. package/src/report/components/attempt-detail/AttemptUsage.tsx +29 -0
  245. package/src/report/components/attempt-detail/attempt-components.test.tsx +680 -0
  246. package/src/report/components/attempt-detail/compute.ts +287 -0
  247. package/src/report/components/attempt-detail/faces.ts +257 -0
  248. package/src/report/components/attempt-detail/index.tsx +583 -0
  249. package/src/report/components/attempt-detail/validate.test.ts +235 -0
  250. package/src/report/{react → components}/cell.tsx +6 -3
  251. package/src/report/{report.test.ts → components/compute.test.ts} +316 -46
  252. package/src/report/{react → components/entity-lists}/AttemptList.tsx +24 -16
  253. package/src/report/{react → components/entity-lists}/EvalList.tsx +0 -0
  254. package/src/report/{react → components/entity-lists}/ExperimentList.tsx +23 -20
  255. package/src/report/components/entity-lists/compute.ts +196 -0
  256. package/src/report/components/entity-lists/faces.ts +207 -0
  257. package/src/report/components/entity-lists/index.tsx +241 -0
  258. package/src/report/components/entity-lists/validate.test.ts +114 -0
  259. package/src/report/{react → components}/fixtures.ts +2 -2
  260. package/src/report/{react → components/metric-views}/DeltaTable.tsx +5 -5
  261. package/src/report/{react → components/metric-views}/MetricBars.tsx +5 -5
  262. package/src/report/{react → components/metric-views}/MetricLine.tsx +9 -6
  263. package/src/report/{react → components/metric-views}/MetricMatrix.tsx +6 -6
  264. package/src/report/{react → components/metric-views}/MetricScatter.tsx +41 -33
  265. package/src/report/{react → components/metric-views}/MetricTable.tsx +6 -6
  266. package/src/report/{react → components/metric-views}/Scoreboard.tsx +4 -4
  267. package/src/report/{compute.ts → components/metric-views/compute.ts} +51 -477
  268. package/src/report/components/metric-views/faces.ts +423 -0
  269. package/src/report/components/metric-views/index.tsx +352 -0
  270. package/src/report/{text → components/metric-views}/plot.ts +1 -1
  271. package/src/report/components/metric-views/validate.test.ts +211 -0
  272. package/src/report/{react → components}/render.test.tsx +47 -17
  273. package/src/report/components/shared-compute.ts +59 -0
  274. package/src/report/components/shared-faces.ts +30 -0
  275. package/src/report/components/shared.ts +192 -0
  276. package/src/report/{react → components/site-components}/CopyFixPrompt.tsx +3 -3
  277. package/src/report/{react → components/site-components}/HeroCard.tsx +3 -3
  278. package/src/report/{react → components/site-components}/ScopeWarnings.tsx +4 -4
  279. package/src/report/{react → components/site-components}/TraceWaterfall.tsx +13 -10
  280. package/src/report/components/site-components/compute.ts +144 -0
  281. package/src/report/components/site-components/faces.ts +80 -0
  282. package/src/report/components/site-components/index.tsx +265 -0
  283. package/src/report/{scope-warnings.ts → components/site-components/scope-warnings.ts} +3 -3
  284. package/src/report/{site-components.test.tsx → components/site-components/site-components.test.tsx} +23 -21
  285. package/src/report/components/site-components/validate.test.ts +89 -0
  286. package/src/report/{react → components/summaries}/ScopeSummary.tsx +26 -11
  287. package/src/report/components/summaries/compute.ts +56 -0
  288. package/src/report/components/summaries/faces.ts +48 -0
  289. package/src/report/components/summaries/index.tsx +127 -0
  290. package/src/report/components/summaries/validate.test.ts +49 -0
  291. package/src/report/definition/grid-layout.test.ts +124 -0
  292. package/src/report/definition/grid-layout.ts +146 -0
  293. package/src/report/{primitives.tsx → definition/primitives.tsx} +168 -18
  294. package/src/report/{report.ts → definition/report.ts} +98 -165
  295. package/src/report/{shell-head.test.ts → definition/shell-head.test.ts} +2 -2
  296. package/src/report/{text/table.ts → definition/table-text.ts} +4 -4
  297. package/src/report/{tree.ts → definition/tree.ts} +56 -20
  298. package/src/report/index.ts +126 -66
  299. package/src/report/{aggregate.ts → model/aggregate.ts} +31 -9
  300. package/src/report/{flag.ts → model/flag.ts} +39 -1
  301. package/src/report/{format.ts → model/format.ts} +59 -5
  302. package/src/report/{locale.ts → model/locale.ts} +34 -36
  303. package/src/report/{metrics.ts → model/metrics.ts} +3 -3
  304. package/src/report/{types.ts → model/types.ts} +143 -22
  305. package/src/report/react/index.tsx +52 -26
  306. package/src/report/{dual-render.test.tsx → runtime/dual-render.test.tsx} +564 -49
  307. package/src/report/runtime/host.test.ts +44 -0
  308. package/src/report/runtime/host.ts +138 -0
  309. package/src/report/{load.ts → runtime/load.ts} +1 -1
  310. package/src/report/runtime/text.ts +192 -0
  311. package/src/report/runtime/web.ts +106 -0
  312. package/src/results/attempt-evidence.ts +5 -1
  313. package/src/results/host-equivalence.test.ts +53 -2
  314. package/src/results/select.ts +29 -5
  315. package/src/runner/attempt.test.ts +48 -1
  316. package/src/runner/attempt.ts +62 -42
  317. package/src/runner/cleanup-timeout.test.ts +16 -0
  318. package/src/runner/cleanup-timeout.ts +23 -0
  319. package/src/runner/discover.ts +1 -2
  320. package/src/runner/eval-selection.test.ts +187 -0
  321. package/src/runner/eval-selection.ts +109 -0
  322. package/src/runner/experiment-cleanup-registry.test.ts +89 -0
  323. package/src/runner/experiment-cleanup-registry.ts +39 -0
  324. package/src/runner/experiment-labels.test.ts +71 -0
  325. package/src/runner/feedback/agent.test.ts +33 -1
  326. package/src/runner/feedback/agent.ts +12 -16
  327. package/src/runner/feedback/ci.test.ts +35 -1
  328. package/src/runner/feedback/ci.ts +16 -18
  329. package/src/runner/feedback/coordinator.ts +28 -0
  330. package/src/runner/feedback/human.test.ts +108 -1
  331. package/src/runner/feedback/human.ts +71 -21
  332. package/src/runner/feedback/reducer.test.ts +64 -0
  333. package/src/runner/feedback/reducer.ts +26 -0
  334. package/src/runner/feedback/sink.ts +40 -1
  335. package/src/runner/fingerprint.ts +2 -1
  336. package/src/runner/report.test.ts +14 -0
  337. package/src/runner/report.ts +3 -1
  338. package/src/runner/run.test.ts +390 -12
  339. package/src/runner/run.ts +259 -26
  340. package/src/runner/sandbox-selection.test.ts +13 -3
  341. package/src/runner/sandbox-selection.ts +4 -2
  342. package/src/runner/types.ts +158 -23
  343. package/src/sandbox/docker.ts +7 -0
  344. package/src/sandbox/e2b.ts +12 -13
  345. package/src/sandbox/types.ts +23 -21
  346. package/src/scoring/display.test.ts +28 -2
  347. package/src/scoring/display.ts +38 -5
  348. package/src/scoring/types.ts +3 -3
  349. package/src/shared/aggregate.test.ts +52 -0
  350. package/src/shared/aggregate.ts +29 -0
  351. package/src/shared/types.ts +6 -1
  352. package/src/show/command.test.ts +34 -0
  353. package/src/show/command.ts +16 -0
  354. package/src/show/compose.ts +1 -1
  355. package/src/show/index.ts +56 -20
  356. package/src/show/render.ts +15 -239
  357. package/src/show/show.test.ts +127 -34
  358. package/src/view/app/App.test.tsx +1 -1
  359. package/src/view/app/App.tsx +119 -47
  360. package/src/view/app/i18n.ts +7 -147
  361. package/src/view/app/lib/attempt-dialog.test.ts +65 -0
  362. package/src/view/app/lib/attempt-dialog.ts +69 -0
  363. package/src/view/app/main.tsx +0 -1
  364. package/src/view/app/types.ts +3 -86
  365. package/src/view/artifact-serving.test.ts +22 -38
  366. package/src/view/client-dist/app.css +1 -1
  367. package/src/view/client-dist/app.js +15 -22
  368. package/src/view/data.test.ts +48 -36
  369. package/src/view/data.ts +105 -83
  370. package/src/view/index.ts +1 -1
  371. package/src/view/server.ts +32 -2
  372. package/src/view/shared/types.ts +6 -31
  373. package/src/view/site-parity.test.ts +24 -1
  374. package/src/view/site.ts +144 -17
  375. package/src/view/styles.css +0 -805
  376. package/src/view/view-report.test.ts +141 -22
  377. package/dist/report/components.d.ts +0 -179
  378. package/dist/report/components.js +0 -544
  379. package/dist/report/compute.d.ts +0 -139
  380. package/dist/report/react/ExperimentComparison.d.ts +0 -10
  381. package/dist/report/react/ExperimentComparison.js +0 -12
  382. package/dist/report/react/MetricScatter.d.ts +0 -9
  383. package/dist/report/react/format.d.ts +0 -3
  384. package/dist/report/react/format.js +0 -7
  385. package/dist/report/text/faces.d.ts +0 -45
  386. package/dist/report/text/faces.js +0 -671
  387. package/dist/report/web.d.ts +0 -32
  388. package/dist/report/web.js +0 -48
  389. package/docs-site/zh/how-to/authoring.mdx +0 -162
  390. package/docs-site/zh/how-to/custom-reports.mdx +0 -414
  391. package/docs-site/zh/how-to/experiments.mdx +0 -86
  392. package/docs-site/zh/how-to/write-experiment.mdx +0 -164
  393. package/src/report/components.tsx +0 -925
  394. package/src/report/react/ExperimentComparison.tsx +0 -73
  395. package/src/report/react/format.ts +0 -9
  396. package/src/report/text/faces.ts +0 -767
  397. package/src/report/web.ts +0 -77
  398. package/src/show/report-host.test.ts +0 -205
  399. package/src/show/report-host.ts +0 -389
  400. package/src/view/app/components/AttemptModal.tsx +0 -496
  401. package/src/view/app/components/CodeView.test.tsx +0 -142
  402. package/src/view/app/components/CodeView.tsx +0 -310
  403. package/src/view/app/components/CopyControls.tsx +0 -106
  404. package/src/view/app/components/Trace.tsx +0 -100
  405. package/src/view/app/components/Transcript.tsx +0 -157
  406. package/src/view/app/components/ui/badge.tsx +0 -21
  407. package/src/view/app/lib/artifact-url.ts +0 -17
  408. package/src/view/app/lib/attempt-route.test.ts +0 -80
  409. package/src/view/app/lib/attempt-route.ts +0 -52
  410. package/src/view/app/lib/format.ts +0 -70
  411. package/src/view/app/lib/guards.test.ts +0 -108
  412. package/src/view/app/lib/guards.ts +0 -71
  413. package/src/view/app/lib/rows.ts +0 -22
  414. package/src/view/app/lib/transcript-data.tsx +0 -151
  415. package/src/view/app/lib/verdict.ts +0 -23
  416. package/src/view/app/shared.ts +0 -8
  417. /package/dist/report/{react → components/metric-views}/chart-math.d.ts +0 -0
  418. /package/dist/report/{react → components/metric-views}/chart-math.js +0 -0
  419. /package/dist/report/{text → components/metric-views}/plot.d.ts +0 -0
  420. /package/dist/report/{react → components/site-components}/PoweredBy.d.ts +0 -0
  421. /package/dist/report/{react → components/site-components}/PoweredBy.js +0 -0
  422. /package/dist/report/{text/layout.d.ts → model/text-layout.d.ts} +0 -0
  423. /package/dist/report/{text/layout.js → model/text-layout.js} +0 -0
  424. /package/dist/report/{types.js → model/types.js} +0 -0
  425. /package/src/report/{react → components/metric-views}/chart-math.test.ts +0 -0
  426. /package/src/report/{react → components/metric-views}/chart-math.ts +0 -0
  427. /package/src/report/{react → components/site-components}/PoweredBy.tsx +0 -0
  428. /package/src/report/{text/layout.ts → model/text-layout.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  // cases: docs/engineering/unit-tests/reports/cases.md
2
- // niceeval show 终端宿主的测试(行为规范:docs-site/zh/how-to/viewing-results.mdx;
2
+ // niceeval show 终端宿主的测试(行为规范:docs-site/zh/tutorials/viewing-results.mdx;
3
3
  // 组合语义:docs/feature/reports/architecture.md「Selection 是计算入口」)。覆盖:
4
4
  // - 榜单合成口径:每 experiment × eval 取最新判定,局部重跑从更早快照补齐,头部标注合成自几个快照;
5
5
  // - 前缀过滤收窄 Selection,覆盖警告分母 = 已知并集 ∩ 范围;
@@ -22,7 +22,7 @@ import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type TimingNod
22
22
  import { selectCurrentResults } from "../results/select.ts";
23
23
  import { attemptHistory } from "./compose.ts";
24
24
  import { runShow, type ShowFlags } from "./index.ts";
25
- import { stringWidth } from "../report/text/layout.ts";
25
+ import { stringWidth } from "../report/model/text-layout.ts";
26
26
 
27
27
  // ───────────────────────── fixture 工具 ─────────────────────────
28
28
 
@@ -152,12 +152,12 @@ describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text
152
152
  const root = await seedComposedRoot();
153
153
  const { out, code } = await show(root, []);
154
154
  expect(code).toBe(0);
155
- expect(out).toContain("No data to plot Cost × End-to-end pass rate");
155
+ expect(out).toContain("No data to plot Cost × Pass rate");
156
156
  expect(out).toContain("1 point missing data");
157
157
  expect(out).not.toContain("needed to compare");
158
158
  expect(out).not.toContain("COMPARISON");
159
159
  expect(out).toMatch(/\bbub\s+default\s+bub\s+1s\s+50%/);
160
- expect(out).toMatch(/1 passed[\s\S]*?\/ 1\s+failed/);
160
+ expect(out).toMatch(/1 passed[\s\S]*?· 1\s+failed/);
161
161
  expect(out).toMatch(/✗ failed\s+fixtures\/button[\s\S]*└─ @1[0-9a-z]{7}[\s\S]*fileChanged/);
162
162
  expect(out).toMatch(/✓ passed\s+weather\/brooklyn[\s\S]*└─ @1[0-9a-z]{7}/);
163
163
  expect(out).not.toMatch(/\[[EXD⏱,]+\]/);
@@ -220,8 +220,8 @@ describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text
220
220
  try {
221
221
  const { out, code } = await show(root, []);
222
222
  expect(code).toBe(0);
223
- expect(out).toContain("成本 × 端到端成功率 没有可绘制的数据");
224
- expect(out).toContain("1 通过 / 1 失败");
223
+ expect(out).toContain("成本 × 通过率 没有可绘制的数据");
224
+ expect(out).toContain("1 通过 · 1 失败");
225
225
  expect(out).toMatch(/\bbub\s+默认\s+bub\s+1s\s+50%/);
226
226
  // 页首 Hero 与尾部页索引同样跟随 locale(内置文案与页名)。
227
227
  expect(out.split("\n")[0]).toBe("Eval 运行结果");
@@ -259,7 +259,10 @@ describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text
259
259
  expect(code).toBe(0);
260
260
  // 成本轴(better: lower)反向,「更好」恒指向右上;两轴都声明 better → 提示在场
261
261
  expect(out).toContain("better → upper right");
262
- expect(out).toContain("A compare/a B compare/b");
262
+ // line 声明:按 agent 归类、不连线;图例一行一个 series(显示键字典序),标记按图例顺序分配
263
+ expect(out).toContain("grouped by agent");
264
+ expect(out).toContain("claude A compare/b");
265
+ expect(out).toContain("codex B compare/a");
263
266
  expect(out).toMatch(/\bb\s+large\s+claude\s+1s\s+100%/);
264
267
  expect(out).toMatch(/\ba\s+mini\s+codex\s+1s\s+50%/);
265
268
  expect(out).toMatch(/✓ passed\s+q1[\s\S]*└─ @1[0-9a-z]{7}/);
@@ -480,12 +483,23 @@ describe("--report 装载", () => {
480
483
  " locator: ctx.scope.snapshots[0].evals[0].attempts[0].locator,",
481
484
  " },",
482
485
  "});",
486
+ "const AttemptPage = () => null;",
487
+ "AttemptPage[FACES] = {",
488
+ " resolve: (props, ctx) => ({ locator: ctx.page.locator, verdict: ctx.page.evidence.result.verdict }),",
489
+ " web: () => null,",
490
+ " text: (props) => `CUSTOM ATTEMPT PAGE · ${props.locator} · ${props.verdict}`,",
491
+ "};",
483
492
  "const definition = {",
484
493
  ' kind: "report",',
485
494
  " links: [],",
486
495
  " scripts: [],",
487
496
  " styles: [],",
488
- ' pages: [{ id: "report", title: "Report", content: { type: Wrapper, props: {} } }],',
497
+ " pages: [",
498
+ ' { id: "report", title: "Report", input: "scope", navigation: true, content: { type: Wrapper, props: {} } },',
499
+ // 报告需要声明一张 attempt-input page,attemptCommand 生成器才存在(architecture.md
500
+ // 「Attempt 详情是一张参数化 page」);navigation:false 不进「其余页」索引,单页断言不受影响。
501
+ ' { id: "attempt", title: "Attempt", input: "attempt", navigation: false, content: { type: AttemptPage, props: {} } },',
502
+ " ],",
489
503
  "};",
490
504
  "Object.defineProperty(definition, DEFINITION, { value: true });",
491
505
  "export default definition;",
@@ -660,6 +674,75 @@ describe("--report 装载", () => {
660
674
  expect(builtin.err).toContain('page "typo" not found in the built-in report');
661
675
  expect(builtin.err).toContain("Available pages: report, attempts, traces");
662
676
  });
677
+
678
+ /** 只有一张 scope-input page,没有声明 attempt-input page。 */
679
+ async function writeReportFileNoAttemptPage(dir: string): Promise<string> {
680
+ const path = join(dir, "no-attempt-page.mjs");
681
+ await writeFile(
682
+ path,
683
+ [
684
+ 'const FACES = Symbol.for("niceeval.report.faces");',
685
+ 'const DEFINITION = Symbol.for("niceeval.report.definition");',
686
+ "const Overview = () => null;",
687
+ "Overview[FACES] = { web: () => null, text: () => \"OVERVIEW\" };",
688
+ "const definition = {",
689
+ ' kind: "report",',
690
+ " links: [],",
691
+ " scripts: [],",
692
+ " styles: [],",
693
+ ' pages: [{ id: "report", title: "Report", input: "scope", navigation: true, content: { type: Overview, props: {} } }],',
694
+ "};",
695
+ "Object.defineProperty(definition, DEFINITION, { value: true });",
696
+ "export default definition;",
697
+ "",
698
+ ].join("\n"),
699
+ "utf-8",
700
+ );
701
+ return path;
702
+ }
703
+
704
+ it("show @<locator> --report <file>:选中该定义自己声明的 attempt-input page,不是内建固定首页(docs/engineering/unit-tests/reports/cases.md 第 202 行)", async () => {
705
+ const root = await seedComposedRoot();
706
+ const report = await writeReportFile(root);
707
+ const results = await openResults(root);
708
+ const evals = results.experiments[0]!.snapshots.flatMap((s) => s.evals);
709
+ const locator = evals.find((e) => e.id === "weather/brooklyn")!.attempts[0]!.locator!;
710
+
711
+ const { out, code } = await show(root, [locator], { report });
712
+ expect(code).toBe(0);
713
+ expect(out).toBe(`CUSTOM ATTEMPT PAGE · ${locator} · passed\n`);
714
+ // 不是内建 standard 的固定首页文案(AttemptSummary/AttemptAssessment 等)
715
+ expect(out).not.toContain("niceeval show");
716
+ });
717
+
718
+ it("自定义报告没有 attempt-input page 时,裸 show @<locator> --report <file> 报完整用户反馈,指引三种解决路径,不回退到内建详情(第 204 行)", async () => {
719
+ const root = await seedComposedRoot();
720
+ const report = await writeReportFileNoAttemptPage(root);
721
+ const results = await openResults(root);
722
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
723
+
724
+ const { err, code } = await show(root, [locator], { report });
725
+ expect(code).toBe(1);
726
+ expect(err).toContain(report);
727
+ expect(err).toContain("has no attempt-input page");
728
+ expect(err).toContain("extends: standard");
729
+ expect(err).toContain("standardAttemptPage");
730
+ expect(err).toContain('input: "attempt"');
731
+ // 不静默回退渲染内建 standard 的详情页(那会让用户以为自定义报告本来就有这页)
732
+ expect(err).not.toContain("Eval Results");
733
+ });
734
+
735
+ it("--source/--execution/--diff 直接读 AttemptEvidence,不经 page content:没有 attempt page 的自定义报告下仍正常工作(第 203 行)", async () => {
736
+ const root = await seedComposedRoot();
737
+ const report = await writeReportFileNoAttemptPage(root);
738
+ const results = await openResults(root);
739
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
740
+
741
+ const { out, code } = await show(root, [locator], { report, diff: true });
742
+ expect(code).toBe(0);
743
+ expect(out).toContain(locator);
744
+ expect(out).not.toContain("has no attempt-input page");
745
+ });
663
746
  });
664
747
 
665
748
  // ───────────────────────── 证据切面:--execution ─────────────────────────
@@ -812,7 +895,7 @@ describe("show @<locator>", () => {
812
895
  const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
813
896
  const { out } = await show(root, [locator]);
814
897
  expect(out).toContain("gate · Issue 15193");
815
- expect(out).toContain("assertion: equals(4)");
898
+ expect(out).toContain("equals(4)");
816
899
  expect(out).toContain("expected: 4");
817
900
  expect(out).toContain("received: 1");
818
901
  expect(out).toContain("source: evals/manager.eval.ts:40:11");
@@ -829,10 +912,13 @@ describe("show @<locator>", () => {
829
912
 
830
913
  const { out, code } = await show(root, [locator]);
831
914
  expect(code).toBe(0);
832
- expect(out).toContain(`${locator} · weather/brooklyn · compare/bub · passed`);
833
- expect(out).toContain("attempt 1 · ");
834
- expect(out).toContain("execution: unavailable (no events recorded for this attempt)");
835
- expect(out).toContain("changes: diff unavailable");
915
+ // 首行不做整段精确匹配:startedAt 按本机时区格式化,时间部分跨时区不稳定,只断言与时区无关的片段。
916
+ expect(out).toContain(`${locator} · weather/brooklyn · compare/bub · attempt 1 · ✓ passed`);
917
+ expect(out).toContain("· 1.0s");
918
+ // 没有 events/diff 的组件零输出(不是"execution: unavailable"这类占位文案)——
919
+ // 与「Attempt 详情组件族:非空/空证据矩阵」的单元测试同一条契约,这里是集成层确认。
920
+ expect(out).not.toContain("execution:");
921
+ expect(out).not.toContain("changes:");
836
922
  expect(out).not.toContain("evidence:");
837
923
  expect(out).not.toContain("available:");
838
924
  });
@@ -857,22 +943,22 @@ describe("show @<locator>", () => {
857
943
 
858
944
  const { out, code } = await show(root, [locator]);
859
945
  expect(code).toBe(0);
860
- expect(out).toContain(`${locator} · agent-029 · compare/claude-e2b · errored`);
861
- // 结构化 error 块:phase 直接展示闭集点分名,code/message/cause 各一行(见 docs/feature/reports/show.md)
862
- expect(out).toContain("error:");
946
+ // 同上:跳过 startedAt 的时区敏感部分,只断言与时区无关的片段。
947
+ expect(out).toContain(`${locator} · agent-029 · compare/claude-e2b · attempt 1 · ! errored`);
948
+ expect(out).toContain("· 1.0s");
949
+ // 结构化 error 块:code 折进 error: 首行,phase/message/cause 各一行(AttemptError)
950
+ expect(out).toContain("error: sandbox-rate-limit");
863
951
  expect(out).toContain("phase: sandbox.create");
864
- expect(out).toContain("code: sandbox-rate-limit");
865
952
  expect(out).toContain("message: E2B sandbox allocation failed after 5 attempts");
866
953
  expect(out).toContain("cause: RateLimitError · too many concurrent sandboxes");
867
- // attempt 级 diagnostics ( verdict 独立)
868
- expect(out).toContain("diagnostics:");
869
- expect(out).toContain("warning · sandbox.teardown · teardown-failed");
870
- expect(out).toContain("container stop timed out");
871
- // errored 且无断言:不打印空的 assertions 汇总行
954
+ // attempt 级 diagnostics 按 lifecycle phase 分组(AttemptDiagnostics),标题即 phase
955
+ expect(out).toContain("sandbox.teardown:");
956
+ expect(out).toContain("warning · teardown-failed container stop timed out");
957
+ // errored 且无断言:AttemptAssertions 零输出,不打印空的 assertions 汇总行
872
958
  expect(out).not.toContain("assertions:");
873
959
  });
874
960
 
875
- it("available 只逐行列出实际存在的证据命令,不打印能力字母或合并式 next", async () => {
961
+ it("证据下钻命令散落在各自区块行尾(带 locator),不存在的能力不生成假命令,也没有合并式 available/next 尾块", async () => {
876
962
  const root = await makeRoot();
877
963
  const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
878
964
  res("weather/brooklyn", "failed", { hasEvents: true }),
@@ -889,9 +975,14 @@ describe("show @<locator>", () => {
889
975
 
890
976
  const { out, code } = await show(root, [locator]);
891
977
  expect(code).toBe(0);
892
- expect(out).toContain(`available:\n niceeval show ${locator} --source\n niceeval show ${locator} --execution`);
978
+ // AttemptSource / AttemptConversation 各自行尾带命令(不是集中列在一个 available: 块里)
979
+ expect(out).toContain(`niceeval show ${locator} --source`);
980
+ expect(out).toContain(`niceeval show ${locator} --execution`);
981
+ // 没有 diff/timing 能力(fixture 没写 diff.json,也没有 phases)时不生成假命令
893
982
  expect(out).not.toContain(`niceeval show ${locator} --diff`);
983
+ expect(out).not.toContain(`niceeval show ${locator} --timing`);
894
984
  expect(out).not.toContain("evidence:");
985
+ expect(out).not.toContain("available:");
895
986
  expect(out).not.toContain("next:");
896
987
  });
897
988
 
@@ -1054,7 +1145,7 @@ describe("show @<locator>", () => {
1054
1145
  expect(out).not.toContain("git show ×");
1055
1146
  });
1056
1147
 
1057
- it("attempt 首页 timing 只列大头,但保留变慢的 telemetry 阶段", async () => {
1148
+ it("attempt 首页 timing 逐阶段一行,收尾段单独归拢(AttemptTimeline 不做单行宽度收窄,--timing 才是完整分解)", async () => {
1058
1149
  const root = await makeRoot();
1059
1150
  const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
1060
1151
  res("weather/brooklyn", "passed", {
@@ -1080,15 +1171,17 @@ describe("show @<locator>", () => {
1080
1171
  const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
1081
1172
 
1082
1173
  const { out } = await show(root, [locator]);
1083
- const timingLine = out.split("\n").find((line) => line.startsWith("timing:"));
1084
- expect(out).toContain("1 AI message");
1085
- expect(timingLine).toContain("sandbox.queue 0ms");
1086
- expect(timingLine).toContain("sandbox.create 3.0s");
1087
- expect(timingLine).toContain("eval.run 45.0s");
1088
- expect(timingLine).toContain("telemetry.collect 35.3s");
1089
- expect(timingLine).toContain("teardown +8.5s");
1090
- expect(timingLine).not.toContain("workspace.baseline");
1091
- expect(timingLine).not.toContain("telemetry.configure");
1174
+ // 每个主链阶段各占一行(不再是旧格式的单行拼接,那条规则本身是为单行可读服务的,
1175
+ // 多行块没有这个约束了),收尾段(sandbox.stop)单独归拢在 teardown: 之下。
1176
+ expect(out).toContain("timing: 1m 24s");
1177
+ expect(out).toContain("sandbox.queue 0ms");
1178
+ expect(out).toContain("sandbox.create 3.0s");
1179
+ expect(out).toContain("workspace.baseline 310ms");
1180
+ expect(out).toContain("telemetry.configure 358ms");
1181
+ expect(out).toContain("eval.run 45.0s");
1182
+ expect(out).toContain("telemetry.collect 35.3s");
1183
+ expect(out).toContain("teardown:");
1184
+ expect(out).toContain("sandbox.stop 8.5s");
1092
1185
  });
1093
1186
 
1094
1187
  it("语法不对的 locator 报「not a valid attempt locator」,退出码 1,不崩", async () => {
@@ -22,7 +22,7 @@ const reportPages = {
22
22
  };
23
23
 
24
24
  function dataWithShell(report: ViewData["report"]): ViewData {
25
- return { composedRuns: 1, snapshots: [], ...(report !== undefined ? { report } : {}) };
25
+ return { composedRuns: 1, ...(report !== undefined ? { report } : {}) };
26
26
  }
27
27
 
28
28
  describe("外壳:宿主恒有 NiceEval 品牌位、无 hero,导航只有报告页", () => {
@@ -1,9 +1,15 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
2
2
  import { detectLocale, makeTranslator, persistLocale, setDocumentLocale } from "./i18n.ts";
3
- import type { Locale, LocalizedText, ReportSlotHtml, Tab, ViewData, ViewReportPageMeta, ViewResult } from "./types.ts";
4
- import { resultFromUrl } from "./lib/rows.ts";
5
- import { parseAttemptHash, resolveAttemptLocator, unresolvedAttemptWarning } from "./lib/attempt-route.ts";
6
- import { AttemptModal } from "./components/AttemptModal.tsx";
3
+ import type { Locale, LocalizedText, ReportSlotHtml, Tab, ViewData, ViewReportPageMeta } from "./types.ts";
4
+ import {
5
+ attemptHrefFor,
6
+ attemptLocatorFromHref,
7
+ hashForAttempt,
8
+ locatorFromHash,
9
+ parseAttemptDocument,
10
+ type AttemptDocumentContent,
11
+ } from "./lib/attempt-dialog.ts";
12
+ import { Dialog, DialogClose, DialogContent, DialogTitle } from "./components/ui/dialog.tsx";
7
13
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs.tsx";
8
14
 
9
15
  // 导航组成只有一条规则(docs/feature/reports/view.md「页面构成」):导航项 = 报告定义声明的页,
@@ -17,6 +23,20 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs.t
17
23
  // utm_medium 区分点击来自哪个品牌位(shell.md「行为约束」)。
18
24
  const BRAND_HREF = "https://niceeval.com/?utm_source=report&utm_medium=brand";
19
25
 
26
+ /** 屏幕阅读器可用、视觉上不占位:Radix Dialog 需要一个可访问标题,内容本身(身份 / verdict)
27
+ * 已经在 dialog 里可见,不需要再视觉重复一遍。 */
28
+ const VISUALLY_HIDDEN: CSSProperties = {
29
+ position: "absolute",
30
+ width: 1,
31
+ height: 1,
32
+ padding: 0,
33
+ margin: -1,
34
+ overflow: "hidden",
35
+ clip: "rect(0, 0, 0, 0)",
36
+ whiteSpace: "nowrap",
37
+ border: 0,
38
+ };
39
+
20
40
  /**
21
41
  * LocalizedText 的确定回退(docs/feature/reports/library/shell.md):当前 locale → en →
22
42
  * 按 locale 键字典序的第一个非空值。字符串原样返回。
@@ -32,19 +52,6 @@ export function localizedText(text: LocalizedText | undefined, locale: Locale):
32
52
  return undefined;
33
53
  }
34
54
 
35
- /** 初始 URL → 直接打开的 attempt:先认 #/attempt/@<locator> 深链,回退旧版 ?modal= 参数。 */
36
- function modalResultFromLocation(snapshots: ViewData["snapshots"]): ViewResult | null {
37
- const locator = parseAttemptHash(location.hash);
38
- if (locator) {
39
- const found = resolveAttemptLocator(snapshots, locator);
40
- if (found) return found;
41
- // 定位不到(locator 不在、快照未加载、旧格式数据):不开空 modal,页面照常渲染。
42
- console.warn(unresolvedAttemptWarning(location.hash));
43
- return null;
44
- }
45
- return resultFromUrl(snapshots);
46
- }
47
-
48
55
  /**
49
56
  * 报告槽:React 19 对 dangerouslySetInnerHTML 只比较对象身份,身份一变就无条件重设
50
57
  * innerHTML(不再比对 __html 字符串值)。{__html} 必须 memo 住,否则 App 任意一次重渲染
@@ -68,7 +75,6 @@ function hashForTab(tab: Tab): string {
68
75
  }
69
76
 
70
77
  export function App({ data, reportPages }: { data: ViewData; reportPages: Record<string, ReportSlotHtml> }) {
71
- const snapshots = data.snapshots ?? [];
72
78
  const [locale, setLocale] = useState<Locale>(() => detectLocale());
73
79
  const t = useMemo(() => makeTranslator(locale), [locale]);
74
80
 
@@ -79,11 +85,16 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
79
85
  const initialPageId = data.report?.initialPageId ?? pages[0]!.id;
80
86
 
81
87
  const [tab, setTab] = useState<Tab>(() => tabFromHash(location.hash, pages) ?? `page:${initialPageId}`);
82
- const [modalResult, setModalResult] = useState<ViewResult | null>(() => modalResultFromLocation(snapshots));
83
- // 当前 modal 的 hash 历史条目前面是否还有本页条目(本页 push 的 / 前进键回到的):
88
+
89
+ // attempt 详情弹窗:内容是独立文档(attempt/<locator>.html)fetch 回来的同一份 server-rendered
90
+ // 片段,不维护第二份客户端渲染(docs/feature/reports/view.md「静态导出」)。dialogLocator 为
91
+ // null 即关闭。
92
+ const [dialogLocator, setDialogLocator] = useState<string | null>(null);
93
+ const [dialogContent, setDialogContent] = useState<AttemptDocumentContent | null>(null);
94
+ // 当前 dialog 的 hash 历史条目前面是否还有本页条目(点击链接 push 的 / 前进键回到的):
84
95
  // true → UI 关闭走 history.back(),前进键还能重新打开;false(深链直接落地)→ 原地抹 hash,
85
96
  // 免得 back 把用户弹出站外。
86
- const modalOwnsHistory = useRef(false);
97
+ const dialogOwnsHistory = useRef(false);
87
98
 
88
99
  useEffect(() => {
89
100
  setDocumentLocale(locale);
@@ -98,48 +109,88 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
98
109
  document.title = shellTitle;
99
110
  }, [shellTitle]);
100
111
 
101
- const closeModal = useCallback(() => {
102
- setModalResult(null);
103
- if (modalOwnsHistory.current) {
104
- modalOwnsHistory.current = false;
112
+ /** fetch 一个 locator 的独立文档、抠出两种语言内容并打开 dialog;定位不到就直说,不开空 dialog。 */
113
+ const openAttempt = useCallback(async (locator: string, ownsHistory: boolean) => {
114
+ try {
115
+ const res = await fetch(attemptHrefFor(locator));
116
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
117
+ const content = parseAttemptDocument(await res.text());
118
+ if (!content) throw new Error("response is not a recognized attempt document");
119
+ dialogOwnsHistory.current = ownsHistory;
120
+ setDialogLocator(locator);
121
+ setDialogContent(content);
122
+ } catch (e) {
123
+ console.warn(
124
+ `[niceeval view] failed to open attempt "${locator}": ${e instanceof Error ? e.message : String(e)}`,
125
+ );
126
+ }
127
+ }, []);
128
+
129
+ const closeDialog = useCallback(() => {
130
+ setDialogLocator(null);
131
+ setDialogContent(null);
132
+ if (dialogOwnsHistory.current) {
133
+ dialogOwnsHistory.current = false;
105
134
  history.back();
106
135
  return;
107
136
  }
108
137
  try {
109
- // 深链直接落地 / 旧版 ?modal= 链接:没有可回退的本页条目,原地还原成无 modal 的 URL。
110
- history.replaceState(null, "", location.pathname);
138
+ // 深链直接落地:没有可回退的本页条目,原地还原成无 hash 的 URL。
139
+ history.replaceState(null, "", location.pathname + location.search);
111
140
  } catch {
112
141
  // 还原 URL 失败不影响关闭。
113
142
  }
114
143
  }, []);
115
144
 
116
- // 浏览器前进/后退、手改 hash、页内链接(attempt 深链与 `#/page/<id>` 页路由)统一从
117
- // hashchange 分发:attempt hash 开证据室弹窗,页 hash 切当前 tab。
118
- // attempt 详情路由对有效根解析(viewData.snapshots 即有效根):收窄之内、不在页面统计
119
- // 口径(现刻水位)里的历史 attempt 仍能经深链打开,报告里的证据引用不因统计口径失效。
145
+ // 初始 URL 已经是 #/attempt/@<locator> 深链:直接打开(不经 hashchange——那只在后续变化时触发)
146
+ // 有效根即收窄后的结果(view.md「打开与收窄」):这份路由对完整结果根解析,不受当前统计口径
147
+ // (现刻水位)限制,深链因此对历史 attempt 同样可达;收窄之外的 locator 由这份文档自身
148
+ // 的宿主寻址语义处理(本地越过收窄解析,导出站按证据缺失呈现,不是这里的关注点)
149
+ // 空依赖数组是有意的:只在挂载时检查一次初始 hash,openAttempt 本身是 useCallback(deps: [])
150
+ // 的稳定引用,不会随后续渲染变化。
151
+ useEffect(() => {
152
+ const locator = locatorFromHash(location.hash);
153
+ if (locator) void openAttempt(locator, false);
154
+ }, [openAttempt]);
155
+
156
+ // 浏览器前进/后退、手改 hash、页内链接点击(经下面的点击拦截转成 hash 变化)统一从
157
+ // hashchange 分发:attempt hash 开证据室 dialog,页 hash 切当前 tab。
120
158
  useEffect(() => {
121
159
  const onHashChange = () => {
122
- const locator = parseAttemptHash(location.hash);
160
+ const locator = locatorFromHash(location.hash);
123
161
  if (locator) {
124
- const found = resolveAttemptLocator(snapshots, locator);
125
- if (!found) {
126
- console.warn(unresolvedAttemptWarning(location.hash));
127
- setModalResult(null);
128
- return;
129
- }
130
- // 经浏览器导航打开:前一条历史仍是本页,UI 关闭可以安全 back()。
131
- modalOwnsHistory.current = true;
132
- setModalResult(found);
162
+ // 经浏览器导航打开:前一条历史仍是本页,dialog 关闭可以安全 back()
163
+ void openAttempt(locator, true);
133
164
  return;
134
165
  }
135
- modalOwnsHistory.current = false;
136
- setModalResult(null);
166
+ setDialogLocator(null);
167
+ setDialogContent(null);
137
168
  const routed = tabFromHash(location.hash, pages);
138
169
  if (routed) setTab(routed);
139
170
  };
140
171
  window.addEventListener("hashchange", onHashChange);
141
172
  return () => window.removeEventListener("hashchange", onHashChange);
142
- }, [snapshots, pages]);
173
+ }, [pages, openAttempt]);
174
+
175
+ // 拦截 attempt 文档链接(报告页里的 locator 引用,attemptHref 缺省产出的
176
+ // `attempt/<encodeURIComponent(locator)>.html`):点击时改写成 hash 路由,交给上面的
177
+ // hashchange 统一打开——无 JavaScript 时这些链接原样导航到独立文档,同样完整可读。
178
+ // 修饰键 / 非左键点击放行,让「新标签页打开」这类浏览器原生行为不受影响。
179
+ useEffect(() => {
180
+ const onClick = (e: MouseEvent) => {
181
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
182
+ const target = e.target;
183
+ if (!(target instanceof Element)) return;
184
+ const anchor = target.closest("a[href]");
185
+ if (!anchor) return;
186
+ const locator = attemptLocatorFromHref(anchor.getAttribute("href") ?? "");
187
+ if (!locator) return;
188
+ e.preventDefault();
189
+ location.hash = hashForAttempt(locator);
190
+ };
191
+ document.addEventListener("click", onClick);
192
+ return () => document.removeEventListener("click", onClick);
193
+ }, []);
143
194
 
144
195
  const selectTab = useCallback((value: Tab) => {
145
196
  setTab(value);
@@ -202,7 +253,7 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
202
253
  <TabsContent key={`page:${page.id}`} value={`page:${page.id}`} id={`tab-page-${page.id}`}>
203
254
  {/* 报告槽:server 侧逐页渲染好的静态 HTML(含 <Style> 产物),按当前页与界面语言
204
255
  摆放对应块;hero、品牌行、Scope 警告、批量修复 prompt 都是页内组件,壳不再渲染。
205
- attempt 深链是普通 <a href="#/attempt/…">,经 hashchange 打开证据室弹窗。 */}
256
+ attempt 深链是普通 <a href="attempt/…html">,经上面的点击拦截打开 dialog。 */}
206
257
  <ReportSlot html={reportPages[page.id]?.[locale] || reportPages[page.id]?.en || ""} />
207
258
  </TabsContent>
208
259
  ))}
@@ -212,7 +263,28 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
212
263
  <span className="site-footer-text">{footerText}</span>
213
264
  </footer>
214
265
  ) : null}
215
- {modalResult && <AttemptModal result={modalResult} onClose={closeModal} t={t} />}
266
+ {dialogLocator && dialogContent ? (
267
+ <Dialog open onOpenChange={(o) => { if (!o) closeDialog(); }}>
268
+ <DialogContent aria-describedby={undefined}>
269
+ {/* 屏幕阅读器用的可访问标题:视觉上隐藏,身份 / verdict 等实际内容已经在下面
270
+ fetch 回来的片段里可见,这里不重复渲染。 */}
271
+ <DialogTitle style={VISUALLY_HIDDEN}>{t("dialog.attemptTitle")}</DialogTitle>
272
+ <div className="flex min-w-0 shrink-0 items-center justify-end border-b border-line px-7 pb-3 pt-4">
273
+ <DialogClose
274
+ aria-label={t("action.close")}
275
+ className="grid h-7 w-7 shrink-0 place-items-center rounded-md border border-transparent text-sm text-muted transition-colors hover:border-line hover:bg-panel-2 hover:text-text"
276
+ >
277
+ x
278
+ </DialogClose>
279
+ </div>
280
+ {/* 与直接打开 attempt/<locator>.html 看到的是同一份 server-rendered 片段
281
+ (docs/engineering/unit-tests/reports/cases.md 第 207 行),不是客户端重新渲染。 */}
282
+ <div className="flex-1 overflow-y-auto px-7 pb-7 pt-2">
283
+ <ReportSlot html={dialogContent[locale] || dialogContent.en} />
284
+ </div>
285
+ </DialogContent>
286
+ </Dialog>
287
+ ) : null}
216
288
  </Tabs>
217
289
  );
218
290
  }