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.
- package/INDEX.md +23 -23
- package/README.zh.md +6 -6
- package/dist/agents/types.d.ts +2 -2
- package/dist/i18n/zh-CN.d.ts +3 -3
- package/dist/report/aggregate.d.ts +32 -26
- package/dist/report/aggregate.js +157 -76
- package/dist/report/built-in/index.d.ts +2 -0
- package/dist/report/built-in/index.js +8 -0
- package/dist/report/components.d.ts +91 -164
- package/dist/report/components.js +377 -114
- package/dist/report/compute.d.ts +86 -73
- package/dist/report/compute.js +592 -432
- package/dist/report/flag.d.ts +28 -17
- package/dist/report/flag.js +86 -16
- package/dist/report/format.d.ts +11 -11
- package/dist/report/format.js +17 -15
- package/dist/report/index.d.ts +16 -17
- package/dist/report/index.js +20 -22
- package/dist/report/load.js +3 -2
- package/dist/report/locale.d.ts +49 -34
- package/dist/report/locale.js +106 -58
- package/dist/report/metrics.d.ts +10 -3
- package/dist/report/metrics.js +46 -12
- package/dist/report/primitives.d.ts +42 -15
- package/dist/report/primitives.js +135 -26
- package/dist/report/react/AttemptList.d.ts +10 -8
- package/dist/report/react/AttemptList.js +18 -10
- package/dist/report/react/DeltaTable.js +19 -18
- package/dist/report/react/EvalList.d.ts +3 -3
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +4 -2
- package/dist/report/react/ExperimentComparison.js +5 -4
- package/dist/report/react/ExperimentList.d.ts +3 -3
- package/dist/report/react/ExperimentList.js +16 -15
- package/dist/report/react/MetricBars.js +5 -4
- package/dist/report/react/MetricLine.js +12 -5
- package/dist/report/react/MetricMatrix.js +1 -1
- package/dist/report/react/MetricScatter.js +54 -17
- package/dist/report/react/MetricTable.js +2 -12
- package/dist/report/react/ScopeSummary.d.ts +10 -0
- package/dist/report/react/ScopeSummary.js +17 -0
- package/dist/report/react/Scoreboard.js +6 -6
- package/dist/report/react/cell.js +2 -2
- package/dist/report/react/fixtures.d.ts +5 -9
- package/dist/report/react/fixtures.js +105 -149
- package/dist/report/react/index.d.ts +15 -5
- package/dist/report/react/index.js +18 -7
- package/dist/report/report.d.ts +137 -20
- package/dist/report/report.js +261 -34
- package/dist/report/text/faces.d.ts +17 -19
- package/dist/report/text/faces.js +225 -157
- package/dist/report/text/plot.js +1 -1
- package/dist/report/text/table.js +2 -2
- package/dist/report/tree.d.ts +90 -40
- package/dist/report/tree.js +252 -94
- package/dist/report/types.d.ts +245 -300
- package/dist/report/types.js +4 -3
- package/dist/report/web.d.ts +21 -5
- package/dist/report/web.js +42 -16
- package/dist/results/select.d.ts +38 -16
- package/dist/results/select.js +73 -25
- package/dist/results/types.d.ts +38 -14
- package/dist/shared/aggregate.d.ts +3 -2
- package/dist/shared/aggregate.js +5 -4
- package/docs-site/zh/README.md +44 -0
- package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
- package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
- package/docs-site/zh/examples/index.mdx +50 -0
- package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
- package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
- package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
- package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
- package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
- package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
- package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
- package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
- package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
- package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
- package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
- package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
- package/docs-site/zh/index.mdx +23 -25
- package/docs-site/zh/introduction.mdx +8 -8
- package/docs-site/zh/reference/builtin-agents.mdx +5 -5
- package/docs-site/zh/reference/capabilities.mdx +6 -6
- package/docs-site/zh/reference/cli.mdx +9 -7
- package/docs-site/zh/reference/define-agent.mdx +1 -1
- package/docs-site/zh/reference/events.mdx +3 -3
- package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
- package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
- package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
- package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
- package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
- package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
- package/package.json +6 -2
- package/src/agents/index.ts +2 -2
- package/src/agents/openai-compat.ts +1 -1
- package/src/agents/streaming.ts +2 -2
- package/src/agents/types.ts +3 -3
- package/src/cli.ts +42 -23
- package/src/context/context.ts +1 -1
- package/src/context/session.test.ts +1 -1
- package/src/context/session.ts +1 -1
- package/src/i18n/en.ts +18 -16
- package/src/i18n/zh-CN.ts +16 -15
- package/src/report/aggregate.ts +175 -87
- package/src/report/built-in/index.tsx +9 -0
- package/src/report/components.tsx +625 -285
- package/src/report/compute.ts +717 -515
- package/src/report/dual-render.test.tsx +738 -1148
- package/src/report/flag.ts +97 -33
- package/src/report/format.ts +18 -22
- package/src/report/index.ts +113 -58
- package/src/report/load.ts +3 -2
- package/src/report/locale.ts +120 -69
- package/src/report/metrics.ts +42 -12
- package/src/report/primitives.tsx +190 -45
- package/src/report/react/AttemptList.tsx +32 -20
- package/src/report/react/DeltaTable.tsx +63 -45
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +12 -7
- package/src/report/react/ExperimentList.tsx +38 -26
- package/src/report/react/MetricBars.tsx +5 -4
- package/src/report/react/MetricLine.tsx +13 -8
- package/src/report/react/MetricMatrix.tsx +2 -2
- package/src/report/react/MetricScatter.tsx +74 -20
- package/src/report/react/MetricTable.tsx +4 -76
- package/src/report/react/ScopeSummary.tsx +86 -0
- package/src/report/react/Scoreboard.tsx +28 -10
- package/src/report/react/cell.tsx +2 -2
- package/src/report/react/enhance.js +57 -5
- package/src/report/react/fixtures.ts +109 -156
- package/src/report/react/index.tsx +24 -39
- package/src/report/react/render.test.tsx +139 -104
- package/src/report/react/styles.css +181 -91
- package/src/report/report.test.ts +761 -1031
- package/src/report/report.ts +425 -47
- package/src/report/text/faces.ts +257 -164
- package/src/report/text/plot.ts +1 -1
- package/src/report/text/table.ts +2 -2
- package/src/report/tree.ts +362 -104
- package/src/report/types.ts +257 -287
- package/src/report/web.ts +63 -20
- package/src/results/attempt-evidence.test.ts +4 -4
- package/src/results/attempt-evidence.ts +5 -5
- package/src/results/copy.ts +6 -6
- package/src/results/host-equivalence.test.ts +26 -14
- package/src/results/index.ts +10 -4
- package/src/results/open.ts +8 -4
- package/src/results/results.test.ts +4 -3
- package/src/results/select.ts +104 -34
- package/src/results/types.ts +36 -14
- package/src/runner/feedback/human.test.ts +1 -1
- package/src/runner/run.ts +1 -1
- package/src/sandbox/cli-commands.ts +2 -2
- package/src/scoring/judge.test.ts +1 -1
- package/src/shared/aggregate.ts +5 -4
- package/src/show/compose.ts +50 -67
- package/src/show/index.ts +107 -56
- package/src/show/render.ts +43 -27
- package/src/show/report-host.test.ts +188 -0
- package/src/show/report-host.ts +375 -0
- package/src/show/show.test.ts +86 -36
- package/src/view/app/App.test.tsx +69 -0
- package/src/view/app/App.tsx +144 -48
- package/src/view/app/components/AttemptModal.tsx +324 -63
- package/src/view/app/components/CodeView.tsx +10 -4
- package/src/view/app/i18n.ts +31 -17
- package/src/view/app/lib/artifact-url.ts +14 -3
- package/src/view/app/main.tsx +13 -8
- package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
- package/src/view/app/types.ts +4 -1
- package/src/view/artifact-serving.test.ts +21 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +6 -6
- package/src/view/data.test.ts +9 -3
- package/src/view/data.ts +145 -49
- package/src/view/index.ts +48 -44
- package/src/view/server.ts +35 -15
- package/src/view/shared/types.ts +34 -5
- package/src/view/styles.css +224 -0
- package/src/view/view-report.test.ts +161 -57
- package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
- package/dist/report/built-ins/experiment-comparison.js +0 -119
- package/dist/report/built-ins/index.d.ts +0 -2
- package/dist/report/built-ins/index.js +0 -2
- package/dist/report/react/GroupSummary.d.ts +0 -8
- package/dist/report/react/GroupSummary.js +0 -8
- package/dist/report/react/RunOverview.d.ts +0 -8
- package/dist/report/react/RunOverview.js +0 -12
- package/docs-site/zh/example/ai-agent-application.mdx +0 -152
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
- package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
- package/docs-site/zh/example/showcase.mdx +0 -39
- package/src/report/built-in-user-parity.test.tsx +0 -597
- package/src/report/built-ins/experiment-comparison.tsx +0 -179
- package/src/report/built-ins/index.ts +0 -7
- package/src/report/react/GroupSummary.tsx +0 -66
- package/src/report/react/RunOverview.tsx +0 -109
- /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
- /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
package/src/report/text/faces.ts
CHANGED
|
@@ -1,194 +1,221 @@
|
|
|
1
1
|
// 官方组件的 text 面:同一份算好的数据,渲染成终端字符(niceeval show 的形态)。
|
|
2
|
-
//
|
|
3
|
-
// 诚实契约:排序随 better、samples < total 角标、缺数据 — 不补 0、截断报剩余。
|
|
2
|
+
// 与 web 面共守诚实契约:排序随 better、samples < total 角标、缺数据 — 不补 0、截断报剩余。
|
|
4
3
|
// 零 react、零 IO、纯同步 —— 这是 text 宿主不需要 react-dom 的那一半。
|
|
5
|
-
// chrome 文案(注脚、verdict 词、截断提示)经 ctx.locale 查 locale
|
|
6
|
-
//
|
|
4
|
+
// chrome 文案(注脚、verdict 词、截断提示)经 ctx.locale 查 locale 字典;
|
|
5
|
+
// 数据 display 是 LocalizedText,按 LocalizedText 回退规则取值。
|
|
7
6
|
|
|
8
7
|
import type {
|
|
9
8
|
AttemptListItem,
|
|
10
9
|
DeltaData,
|
|
11
10
|
EvalListItem,
|
|
11
|
+
ExperimentComparisonData,
|
|
12
|
+
ExperimentComparisonGroupData,
|
|
12
13
|
ExperimentListItem,
|
|
13
|
-
GroupSummaryData,
|
|
14
14
|
LineData,
|
|
15
15
|
MatrixData,
|
|
16
|
+
MetricCell,
|
|
16
17
|
MetricColumn,
|
|
17
|
-
OverviewData,
|
|
18
18
|
ScatterData,
|
|
19
|
+
ScopeSummaryData,
|
|
19
20
|
ScoreboardData,
|
|
20
21
|
TableData,
|
|
22
|
+
VerdictTally,
|
|
21
23
|
} from "../types.ts";
|
|
22
24
|
import type { TextContext } from "../tree.ts";
|
|
23
25
|
import type { TableColumn, TableRow } from "../primitives.tsx";
|
|
24
26
|
import {
|
|
25
|
-
attemptItemReason,
|
|
26
27
|
experimentDisplayName,
|
|
28
|
+
fitFailureSummary,
|
|
27
29
|
formatDurationMs,
|
|
28
30
|
formatMetricValue,
|
|
29
31
|
formatPlainNumber,
|
|
30
32
|
formatUSD,
|
|
31
33
|
verdictMark,
|
|
32
34
|
} from "../format.ts";
|
|
33
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
countText,
|
|
37
|
+
localeText,
|
|
38
|
+
resolveLocalizedText,
|
|
39
|
+
resolveMetricLabel,
|
|
40
|
+
type ReportLocale,
|
|
41
|
+
} from "../locale.ts";
|
|
34
42
|
import { indentBlock, padDisplay, stringWidth, textBar, wrapDisplay } from "./layout.ts";
|
|
35
43
|
import { renderTableText } from "./table.ts";
|
|
36
44
|
import { renderCharPlot, renderCoordinateTable, type PlotPoint } from "./plot.ts";
|
|
37
45
|
|
|
38
46
|
const MISSING_MARK = "—";
|
|
39
47
|
|
|
40
|
-
/** 缺数据文案随 locale(en = "no data"
|
|
48
|
+
/** 缺数据文案随 locale(en = "no data")。 */
|
|
41
49
|
function missingText(locale: ReportLocale): string {
|
|
42
50
|
return localeText(locale, "cell.missing");
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
/** 格子的文本形态:缺数据 —,覆盖不全带 samples/total
|
|
46
|
-
export function cellText(cell:
|
|
53
|
+
/** 格子的文本形态:缺数据 —,覆盖不全带 samples/total 角标;display 按 locale 解析。 */
|
|
54
|
+
export function cellText(cell: MetricCell, locale: ReportLocale): string {
|
|
47
55
|
if (cell.value === null) return MISSING_MARK;
|
|
48
|
-
|
|
56
|
+
const display = resolveLocalizedText(cell.display, locale);
|
|
57
|
+
return cell.samples < cell.total ? `${display} ${cell.samples}/${cell.total}` : display;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
|
|
60
|
+
/** verdict 计票的紧凑文案("3 passed / 1 failed"):非零判定逐个列,全部为零如实 —。 */
|
|
61
|
+
export function verdictTallyText(tally: VerdictTally, locale: ReportLocale): string {
|
|
62
|
+
const parts: string[] = [];
|
|
63
|
+
for (const kind of ["passed", "failed", "errored", "skipped"] as const) {
|
|
64
|
+
if (tally[kind] > 0) parts.push(`${tally[kind]} ${localeText(locale, `verdict.${kind}`)}`);
|
|
65
|
+
}
|
|
66
|
+
return parts.length > 0 ? parts.join(" / ") : MISSING_MARK;
|
|
67
|
+
}
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
countText(locale, "overview.experiments", snapshots.length),
|
|
60
|
-
localeText(locale, "overview.evalsCount", { n: totals.evals }),
|
|
61
|
-
localeText(locale, "overview.attemptsCount", { n: totals.attempts }),
|
|
62
|
-
// 通过率只渲染 computeCell 算好的同一个 MetricCell(cellText 复用缺数据/coverage 角标语义),
|
|
63
|
-
// 不从 passed/failed/errored 现场重算——与 web 面 RunOverview 同一份数据同一种读法。
|
|
64
|
-
`${localeText(locale, "overview.passRate")} ${cellText(totals.passRate)}`,
|
|
65
|
-
countText(locale, "composedFrom", runs),
|
|
66
|
-
...(latest ? [localeText(locale, "latestRun", { run: latest })] : []),
|
|
67
|
-
].join(" · ");
|
|
68
|
-
const tallies = [
|
|
69
|
-
`${localeText(locale, "verdict.passed")} ${totals.passed}`,
|
|
70
|
-
`${localeText(locale, "verdict.failed")} ${totals.failed}`,
|
|
71
|
-
`${localeText(locale, "verdict.errored")} ${totals.errored}`,
|
|
72
|
-
`${localeText(locale, "verdict.skipped")} ${totals.skipped}`,
|
|
73
|
-
totals.costUSD === null ? missingText(locale) : formatUSD(totals.costUSD),
|
|
74
|
-
formatDurationMs(totals.durationMs),
|
|
75
|
-
].join(" · ");
|
|
76
|
-
const lines = [head, tallies];
|
|
77
|
-
for (const warning of data.warnings) lines.push(`! ${warning.message}`);
|
|
78
|
-
return lines.join("\n");
|
|
69
|
+
/** ISO 时间 → "YYYY-MM-DD HH:mm"(本地时区);不可解析原样返回。 */
|
|
70
|
+
function formatDateTimeMinute(iso: string): string {
|
|
71
|
+
const date = new Date(iso);
|
|
72
|
+
if (Number.isNaN(date.valueOf())) return iso;
|
|
73
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
74
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
// ─────────────────────────
|
|
77
|
+
// ───────────────────────── ScopeSummary ─────────────────────────
|
|
82
78
|
|
|
83
79
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* 不依赖固定网格宽度,窄终端自然换行。
|
|
80
|
+
* 一至两行:头行是端到端成功率(官方 MetricCell,不重算)+ experiment/eval/attempt 数 +
|
|
81
|
+
* `votes` 选中的那级计票 + 总成本;第二行(有则加)是快照时间窗。
|
|
87
82
|
*/
|
|
88
|
-
export function
|
|
83
|
+
export function scopeSummaryText(data: ScopeSummaryData, votes: "eval" | "attempt", ctx: TextContext): string {
|
|
89
84
|
const locale = ctx.locale;
|
|
85
|
+
const tally = votes === "attempt" ? data.attemptVerdicts : data.evalVerdicts;
|
|
90
86
|
const head = [
|
|
91
|
-
`${localeText(locale, "
|
|
87
|
+
`${localeText(locale, "scopeSummary.passRate")} ${cellText(data.endToEndPassRate, locale)}`,
|
|
92
88
|
countText(locale, "overview.experiments", data.experiments),
|
|
93
89
|
localeText(locale, "overview.evalsCount", { n: data.evals }),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
localeText(locale, "overview.attemptsCount", { n: data.attempts }),
|
|
91
|
+
verdictTallyText(tally, locale),
|
|
92
|
+
`${localeText(locale, "scopeSummary.totalCost")} ${cellText(data.totalCostUSD, locale)}`,
|
|
97
93
|
].join(" · ");
|
|
98
94
|
const lines = [head];
|
|
99
|
-
if (data.
|
|
95
|
+
if (data.range.latestStartedAt !== null) {
|
|
96
|
+
const from = data.range.earliestStartedAt;
|
|
97
|
+
const to = data.range.latestStartedAt;
|
|
98
|
+
lines.push(
|
|
99
|
+
from !== null && from !== to
|
|
100
|
+
? localeText(locale, "scopeSummary.range", { from: formatDateTimeMinute(from), to: formatDateTimeMinute(to) })
|
|
101
|
+
: formatDateTimeMinute(to),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
100
104
|
return lines.join("\n");
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
// ─────────────────────────
|
|
107
|
+
// ───────────────────────── ExperimentComparison ─────────────────────────
|
|
108
|
+
|
|
109
|
+
/** 单组详情:标题行 + 缩进的散点与实验列表(与 web 面同一份预分区数据)。 */
|
|
110
|
+
function comparisonGroupText(group: ExperimentComparisonGroupData, ctx: TextContext): string {
|
|
111
|
+
const inner = Math.max(20, ctx.width - 2);
|
|
112
|
+
const innerCtx: TextContext = {
|
|
113
|
+
width: inner,
|
|
114
|
+
locale: ctx.locale,
|
|
115
|
+
attemptCommand: ctx.attemptCommand,
|
|
116
|
+
experimentCommand: ctx.experimentCommand,
|
|
117
|
+
render: (node, w) => ctx.render(node, w ?? inner),
|
|
118
|
+
};
|
|
119
|
+
const body = [scatterText(group.scatter, innerCtx), experimentListText(group.experiments, innerCtx, group.key)]
|
|
120
|
+
.filter((block) => block.length > 0)
|
|
121
|
+
.join("\n\n");
|
|
122
|
+
return `${localeText(ctx.locale, "experimentComparison.group")} ${group.key}\n\n${indentBlock(body, " ")}`;
|
|
123
|
+
}
|
|
104
124
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
/**
|
|
126
|
+
* text 面:命中多个可比组时只输出组索引与可复制的单组查看命令,不倾倒全部组详情;
|
|
127
|
+
* 命中单组时省略索引,直接输出该组的散点与实验列表。
|
|
128
|
+
*/
|
|
129
|
+
export function experimentComparisonText(
|
|
130
|
+
data: ExperimentComparisonData,
|
|
131
|
+
_className: string | undefined,
|
|
132
|
+
ctx: TextContext,
|
|
109
133
|
): string {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
134
|
+
const locale = ctx.locale;
|
|
135
|
+
if (data.groups.length === 0) return localeText(locale, "experimentComparison.empty");
|
|
136
|
+
if (data.groups.length === 1) return comparisonGroupText(data.groups[0]!, ctx);
|
|
137
|
+
|
|
138
|
+
const columns: TableColumn[] = [
|
|
139
|
+
{ key: "group", header: localeText(locale, "experimentComparison.group") },
|
|
140
|
+
{ key: "experiments", header: localeText(locale, "scopeSummary.experiments"), align: "right" },
|
|
141
|
+
{ key: "evals", header: localeText(locale, "scopeSummary.evals"), align: "right" },
|
|
142
|
+
{ key: "passRate", header: localeText(locale, "scopeSummary.passRate"), align: "right" },
|
|
143
|
+
{ key: "results", header: localeText(locale, "experimentComparison.results") },
|
|
144
|
+
{ key: "cost", header: localeText(locale, "scopeSummary.totalCost"), align: "right" },
|
|
145
|
+
{ key: "lastRun", header: localeText(locale, "experimentComparison.lastRun") },
|
|
146
|
+
] as unknown as TableColumn[];
|
|
147
|
+
const rows: TableRow[] = data.groups.map((group) => ({
|
|
148
|
+
key: group.key,
|
|
149
|
+
cells: {
|
|
150
|
+
group: group.key,
|
|
151
|
+
experiments: String(group.summary.experiments),
|
|
152
|
+
evals: String(group.summary.evals),
|
|
153
|
+
passRate: cellText(group.summary.endToEndPassRate, locale),
|
|
154
|
+
results: verdictTallyText(group.summary.evalVerdicts, locale),
|
|
155
|
+
cost: group.summary.totalCostUSD.value === null ? null : cellText(group.summary.totalCostUSD, locale),
|
|
156
|
+
lastRun: group.summary.range.latestStartedAt === null ? null : formatDateTimeMinute(group.summary.range.latestStartedAt),
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
const table = renderTableText(
|
|
160
|
+
{ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale },
|
|
161
|
+
ctx,
|
|
162
|
+
);
|
|
163
|
+
const commands = [
|
|
164
|
+
localeText(locale, "experimentComparison.commandsHead"),
|
|
165
|
+
...data.groups.map((group) => ` ${ctx.experimentCommand(group.key)}`),
|
|
166
|
+
].join("\n");
|
|
167
|
+
return `${table}\n\n${commands}`;
|
|
115
168
|
}
|
|
116
169
|
|
|
170
|
+
// ───────────────────────── MetricTable ─────────────────────────
|
|
171
|
+
|
|
117
172
|
export function tableText(data: TableData, ctx: TextContext): string {
|
|
118
173
|
const locale = ctx.locale;
|
|
119
|
-
// meta 在场时补 Model / Agent / Verdicts 列(rows: "experiment" 的榜单 parity);
|
|
120
|
-
// 列序对齐 view 原生榜单:experiment、model、agent、指标列…、verdicts
|
|
121
|
-
const hasMeta = data.rows.some((row) => row.meta !== undefined);
|
|
122
|
-
const hasModel = data.rows.some((row) => row.meta?.model !== undefined);
|
|
123
|
-
const hasVerdicts = data.rows.some((row) => row.meta?.verdicts !== undefined);
|
|
124
174
|
const columns: TableColumn[] = [
|
|
125
|
-
{ key: "dimension", header: data.
|
|
126
|
-
|
|
127
|
-
...(hasMeta ? [{ key: "agent", header: localeText(locale, "table.agent") }] : []),
|
|
128
|
-
// 指标列的键用序号,不用 metric name —— 维度名、"model" 这些键都在同一个命名空间里,
|
|
129
|
-
// 指标恰好叫 "agent" 时不能把 meta 列顶掉。
|
|
175
|
+
{ key: "dimension", header: data.rowDimension },
|
|
176
|
+
// 指标列的键用序号,不用 metric name —— 维度名和指标名在同一个命名空间里,可能撞键。
|
|
130
177
|
...data.columns.map((c, i) => ({ key: `metric${i}`, header: resolveMetricLabel(c.label, locale, c.key) })),
|
|
131
|
-
...(hasVerdicts ? [{ key: "verdicts", header: localeText(locale, "table.verdicts") }] : []),
|
|
132
178
|
];
|
|
133
179
|
const rows: TableRow[] = data.rows.map((row) => {
|
|
134
180
|
const cells: Record<string, string | null> = { dimension: row.key };
|
|
135
|
-
if (hasMeta && hasModel) cells.model = row.meta?.model ?? null;
|
|
136
|
-
if (hasMeta) cells.agent = row.meta?.agent ?? null;
|
|
137
181
|
data.columns.forEach((col, i) => {
|
|
138
|
-
const cell =
|
|
139
|
-
cells[`metric${i}`] = cell ? cellText(cell) : null;
|
|
182
|
+
const cell = row.cells[col.key];
|
|
183
|
+
cells[`metric${i}`] = cell ? cellText(cell, locale) : null;
|
|
140
184
|
});
|
|
141
|
-
if (hasVerdicts) cells.verdicts = row.meta?.verdicts ? verdictTallyText(row.meta.verdicts, locale) : null;
|
|
142
185
|
return { key: row.key, cells };
|
|
143
186
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// rows: "experiment" 专属的行摘要(eval/attempt 数 + 最后运行时间):表格列已经挤满
|
|
147
|
-
// dimension/model/agent/指标/verdicts,这几个数字挤进行键下面单独一行,与 web 面
|
|
148
|
-
// 「行键下的小 sub-line」同一份信息、同一种「不进列」的取舍。
|
|
149
|
-
const metaLines: string[] = [];
|
|
150
|
-
for (const row of data.rows) {
|
|
151
|
-
if (row.meta?.evals === undefined) continue;
|
|
152
|
-
const parts = [localeText(locale, "overview.evalsCount", { n: row.meta.evals })];
|
|
153
|
-
if (row.meta.attempts !== undefined && row.meta.attempts > row.meta.evals) {
|
|
154
|
-
parts.push(localeText(locale, "overview.attemptsCount", { n: row.meta.attempts }));
|
|
155
|
-
}
|
|
156
|
-
if (row.meta.lastRunAt) parts.push(localeText(locale, "latestRun", { run: row.meta.lastRunAt }));
|
|
157
|
-
metaLines.push(` ${row.key}: ${parts.join(" · ")}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const blocks = [table, ...(metaLines.length > 0 ? [metaLines.join("\n")] : [])];
|
|
161
|
-
return blocks.join("\n\n");
|
|
187
|
+
return renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx);
|
|
162
188
|
}
|
|
163
189
|
|
|
164
190
|
// ───────────────────────── MetricMatrix ─────────────────────────
|
|
165
191
|
|
|
166
192
|
export function matrixText(data: MatrixData, ctx: TextContext): string {
|
|
167
193
|
// 表体全是维度键与 display,没有 chrome 文案;"next:" 是命令提示,不本地化。
|
|
194
|
+
const locale = ctx.locale;
|
|
168
195
|
const rowKeys: string[] = [];
|
|
169
196
|
const columnKeys: string[] = [];
|
|
170
|
-
const byPosition = new Map<string,
|
|
197
|
+
const byPosition = new Map<string, MetricCell>();
|
|
171
198
|
for (const entry of data.cells) {
|
|
172
199
|
if (!rowKeys.includes(entry.row)) rowKeys.push(entry.row);
|
|
173
200
|
if (!columnKeys.includes(entry.column)) columnKeys.push(entry.column);
|
|
174
201
|
byPosition.set(JSON.stringify([entry.row, entry.column]), entry.cell);
|
|
175
202
|
}
|
|
176
203
|
const columns: TableColumn[] = [
|
|
177
|
-
{ key: "dimension", header: data.
|
|
204
|
+
{ key: "dimension", header: data.rowDimension },
|
|
178
205
|
...columnKeys.map((column, i) => ({ key: `column${i}`, header: column })),
|
|
179
206
|
];
|
|
180
207
|
const rows: TableRow[] = rowKeys.map((row) => {
|
|
181
208
|
const cells: Record<string, string | null> = { dimension: row };
|
|
182
209
|
columnKeys.forEach((column, i) => {
|
|
183
210
|
const cell = byPosition.get(JSON.stringify([row, column]));
|
|
184
|
-
cells[`column${i}`] = cell ? cellText(cell) : null; // 稀疏格子在文本里以 — 呈现,不编数
|
|
211
|
+
cells[`column${i}`] = cell ? cellText(cell, locale) : null; // 稀疏格子在文本里以 — 呈现,不编数
|
|
185
212
|
});
|
|
186
213
|
return { key: row, cells };
|
|
187
214
|
});
|
|
188
|
-
const table = renderTableText({ columns, rows, locale
|
|
215
|
+
const table = renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx);
|
|
189
216
|
|
|
190
217
|
// 下钻命令:行维度是 eval 时,指向最值得看的一行(先挑有缺格的,再挑按 better 最差的)
|
|
191
|
-
if (data.
|
|
218
|
+
if (data.rowDimension !== "eval" || rowKeys.length === 0) return table;
|
|
192
219
|
const better = data.metric.better ?? "higher";
|
|
193
220
|
let next: string | undefined;
|
|
194
221
|
let worst: { key: string; value: number } | undefined;
|
|
@@ -218,10 +245,11 @@ export function matrixText(data: MatrixData, ctx: TextContext): string {
|
|
|
218
245
|
|
|
219
246
|
const BAR_WIDTH = 20;
|
|
220
247
|
|
|
221
|
-
export function barsText(data: MatrixData): string {
|
|
248
|
+
export function barsText(data: MatrixData, ctx: TextContext): string {
|
|
249
|
+
const locale = ctx.locale;
|
|
222
250
|
const groupKeys: string[] = [];
|
|
223
251
|
const seriesKeys: string[] = [];
|
|
224
|
-
const byPosition = new Map<string,
|
|
252
|
+
const byPosition = new Map<string, MetricCell>();
|
|
225
253
|
for (const entry of data.cells) {
|
|
226
254
|
if (!groupKeys.includes(entry.row)) groupKeys.push(entry.row);
|
|
227
255
|
if (!seriesKeys.includes(entry.column)) seriesKeys.push(entry.column);
|
|
@@ -231,10 +259,9 @@ export function barsText(data: MatrixData): string {
|
|
|
231
259
|
// 条长刻度:% 的天然域是 [0,1],其余以全图最大值为满条
|
|
232
260
|
const values = data.cells.map((c) => c.cell.value).filter((v): v is number => v !== null);
|
|
233
261
|
const maxValue = values.length > 0 ? Math.max(...values) : 0;
|
|
234
|
-
const ratioOf = (value: number) =>
|
|
235
|
-
data.metric.unit === "%" ? value : maxValue === 0 ? 0 : value / maxValue;
|
|
262
|
+
const ratioOf = (value: number) => (data.metric.unit === "%" ? value : maxValue === 0 ? 0 : value / maxValue);
|
|
236
263
|
|
|
237
|
-
const seriesWidth = Math.max(...seriesKeys.map((k) => k
|
|
264
|
+
const seriesWidth = Math.max(...seriesKeys.map((k) => stringWidth(k)), 0);
|
|
238
265
|
const lines: string[] = [];
|
|
239
266
|
for (const group of groupKeys) {
|
|
240
267
|
lines.push(group);
|
|
@@ -257,7 +284,7 @@ export function barsText(data: MatrixData): string {
|
|
|
257
284
|
lines.push(` ${label} ${MISSING_MARK}`);
|
|
258
285
|
continue;
|
|
259
286
|
}
|
|
260
|
-
lines.push(` ${label} ${textBar(ratioOf(cell.value), BAR_WIDTH)} ${cellText(cell)}`);
|
|
287
|
+
lines.push(` ${label} ${textBar(ratioOf(cell.value), BAR_WIDTH)} ${cellText(cell, locale)}`);
|
|
261
288
|
}
|
|
262
289
|
}
|
|
263
290
|
return lines.join("\n");
|
|
@@ -274,14 +301,18 @@ export function scoreboardText(data: ScoreboardData, ctx: TextContext): string {
|
|
|
274
301
|
}
|
|
275
302
|
}
|
|
276
303
|
const columns: TableColumn[] = [
|
|
277
|
-
{ key: "dimension", header: data.
|
|
304
|
+
{ key: "dimension", header: data.rowDimension },
|
|
278
305
|
{ key: "total", header: localeText(locale, "scoreboard.totalText") },
|
|
279
306
|
...subjectKeys.map((key, i) => ({ key: `subject${i}`, header: key })),
|
|
280
307
|
];
|
|
281
308
|
const rows: TableRow[] = data.rows.map((row) => {
|
|
309
|
+
const totalNotes = [
|
|
310
|
+
...(row.total.notRun > 0 ? [localeText(locale, "scoreboard.notRunText", { n: row.total.notRun })] : []),
|
|
311
|
+
...(row.total.unscorable > 0 ? [localeText(locale, "scoreboard.unscorableText", { n: row.total.unscorable })] : []),
|
|
312
|
+
];
|
|
282
313
|
const cells: Record<string, string | null> = {
|
|
283
314
|
dimension: row.key,
|
|
284
|
-
total: `${row.total.display}/${data.fullMarks}`,
|
|
315
|
+
total: `${resolveLocalizedText(row.total.display, locale)}/${data.fullMarks}${totalNotes.length > 0 ? ` ${totalNotes.join(" ")}` : ""}`,
|
|
285
316
|
};
|
|
286
317
|
subjectKeys.forEach((key, i) => {
|
|
287
318
|
const subject = row.subjects.find((s) => s.key === key);
|
|
@@ -290,18 +321,22 @@ export function scoreboardText(data: ScoreboardData, ctx: TextContext): string {
|
|
|
290
321
|
return;
|
|
291
322
|
}
|
|
292
323
|
const score = `${formatPlainNumber(subject.earned)}/${formatPlainNumber(subject.possible)}`;
|
|
293
|
-
|
|
294
|
-
subject.
|
|
295
|
-
|
|
296
|
-
|
|
324
|
+
const notes = [
|
|
325
|
+
...(subject.notRun > 0 ? [localeText(locale, "scoreboard.notRunText", { n: subject.notRun })] : []),
|
|
326
|
+
...(subject.unscorable > 0 ? [localeText(locale, "scoreboard.unscorableText", { n: subject.unscorable })] : []),
|
|
327
|
+
];
|
|
328
|
+
cells[`subject${i}`] = notes.length > 0 ? `${score} ${notes.join(" ")}` : score;
|
|
297
329
|
});
|
|
298
330
|
return { key: row.key, cells };
|
|
299
331
|
});
|
|
300
|
-
const table = renderTableText({ columns, rows, locale }, ctx);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
332
|
+
const table = renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx);
|
|
333
|
+
const footnotes: string[] = [];
|
|
334
|
+
if (data.weights.length > 0) {
|
|
335
|
+
const weights = data.weights.map((w) => `${w.prefix} ×${w.weight}`).join(" · ");
|
|
336
|
+
footnotes.push(`${localeText(locale, "scoreboard.weights")} ${weights} · ${localeText(locale, "scoreboard.othersWeight")}`);
|
|
337
|
+
}
|
|
338
|
+
if (data.ignoredEvals > 0) footnotes.push(countText(locale, "scoreboard.ignored", data.ignoredEvals));
|
|
339
|
+
return footnotes.length > 0 ? `${table}\n${footnotes.join("\n")}` : table;
|
|
305
340
|
}
|
|
306
341
|
|
|
307
342
|
// ───────────────────────── MetricScatter ─────────────────────────
|
|
@@ -312,6 +347,26 @@ function axisLabel(col: MetricColumn, locale: ReportLocale): string {
|
|
|
312
347
|
return resolveMetricLabel(col.label, locale, col.key);
|
|
313
348
|
}
|
|
314
349
|
|
|
350
|
+
/**
|
|
351
|
+
* 方向提示:轴向已随 better 反正(lower 反向),「更好」恒指向右上,文案恒为
|
|
352
|
+
* 「越靠右上越好」;仅当 x、y 都声明 better 时显示——任一轴未声明,组件不猜
|
|
353
|
+
* 「更好」朝哪边,整图无提示(返回 undefined)。
|
|
354
|
+
*/
|
|
355
|
+
function betterCornerText(x: MetricColumn, y: MetricColumn, locale: ReportLocale): string | undefined {
|
|
356
|
+
if (x.better === undefined || y.better === undefined) return undefined;
|
|
357
|
+
return localeText(locale, "scatter.betterUpperRight");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 轴标题行:"<x label>(越低越好) × <y label>"——better 方向如实标注,不虚构轴向。 */
|
|
361
|
+
function scatterHeading(data: ScatterData, locale: ReportLocale): string {
|
|
362
|
+
const withBetter = (col: MetricColumn) => {
|
|
363
|
+
const label = axisLabel(col, locale);
|
|
364
|
+
if (col.better === undefined) return label;
|
|
365
|
+
return `${label}(${localeText(locale, col.better === "lower" ? "table.lowerBetter" : "table.higherBetter")})`;
|
|
366
|
+
};
|
|
367
|
+
return `${withBetter(data.x)} × ${withBetter(data.y)}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
315
370
|
export function scatterText(data: ScatterData, ctx: TextContext): string {
|
|
316
371
|
const locale = ctx.locale;
|
|
317
372
|
const drawable = data.rows.filter((r) => r.x.value !== null && r.y.value !== null);
|
|
@@ -327,10 +382,14 @@ export function scatterText(data: ScatterData, ctx: TextContext): string {
|
|
|
327
382
|
// 点太密排不下时降级为坐标表,不硬挤
|
|
328
383
|
if (drawable.length > POINT_MARKS.length || ctx.width < 44) {
|
|
329
384
|
const table = renderCoordinateTable(
|
|
330
|
-
drawable.map((r) => ({
|
|
331
|
-
|
|
385
|
+
drawable.map((r) => ({
|
|
386
|
+
key: r.key,
|
|
387
|
+
x: resolveLocalizedText(r.x.display, locale),
|
|
388
|
+
y: resolveLocalizedText(r.y.display, locale),
|
|
389
|
+
})),
|
|
390
|
+
{ key: data.pointDimension, x: axes.x, y: axes.y },
|
|
332
391
|
);
|
|
333
|
-
return [table, ...footnotes].join("\n");
|
|
392
|
+
return [scatterHeading(data, locale), "", table, ...footnotes].join("\n");
|
|
334
393
|
}
|
|
335
394
|
|
|
336
395
|
const points: PlotPoint[] = drawable.map((r, i) => ({
|
|
@@ -348,20 +407,21 @@ export function scatterText(data: ScatterData, ctx: TextContext): string {
|
|
|
348
407
|
}
|
|
349
408
|
for (const list of bySeries.values()) list.sort((a, b) => a.x - b.x);
|
|
350
409
|
|
|
351
|
-
const invertX = data.x.better === "lower";
|
|
352
410
|
const plot = renderCharPlot({
|
|
353
411
|
width: ctx.width,
|
|
354
412
|
points,
|
|
355
413
|
lines: [...bySeries.values()].filter((l) => l.length > 1),
|
|
356
|
-
xLabel:
|
|
357
|
-
yLabel:
|
|
414
|
+
xLabel: axes.x,
|
|
415
|
+
yLabel: axes.y,
|
|
358
416
|
formatX: (v) => formatMetricValue(v, data.x.unit),
|
|
359
417
|
formatY: (v) => formatMetricValue(v, data.y.unit),
|
|
360
|
-
|
|
418
|
+
// 轴方向跟随 better(与 web 面同规则):lower 反向,higher 与未声明正向。
|
|
419
|
+
invertX: data.x.better === "lower",
|
|
361
420
|
invertY: data.y.better === "lower",
|
|
362
421
|
});
|
|
363
422
|
const legend = drawable.map((r, i) => `${POINT_MARKS[i]} ${r.key}`).join(" ");
|
|
364
|
-
|
|
423
|
+
const hint = betterCornerText(data.x, data.y, locale);
|
|
424
|
+
return [scatterHeading(data, locale), plot, "", ...(hint !== undefined ? [hint] : []), legend, ...footnotes].join("\n");
|
|
365
425
|
}
|
|
366
426
|
|
|
367
427
|
// ───────────────────────── MetricLine ─────────────────────────
|
|
@@ -375,6 +435,8 @@ export function lineText(data: LineData, ctx: TextContext): string {
|
|
|
375
435
|
|
|
376
436
|
if (drawable.length === 0) return [missingText(locale), ...footnotes].join("\n");
|
|
377
437
|
|
|
438
|
+
const xLabel = resolveMetricLabel(data.x.label, locale, data.x.key);
|
|
439
|
+
|
|
378
440
|
// 系列 → 字母;无系列 = 单系列
|
|
379
441
|
const seriesKeys: string[] = [];
|
|
380
442
|
for (const r of drawable) {
|
|
@@ -384,8 +446,12 @@ export function lineText(data: LineData, ctx: TextContext): string {
|
|
|
384
446
|
|
|
385
447
|
if (seriesKeys.length > POINT_MARKS.length || ctx.width < 44) {
|
|
386
448
|
const table = renderCoordinateTable(
|
|
387
|
-
drawable.map((r) => ({
|
|
388
|
-
|
|
449
|
+
drawable.map((r) => ({
|
|
450
|
+
key: r.series !== undefined ? `${r.series} (${r.key})` : r.key,
|
|
451
|
+
x: resolveLocalizedText(r.xDisplay, locale),
|
|
452
|
+
y: resolveLocalizedText(r.y.display, locale),
|
|
453
|
+
})),
|
|
454
|
+
{ key: data.seriesDimension ?? xLabel, x: xLabel, y: axisLabel(data.y, locale) },
|
|
389
455
|
);
|
|
390
456
|
return [table, ...footnotes].join("\n");
|
|
391
457
|
}
|
|
@@ -407,11 +473,10 @@ export function lineText(data: LineData, ctx: TextContext): string {
|
|
|
407
473
|
width: ctx.width,
|
|
408
474
|
points,
|
|
409
475
|
lines: lines.filter((l) => l.length > 1),
|
|
410
|
-
xLabel
|
|
476
|
+
xLabel,
|
|
411
477
|
yLabel: axisLabel(data.y, locale),
|
|
412
478
|
formatX: (v) => formatMetricValue(v, data.x.unit),
|
|
413
479
|
formatY: (v) => formatMetricValue(v, data.y.unit),
|
|
414
|
-
invertY: data.y.better === "lower",
|
|
415
480
|
});
|
|
416
481
|
const legend = seriesKeys
|
|
417
482
|
.map((key, i) => `${POINT_MARKS[i]} ${key === "" ? axisLabel(data.y, locale) : key}`)
|
|
@@ -422,41 +487,62 @@ export function lineText(data: LineData, ctx: TextContext): string {
|
|
|
422
487
|
// ───────────────────────── DeltaTable ─────────────────────────
|
|
423
488
|
|
|
424
489
|
export function deltaText(data: DeltaData, ctx: TextContext): string {
|
|
490
|
+
const locale = ctx.locale;
|
|
491
|
+
// 0 对不是错误:明确空态并报告配对域实验数(派生形态携带;字面形态缺省按 0)。
|
|
492
|
+
if (data.rows.length === 0) {
|
|
493
|
+
return localeText(locale, "delta.empty", { experiments: data.experiments ?? 0 });
|
|
494
|
+
}
|
|
425
495
|
const columns: TableColumn[] = [
|
|
426
|
-
{ key: "pair", header: "
|
|
427
|
-
...data.columns.map((c, i) => ({ key: `metric${i}`, header: resolveMetricLabel(c.label,
|
|
496
|
+
{ key: "pair", header: localeText(locale, "delta.pairHeader") },
|
|
497
|
+
...data.columns.map((c, i) => ({ key: `metric${i}`, header: resolveMetricLabel(c.label, locale, c.key) })),
|
|
428
498
|
];
|
|
429
499
|
const rows: TableRow[] = data.rows.map((row) => {
|
|
430
|
-
const cells: Record<string, string | null> = { pair: row.
|
|
500
|
+
const cells: Record<string, string | null> = { pair: resolveLocalizedText(row.label, locale) };
|
|
431
501
|
data.columns.forEach((col, i) => {
|
|
432
|
-
const cell =
|
|
502
|
+
const cell = row.cells[col.key];
|
|
433
503
|
if (!cell) {
|
|
434
504
|
cells[`metric${i}`] = null;
|
|
435
505
|
return;
|
|
436
506
|
}
|
|
437
|
-
const a = cell.a.value === null ? MISSING_MARK : cell.a.display;
|
|
438
|
-
const b = cell.b.value === null ? MISSING_MARK : cell.b.display;
|
|
439
|
-
cells[`metric${i}`] = `${a} → ${b} ${cell.display}`;
|
|
507
|
+
const a = cell.a.value === null ? MISSING_MARK : resolveLocalizedText(cell.a.display, locale);
|
|
508
|
+
const b = cell.b.value === null ? MISSING_MARK : resolveLocalizedText(cell.b.display, locale);
|
|
509
|
+
cells[`metric${i}`] = `${a} → ${b} ${resolveLocalizedText(cell.display, locale)}`;
|
|
440
510
|
});
|
|
441
511
|
return { key: row.key, cells };
|
|
442
512
|
});
|
|
443
|
-
return renderTableText({ columns, rows, locale
|
|
513
|
+
return renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx);
|
|
444
514
|
}
|
|
445
515
|
|
|
446
516
|
// ───────────────────────── 实体列表(ExperimentList / EvalList / AttemptList)─────────────────────────
|
|
447
517
|
//
|
|
448
518
|
// 三面共用的紧凑标记:`locator✓`(判定符紧跟 locator,中间不留空格)。
|
|
449
|
-
// ExperimentList / EvalList 逐 attempt 只列这一个标记 +
|
|
450
|
-
// niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator
|
|
451
|
-
// 比较列表都不内联完整断言或命令模板。
|
|
519
|
+
// ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的摘要,不重复整段
|
|
520
|
+
// niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`。
|
|
452
521
|
|
|
453
522
|
function locatorBadge(item: { locator: string; verdict: AttemptListItem["verdict"] }): string {
|
|
454
523
|
return `${item.locator}${verdictMark(item.verdict)}`;
|
|
455
524
|
}
|
|
456
525
|
|
|
526
|
+
/**
|
|
527
|
+
* failureSummary + moreFailures 的展示形态:摘要在计算侧已按 Scoring display 契约折好,
|
|
528
|
+
* 这里只加 "+N more failures" 计数与宽度收口,不重算摘要。
|
|
529
|
+
*/
|
|
530
|
+
function attemptReasonText(item: AttemptListItem, locale: ReportLocale, maxChars: number): string | undefined {
|
|
531
|
+
if (item.failureSummary === null) return undefined;
|
|
532
|
+
const withMore =
|
|
533
|
+
item.moreFailures > 0
|
|
534
|
+
? `${item.failureSummary} · ${countText(locale, "entityList.moreFailures", item.moreFailures)}`
|
|
535
|
+
: item.failureSummary;
|
|
536
|
+
return fitFailureSummary(withMore, Math.max(24, maxChars));
|
|
537
|
+
}
|
|
538
|
+
|
|
457
539
|
// ── ExperimentList ──
|
|
458
540
|
|
|
459
|
-
function experimentSummaryTable(
|
|
541
|
+
function experimentSummaryTable(
|
|
542
|
+
items: readonly ExperimentListItem[],
|
|
543
|
+
ctx: TextContext,
|
|
544
|
+
relativeTo?: string,
|
|
545
|
+
): string {
|
|
460
546
|
const locale = ctx.locale;
|
|
461
547
|
const compact = ctx.width < 100;
|
|
462
548
|
const columns: TableColumn[] = [
|
|
@@ -467,7 +553,7 @@ function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext, r
|
|
|
467
553
|
{ key: "passRate", header: compact && locale === "en" ? "Pass" : localeText(locale, "experimentList.passRate"), align: "right" },
|
|
468
554
|
{ key: "result", header: localeText(locale, "experimentList.result") },
|
|
469
555
|
{ key: "tokens", header: localeText(locale, "experimentList.tokens"), align: "right" },
|
|
470
|
-
{ key: "cost", header:
|
|
556
|
+
{ key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
|
|
471
557
|
];
|
|
472
558
|
const rows: TableRow[] = items.map((item) => ({
|
|
473
559
|
key: item.experimentId,
|
|
@@ -475,11 +561,11 @@ function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext, r
|
|
|
475
561
|
experiment: experimentDisplayName(item.experimentId, relativeTo),
|
|
476
562
|
model: item.model ?? localeText(locale, "experimentList.defaultModel"),
|
|
477
563
|
agent: item.agent,
|
|
478
|
-
duration: cellText(item.
|
|
479
|
-
passRate: cellText(item.
|
|
480
|
-
result: verdictTallyText(item.
|
|
481
|
-
tokens: cellText(item.tokens),
|
|
482
|
-
cost: cellText(item.
|
|
564
|
+
duration: cellText(item.durationMs, locale),
|
|
565
|
+
passRate: cellText(item.endToEndPassRate, locale),
|
|
566
|
+
result: verdictTallyText(item.evalVerdicts, locale),
|
|
567
|
+
tokens: cellText(item.tokens, locale),
|
|
568
|
+
cost: cellText(item.costUSD, locale),
|
|
483
569
|
},
|
|
484
570
|
}));
|
|
485
571
|
const metadata = items.flatMap((item) =>
|
|
@@ -488,7 +574,7 @@ function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext, r
|
|
|
488
574
|
Math.max(8, ctx.width - 2),
|
|
489
575
|
).map((line) => ` ${line}`),
|
|
490
576
|
);
|
|
491
|
-
return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
|
|
577
|
+
return [renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx), metadata.join("\n")].join("\n");
|
|
492
578
|
}
|
|
493
579
|
|
|
494
580
|
function experimentDetailTable(item: ExperimentListItem, ctx: TextContext, relativeTo?: string): string {
|
|
@@ -502,7 +588,7 @@ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext, relat
|
|
|
502
588
|
{ key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
|
|
503
589
|
];
|
|
504
590
|
// Result 的字符预算 ≈ 两行 × 它能分到的列宽(总宽减其它列的自然宽与列距)。这里只做
|
|
505
|
-
//
|
|
591
|
+
// 粗预算;精确的按宽度收口由列的 maxLines 兜底。
|
|
506
592
|
const statusWidth = Math.max(
|
|
507
593
|
stringWidth(localeText(locale, "experimentList.status")),
|
|
508
594
|
...item.evalRows.map((row) => stringWidth(`${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`)),
|
|
@@ -514,14 +600,15 @@ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext, relat
|
|
|
514
600
|
const fixedWidth = statusWidth + entityWidth + 8 /* duration */ + 6 /* cost */ + 3 * 4; /* 4 段列距 */
|
|
515
601
|
const resultBudget = Math.max(24, (ctx.width - fixedWidth) * 2);
|
|
516
602
|
const rows: TableRow[] = item.evalRows.flatMap((row) => {
|
|
603
|
+
// Eval 父行只承载折叠判定与题级聚合;失败摘要只在 Attempt 子行出现。
|
|
517
604
|
const parent: TableRow = {
|
|
518
605
|
key: row.evalId,
|
|
519
606
|
cells: {
|
|
520
607
|
status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
|
|
521
608
|
entity: row.evalId,
|
|
522
609
|
result: "",
|
|
523
|
-
duration: localeText(locale, "entityList.average", { value: cellText(row.
|
|
524
|
-
cost: localeText(locale, "entityList.average", { value: cellText(row.
|
|
610
|
+
duration: localeText(locale, "entityList.average", { value: cellText(row.durationMs, locale) }),
|
|
611
|
+
cost: localeText(locale, "entityList.average", { value: cellText(row.costUSD, locale) }),
|
|
525
612
|
},
|
|
526
613
|
};
|
|
527
614
|
const attempts: TableRow[] = row.attempts.map((attempt, index) => ({
|
|
@@ -529,22 +616,28 @@ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext, relat
|
|
|
529
616
|
cells: {
|
|
530
617
|
status: ` ${verdictMark(attempt.verdict)}`,
|
|
531
618
|
entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
|
|
532
|
-
result:
|
|
619
|
+
result: attemptReasonText(attempt, locale, resultBudget) ?? MISSING_MARK,
|
|
533
620
|
duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
|
|
534
|
-
cost: attempt.costUSD ===
|
|
621
|
+
cost: attempt.costUSD === null ? null : formatUSD(attempt.costUSD),
|
|
535
622
|
},
|
|
536
623
|
}));
|
|
537
624
|
return [parent, ...attempts];
|
|
538
625
|
});
|
|
539
626
|
const flags = item.flags && Object.keys(item.flags).length > 0
|
|
540
|
-
? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags)
|
|
627
|
+
? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags)
|
|
628
|
+
.map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`)
|
|
629
|
+
.join(" · ")}`
|
|
541
630
|
: undefined;
|
|
542
|
-
return [
|
|
631
|
+
return [
|
|
632
|
+
experimentDisplayName(item.experimentId, relativeTo),
|
|
633
|
+
flags,
|
|
634
|
+
renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx),
|
|
635
|
+
]
|
|
543
636
|
.filter(Boolean)
|
|
544
637
|
.join("\n");
|
|
545
638
|
}
|
|
546
639
|
|
|
547
|
-
export function experimentListText(items: ExperimentListItem[], ctx: TextContext, relativeTo?: string): string {
|
|
640
|
+
export function experimentListText(items: readonly ExperimentListItem[], ctx: TextContext, relativeTo?: string): string {
|
|
548
641
|
if (items.length === 0) return localeText(ctx.locale, "attemptList.empty");
|
|
549
642
|
return [
|
|
550
643
|
experimentSummaryTable(items, ctx, relativeTo),
|
|
@@ -555,24 +648,24 @@ export function experimentListText(items: ExperimentListItem[], ctx: TextContext
|
|
|
555
648
|
// ── EvalList ──
|
|
556
649
|
|
|
557
650
|
function evalListAttemptLine(item: AttemptListItem, ctx: TextContext): string {
|
|
558
|
-
// 行式列表同守「Result 最多两行」:预算 =
|
|
559
|
-
const reason =
|
|
651
|
+
// 行式列表同守「Result 最多两行」:预算 = 两行终端宽,超出按尾截收口。
|
|
652
|
+
const reason = attemptReasonText(item, ctx.locale, ctx.width * 2 - stringWidth(locatorBadge(item)) - 6);
|
|
560
653
|
return ` ${locatorBadge(item)}${reason ? ` · ${reason}` : ""}`;
|
|
561
654
|
}
|
|
562
655
|
|
|
563
|
-
export function evalListText(items: EvalListItem[], ctx: TextContext): string {
|
|
656
|
+
export function evalListText(items: readonly EvalListItem[], ctx: TextContext): string {
|
|
564
657
|
const locale = ctx.locale;
|
|
565
658
|
if (items.length === 0) return localeText(locale, "attemptList.empty");
|
|
566
659
|
const blocks = items.map((item) => {
|
|
567
660
|
const identity = `${item.evalId} · ${item.experimentId} · ${localeText(locale, `verdict.${item.verdict}`)}`;
|
|
568
661
|
const summary = [
|
|
569
|
-
localeText(locale, "attemptList.score", { score: cellText(item.
|
|
662
|
+
localeText(locale, "attemptList.score", { score: cellText(item.examScore, locale) }),
|
|
570
663
|
localeText(locale, "overview.attemptsCount", { n: item.attempts.length }),
|
|
571
664
|
localeText(locale, "entityList.average", {
|
|
572
|
-
value: item.
|
|
665
|
+
value: item.durationMs.value === null ? missingText(locale) : formatDurationMs(item.durationMs.value),
|
|
573
666
|
}),
|
|
574
667
|
localeText(locale, "entityList.average", {
|
|
575
|
-
value: item.
|
|
668
|
+
value: item.costUSD.value === null ? missingText(locale) : formatUSD(item.costUSD.value),
|
|
576
669
|
}),
|
|
577
670
|
].join(" · ");
|
|
578
671
|
const attemptLines = item.attempts.map((attempt) => evalListAttemptLine(attempt, ctx));
|
|
@@ -583,22 +676,22 @@ export function evalListText(items: EvalListItem[], ctx: TextContext): string {
|
|
|
583
676
|
|
|
584
677
|
// ── AttemptList ──
|
|
585
678
|
|
|
586
|
-
/** Attempt
|
|
679
|
+
/** Attempt 比较卡片:只显示一条主失败摘要(至多两行终端宽);完整 assertions 走 locator 下钻。 */
|
|
587
680
|
function attemptListItemText(item: AttemptListItem, ctx: TextContext): string {
|
|
588
681
|
const head = [
|
|
589
682
|
`${verdictMark(item.verdict)} ${item.locator}`,
|
|
590
683
|
item.evalId,
|
|
591
684
|
item.experimentId,
|
|
592
685
|
formatDurationMs(item.durationMs),
|
|
593
|
-
...(item.costUSD !==
|
|
686
|
+
...(item.costUSD !== null ? [formatUSD(item.costUSD)] : []),
|
|
594
687
|
].join(" · ");
|
|
595
688
|
const lines = [head];
|
|
596
|
-
const reason =
|
|
689
|
+
const reason = attemptReasonText(item, ctx.locale, ctx.width * 2 - 4);
|
|
597
690
|
if (reason) lines.push(` ${reason}`);
|
|
598
691
|
return lines.join("\n");
|
|
599
692
|
}
|
|
600
693
|
|
|
601
|
-
export function attemptListText(items: AttemptListItem[], total: number | undefined, ctx: TextContext): string {
|
|
694
|
+
export function attemptListText(items: readonly AttemptListItem[], total: number | undefined, ctx: TextContext): string {
|
|
602
695
|
const locale = ctx.locale;
|
|
603
696
|
if (items.length === 0) return localeText(locale, "attemptList.empty");
|
|
604
697
|
const blocks = items.map((item) => attemptListItemText(item, ctx));
|