niceeval 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +2 -2
  4. package/dist/i18n/zh-CN.d.ts +3 -3
  5. package/dist/report/aggregate.d.ts +32 -26
  6. package/dist/report/aggregate.js +157 -76
  7. package/dist/report/built-in/index.d.ts +2 -0
  8. package/dist/report/built-in/index.js +8 -0
  9. package/dist/report/components.d.ts +91 -164
  10. package/dist/report/components.js +377 -114
  11. package/dist/report/compute.d.ts +86 -73
  12. package/dist/report/compute.js +592 -432
  13. package/dist/report/flag.d.ts +28 -17
  14. package/dist/report/flag.js +86 -16
  15. package/dist/report/format.d.ts +11 -11
  16. package/dist/report/format.js +17 -15
  17. package/dist/report/index.d.ts +16 -17
  18. package/dist/report/index.js +20 -22
  19. package/dist/report/load.js +3 -2
  20. package/dist/report/locale.d.ts +49 -34
  21. package/dist/report/locale.js +106 -58
  22. package/dist/report/metrics.d.ts +10 -3
  23. package/dist/report/metrics.js +46 -12
  24. package/dist/report/primitives.d.ts +42 -15
  25. package/dist/report/primitives.js +135 -26
  26. package/dist/report/react/AttemptList.d.ts +10 -8
  27. package/dist/report/react/AttemptList.js +18 -10
  28. package/dist/report/react/DeltaTable.js +19 -18
  29. package/dist/report/react/EvalList.d.ts +3 -3
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +4 -2
  32. package/dist/report/react/ExperimentComparison.js +5 -4
  33. package/dist/report/react/ExperimentList.d.ts +3 -3
  34. package/dist/report/react/ExperimentList.js +16 -15
  35. package/dist/report/react/MetricBars.js +5 -4
  36. package/dist/report/react/MetricLine.js +12 -5
  37. package/dist/report/react/MetricMatrix.js +1 -1
  38. package/dist/report/react/MetricScatter.js +54 -17
  39. package/dist/report/react/MetricTable.js +2 -12
  40. package/dist/report/react/ScopeSummary.d.ts +10 -0
  41. package/dist/report/react/ScopeSummary.js +17 -0
  42. package/dist/report/react/Scoreboard.js +6 -6
  43. package/dist/report/react/cell.js +2 -2
  44. package/dist/report/react/fixtures.d.ts +5 -9
  45. package/dist/report/react/fixtures.js +105 -149
  46. package/dist/report/react/index.d.ts +15 -5
  47. package/dist/report/react/index.js +18 -7
  48. package/dist/report/report.d.ts +137 -20
  49. package/dist/report/report.js +261 -34
  50. package/dist/report/text/faces.d.ts +17 -19
  51. package/dist/report/text/faces.js +225 -157
  52. package/dist/report/text/plot.js +1 -1
  53. package/dist/report/text/table.js +2 -2
  54. package/dist/report/tree.d.ts +90 -40
  55. package/dist/report/tree.js +252 -94
  56. package/dist/report/types.d.ts +245 -300
  57. package/dist/report/types.js +4 -3
  58. package/dist/report/web.d.ts +21 -5
  59. package/dist/report/web.js +42 -16
  60. package/dist/results/select.d.ts +38 -16
  61. package/dist/results/select.js +73 -25
  62. package/dist/results/types.d.ts +38 -14
  63. package/dist/shared/aggregate.d.ts +3 -2
  64. package/dist/shared/aggregate.js +5 -4
  65. package/docs-site/zh/README.md +44 -0
  66. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  67. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  68. package/docs-site/zh/examples/index.mdx +50 -0
  69. package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
  70. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  71. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  72. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  73. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  74. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  75. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  76. package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
  77. package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
  78. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  79. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
  80. package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
  81. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  82. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
  83. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
  84. package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
  85. package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
  86. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
  87. package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
  88. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
  89. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
  90. package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
  91. package/docs-site/zh/index.mdx +23 -25
  92. package/docs-site/zh/introduction.mdx +8 -8
  93. package/docs-site/zh/reference/builtin-agents.mdx +5 -5
  94. package/docs-site/zh/reference/capabilities.mdx +6 -6
  95. package/docs-site/zh/reference/cli.mdx +9 -7
  96. package/docs-site/zh/reference/define-agent.mdx +1 -1
  97. package/docs-site/zh/reference/events.mdx +3 -3
  98. package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
  99. package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
  100. package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
  101. package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
  102. package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
  103. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  104. package/package.json +6 -2
  105. package/src/agents/index.ts +2 -2
  106. package/src/agents/openai-compat.ts +1 -1
  107. package/src/agents/streaming.ts +2 -2
  108. package/src/agents/types.ts +3 -3
  109. package/src/cli.ts +42 -23
  110. package/src/context/context.ts +1 -1
  111. package/src/context/session.test.ts +1 -1
  112. package/src/context/session.ts +1 -1
  113. package/src/i18n/en.ts +18 -16
  114. package/src/i18n/zh-CN.ts +16 -15
  115. package/src/report/aggregate.ts +175 -87
  116. package/src/report/built-in/index.tsx +9 -0
  117. package/src/report/components.tsx +625 -285
  118. package/src/report/compute.ts +717 -515
  119. package/src/report/dual-render.test.tsx +738 -1148
  120. package/src/report/flag.ts +97 -33
  121. package/src/report/format.ts +18 -22
  122. package/src/report/index.ts +113 -58
  123. package/src/report/load.ts +3 -2
  124. package/src/report/locale.ts +120 -69
  125. package/src/report/metrics.ts +42 -12
  126. package/src/report/primitives.tsx +190 -45
  127. package/src/report/react/AttemptList.tsx +32 -20
  128. package/src/report/react/DeltaTable.tsx +63 -45
  129. package/src/report/react/EvalList.tsx +0 -0
  130. package/src/report/react/ExperimentComparison.tsx +12 -7
  131. package/src/report/react/ExperimentList.tsx +38 -26
  132. package/src/report/react/MetricBars.tsx +5 -4
  133. package/src/report/react/MetricLine.tsx +13 -8
  134. package/src/report/react/MetricMatrix.tsx +2 -2
  135. package/src/report/react/MetricScatter.tsx +74 -20
  136. package/src/report/react/MetricTable.tsx +4 -76
  137. package/src/report/react/ScopeSummary.tsx +86 -0
  138. package/src/report/react/Scoreboard.tsx +28 -10
  139. package/src/report/react/cell.tsx +2 -2
  140. package/src/report/react/enhance.js +57 -5
  141. package/src/report/react/fixtures.ts +109 -156
  142. package/src/report/react/index.tsx +24 -39
  143. package/src/report/react/render.test.tsx +139 -104
  144. package/src/report/react/styles.css +181 -91
  145. package/src/report/report.test.ts +761 -1031
  146. package/src/report/report.ts +425 -47
  147. package/src/report/text/faces.ts +257 -164
  148. package/src/report/text/plot.ts +1 -1
  149. package/src/report/text/table.ts +2 -2
  150. package/src/report/tree.ts +362 -104
  151. package/src/report/types.ts +257 -287
  152. package/src/report/web.ts +63 -20
  153. package/src/results/attempt-evidence.test.ts +4 -4
  154. package/src/results/attempt-evidence.ts +5 -5
  155. package/src/results/copy.ts +6 -6
  156. package/src/results/host-equivalence.test.ts +26 -14
  157. package/src/results/index.ts +10 -4
  158. package/src/results/open.ts +8 -4
  159. package/src/results/results.test.ts +4 -3
  160. package/src/results/select.ts +104 -34
  161. package/src/results/types.ts +36 -14
  162. package/src/runner/feedback/human.test.ts +1 -1
  163. package/src/runner/run.ts +1 -1
  164. package/src/sandbox/cli-commands.ts +2 -2
  165. package/src/scoring/judge.test.ts +1 -1
  166. package/src/shared/aggregate.ts +5 -4
  167. package/src/show/compose.ts +50 -67
  168. package/src/show/index.ts +107 -56
  169. package/src/show/render.ts +43 -27
  170. package/src/show/report-host.test.ts +188 -0
  171. package/src/show/report-host.ts +375 -0
  172. package/src/show/show.test.ts +86 -36
  173. package/src/view/app/App.test.tsx +69 -0
  174. package/src/view/app/App.tsx +144 -48
  175. package/src/view/app/components/AttemptModal.tsx +324 -63
  176. package/src/view/app/components/CodeView.tsx +10 -4
  177. package/src/view/app/i18n.ts +31 -17
  178. package/src/view/app/lib/artifact-url.ts +14 -3
  179. package/src/view/app/main.tsx +13 -8
  180. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  181. package/src/view/app/types.ts +4 -1
  182. package/src/view/artifact-serving.test.ts +21 -1
  183. package/src/view/client-dist/app.css +1 -1
  184. package/src/view/client-dist/app.js +6 -6
  185. package/src/view/data.test.ts +9 -3
  186. package/src/view/data.ts +145 -49
  187. package/src/view/index.ts +48 -44
  188. package/src/view/server.ts +35 -15
  189. package/src/view/shared/types.ts +34 -5
  190. package/src/view/styles.css +224 -0
  191. package/src/view/view-report.test.ts +161 -57
  192. package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
  193. package/dist/report/built-ins/experiment-comparison.js +0 -119
  194. package/dist/report/built-ins/index.d.ts +0 -2
  195. package/dist/report/built-ins/index.js +0 -2
  196. package/dist/report/react/GroupSummary.d.ts +0 -8
  197. package/dist/report/react/GroupSummary.js +0 -8
  198. package/dist/report/react/RunOverview.d.ts +0 -8
  199. package/dist/report/react/RunOverview.js +0 -12
  200. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  201. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  202. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  203. package/docs-site/zh/example/showcase.mdx +0 -39
  204. package/src/report/built-in-user-parity.test.tsx +0 -597
  205. package/src/report/built-ins/experiment-comparison.tsx +0 -179
  206. package/src/report/built-ins/index.ts +0 -7
  207. package/src/report/react/GroupSummary.tsx +0 -66
  208. package/src/report/react/RunOverview.tsx +0 -109
  209. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  210. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  211. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  212. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  213. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  214. /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
  215. /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
  216. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
  217. /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
  218. /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
