niceeval 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +23 -23
- package/README.zh.md +6 -6
- package/dist/agents/types.d.ts +69 -7
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +57 -3
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +32 -24
- package/dist/report/aggregate.js +158 -50
- 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 +93 -160
- package/dist/report/components.js +377 -114
- package/dist/report/compute.d.ts +87 -81
- package/dist/report/compute.js +597 -417
- package/dist/report/flag.d.ts +32 -6
- package/dist/report/flag.js +92 -4
- package/dist/report/format.d.ts +19 -11
- package/dist/report/format.js +30 -13
- package/dist/report/index.d.ts +16 -16
- package/dist/report/index.js +20 -21
- package/dist/report/load.js +3 -2
- package/dist/report/locale.d.ts +57 -33
- package/dist/report/locale.js +122 -56
- package/dist/report/metrics.d.ts +23 -4
- package/dist/report/metrics.js +110 -25
- package/dist/report/primitives.d.ts +48 -15
- package/dist/report/primitives.js +135 -26
- package/dist/report/react/AttemptList.d.ts +9 -7
- package/dist/report/react/AttemptList.js +17 -10
- package/dist/report/react/DeltaTable.js +19 -18
- package/dist/report/react/EvalList.d.ts +4 -4
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +10 -0
- package/dist/report/react/ExperimentComparison.js +12 -0
- package/dist/report/react/ExperimentList.d.ts +4 -3
- package/dist/report/react/ExperimentList.js +17 -18
- 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 +59 -28
- 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/chart-math.d.ts +23 -6
- package/dist/report/react/chart-math.js +71 -19
- package/dist/report/react/fixtures.d.ts +5 -9
- package/dist/report/react/fixtures.js +110 -147
- package/dist/report/react/index.d.ts +15 -5
- package/dist/report/react/index.js +18 -7
- package/dist/report/report.d.ts +137 -16
- package/dist/report/report.js +259 -28
- package/dist/report/text/faces.d.ts +17 -19
- package/dist/report/text/faces.js +253 -184
- package/dist/report/text/plot.js +1 -1
- package/dist/report/text/table.js +38 -7
- package/dist/report/tree.d.ts +90 -40
- package/dist/report/tree.js +252 -94
- package/dist/report/types.d.ts +247 -284
- 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 +49 -14
- package/dist/runner/feedback/sink.d.ts +110 -0
- package/dist/runner/types.d.ts +513 -22
- package/dist/sandbox/docker.d.ts +23 -2
- package/dist/sandbox/e2b.d.ts +15 -1
- package/dist/sandbox/errors.d.ts +30 -3
- package/dist/sandbox/io-retry.d.ts +17 -0
- package/dist/sandbox/registry.d.ts +2 -0
- package/dist/sandbox/resolve.d.ts +18 -5
- package/dist/sandbox/retry.d.ts +11 -1
- package/dist/sandbox/types.d.ts +39 -5
- package/dist/sandbox/vercel.d.ts +7 -1
- package/dist/scoring/coverage.d.ts +30 -0
- package/dist/scoring/display.d.ts +21 -0
- package/dist/scoring/display.js +120 -0
- package/dist/scoring/types.d.ts +103 -20
- package/dist/shared/aggregate.d.ts +4 -2
- package/dist/shared/aggregate.js +8 -7
- package/dist/shared/types.d.ts +28 -0
- package/dist/tty-line.d.ts +0 -4
- package/dist/util.d.ts +23 -0
- 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 +31 -13
- 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 +11 -11
- package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
- package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
- package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
- package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
- package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
- package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
- package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
- package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
- package/docs-site/zh/how-to/publish-report.mdx +105 -0
- package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
- package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
- package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
- package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
- package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
- package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
- package/docs-site/zh/index.mdx +24 -26
- package/docs-site/zh/introduction.mdx +8 -8
- package/docs-site/zh/reference/builtin-agents.mdx +32 -5
- package/docs-site/zh/reference/capabilities.mdx +8 -8
- package/docs-site/zh/reference/cli.mdx +40 -12
- package/docs-site/zh/reference/define-agent.mdx +58 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +42 -9
- package/docs-site/zh/reference/events.mdx +3 -3
- package/docs-site/zh/reference/expect.mdx +26 -1
- package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
- package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
- package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
- package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
- package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
- package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
- package/package.json +10 -2
- package/src/agents/ai-sdk-otel.test.ts +1 -0
- package/src/agents/ai-sdk.test.ts +3 -0
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/bub-install-spec.test.ts +34 -0
- package/src/agents/bub-install-spec.ts +32 -0
- package/src/agents/bub.ts +31 -32
- package/src/agents/claude-code.test.ts +130 -9
- package/src/agents/claude-code.ts +76 -4
- package/src/agents/codex.test.ts +189 -40
- package/src/agents/codex.ts +155 -14
- package/src/agents/coding-cli-versions.test.ts +15 -0
- package/src/agents/coding-cli-versions.ts +3 -0
- package/src/agents/index.ts +13 -2
- package/src/agents/langgraph.test.ts +204 -0
- package/src/agents/langgraph.ts +495 -0
- package/src/agents/marketplace.ts +85 -0
- package/src/agents/native-config.test.ts +179 -0
- package/src/agents/native-config.ts +267 -0
- package/src/agents/openai-compat.test.ts +1 -0
- package/src/agents/openai-compat.ts +1 -1
- package/src/agents/openclaw.test.ts +31 -0
- package/src/agents/openclaw.ts +171 -0
- package/src/agents/plugin-config.test.ts +1 -0
- package/src/agents/sdk-streams.test.ts +79 -0
- package/src/agents/sdk-streams.ts +55 -10
- package/src/agents/skills.test.ts +1 -0
- package/src/agents/streaming.test.ts +3 -9
- package/src/agents/streaming.ts +2 -2
- package/src/agents/types.ts +71 -8
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +446 -124
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +162 -30
- package/src/context/session.test.ts +2 -1
- package/src/context/session.ts +115 -7
- package/src/context/types.ts +30 -12
- package/src/define.test.ts +13 -8
- package/src/define.ts +25 -4
- package/src/expect/index.ts +53 -23
- package/src/i18n/en.ts +81 -17
- package/src/i18n/zh-CN.ts +80 -17
- package/src/o11y/cost.test.ts +1 -0
- package/src/o11y/execution-tree.test.ts +1 -20
- package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +1 -0
- package/src/o11y/otlp/turn-otel.test.ts +1 -0
- package/src/o11y/parsers/bub.test.ts +1 -0
- package/src/o11y/parsers/claude-code.test.ts +1 -34
- package/src/o11y/parsers/openclaw.test.ts +154 -0
- package/src/o11y/parsers/openclaw.ts +310 -0
- package/src/o11y/prices.json +746 -311
- package/src/o11y/tool-names.test.ts +1 -0
- package/src/o11y/types.ts +16 -2
- package/src/report/aggregate.ts +178 -61
- package/src/report/built-in/index.tsx +9 -0
- package/src/report/components.tsx +625 -279
- package/src/report/compute.ts +723 -491
- package/src/report/dual-render.test.tsx +741 -1024
- package/src/report/flag.ts +104 -12
- package/src/report/format.ts +32 -12
- package/src/report/index.ts +119 -46
- package/src/report/load.ts +3 -2
- package/src/report/locale.ts +136 -65
- package/src/report/metrics.ts +108 -25
- package/src/report/primitives.tsx +196 -45
- package/src/report/react/AttemptList.tsx +30 -43
- package/src/report/react/DeltaTable.tsx +63 -45
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +73 -0
- package/src/report/react/ExperimentList.tsx +50 -32
- 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 +86 -34
- 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/chart-math.test.ts +85 -0
- package/src/report/react/chart-math.ts +101 -22
- package/src/report/react/enhance.js +89 -5
- package/src/report/react/fixtures.ts +114 -154
- package/src/report/react/index.tsx +24 -39
- package/src/report/react/render.test.tsx +138 -158
- package/src/report/react/styles.css +243 -82
- package/src/report/report.test.ts +779 -841
- package/src/report/report.ts +423 -41
- package/src/report/text/faces.ts +290 -193
- package/src/report/text/plot.ts +1 -1
- package/src/report/text/table.ts +44 -7
- package/src/report/tree.ts +362 -104
- package/src/report/types.ts +261 -271
- package/src/report/web.ts +63 -20
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +13 -11
- package/src/results/attempt-evidence.ts +20 -13
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +150 -60
- package/src/results/host-equivalence.test.ts +34 -20
- package/src/results/index.ts +12 -4
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +15 -5
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +89 -54
- package/src/results/select.ts +104 -34
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +43 -14
- package/src/results/writer.ts +31 -13
- package/src/runner/attempt.test.ts +138 -7
- package/src/runner/attempt.ts +603 -104
- package/src/runner/discover.test.ts +47 -0
- package/src/runner/discover.ts +36 -2
- package/src/runner/eval-source.test.ts +1 -27
- package/src/runner/feedback/agent.test.ts +504 -0
- package/src/runner/feedback/agent.ts +409 -0
- package/src/runner/feedback/ci.test.ts +562 -0
- package/src/runner/feedback/ci.ts +401 -0
- package/src/runner/feedback/coordinator.test.ts +317 -0
- package/src/runner/feedback/coordinator.ts +397 -0
- package/src/runner/feedback/failure.ts +40 -0
- package/src/runner/feedback/human.test.ts +616 -0
- package/src/runner/feedback/human.ts +535 -0
- package/src/runner/feedback/index.ts +66 -0
- package/src/runner/feedback/io.ts +78 -0
- package/src/runner/feedback/profile.test.ts +50 -0
- package/src/runner/feedback/profile.ts +58 -0
- package/src/runner/feedback/reducer.test.ts +395 -0
- package/src/runner/feedback/reducer.ts +260 -0
- package/src/runner/feedback/renderer.ts +82 -0
- package/src/runner/feedback/sink.ts +203 -0
- package/src/runner/feedback/testing.ts +106 -0
- package/src/runner/ledger.test.ts +230 -0
- package/src/runner/ledger.ts +329 -0
- package/src/runner/report.test.ts +128 -3
- package/src/runner/report.ts +33 -9
- package/src/runner/reporters/artifacts.ts +8 -2
- package/src/runner/reporters/braintrust.test.ts +8 -7
- package/src/runner/reporters/braintrust.ts +9 -2
- package/src/runner/reporters/index.ts +2 -2
- package/src/runner/reporters/json.test.ts +162 -0
- package/src/runner/reporters/json.ts +35 -8
- package/src/runner/reporters/shared.ts +1 -5
- package/src/runner/run.test.ts +760 -3
- package/src/runner/run.ts +243 -37
- package/src/runner/sandbox-prep.ts +3 -42
- package/src/runner/timing.ts +158 -0
- package/src/runner/types.ts +518 -22
- package/src/sandbox/checkpoint.test.ts +55 -0
- package/src/sandbox/checkpoint.ts +29 -8
- package/src/sandbox/cli-commands.ts +407 -0
- package/src/sandbox/docker.ts +115 -16
- package/src/sandbox/e2b-agent-template.test.ts +56 -0
- package/src/sandbox/e2b-agent-template.ts +94 -0
- package/src/sandbox/e2b.ts +74 -9
- package/src/sandbox/errors.ts +111 -4
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/io-retry.test.ts +58 -0
- package/src/sandbox/io-retry.ts +45 -0
- package/src/sandbox/keep-registry.test.ts +86 -0
- package/src/sandbox/keep-registry.ts +142 -0
- package/src/sandbox/keep.ts +178 -0
- package/src/sandbox/paths.test.ts +1 -0
- package/src/sandbox/paths.ts +19 -8
- package/src/sandbox/registry.ts +20 -3
- package/src/sandbox/resolve.ts +76 -11
- package/src/sandbox/retry.test.ts +70 -0
- package/src/sandbox/retry.ts +46 -4
- package/src/sandbox/types.ts +44 -6
- package/src/sandbox/vercel.ts +43 -20
- package/src/scoring/collector.ts +60 -17
- package/src/scoring/coverage.ts +95 -0
- package/src/scoring/diff.ts +81 -0
- package/src/scoring/display.test.ts +121 -0
- package/src/scoring/display.ts +133 -0
- package/src/scoring/evidence.test.ts +189 -0
- package/src/scoring/judge.test.ts +142 -0
- package/src/scoring/judge.ts +15 -18
- package/src/scoring/scoped.ts +217 -50
- package/src/scoring/types.ts +117 -20
- package/src/scoring/verdict.ts +16 -4
- package/src/shared/aggregate.ts +8 -6
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +50 -67
- package/src/show/index.ts +127 -56
- package/src/show/render.ts +662 -131
- package/src/show/report-host.test.ts +188 -0
- package/src/show/report-host.ts +375 -0
- package/src/show/show.test.ts +320 -54
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/App.test.tsx +69 -0
- package/src/view/app/App.tsx +144 -48
- package/src/view/app/components/AttemptModal.tsx +423 -11
- package/src/view/app/components/CodeView.tsx +41 -14
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +37 -17
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- 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 +2 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +17 -17
- package/src/view/data.test.ts +10 -3
- package/src/view/data.ts +155 -49
- package/src/view/index.ts +56 -41
- package/src/view/server.ts +37 -15
- package/src/view/shared/types.ts +34 -5
- package/src/view/styles.css +227 -0
- package/src/view/view-report.test.ts +167 -62
- package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
- package/dist/report/built-ins/experiment-comparison.js +0 -13
- package/dist/report/built-ins/index.d.ts +0 -1
- 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/docs-site/zh/guides/publish-report.mdx +0 -91
- package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
- package/src/report/built-in-user-parity.test.tsx +0 -640
- package/src/report/built-ins/experiment-comparison.tsx +0 -19
- package/src/report/built-ins/index.ts +0 -2
- package/src/report/react/GroupSummary.tsx +0 -66
- package/src/report/react/RunOverview.tsx +0 -109
- package/src/runner/reporters/console.ts +0 -70
- package/src/runner/reporters/live.test.ts +0 -56
- package/src/runner/reporters/live.ts +0 -247
- package/src/runner/reporters/quiet.test.ts +0 -66
- package/src/runner/reporters/quiet.ts +0 -49
- package/src/runner/reporters/table.ts +0 -277
- /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}/fixtures.mdx +0 -0
|
@@ -1,43 +1,20 @@
|
|
|
1
1
|
// MetricTable:行维度 × 指标列。没有实体下钻——要展开到 experiment 的 Eval 或 Eval 的
|
|
2
2
|
// Attempt,用 ExperimentList / EvalList,这个组件只表达任意维度 × 任意指标。
|
|
3
|
-
// 行按传入顺序渲染——排序发生在计算侧(
|
|
3
|
+
// 行按传入顺序渲染——排序发生在计算侧(metricTableData 的 sort 参数),组件不重排;
|
|
4
4
|
// 静态 HTML 以数据侧预排的顺序呈现即完整。web 面额外输出渐进增强的 data 属性:
|
|
5
5
|
// 所有表头带 data-nre-sort、格子带 data-sort-value,enhance.js 在场时点表头
|
|
6
6
|
// 可就地重排(纯展示态交互,不改口径);filter 开时在表格前渲染过滤输入框。
|
|
7
|
-
// meta 在场(rows: "experiment")时补 Model / Agent / Verdicts 列,列序对齐 view
|
|
8
|
-
// 原生榜单:experiment、model、agent、指标列…、verdicts。
|
|
9
7
|
// 列头以箭头标注 better 方向;samples < total 的格子带覆盖率角标;
|
|
10
8
|
// 一组全 null 渲染成「缺数据」,绝不画 0(逻辑在 MetricCellView)。
|
|
11
9
|
|
|
12
10
|
import type { ReactElement } from "react";
|
|
13
|
-
import type { TableData
|
|
11
|
+
import type { TableData } from "../types.ts";
|
|
14
12
|
import type { AttemptLocator } from "../../results/locator.ts";
|
|
15
13
|
import { DEFAULT_REPORT_LOCALE, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
|
|
16
14
|
import { MetricCellView } from "./cell.tsx";
|
|
17
15
|
import { colorClassForKey } from "./colors.ts";
|
|
18
16
|
import { cx } from "./format.ts";
|
|
19
17
|
|
|
20
|
-
/** verdict 计票 pill(「3 passed / 1 failed」):非零判定各一枚,全零如实空。 */
|
|
21
|
-
function VerdictTally({
|
|
22
|
-
verdicts,
|
|
23
|
-
locale,
|
|
24
|
-
}: {
|
|
25
|
-
verdicts: NonNullable<TableRowMeta["verdicts"]>;
|
|
26
|
-
locale: ReportLocale;
|
|
27
|
-
}): ReactElement {
|
|
28
|
-
const kinds = (["passed", "failed", "errored", "skipped"] as const).filter((k) => verdicts[k] > 0);
|
|
29
|
-
return (
|
|
30
|
-
<span className="nre-verdict-tally">
|
|
31
|
-
{kinds.map((kind) => (
|
|
32
|
-
<span key={kind} className={cx("nre-verdict-pill", `nre-verdict-${kind}`)}>
|
|
33
|
-
{verdicts[kind]} {localeText(locale, `verdict.${kind}`)}
|
|
34
|
-
</span>
|
|
35
|
-
))}
|
|
36
|
-
{kinds.length === 0 && <span className="nre-missing">—</span>}
|
|
37
|
-
</span>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
18
|
export function MetricTable({
|
|
42
19
|
data,
|
|
43
20
|
attemptHref,
|
|
@@ -51,27 +28,13 @@ export function MetricTable({
|
|
|
51
28
|
className?: string;
|
|
52
29
|
locale?: ReportLocale;
|
|
53
30
|
}): ReactElement {
|
|
54
|
-
const hasMeta = data.rows.some((row) => row.meta !== undefined);
|
|
55
|
-
const hasModel = data.rows.some((row) => row.meta?.model !== undefined);
|
|
56
|
-
const hasVerdicts = data.rows.some((row) => row.meta?.verdicts !== undefined);
|
|
57
|
-
|
|
58
31
|
const table = (
|
|
59
32
|
<table className={cx("nre", "nre-metric-table", !filter && className)}>
|
|
60
33
|
<thead>
|
|
61
34
|
<tr>
|
|
62
35
|
<th scope="col" className="nre-dimension" data-nre-sort="">
|
|
63
|
-
{data.
|
|
36
|
+
{data.rowDimension}
|
|
64
37
|
</th>
|
|
65
|
-
{hasMeta && hasModel && (
|
|
66
|
-
<th scope="col" className="nre-meta-col" data-nre-sort="">
|
|
67
|
-
{localeText(locale, "table.model")}
|
|
68
|
-
</th>
|
|
69
|
-
)}
|
|
70
|
-
{hasMeta && (
|
|
71
|
-
<th scope="col" className="nre-meta-col" data-nre-sort="">
|
|
72
|
-
{localeText(locale, "table.agent")}
|
|
73
|
-
</th>
|
|
74
|
-
)}
|
|
75
38
|
{data.columns.map((col) => (
|
|
76
39
|
<th scope="col" key={col.key} className="nre-metric-col" data-nre-sort="">
|
|
77
40
|
{resolveMetricLabel(col.label, locale, col.key)}
|
|
@@ -87,11 +50,6 @@ export function MetricTable({
|
|
|
87
50
|
)}
|
|
88
51
|
</th>
|
|
89
52
|
))}
|
|
90
|
-
{hasVerdicts && (
|
|
91
|
-
<th scope="col" className="nre-verdicts-col" data-nre-sort="">
|
|
92
|
-
{localeText(locale, "table.verdicts")}
|
|
93
|
-
</th>
|
|
94
|
-
)}
|
|
95
53
|
</tr>
|
|
96
54
|
</thead>
|
|
97
55
|
<tbody>
|
|
@@ -104,28 +62,7 @@ export function MetricTable({
|
|
|
104
62
|
data-sort-value={row.key}
|
|
105
63
|
>
|
|
106
64
|
{row.key}
|
|
107
|
-
{/* rows: "experiment" 专属:eval/attempt 数 + 最后运行时间,行键下的一行紧凑摘要
|
|
108
|
-
(旧 ExperimentRow 的 "N eval results · N runs · 最后运行时间") */}
|
|
109
|
-
{row.meta?.evals !== undefined && (
|
|
110
|
-
<div className="nre-row-meta-sub">
|
|
111
|
-
{localeText(locale, "overview.evalsCount", { n: row.meta.evals })}
|
|
112
|
-
{row.meta.attempts !== undefined && row.meta.attempts > row.meta.evals
|
|
113
|
-
? ` · ${localeText(locale, "overview.attemptsCount", { n: row.meta.attempts })}`
|
|
114
|
-
: ""}
|
|
115
|
-
{row.meta.lastRunAt ? ` · ${localeText(locale, "latestRun", { run: row.meta.lastRunAt })}` : ""}
|
|
116
|
-
</div>
|
|
117
|
-
)}
|
|
118
65
|
</th>
|
|
119
|
-
{hasMeta && hasModel && (
|
|
120
|
-
<td className="nre-td nre-meta-cell" data-sort-value={row.meta?.model ?? ""}>
|
|
121
|
-
{row.meta?.model ?? <span className="nre-missing">—</span>}
|
|
122
|
-
</td>
|
|
123
|
-
)}
|
|
124
|
-
{hasMeta && (
|
|
125
|
-
<td className="nre-td nre-meta-cell" data-sort-value={row.meta?.agent ?? ""}>
|
|
126
|
-
{row.meta?.agent ?? <span className="nre-missing">—</span>}
|
|
127
|
-
</td>
|
|
128
|
-
)}
|
|
129
66
|
{data.columns.map((col) => {
|
|
130
67
|
const cell = row.cells[col.key];
|
|
131
68
|
return (
|
|
@@ -133,21 +70,12 @@ export function MetricTable({
|
|
|
133
70
|
{cell ? (
|
|
134
71
|
<MetricCellView cell={cell} attemptHref={attemptHref} locale={locale} />
|
|
135
72
|
) : (
|
|
136
|
-
// 数据侧没给这个格子(理论上
|
|
73
|
+
// 数据侧没给这个格子(理论上 metricTableData 不会缺列)——按空处理,不编数
|
|
137
74
|
<span className="nre-empty" />
|
|
138
75
|
)}
|
|
139
76
|
</td>
|
|
140
77
|
);
|
|
141
78
|
})}
|
|
142
|
-
{hasVerdicts && (
|
|
143
|
-
<td className="nre-td nre-verdicts-cell" data-sort-value={row.meta?.verdicts?.passed ?? ""}>
|
|
144
|
-
{row.meta?.verdicts ? (
|
|
145
|
-
<VerdictTally verdicts={row.meta.verdicts} locale={locale} />
|
|
146
|
-
) : (
|
|
147
|
-
<span className="nre-missing">—</span>
|
|
148
|
-
)}
|
|
149
|
-
</td>
|
|
150
|
-
)}
|
|
151
79
|
</tr>
|
|
152
80
|
))}
|
|
153
81
|
</tbody>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ScopeSummary:一个范围的摘要卡——快照时间窗、experiment / eval / attempt 数、判定计票、
|
|
2
|
+
// 端到端成功率与总成本。data 恒携带 eval 级与 attempt 级两份计票;呈现 prop `votes`
|
|
3
|
+
// 只选择显示哪一级(默认 "eval"),不改变 data。成功率与总成本只渲染算好的 MetricCell,
|
|
4
|
+
// 不现场重算(docs/feature/reports/library/summaries.md「ScopeSummary」)。
|
|
5
|
+
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import type { ScopeSummaryData, VerdictTally } from "../types.ts";
|
|
8
|
+
import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
|
|
9
|
+
import { MetricCellView } from "./cell.tsx";
|
|
10
|
+
import { cx } from "./format.ts";
|
|
11
|
+
|
|
12
|
+
function VerdictTallyView({ tally, locale }: { tally: VerdictTally; locale: ReportLocale }): ReactElement {
|
|
13
|
+
const kinds = (["passed", "failed", "errored", "skipped"] as const).filter((k) => tally[k] > 0);
|
|
14
|
+
return (
|
|
15
|
+
<span className="nre-verdict-tally">
|
|
16
|
+
{kinds.map((kind) => (
|
|
17
|
+
<span key={kind} className={cx("nre-verdict-pill", `nre-verdict-${kind}`)}>
|
|
18
|
+
{tally[kind]} {localeText(locale, `verdict.${kind}`)}
|
|
19
|
+
</span>
|
|
20
|
+
))}
|
|
21
|
+
{kinds.length === 0 && <span className="nre-missing">—</span>}
|
|
22
|
+
</span>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ScopeSummary({
|
|
27
|
+
data,
|
|
28
|
+
votes = "eval",
|
|
29
|
+
className,
|
|
30
|
+
locale = DEFAULT_REPORT_LOCALE,
|
|
31
|
+
}: {
|
|
32
|
+
data: ScopeSummaryData;
|
|
33
|
+
/** 显示哪一级计票;默认 "eval"。data 恒携带两级,votes 只选择呈现。 */
|
|
34
|
+
votes?: "eval" | "attempt";
|
|
35
|
+
className?: string;
|
|
36
|
+
locale?: ReportLocale;
|
|
37
|
+
}): ReactElement {
|
|
38
|
+
const tally = votes === "attempt" ? data.attemptVerdicts : data.evalVerdicts;
|
|
39
|
+
return (
|
|
40
|
+
<div className={cx("nre", "nre-scope-summary", className)} data-votes={votes}>
|
|
41
|
+
<dl className="nre-scope-kpis">
|
|
42
|
+
<div className="nre-scope-kpi nre-scope-kpi-rate">
|
|
43
|
+
<dt>{localeText(locale, "scopeSummary.passRate")}</dt>
|
|
44
|
+
<dd>
|
|
45
|
+
<MetricCellView cell={data.endToEndPassRate} locale={locale} />
|
|
46
|
+
</dd>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="nre-scope-kpi">
|
|
49
|
+
<dt>{localeText(locale, "scopeSummary.experiments")}</dt>
|
|
50
|
+
<dd>{data.experiments}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="nre-scope-kpi">
|
|
53
|
+
<dt>{localeText(locale, "scopeSummary.evals")}</dt>
|
|
54
|
+
<dd>{data.evals}</dd>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="nre-scope-kpi">
|
|
57
|
+
<dt>{localeText(locale, "scopeSummary.attempts")}</dt>
|
|
58
|
+
<dd>{data.attempts}</dd>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="nre-scope-kpi nre-scope-kpi-verdicts">
|
|
61
|
+
<dt>{localeText(locale, votes === "attempt" ? "scopeSummary.votesAttempt" : "scopeSummary.votesEval")}</dt>
|
|
62
|
+
<dd>
|
|
63
|
+
<VerdictTallyView tally={tally} locale={locale} />
|
|
64
|
+
</dd>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="nre-scope-kpi">
|
|
67
|
+
<dt>{localeText(locale, "scopeSummary.totalCost")}</dt>
|
|
68
|
+
<dd>
|
|
69
|
+
<MetricCellView cell={data.totalCostUSD} locale={locale} />
|
|
70
|
+
</dd>
|
|
71
|
+
</div>
|
|
72
|
+
</dl>
|
|
73
|
+
{/* 时间窗:贡献当前数据的快照 startedAt 范围;空范围不编造当前时间 */}
|
|
74
|
+
{data.range.latestStartedAt !== null && (
|
|
75
|
+
<p className="nre-scope-summary-range">
|
|
76
|
+
{data.range.earliestStartedAt !== null && data.range.earliestStartedAt !== data.range.latestStartedAt
|
|
77
|
+
? localeText(locale, "scopeSummary.range", {
|
|
78
|
+
from: data.range.earliestStartedAt,
|
|
79
|
+
to: data.range.latestStartedAt,
|
|
80
|
+
})
|
|
81
|
+
: data.range.latestStartedAt}
|
|
82
|
+
</p>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Scoreboard:考试成绩单——总分 + 分科小计。
|
|
2
|
-
// 固定分母的口径不藏:没跑的题挣 0
|
|
3
|
-
//
|
|
2
|
+
// 固定分母的口径不藏:没跑的题挣 0 分但留在分母里(notRun),跑了但测不了的题同样按 0 分
|
|
3
|
+
// (unscorable),两个计数分开显示,连同 refs 一起,不合并成一个笼统的缺失数;
|
|
4
|
+
// weights 是「实际生效的权重表」,渲染出来让成绩单可审计;题集之外被忽略的 eval 数在注脚。
|
|
4
5
|
|
|
5
6
|
import type { ReactElement } from "react";
|
|
6
7
|
import type { ScoreboardData } from "../types.ts";
|
|
7
|
-
import { DEFAULT_REPORT_LOCALE, countText, localeText, type ReportLocale } from "../locale.ts";
|
|
8
|
+
import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveLocalizedText, type ReportLocale } from "../locale.ts";
|
|
8
9
|
import { colorClassForKey } from "./colors.ts";
|
|
9
10
|
import { cx } from "./format.ts";
|
|
10
11
|
|
|
@@ -31,7 +32,7 @@ export function Scoreboard({
|
|
|
31
32
|
<thead>
|
|
32
33
|
<tr>
|
|
33
34
|
<th scope="col" className="nre-dimension">
|
|
34
|
-
{data.
|
|
35
|
+
{data.rowDimension}
|
|
35
36
|
</th>
|
|
36
37
|
<th scope="col" className="nre-total-col">
|
|
37
38
|
{localeText(locale, "scoreboard.total")}
|
|
@@ -51,7 +52,16 @@ export function Scoreboard({
|
|
|
51
52
|
<th scope="row" className={cx("nre-row-key", "nre-key", colorClassForKey(row.key))}>
|
|
52
53
|
{row.key}
|
|
53
54
|
</th>
|
|
54
|
-
<td className="nre-total">
|
|
55
|
+
<td className="nre-total">
|
|
56
|
+
{resolveLocalizedText(row.total.display, locale)}
|
|
57
|
+
{/* 两种 0 分分开注脚:没去考(notRun)与考了判不了(unscorable) */}
|
|
58
|
+
{row.total.notRun > 0 && (
|
|
59
|
+
<span className="nre-total-not-run">{countText(locale, "scoreboard.notRun", row.total.notRun)}</span>
|
|
60
|
+
)}
|
|
61
|
+
{row.total.unscorable > 0 && (
|
|
62
|
+
<span className="nre-total-unscorable">{countText(locale, "scoreboard.unscorable", row.total.unscorable)}</span>
|
|
63
|
+
)}
|
|
64
|
+
</td>
|
|
55
65
|
{subjectKeys.map((key) => {
|
|
56
66
|
const subject = row.subjects.find((s) => s.key === key);
|
|
57
67
|
if (!subject) return <td key={key} className="nre-td-empty" />;
|
|
@@ -60,16 +70,20 @@ export function Scoreboard({
|
|
|
60
70
|
<span
|
|
61
71
|
className="nre-subject-score"
|
|
62
72
|
title={localeText(locale, "scoreboard.subjectTitle", {
|
|
63
|
-
|
|
73
|
+
questions: subject.questions,
|
|
64
74
|
earned: subject.earned,
|
|
65
75
|
possible: subject.possible,
|
|
66
76
|
})}
|
|
67
77
|
>
|
|
68
|
-
{subject.
|
|
78
|
+
{resolveLocalizedText(subject.display, locale)}
|
|
69
79
|
</span>
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
{subject.notRun > 0 && (
|
|
81
|
+
<span className="nre-subject-not-run">{localeText(locale, "scoreboard.notRunText", { n: subject.notRun })}</span>
|
|
82
|
+
)}
|
|
83
|
+
{subject.unscorable > 0 && (
|
|
84
|
+
<span className="nre-subject-unscorable">
|
|
85
|
+
{localeText(locale, "scoreboard.unscorableText", { n: subject.unscorable })}
|
|
86
|
+
</span>
|
|
73
87
|
)}
|
|
74
88
|
</td>
|
|
75
89
|
);
|
|
@@ -90,6 +104,10 @@ export function Scoreboard({
|
|
|
90
104
|
))}
|
|
91
105
|
{data.weights.length > 0 && <span className="nre-weight-rest">{localeText(locale, "scoreboard.othersWeight")}</span>}
|
|
92
106
|
</p>
|
|
107
|
+
{/* 题集之外的 eval:忽略但如实报数 */}
|
|
108
|
+
{data.ignoredEvals > 0 && (
|
|
109
|
+
<p className="nre-scoreboard-ignored">{countText(locale, "scoreboard.ignored", data.ignoredEvals)}</p>
|
|
110
|
+
)}
|
|
93
111
|
</section>
|
|
94
112
|
);
|
|
95
113
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { ReactElement } from "react";
|
|
6
6
|
import type { MetricCell } from "../types.ts";
|
|
7
7
|
import type { AttemptLocator } from "../../results/locator.ts";
|
|
8
|
-
import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
|
|
8
|
+
import { DEFAULT_REPORT_LOCALE, localeText, resolveLocalizedText, type ReportLocale } from "../locale.ts";
|
|
9
9
|
|
|
10
10
|
export function MetricCellView({
|
|
11
11
|
cell,
|
|
@@ -32,7 +32,7 @@ export function MetricCellView({
|
|
|
32
32
|
className="nre-value"
|
|
33
33
|
title={localeText(locale, "cell.measuredTitle", { samples: cell.samples, total: cell.total })}
|
|
34
34
|
>
|
|
35
|
-
{cell.display}
|
|
35
|
+
{resolveLocalizedText(cell.display, locale)}
|
|
36
36
|
</span>
|
|
37
37
|
{/* samples < total:有 attempt 测不了这个指标,覆盖率角标如实标出 */}
|
|
38
38
|
{cell.samples < cell.total && (
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/reports/cases.md
|
|
2
|
+
// 分区「MetricScatter 点标签布局(web 面)」:对 placePointLabels 直接断言标签框与点框的几何关系,不经 HTML。
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { placePointLabels, type LabelBounds, type PlacedPointLabel, type PointLabelInput } from "./chart-math.ts";
|
|
6
|
+
|
|
7
|
+
const BOUNDS: LabelBounds = { x0: 2, y0: 2, x1: 638, y1: 358 };
|
|
8
|
+
const WIDTH = 90;
|
|
9
|
+
|
|
10
|
+
interface Box {
|
|
11
|
+
x0: number;
|
|
12
|
+
y0: number;
|
|
13
|
+
x1: number;
|
|
14
|
+
y1: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 与契约声明的标签框一致:基线上方 10px、下方 2px,锚向决定水平延伸方向
|
|
18
|
+
function labelBox(l: PlacedPointLabel, width: number): Box {
|
|
19
|
+
const x0 = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - width : l.x - width / 2;
|
|
20
|
+
return { x0, y0: l.y - 10, x1: x0 + width, y1: l.y + 2 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function dotBox(p: PointLabelInput): Box {
|
|
24
|
+
return { x0: p.cx - 6, y0: p.cy - 6, x1: p.cx + 6, y1: p.cy + 6 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function overlaps(a: Box, b: Box): boolean {
|
|
28
|
+
return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("placePointLabels", () => {
|
|
32
|
+
it("近重合点簇:标签框两两不叠、不压任何数据点(只向下推的级联会把第三个标签推到下方点上)", () => {
|
|
33
|
+
// 三点近重合 + 右下方另一点:down-only 布局把第三个标签推到 y∈[122,134]、x≥310,
|
|
34
|
+
// 正好压住 (330,128) 的点框;候选位择优必须避开
|
|
35
|
+
const points: PointLabelInput[] = [
|
|
36
|
+
{ cx: 300, cy: 100, width: WIDTH },
|
|
37
|
+
{ cx: 303, cy: 102, width: WIDTH },
|
|
38
|
+
{ cx: 298, cy: 104, width: WIDTH },
|
|
39
|
+
{ cx: 330, cy: 128, width: WIDTH },
|
|
40
|
+
];
|
|
41
|
+
const labels = placePointLabels(points, BOUNDS);
|
|
42
|
+
const boxes = labels.map((l) => labelBox(l, WIDTH));
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
45
|
+
for (let j = i + 1; j < boxes.length; j++) {
|
|
46
|
+
expect(overlaps(boxes[i], boxes[j]), `标签 ${i} 与标签 ${j} 重叠`).toBe(false);
|
|
47
|
+
}
|
|
48
|
+
for (let j = 0; j < points.length; j++) {
|
|
49
|
+
expect(overlaps(boxes[i], dotBox(points[j])), `标签 ${i} 压住点 ${j}`).toBe(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// 离开左右紧邻位的标签必须带 leader 标记,渲染层据此连回原点
|
|
53
|
+
for (let i = 0; i < labels.length; i++) {
|
|
54
|
+
const adjacent =
|
|
55
|
+
labels[i].y === points[i].cy + 4 &&
|
|
56
|
+
((labels[i].anchor === "start" && labels[i].x === points[i].cx + 10) ||
|
|
57
|
+
(labels[i].anchor === "end" && labels[i].x === points[i].cx - 10));
|
|
58
|
+
expect(labels[i].leader, `标签 ${i} 的 leader 标记与位置不符`).toBe(!adjacent);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("无冲突取右侧紧邻位不带 leader;右缘点锚到左侧紧邻位且标签框不越出画布", () => {
|
|
63
|
+
const sparse = placePointLabels(
|
|
64
|
+
[
|
|
65
|
+
{ cx: 100, cy: 100, width: WIDTH },
|
|
66
|
+
{ cx: 400, cy: 250, width: WIDTH },
|
|
67
|
+
],
|
|
68
|
+
BOUNDS,
|
|
69
|
+
);
|
|
70
|
+
for (const [i, l] of sparse.entries()) {
|
|
71
|
+
expect(l.anchor).toBe("start");
|
|
72
|
+
expect(l.x).toBe([100, 400][i] + 10);
|
|
73
|
+
expect(l.leader).toBe(false);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const [edge] = placePointLabels([{ cx: 630, cy: 200, width: WIDTH }], BOUNDS);
|
|
77
|
+
const box = labelBox(edge, WIDTH);
|
|
78
|
+
expect(edge.anchor).toBe("end");
|
|
79
|
+
expect(edge.leader).toBe(false);
|
|
80
|
+
expect(box.x0).toBeGreaterThanOrEqual(BOUNDS.x0);
|
|
81
|
+
expect(box.x1).toBeLessThanOrEqual(BOUNDS.x1);
|
|
82
|
+
expect(box.y0).toBeGreaterThanOrEqual(BOUNDS.y0);
|
|
83
|
+
expect(box.y1).toBeLessThanOrEqual(BOUNDS.y1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// 通用图表数值工具:round-number
|
|
2
|
-
// 从 src/view/app/lib/chart.ts 迁来(view 侧从这里 re-export),报告组件与 view 共用同一份。
|
|
1
|
+
// 通用图表数值工具:round-number 刻度生成与点标签布局,零依赖纯函数。
|
|
3
2
|
|
|
4
3
|
function niceNumber(range: number, round: boolean): number {
|
|
5
4
|
if (range <= 0) return 1;
|
|
@@ -35,34 +34,114 @@ export function niceTicks(min: number, max: number, count = 5): number[] {
|
|
|
35
34
|
return ticks;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
export interface
|
|
37
|
+
export interface PointLabelInput {
|
|
39
38
|
cx: number;
|
|
40
39
|
cy: number;
|
|
40
|
+
/** 标签文本的估算宽度(px)。 */
|
|
41
41
|
width: number;
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 标签允许占用的画布范围(含坐标轴边距,标签可以借用空白边距)。 */
|
|
45
|
+
export interface LabelBounds {
|
|
46
|
+
x0: number;
|
|
47
|
+
y0: number;
|
|
48
|
+
x1: number;
|
|
49
|
+
y1: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PlacedPointLabel {
|
|
53
|
+
/** <text> 的锚点坐标(y 为文字基线)。 */
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
anchor: "start" | "end" | "middle";
|
|
57
|
+
/** 标签不在点左右紧邻位:渲染层补 leader line 连回原点。 */
|
|
58
|
+
leader: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 标签框高度(基线上方 10px、下方 2px,对应 11px 字号)。 */
|
|
62
|
+
const LABEL_ASCENT = 10;
|
|
63
|
+
const LABEL_DESCENT = 2;
|
|
64
|
+
/** 数据点视为边长 2×DOT_R 的方形障碍(点半径 4.5 + 命中余量)。 */
|
|
65
|
+
const DOT_R = 6;
|
|
66
|
+
/** 紧邻位与点的水平间距;候选环按它逐环外扩。 */
|
|
67
|
+
const NEAR = 10;
|
|
68
|
+
const RINGS = [NEAR, NEAR + 12, NEAR + 26];
|
|
69
|
+
|
|
70
|
+
interface Box {
|
|
71
|
+
x0: number;
|
|
72
|
+
y0: number;
|
|
73
|
+
x1: number;
|
|
74
|
+
y1: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function labelBoxAt(x: number, y: number, anchor: PlacedPointLabel["anchor"], width: number): Box {
|
|
78
|
+
const x0 = anchor === "start" ? x : anchor === "end" ? x - width : x - width / 2;
|
|
79
|
+
return { x0, y0: y - LABEL_ASCENT, x1: x0 + width, y1: y + LABEL_DESCENT };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function overlapArea(a: Box, b: Box): number {
|
|
83
|
+
const w = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
|
|
84
|
+
const h = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
|
|
85
|
+
return w > 0 && h > 0 ? w * h : 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function outOfBoundsArea(box: Box, bounds: LabelBounds): number {
|
|
89
|
+
const total = (box.x1 - box.x0) * (box.y1 - box.y0);
|
|
90
|
+
const w = Math.min(box.x1, bounds.x1) - Math.max(box.x0, bounds.x0);
|
|
91
|
+
const h = Math.min(box.y1, bounds.y1) - Math.max(box.y0, bounds.y0);
|
|
92
|
+
const inside = w > 0 && h > 0 ? w * h : 0;
|
|
93
|
+
return total - inside;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 一个点在距离 d 的候选环:左右紧邻、四个斜角、正上正下。顺序即同代价时的偏好序。 */
|
|
97
|
+
function ringCandidates(p: PointLabelInput, d: number): Omit<PlacedPointLabel, "leader">[] {
|
|
98
|
+
const diag = d * 0.75;
|
|
99
|
+
return [
|
|
100
|
+
{ x: p.cx + d, y: p.cy + 4, anchor: "start" },
|
|
101
|
+
{ x: p.cx - d, y: p.cy + 4, anchor: "end" },
|
|
102
|
+
{ x: p.cx + diag, y: p.cy - diag - 2, anchor: "start" },
|
|
103
|
+
{ x: p.cx - diag, y: p.cy - diag - 2, anchor: "end" },
|
|
104
|
+
{ x: p.cx, y: p.cy - d - LABEL_DESCENT - 2, anchor: "middle" },
|
|
105
|
+
{ x: p.cx + diag, y: p.cy + diag + LABEL_ASCENT, anchor: "start" },
|
|
106
|
+
{ x: p.cx - diag, y: p.cy + diag + LABEL_ASCENT, anchor: "end" },
|
|
107
|
+
{ x: p.cx, y: p.cy + d + LABEL_ASCENT + 2, anchor: "middle" },
|
|
108
|
+
];
|
|
44
109
|
}
|
|
45
110
|
|
|
46
111
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
112
|
+
* 散点直接标签的候选位择优布局(docs/feature/reports/library.md「MetricScatter」):
|
|
113
|
+
* 每个标签在点四周由近及远的候选环上打分——与已放置标签的重叠、与任何数据点的重叠、
|
|
114
|
+
* 越出画布的面积、离点距离逐项累加,取代价最小的候选。存在无冲突候选时标签必不遮点、
|
|
115
|
+
* 不叠标签、不越界;全候选冲突时取重叠最小者,绝不丢标签。重合点簇因此向不同方向散开,
|
|
116
|
+
* 而不是单向堆叠压到下方的数据点上。
|
|
49
117
|
*/
|
|
50
|
-
export function
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
118
|
+
export function placePointLabels(points: PointLabelInput[], bounds: LabelBounds): PlacedPointLabel[] {
|
|
119
|
+
const dots: Box[] = points.map((p) => ({ x0: p.cx - DOT_R, y0: p.cy - DOT_R, x1: p.cx + DOT_R, y1: p.cy + DOT_R }));
|
|
120
|
+
const placedBoxes: Box[] = [];
|
|
121
|
+
const result: PlacedPointLabel[] = new Array(points.length);
|
|
122
|
+
// 由上到下、由左到右逐个放置,布局确定可复现
|
|
123
|
+
const order = [...points.keys()].sort((a, b) => points[a].cy - points[b].cy || points[a].cx - points[b].cx);
|
|
124
|
+
|
|
54
125
|
for (const i of order) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
126
|
+
const p = points[i];
|
|
127
|
+
let best: { candidate: Omit<PlacedPointLabel, "leader">; box: Box; cost: number; leader: boolean } | null = null;
|
|
128
|
+
for (let ring = 0; ring < RINGS.length; ring++) {
|
|
129
|
+
const candidates = ringCandidates(p, RINGS[ring]);
|
|
130
|
+
for (let c = 0; c < candidates.length; c++) {
|
|
131
|
+
const candidate = candidates[c];
|
|
132
|
+
const box = labelBoxAt(candidate.x, candidate.y, candidate.anchor, p.width);
|
|
133
|
+
let cost = 0;
|
|
134
|
+
for (const placed of placedBoxes) cost += overlapArea(box, placed) * 4;
|
|
135
|
+
for (const dot of dots) cost += overlapArea(box, dot) * 3;
|
|
136
|
+
cost += outOfBoundsArea(box, bounds) * 6;
|
|
137
|
+
cost += ring * 8 + c * 0.5; // 近环、偏好序靠前者优先,只作同冲突程度下的排序
|
|
138
|
+
if (best === null || cost < best.cost) {
|
|
139
|
+
best = { candidate, box, cost, leader: !(ring === 0 && c <= 1) };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
63
142
|
}
|
|
64
|
-
|
|
65
|
-
|
|
143
|
+
placedBoxes.push(best!.box);
|
|
144
|
+
result[i] = { ...best!.candidate, leader: best!.leader };
|
|
66
145
|
}
|
|
67
|
-
return
|
|
146
|
+
return result;
|
|
68
147
|
}
|
|
@@ -1,19 +1,103 @@
|
|
|
1
1
|
// niceeval 报告的渐进增强 runtime:纯 vanilla JS、零依赖、IIFE、幂等。
|
|
2
|
-
// 只作用于 .nre DOM 与 data-nre-*
|
|
3
|
-
//
|
|
4
|
-
// <title
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// 只作用于 .nre DOM 与 data-nre-* 属性;六个行为——实验组切换、Tabs 单选切换、表格排序、
|
|
3
|
+
// 行过滤、SVG 点 tooltip、警告命令复制。全部只改浏览状态,不改数据、指标口径或初始 HTML 数值。
|
|
4
|
+
// 静态 HTML 无 JS 时内容完整可读是硬约束:排序有数据侧预排、tooltip 退化为原生 <title>、
|
|
5
|
+
// 过滤输入框静默无功能、Tabs 退化为原生 <details> 手风琴、命令块退化为点击全选。
|
|
6
|
+
// 全部经 document 级事件委托绑定,重复注入本文件只在首次生效(window.__nreEnhanced 守卫),
|
|
7
|
+
// DOM 被搬动(如 view 把 <template> 内容摆进报告槽)也无需重新绑定。
|
|
7
8
|
|
|
8
9
|
(function () {
|
|
9
10
|
"use strict";
|
|
10
11
|
if (typeof window === "undefined" || window.__nreEnhanced) return;
|
|
11
12
|
window.__nreEnhanced = true;
|
|
12
13
|
|
|
14
|
+
// 根类 nre-js:styles.css 用它把仅增强态的布局(单选 tab 条、复制指针)限定在 JS 在场时。
|
|
15
|
+
// 挂在 documentElement 上与报告块位置无关,块之后被搬进槽位也不需要补标记。
|
|
16
|
+
document.documentElement.classList.add("nre-js");
|
|
17
|
+
|
|
13
18
|
function closest(target, selector) {
|
|
14
19
|
return target && target.closest ? target.closest(selector) : null;
|
|
15
20
|
}
|
|
16
21
|
|
|
22
|
+
// ───────────────────────── 实验组:[data-nre-experiment-group-select] ─────────────────────────
|
|
23
|
+
// 数据侧已经把每组的摘要、散点和列表分别算好;这里仅在同一报告节点内切换 <details>,
|
|
24
|
+
// 不重新请求、不重算,也不触碰 Runs / Traces / Attempt 证据室。
|
|
25
|
+
|
|
26
|
+
function selectExperimentGroup(control) {
|
|
27
|
+
var root = control.closest("[data-nre-experiment-groups]");
|
|
28
|
+
if (!root) return;
|
|
29
|
+
var selected = control.getAttribute("data-nre-experiment-group-select");
|
|
30
|
+
var controls = root.querySelectorAll("[data-nre-experiment-group-select]");
|
|
31
|
+
var panels = root.querySelectorAll("[data-nre-experiment-group-panel]");
|
|
32
|
+
for (var i = 0; i < controls.length; i++) {
|
|
33
|
+
var active = controls[i].getAttribute("data-nre-experiment-group-select") === selected;
|
|
34
|
+
controls[i].setAttribute("aria-selected", active ? "true" : "false");
|
|
35
|
+
controls[i].tabIndex = active ? 0 : -1;
|
|
36
|
+
}
|
|
37
|
+
for (var j = 0; j < panels.length; j++) {
|
|
38
|
+
panels[j].open = panels[j].getAttribute("data-nre-experiment-group-panel") === selected;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
document.addEventListener("click", function (e) {
|
|
43
|
+
var control = closest(e.target, "[data-nre-experiment-group-select]");
|
|
44
|
+
if (control) selectExperimentGroup(control);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
document.addEventListener("keydown", function (e) {
|
|
48
|
+
var control = closest(e.target, "[data-nre-experiment-group-select]");
|
|
49
|
+
if (!control || (e.key !== "Enter" && e.key !== " ")) return;
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
selectExperimentGroup(control);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ───────────────────────── Tabs:[data-nre-tabs] 单选切换 ─────────────────────────
|
|
55
|
+
// 静态 HTML 每 tab 一个 <details> 且仅首个 open;点击 summary 时接管原生 toggle:
|
|
56
|
+
// 打开所点 tab、收起同组其余,点已开的 tab 保持打开(单选语义,恒有一个面板可见)。
|
|
57
|
+
// 只切换 open 状态,不触碰 tab 内任何数据;键盘 Enter/Space 走 summary 的原生激活(即 click)。
|
|
58
|
+
|
|
59
|
+
document.addEventListener("click", function (e) {
|
|
60
|
+
var title = closest(e.target, "[data-nre-tabs] > details > summary");
|
|
61
|
+
if (!title) return;
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
var tab = title.parentNode;
|
|
64
|
+
var group = tab.parentNode;
|
|
65
|
+
for (var i = 0; i < group.children.length; i++) {
|
|
66
|
+
var child = group.children[i];
|
|
67
|
+
if (child.tagName === "DETAILS") child.open = child === tab;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ───────────────────────── 复制:[data-nre-copy](宿主警告块的命令) ─────────────────────────
|
|
72
|
+
// 点击把 data-nre-copy 携带的完整命令写进剪贴板,成功后短暂打上 data-nre-copied
|
|
73
|
+
// (styles.css 显示 ✓);剪贴板不可用时退化为全选该块文本,用户手动复制。
|
|
74
|
+
// 块内文本与属性值恒不变,复制的是数据侧已写好的命令原文。
|
|
75
|
+
|
|
76
|
+
document.addEventListener("click", function (e) {
|
|
77
|
+
var block = closest(e.target, "[data-nre-copy]");
|
|
78
|
+
if (!block) return;
|
|
79
|
+
var command = block.getAttribute("data-nre-copy") || "";
|
|
80
|
+
function mark() {
|
|
81
|
+
block.setAttribute("data-nre-copied", "");
|
|
82
|
+
setTimeout(function () {
|
|
83
|
+
block.removeAttribute("data-nre-copied");
|
|
84
|
+
}, 1500);
|
|
85
|
+
}
|
|
86
|
+
function selectFallback() {
|
|
87
|
+
var selection = window.getSelection();
|
|
88
|
+
if (!selection) return;
|
|
89
|
+
var range = document.createRange();
|
|
90
|
+
range.selectNodeContents(block);
|
|
91
|
+
selection.removeAllRanges();
|
|
92
|
+
selection.addRange(range);
|
|
93
|
+
}
|
|
94
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
95
|
+
navigator.clipboard.writeText(command).then(mark, selectFallback);
|
|
96
|
+
} else {
|
|
97
|
+
selectFallback();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
17
101
|
// ───────────────────────── 排序:th[data-nre-sort] ─────────────────────────
|
|
18
102
|
// 点击按该列排序 tbody 行(td/th 的 data-sort-value,数值优先、退回字符串;
|
|
19
103
|
// 空值恒沉底),再点反向;方向指示由 th 上的 nre-sort-asc / nre-sort-desc 类驱动。
|