@@ -1,19 +1,19 @@
1
- // 排版原语 Row / Col / Section / Text / Style / Table:六个内置双面组件,没有特殊机制。
2
- // web 面是普通 React 渲染;text 面用 ctx.render(child, 子宽) 显式传宽,
3
- // Row 分栏、宽度不足降级纵向。Style 给自定义组件带样式:web 面吐 <style> 标签,
4
- // text 面渲染为空 —— 静态导出不打包用户代码,className 引用的 CSS 靠它随树走。
5
- // Table 是自定义表的标准件,官方表状组件的 text 面也建在它上面(见 ./text/table.ts)。
1
+ // 排版原语 Row / Col / Section / Text / Style / Tabs / Tab / Table:八个内置双面组件,
2
+ // 没有特殊机制(docs/feature/reports/library/layout.md)。web 面是普通 React 渲染;
3
+ // text 面用 ctx.render(child, 子宽) 显式传宽。Style 注入页级全局 CSS(树位置只决定声明
4
+ // 顺序),text 面渲染为空。Table 是自定义表的标准件,官方表状组件的 text 面也建在它上面。
6
5
 
7
6
  import type { ReactNode } from "react";
8
7
  import type { AttemptLocator } from "../results/locator.ts";
9
- import { defineComponent, type ReportNode } from "./tree.ts";
10
- import { localeText, type ReportLocale } from "./locale.ts";
11
- import { indentBlock, joinColumns, wrapDisplay, type ColumnAlign } from "./text/layout.ts";
8
+ import { COMPONENT_RAW_CHILDREN, COMPONENT_ROLE, defineComponent, type ReportNode } from "./tree.ts";
9
+ import { localeText, resolveLocalizedText, type LocalizedText, type ReportLocale } from "./locale.ts";
10
+ import { indentBlock, joinColumns, stringWidth, wrapDisplay } from "./text/layout.ts";
11
+ import type { ColumnAlign } from "./text/layout.ts";
12
12
  import { renderTableText } from "./text/table.ts";
13
13
 
14
14
  function childArray(children: ReportNode): ReportNode[] {
15
15
  if (children === null || children === undefined || typeof children === "boolean") return [];
16
- return Array.isArray(children) ? children : [children];
16
+ return Array.isArray(children) ? [...children] : [children];
17
17
  }
18
18
 
19
19
  function cx(...parts: (string | undefined)[]): string {
@@ -25,8 +25,11 @@ export interface LayoutProps {
25
25
  className?: string;
26
26
  }
27
27
 
28
- /** 纵向依次排列:网页是块级堆叠,终端是逐块输出(块间空一行)。 */
29
- export const Col = defineComponent<LayoutProps>({
28
+ export type RowProps = LayoutProps;
29
+ export type ColProps = LayoutProps;
30
+
31
+ /** 纵向依次排列:网页是块级堆叠,终端是逐块输出(块间空一行)。两面都按声明序。 */
32
+ export const Col = defineComponent<ColProps>({
30
33
  web({ children, className }) {
31
34
  return <div className={cx("nre", "nre-col", className)}>{children as ReactNode}</div>;
32
35
  },
@@ -39,12 +42,18 @@ export const Col = defineComponent<LayoutProps>({
39
42
  });
40
43
  Col.displayName = "Col";
41
44
 
42
- // Row 的每栏至少留这个宽度,不硬挤;不够就降级纵向
43
- const MIN_COLUMN_WIDTH = 24;
44
45
  const COLUMN_SEPARATOR = " │ ";
45
46
 
46
- /** 并排:网页横向排布,终端字符分栏;终端宽度不够时自动降级为纵向。 */
47
- export const Row = defineComponent<LayoutProps>({
47
+ /** 一段已渲染文本的自然显示宽度(最长一行)。 */
48
+ function blockWidth(block: string): number {
49
+ return Math.max(...block.split("\n").map((line) => stringWidth(line)), 0);
50
+ }
51
+
52
+ /**
53
+ * 并排:web 面横排;text 面在可用宽度装得下全部子块时按显示宽度并排(与 `columns` 工具
54
+ * 同一把尺),装不下时整块退化为纵向堆叠——不截断、不隐藏任何子块。
55
+ */
56
+ export const Row = defineComponent<RowProps>({
48
57
  web({ children, className }) {
49
58
  return <div className={cx("nre", "nre-row", className)}>{children as ReactNode}</div>;
50
59
  },
@@ -54,62 +63,70 @@ export const Row = defineComponent<LayoutProps>({
54
63
  );
55
64
  if (blocks.length === 0) return "";
56
65
  if (blocks.length === 1) return ctx.render(blocks[0]);
57
- const columnWidth = Math.floor(
58
- (ctx.width - COLUMN_SEPARATOR.length * (blocks.length - 1)) / blocks.length,
59
- );
60
- if (columnWidth < MIN_COLUMN_WIDTH) {
61
- // 宽度不足:降级纵向,与 Col 同一形态
62
- return blocks
63
- .map((child) => ctx.render(child))
64
- .filter((block) => block.length > 0)
65
- .join("\n\n");
66
+ const rendered = blocks.map((child) => ctx.render(child)).filter((block) => block.length > 0);
67
+ if (rendered.length === 0) return "";
68
+ const widths = rendered.map(blockWidth);
69
+ const total = widths.reduce((sum, w) => sum + w, 0) + COLUMN_SEPARATOR.length * (rendered.length - 1);
70
+ if (total > ctx.width) {
71
+ // 装不下:整块退化为纵向堆叠,与 Col 同一形态
72
+ return rendered.join("\n\n");
66
73
  }
67
- const rendered = blocks.map((child) => ctx.render(child, columnWidth));
68
- return joinColumns(rendered, rendered.map(() => columnWidth), COLUMN_SEPARATOR);
74
+ return joinColumns(rendered, widths, COLUMN_SEPARATOR);
69
75
  },
70
76
  });
71
77
  Row.displayName = "Row";
72
78
 
73
79
  export interface SectionProps extends LayoutProps {
74
- title: string;
80
+ title: LocalizedText;
75
81
  }
76
82
 
77
83
  /** 带标题的块:网页是标题层级,终端是标题行加缩进。 */
78
84
  export const Section = defineComponent<SectionProps>({
79
- web({ title, children, className }) {
85
+ web({ title, children, className }, ctx) {
80
86
  return (
81
87
  <section className={cx("nre", "nre-section", className)}>
82
- <h2 className="nre-section-title">{title}</h2>
88
+ <h2 className="nre-section-title">{resolveLocalizedText(title, ctx.locale)}</h2>
83
89
  {children as ReactNode}
84
90
  </section>
85
91
  );
86
92
  },
87
93
  text({ title, children }, ctx) {
94
+ const heading = resolveLocalizedText(title, ctx.locale);
88
95
  const body = childArray(children)
89
96
  .map((child) => ctx.render(child, ctx.width - 2))
90
97
  .filter((block) => block.length > 0)
91
98
  .join("\n\n");
92
- return body.length > 0 ? `${title}\n${indentBlock(body, " ")}` : title;
99
+ return body.length > 0 ? `${heading}\n${indentBlock(body, " ")}` : heading;
93
100
  },
94
101
  });
95
102
  Section.displayName = "Section";
96
103
 
97
- /** 说明文字:网页是段落,终端是折行文本。 */
98
- export const Text = defineComponent<LayoutProps>({
104
+ export interface TextProps {
105
+ /** 自由正文原样渲染,不随 locale 自动翻译。 */
106
+ children: string | number;
107
+ className?: string;
108
+ }
109
+
110
+ /** 自由文本的显式载体:web 面负责转义,text 面按显示宽度折行。 */
111
+ export const Text = defineComponent<TextProps>({
99
112
  web({ children, className }) {
100
- return <p className={cx("nre", "nre-text", className)}>{children as ReactNode}</p>;
113
+ return <p className={cx("nre", "nre-text", className)}>{children}</p>;
101
114
  },
102
115
  text({ children }, ctx) {
103
- return wrapDisplay(ctx.render(children), ctx.width).join("\n");
116
+ return wrapDisplay(String(children), ctx.width).join("\n");
104
117
  },
105
118
  });
106
119
  Text.displayName = "Text";
120
+ Text[COMPONENT_RAW_CHILDREN] = true;
107
121
 
108
122
  export interface StyleProps {
109
- children?: string;
123
+ children: string;
110
124
  }
111
125
 
112
- /** 自定义组件的样式随树带走:web 面吐 <style> 标签,text 面渲染为空。 */
126
+ /**
127
+ * 注入页级全局 CSS:树位置只决定声明顺序,不限定作用域;text 面零输出。
128
+ * 配置对象形态的报告要全站样式优先用外壳 styles,两条通道注入同一增强层。
129
+ */
113
130
  export const Style = defineComponent<StyleProps>({
114
131
  web({ children }) {
115
132
  return <style>{children}</style>;
@@ -119,13 +136,108 @@ export const Style = defineComponent<StyleProps>({
119
136
  },
120
137
  });
121
138
  Style.displayName = "Style";
139
+ Style[COMPONENT_RAW_CHILDREN] = true;
140
+
141
+ // ───────────────────────── Tabs / Tab ─────────────────────────
142
+
143
+ export interface TabsProps extends LayoutProps {}
144
+
145
+ export interface TabProps extends LayoutProps {
146
+ title: LocalizedText;
147
+ }
148
+
149
+ interface TabEntry {
150
+ title: LocalizedText;
151
+ children: ReportNode;
152
+ className?: string;
153
+ }
154
+
155
+ /** Tabs 的直接 Tab 子项(数组 / Fragment 已展平;结构合法性由树校验保证)。 */
156
+ function tabEntries(children: ReportNode): TabEntry[] {
157
+ const out: TabEntry[] = [];
158
+ const visit = (node: ReportNode): void => {
159
+ if (node === null || node === undefined || typeof node === "boolean") return;
160
+ if (Array.isArray(node)) {
161
+ for (const child of node) visit(child);
162
+ return;
163
+ }
164
+ if (typeof node === "object" && node !== null && "props" in node) {
165
+ const element = node as { type: unknown; props: Record<string, unknown> };
166
+ if (element.type === Symbol.for("react.fragment")) {
167
+ visit(element.props.children as ReportNode);
168
+ return;
169
+ }
170
+ out.push({
171
+ title: (element.props.title as LocalizedText) ?? "",
172
+ children: element.props.children as ReportNode,
173
+ className: element.props.className as string | undefined,
174
+ });
175
+ }
176
+ };
177
+ visit(children);
178
+ return out;
179
+ }
180
+
181
+ /**
182
+ * 页内并列视图的可切换块。tab 是页内浏览状态,不是数据边界,也不是宿主寻址单位——
183
+ * 两个渲染面都输出全部 tab 的完整内容:web 静态 HTML 每 tab 一个 <details>(首个默认展开,
184
+ * 渐进增强升级成单选 tab 条,切换不改变数据);text 面按声明序输出带标题分节,不折成索引
185
+ * 也不省略(tab 没有选择器,索引只能是死路)。
186
+ */
187
+ export const Tabs = defineComponent<TabsProps>({
188
+ web({ children, className }, ctx) {
189
+ const tabs = tabEntries(children);
190
+ return (
191
+ <div className={cx("nre", "nre-tabs", className)} data-nre-tabs>
192
+ {tabs.map((tab, i) => (
193
+ <details key={i} className={cx("nre-tab", tab.className)} open={i === 0}>
194
+ <summary className="nre-tab-title">{resolveLocalizedText(tab.title, ctx.locale)}</summary>
195
+ <div className="nre-tab-body">{tab.children as ReactNode}</div>
196
+ </details>
197
+ ))}
198
+ </div>
199
+ );
200
+ },
201
+ text({ children }, ctx) {
202
+ const tabs = tabEntries(children);
203
+ return tabs
204
+ .map((tab) => {
205
+ const heading = resolveLocalizedText(tab.title, ctx.locale);
206
+ const body = childArray(tab.children)
207
+ .map((child) => ctx.render(child, ctx.width - 2))
208
+ .filter((block) => block.length > 0)
209
+ .join("\n\n");
210
+ return body.length > 0 ? `${heading}\n${indentBlock(body, " ")}` : heading;
211
+ })
212
+ .join("\n\n");
213
+ },
214
+ });
215
+ Tabs.displayName = "Tabs";
216
+ Tabs[COMPONENT_ROLE] = "tabs";
217
+
218
+ /** 只能直接放在 <Tabs> 下;除通用 children / className 外只有 title。不参与路由,没有 id。 */
219
+ export const Tab = defineComponent<TabProps>({
220
+ web({ children, className }) {
221
+ return <div className={cx("nre", "nre-tab-body", className)}>{children as ReactNode}</div>;
222
+ },
223
+ text({ children }, ctx) {
224
+ return childArray(children)
225
+ .map((child) => ctx.render(child))
226
+ .filter((block) => block.length > 0)
227
+ .join("\n\n");
228
+ },
229
+ });
230
+ Tab.displayName = "Tab";
231
+ Tab[COMPONENT_ROLE] = "tab";
232
+
233
+ // ───────────────────────── Table ─────────────────────────
122
234
 
123
235
  /** 一列的定义:取哪个 cells 键、表头写什么、往哪边对齐。 */
124
236
  export interface TableColumn {
125
237
  /** 取 `row.cells[key]` 的键。 */
126
238
  key: string;
127
- /** 表头文案,两个面都原样渲染。 */
128
- header: string;
239
+ /** 表头文案,按渲染 locale 选择。 */
240
+ header: LocalizedText;
129
241
  /** 对齐方向,默认 `"left"`;`"right"` 按显示宽度右对齐,数字列用。 */
130
242
  align?: ColumnAlign;
131
243
  /**
@@ -141,16 +253,16 @@ export interface TableRow {
141
253
  /** 行身份。 */
142
254
  key: string;
143
255
  /** 已格式化的显示值;`null`(或缺这个键)渲染成 `—`,不补 0。 */
144
- cells: Record<string, string | null>;
256
+ cells: Readonly<Record<string, string | null>>;
145
257
  /** 带上就多一列 attempt:web 面链到证据室,text 面列出 locator。 */
146
258
  locator?: AttemptLocator;
147
259
  }
148
260
 
149
261
  export interface TableProps {
150
- /** 列定义;数组顺序即渲染顺序。 */
151
- columns: TableColumn[];
262
+ /** 非空列定义;数组顺序即渲染顺序。 */
263
+ columns: readonly [TableColumn, ...TableColumn[]];
152
264
  /** 行数据;数组顺序即渲染顺序,组件不重排也不过滤。 */
153
- rows: TableRow[];
265
+ rows: readonly TableRow[];
154
266
  /** 组件自带文案(attempt 表头、丢列提示)的语言;省略时随宿主。 */
155
267
  locale?: ReportLocale;
156
268
  /** web 面挂到 `<table>` 上。 */
@@ -159,6 +271,34 @@ export interface TableProps {
159
271
 
160
272
  const MISSING_MARK = "—";
161
273
 
274
+ /** 列 key 唯一、行 cells 不携带未声明 key、空列拒绝——无类型 JS 输入在渲染前同样校验。 */
275
+ function validateTableProps(props: TableProps): void {
276
+ if (!Array.isArray(props.columns) || props.columns.length === 0) {
277
+ throw new Error("Table needs at least one column: pass columns: [{ key, header }] — an empty column list renders nothing readable.");
278
+ }
279
+ const keys = new Set<string>();
280
+ for (const column of props.columns) {
281
+ if (keys.has(column.key)) {
282
+ throw new Error(`Table column key "${column.key}" is declared twice — column keys address row.cells and must be unique. Rename one column.`);
283
+ }
284
+ keys.add(column.key);
285
+ }
286
+ const rowKeys = new Set<string>();
287
+ for (const row of props.rows) {
288
+ if (rowKeys.has(row.key)) {
289
+ throw new Error(`Table row key "${row.key}" is declared twice — row keys are the row identity and must be unique.`);
290
+ }
291
+ rowKeys.add(row.key);
292
+ for (const cellKey of Object.keys(row.cells)) {
293
+ if (!keys.has(cellKey)) {
294
+ throw new Error(
295
+ `Table row "${row.key}" has a cell "${cellKey}" that no column declares. Declare the column in columns, or drop the stray cell.`,
296
+ );
297
+ }
298
+ }
299
+ }
300
+ }
301
+
162
302
  /**
163
303
  * 自定义表的标准件:列由报告作者定,格子是算好的显示值,两个面各自排整齐。
164
304
  *
@@ -171,7 +311,9 @@ const MISSING_MARK = "—";
171
311
  * 这个组件上:自定义表和官方表用同一把尺子。
172
312
  */
173
313
  export const Table = defineComponent<TableProps>({
174
- web({ columns, rows, locale, className }, ctx) {
314
+ web(props, ctx) {
315
+ validateTableProps(props);
316
+ const { columns, rows, locale, className } = props;
175
317
  const chrome = locale ?? ctx.locale;
176
318
  const hasLocator = rows.some((row) => row.locator !== undefined);
177
319
  const alignClass = (align?: ColumnAlign) => (align === "right" ? "nre-align-right" : undefined);
@@ -181,7 +323,7 @@ export const Table = defineComponent<TableProps>({
181
323
  <tr>
182
324
  {columns.map((column) => (
183
325
  <th key={column.key} scope="col" className={alignClass(column.align)}>
184
- {column.header}
326
+ {resolveLocalizedText(column.header, chrome)}
185
327
  </th>
186
328
  ))}
187
329
  {hasLocator ? <th scope="col">{localeText(chrome, "table.attempt")}</th> : null}
@@ -216,6 +358,9 @@ export const Table = defineComponent<TableProps>({
216
358
  </table>
217
359
  );
218
360
  },
219
- text: renderTableText,
361
+ text(props, ctx) {
362
+ validateTableProps(props);
363
+ return renderTableText(props, ctx);
364
+ },
220
365
  });
221
366
  Table.displayName = "Table";
@@ -1,24 +1,26 @@
1
- // AttemptList:实体列表的叶子层——每项一个 Attempt,固定展示判定、一条主失败摘要或 error
2
- // 摘要与证据引用(locator)。完整 assertions / evidence 只经 locator 下钻。它不预设只看失败,
3
- // 报告作者过滤 items、用 .slice() 限量,total 让渲染面如实报告剩余数量,不静默截断。
1
+ // AttemptList:实体列表的叶子层——每项一个 Attempt,固定展示判定、算好的单行结果摘要
2
+ // (failureSummary)与证据引用(locator)。完整 assertions / evidence 只经 locator 下钻;
3
+ // 渲染面只做宽度截断,不重算摘要。它不预设只看失败,报告作者过滤 data、用 .slice() 限量,
4
+ // total 让渲染面如实报告剩余数量,不静默截断。
4
5
  // ExperimentList / EvalList 的下钻数组是同一个 AttemptListItem[],这里的渲染逻辑因此
5
6
  // 也是它们展开区里"逐条 attempt"那一层的唯一实现(通过 AttemptRow 导出复用,不重写一遍)。
6
7
 
7
8
  import type { ReactElement } from "react";
8
9
  import type { AttemptListItem } from "../types.ts";
9
10
  import type { AttemptLocator } from "../../results/locator.ts";
10
- import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
11
- import { attemptItemReason } from "../format.ts";
11
+ import { DEFAULT_REPORT_LOCALE, countText, localeText, type ReportLocale } from "../locale.ts";
12
12
  import { colorClassForKey } from "./colors.ts";
13
13
  import { cx, formatDurationMs, formatUSD, verdictMark } from "./format.ts";
14
14
 
15
+ const DEFAULT_ATTEMPT_HREF = (locator: AttemptLocator): string => `#/attempt/${locator}`;
16
+
15
17
  /** locator + 判定符的普通 <a>,AttemptList/EvalList/ExperimentList 共用。 */
16
18
  export function AttemptLocatorBadge({
17
19
  item,
18
- attemptHref,
20
+ attemptHref = DEFAULT_ATTEMPT_HREF,
19
21
  }: {
20
22
  item: Pick<AttemptListItem, "locator" | "verdict">;
21
- attemptHref: (locator: AttemptLocator) => string;
23
+ attemptHref?: (locator: AttemptLocator) => string;
22
24
  }): ReactElement {
23
25
  return (
24
26
  <a
@@ -31,16 +33,25 @@ export function AttemptLocatorBadge({
31
33
  );
32
34
  }
33
35
 
34
- /** 一条 Attempt 的比较卡片;完整 assertions 通过 locator 下钻,不在列表内展开。 */
36
+ /** failureSummary + moreFailures 的展示形态:摘要原样,+N 计数由 moreFailures 驱动。 */
37
+ export function failureSummaryText(item: Pick<AttemptListItem, "failureSummary" | "moreFailures">, locale: ReportLocale): string | null {
38
+ if (item.failureSummary === null) return null;
39
+ return item.moreFailures > 0
40
+ ? `${item.failureSummary} · ${countText(locale, "entityList.moreFailures", item.moreFailures)}`
41
+ : item.failureSummary;
42
+ }
43
+
44
+ /** 一条 Attempt 的比较卡片;完整 assertions 通过 locator 下钻,不在列表内展开。 */
35
45
  export function AttemptRow({
36
46
  item,
37
- attemptHref,
47
+ attemptHref = DEFAULT_ATTEMPT_HREF,
48
+ locale = DEFAULT_REPORT_LOCALE,
38
49
  }: {
39
50
  item: AttemptListItem;
40
- attemptHref: (locator: AttemptLocator) => string;
51
+ attemptHref?: (locator: AttemptLocator) => string;
41
52
  locale?: ReportLocale;
42
53
  }): ReactElement {
43
- const reason = attemptItemReason(item);
54
+ const reason = failureSummaryText(item, locale);
44
55
  return (
45
56
  <li className={cx("nre-attempt", `nre-attempt-${item.verdict}`)}>
46
57
  <div className="nre-attempt-head">
@@ -50,7 +61,8 @@ export function AttemptRow({
50
61
  {/* agent 键:稳定散列上色,与其它块(MetricMatrix 列头、DeltaTable 行…)同键同色 */}
51
62
  <span className={cx("nre-attempt-agent", "nre-key", colorClassForKey(item.agent))}>{item.agent}</span>
52
63
  <span className="nre-attempt-duration">{formatDurationMs(item.durationMs)}</span>
53
- {item.costUSD !== undefined && <span className="nre-attempt-cost">{formatUSD(item.costUSD)}</span>}
64
+ {/* costUSD 缺失一律 null(测不了),不显示也不伪造 0 */}
65
+ {item.costUSD !== null && <span className="nre-attempt-cost">{formatUSD(item.costUSD)}</span>}
54
66
  </div>
55
67
  {reason && <p className="nre-attempt-result">{reason}</p>}
56
68
  </li>
@@ -58,25 +70,25 @@ export function AttemptRow({
58
70
  }
59
71
 
60
72
  export function AttemptList({
61
- items,
73
+ data,
62
74
  total,
63
- attemptHref,
75
+ attemptHref = DEFAULT_ATTEMPT_HREF,
64
76
  className,
65
77
  locale = DEFAULT_REPORT_LOCALE,
66
78
  }: {
67
- items: AttemptListItem[];
68
- /** items 被 slice 时的原始数量;如实显示还剩多少条没展示。 */
79
+ data: readonly AttemptListItem[];
80
+ /** data 被 slice 时的原始数量;如实显示还剩多少条没展示。 */
69
81
  total?: number;
70
- attemptHref: (locator: AttemptLocator) => string;
82
+ attemptHref?: (locator: AttemptLocator) => string;
71
83
  className?: string;
72
84
  locale?: ReportLocale;
73
85
  }): ReactElement {
74
- const remaining = (total ?? items.length) - items.length;
86
+ const remaining = (total ?? data.length) - data.length;
75
87
  return (
76
88
  <section className={cx("nre", "nre-attempt-list", className)}>
77
- {items.length === 0 && <p className="nre-attempt-list-empty">{localeText(locale, "attemptList.empty")}</p>}
89
+ {data.length === 0 && <p className="nre-attempt-list-empty">{localeText(locale, "attemptList.empty")}</p>}
78
90
  <ul className="nre-attempts">
79
- {items.map((item) => (
91
+ {data.map((item) => (
80
92
  <AttemptRow key={item.locator} item={item} attemptHref={attemptHref} locale={locale} />
81
93
  ))}
82
94
  </ul>
@@ -1,22 +1,21 @@
1
1
  // DeltaTable:成对对比(B 相对 A)。每格三个值:A、B、Δ;
2
- // Δ 的涨跌好坏由指标的 better 判定并配色(涨不一定是好——成本涨了是坏);
3
- // 任一侧缺数据时 Δ 显示为缺,不硬算(数据侧已给 delta: null,这里只如实渲染)。
2
+ // Δ 的涨跌好坏由数据侧算好的 outcome 驱动配色(涨不一定是好——成本涨了是坏);
3
+ // 任一侧缺数据时 Δ 显示为缺,不硬算(数据侧已给 delta: null / outcome: "unavailable")。
4
+ // 0 对不是错误:派生形态(pairsByFlag)配不出对时显示明确空态并报告配对域实验数。
4
5
 
5
6
  import type { ReactElement } from "react";
6
- import type { DeltaData, MetricColumn } from "../types.ts";
7
- import { DEFAULT_REPORT_LOCALE, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
7
+ import type { DeltaData } from "../types.ts";
8
+ import { DEFAULT_REPORT_LOCALE, localeText, resolveLocalizedText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
8
9
  import { MetricCellView } from "./cell.tsx";
9
10
  import { colorClassForKey } from "./colors.ts";
10
11
  import { cx } from "./format.ts";
11
12
 
12
- /** Δ 的语义配色 class:好/坏由 better 方向判定,不看正负号本身。 */
13
- function deltaToneClass(delta: number | null, better: MetricColumn["better"]): string {
14
- if (delta === null) return "nre-delta-missing";
15
- if (delta === 0) return "nre-delta-flat";
16
- if (!better) return "nre-delta-neutral"; // 指标没表态方向,只显示不评判
17
- const improved = better === "higher" ? delta > 0 : delta < 0;
18
- return improved ? "nre-delta-good" : "nre-delta-bad";
19
- }
13
+ const OUTCOME_CLASS: Record<string, string> = {
14
+ improved: "nre-delta-good",
15
+ regressed: "nre-delta-bad",
16
+ unchanged: "nre-delta-flat",
17
+ unavailable: "nre-delta-missing",
18
+ };
20
19
 
21
20
  export function DeltaTable({
22
21
  data,
@@ -27,6 +26,15 @@ export function DeltaTable({
27
26
  className?: string;
28
27
  locale?: ReportLocale;
29
28
  }): ReactElement {
29
+ if (data.rows.length === 0) {
30
+ return (
31
+ <div className={cx("nre", "nre-delta-table-empty", className)}>
32
+ <p className="nre-missing">
33
+ {localeText(locale, "delta.empty", { experiments: data.experiments ?? 0 })}
34
+ </p>
35
+ </div>
36
+ );
37
+ }
30
38
  return (
31
39
  <table className={cx("nre", "nre-delta-table", className)}>
32
40
  <thead>
@@ -43,39 +51,49 @@ export function DeltaTable({
43
51
  </tr>
44
52
  </thead>
45
53
  <tbody>
46
- {data.rows.map((row) => (
47
- <tr key={row.key}>
48
- <th scope="row" className="nre-pair">
49
- {/* pair 的 label(如 agent 名):稳定散列上色,与 scatter 的线同色 */}
50
- <span className={cx("nre-row-key", "nre-key", colorClassForKey(row.key))}>{row.key}</span>
51
- <span className="nre-pair-ids">
52
- {row.a.experimentId} {row.b.experimentId}
53
- </span>
54
- </th>
55
- {data.columns.map((col) => {
56
- const cell = row.cells[col.key];
57
- if (!cell) return <td key={col.key} className="nre-td-empty" />;
58
- return (
59
- <td key={col.key} className="nre-delta-cell">
60
- {/* A/B 走统一的 MetricCellView:缺数据文案与覆盖率角标同一套 */}
61
- <span className="nre-delta-side nre-delta-a">
62
- <span className="nre-delta-tag">A</span>
63
- <MetricCellView cell={cell.a} locale={locale} />
64
- </span>
65
- <span className="nre-delta-side nre-delta-b">
66
- <span className="nre-delta-tag">B</span>
67
- <MetricCellView cell={cell.b} locale={locale} />
68
- </span>
69
- <span className={cx("nre-delta-side", "nre-delta-d", deltaToneClass(cell.delta, col.better))}>
70
- <span className="nre-delta-tag">Δ</span>
71
- {/* 任一侧 null → delta null:显示缺,不硬算 */}
72
- {cell.delta === null ? <span className="nre-missing">{localeText(locale, "cell.missing")}</span> : cell.display}
73
- </span>
74
- </td>
75
- );
76
- })}
77
- </tr>
78
- ))}
54
+ {data.rows.map((row) => {
55
+ const label = resolveLocalizedText(row.label, locale);
56
+ return (
57
+ <tr key={row.key}>
58
+ <th scope="row" className="nre-pair">
59
+ {/* pair 的 label:作者声明(或派生规则生成)原样透传;稳定散列上色 */}
60
+ <span className={cx("nre-row-key", "nre-key", colorClassForKey(label))}>{label}</span>
61
+ <span className="nre-pair-ids">
62
+ {row.a.key} → {row.b.key}
63
+ </span>
64
+ </th>
65
+ {data.columns.map((col) => {
66
+ const cell = row.cells[col.key];
67
+ if (!cell) return <td key={col.key} className="nre-td-empty" />;
68
+ return (
69
+ <td key={col.key} className="nre-delta-cell">
70
+ {/* A/B 走统一的 MetricCellView:缺数据文案与覆盖率角标同一套 */}
71
+ <span className="nre-delta-side nre-delta-a">
72
+ <span className="nre-delta-tag">A</span>
73
+ <MetricCellView cell={cell.a} locale={locale} />
74
+ </span>
75
+ <span className="nre-delta-side nre-delta-b">
76
+ <span className="nre-delta-tag">B</span>
77
+ <MetricCellView cell={cell.b} locale={locale} />
78
+ </span>
79
+ <span
80
+ className={cx("nre-delta-side", "nre-delta-d", OUTCOME_CLASS[cell.outcome])}
81
+ data-outcome={cell.outcome}
82
+ >
83
+ <span className="nre-delta-tag">Δ</span>
84
+ {/* 任一侧 null → delta null:显示缺,不硬算 */}
85
+ {cell.delta === null ? (
86
+ <span className="nre-missing">{localeText(locale, "cell.missing")}</span>
87
+ ) : (
88
+ resolveLocalizedText(cell.display, locale)
89
+ )}
90
+ </span>
91
+ </td>
92
+ );
93
+ })}
94
+ </tr>
95
+ );
96
+ })}
79
97
  </tbody>
80
98
  </table>
81
99
  );
Binary file
@@ -1,21 +1,26 @@
1
- // ExperimentComparison 的 web 面:完整持有所有可比组,组选择只切换已经独立计算好的 panel。
2
- // 静态 HTML 用原生 <details> 保留每组完整内容;enhance.js 再把上方摘要卡变成单选切换。
1
+ // ExperimentComparison 的 web 面:完整持有所有可比组,组选择只切换已经独立计算好的 panel。
2
+ // 静态 HTML 用原生 <details> 保留每组完整内容(第一组默认展开、无 JS 完整可读);
3
+ // enhance.js 再把上方摘要卡变成单选切换。切组是纯浏览状态,不重新计算任何数字。
3
4
 
4
5
  import type { ReactElement } from "react";
5
- import type { ExperimentComparisonData } from "../built-ins/experiment-comparison.tsx";
6
+ import type { ExperimentComparisonData } from "../types.ts";
6
7
  import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
7
- import { ExperimentList, MetricScatter } from "../components.tsx";
8
- import { GroupSummary } from "./GroupSummary.tsx";
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";
9
12
  import { cx } from "./format.ts";
10
13
 
11
14
  export function ExperimentComparisonView({
12
15
  data,
13
16
  className,
14
17
  locale = DEFAULT_REPORT_LOCALE,
18
+ attemptHref,
15
19
  }: {
16
20
  data: ExperimentComparisonData;
17
21
  className?: string;
18
22
  locale?: ReportLocale;
23
+ attemptHref?: (locator: AttemptLocator) => string;
19
24
  }): ReactElement {
20
25
  if (data.groups.length === 0) {
21
26
  return (
@@ -43,7 +48,7 @@ export function ExperimentComparisonView({
43
48
  data-nre-experiment-group-select={index}
44
49
  >
45
50
  <strong className="nre-experiment-group-name">{group.key}</strong>
46
- <GroupSummary data={group.summary} locale={locale} />
51
+ <ScopeSummary data={group.summary} locale={locale} />
47
52
  </div>
48
53
  ))}
49
54
  </div>
@@ -59,7 +64,7 @@ export function ExperimentComparisonView({
59
64
  >
60
65
  <summary>{group.key}</summary>
61
66
  <MetricScatter data={group.scatter} locale={locale} />
62
- <ExperimentList items={group.experiments} filter locale={locale} relativeTo={group.key} />
67
+ <ExperimentList data={group.experiments} filter locale={locale} relativeTo={group.key} attemptHref={attemptHref} />
63
68
  </details>
64
69
  ))}
65
70
  </div>