niceeval 0.4.6 → 0.5.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/docs-site/zh/concepts/adapter.mdx +6 -6
- package/docs-site/zh/concepts/experiment.mdx +7 -7
- package/docs-site/zh/concepts/overview.mdx +1 -1
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +64 -21
- package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
- package/docs-site/zh/guides/custom-reports.mdx +243 -0
- package/docs-site/zh/guides/experiments.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +252 -0
- package/docs-site/zh/guides/reporters.mdx +1 -1
- package/docs-site/zh/guides/results-data.mdx +224 -0
- package/docs-site/zh/guides/runner.mdx +1 -1
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/viewing-results.mdx +147 -77
- package/docs-site/zh/guides/write-experiment.mdx +9 -9
- package/docs-site/zh/guides/write-send.mdx +6 -6
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/introduction.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +11 -3
- package/docs-site/zh/reference/define-agent.mdx +5 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/package.json +3 -2
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/ai-sdk.ts +2 -2
- package/src/agents/claude-code.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/streaming.test.ts +1 -1
- package/src/agents/types.ts +4 -4
- package/src/agents/ui-message-stream.test.ts +1 -1
- package/src/cli.ts +120 -18
- package/src/context/context.test.ts +1 -1
- package/src/context/context.ts +3 -3
- package/src/context/session.ts +2 -2
- package/src/context/types.ts +2 -2
- package/src/i18n/en.ts +29 -7
- package/src/i18n/zh-CN.ts +24 -2
- package/src/report/aggregate.ts +79 -50
- package/src/report/components.tsx +190 -0
- package/src/report/compute.ts +149 -84
- package/src/report/default-report.tsx +222 -0
- package/src/report/dual-face.test.tsx +527 -0
- package/src/report/format.ts +29 -0
- package/src/report/index.ts +79 -24
- package/src/report/load.ts +67 -0
- package/src/report/metrics.ts +2 -1
- package/src/report/param.ts +18 -0
- package/src/report/primitives.tsx +117 -0
- package/src/report/react/CaseList.tsx +2 -2
- package/src/report/react/DeltaTable.tsx +2 -2
- package/src/report/react/MetricBars.tsx +109 -0
- package/src/report/react/MetricLine.tsx +200 -0
- package/src/report/react/MetricMatrix.tsx +1 -1
- package/src/report/react/MetricScatter.tsx +1 -1
- package/src/report/react/MetricTable.tsx +1 -1
- package/src/report/react/RunOverview.tsx +4 -4
- package/src/report/react/Scoreboard.tsx +3 -3
- package/src/report/react/cell.tsx +1 -1
- package/src/report/react/fixtures.ts +98 -36
- package/src/report/react/format.ts +3 -28
- package/src/report/react/index.tsx +41 -24
- package/src/report/react/render.test.tsx +8 -8
- package/src/report/react/styles.css +77 -0
- package/src/report/report.test.ts +248 -128
- package/src/report/report.ts +69 -0
- package/src/report/text/faces.ts +356 -0
- package/src/report/text/layout.ts +125 -0
- package/src/report/text/plot.ts +127 -0
- package/src/report/tree.ts +220 -0
- package/src/report/types.ts +75 -27
- package/src/report/web.ts +40 -0
- package/src/results/copy.ts +95 -41
- package/src/results/format.ts +12 -11
- package/src/results/index.ts +34 -17
- package/src/results/open.ts +201 -81
- package/src/results/results.test.ts +595 -191
- package/src/results/select.ts +107 -55
- package/src/results/types.ts +137 -45
- package/src/results/writer.ts +258 -0
- package/src/runner/attempt.ts +3 -3
- package/src/runner/fingerprint.ts +1 -1
- package/src/runner/reporters/artifacts.ts +38 -78
- package/src/runner/reporters/braintrust.test.ts +2 -2
- package/src/runner/reporters/braintrust.ts +3 -3
- package/src/runner/run.ts +1 -1
- package/src/runner/types.ts +18 -12
- package/src/show/compose.ts +215 -0
- package/src/show/index.ts +263 -0
- package/src/show/render.ts +456 -0
- package/src/show/show.test.ts +505 -0
- package/src/view/app/App.tsx +85 -24
- package/src/view/app/components/CostScoreChart.tsx +29 -10
- package/src/view/app/components/ExperimentTable.tsx +27 -9
- package/src/view/app/components/GroupSelector.tsx +1 -1
- package/src/view/app/i18n.ts +18 -3
- package/src/view/app/lib/attempt-route.test.ts +9 -9
- package/src/view/app/lib/attempt-route.ts +5 -5
- package/src/view/app/lib/outcome.ts +1 -3
- package/src/view/app/lib/rows.ts +95 -15
- package/src/view/app/main.tsx +18 -6
- package/src/view/app/pages/RunsPage.tsx +4 -7
- package/src/view/app/pages/TracesPage.tsx +3 -8
- package/src/view/app/types.ts +50 -6
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.test.ts +254 -0
- package/src/view/data.ts +336 -0
- package/src/view/index.ts +64 -10
- package/src/view/server.ts +31 -9
- package/src/view/shared/types.ts +47 -41
- package/src/view/styles.css +5 -0
- package/src/view/template.html +1 -0
- package/src/view/view-report.test.ts +308 -0
- package/docs-site/.mintignore +0 -13
- package/docs-site/AGENTS.md +0 -46
- package/docs-site/concepts/adapter.mdx +0 -287
- package/docs-site/concepts/assert.mdx +0 -243
- package/docs-site/concepts/drive.mdx +0 -118
- package/docs-site/concepts/evals.mdx +0 -108
- package/docs-site/concepts/experiment.mdx +0 -37
- package/docs-site/concepts/hitl.mdx +0 -83
- package/docs-site/concepts/judge.mdx +0 -129
- package/docs-site/concepts/overview.mdx +0 -98
- package/docs-site/concepts/tier.mdx +0 -42
- package/docs-site/docs-ref/00-index.md +0 -23
- package/docs-site/docs-ref/01-page-types.md +0 -43
- package/docs-site/docs-ref/03-style-rules.md +0 -45
- package/docs-site/docs-ref/04-code-examples.md +0 -37
- package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
- package/docs-site/docs-ref/06-checklists.md +0 -53
- package/docs-site/docs.json +0 -256
- package/docs-site/example/ai-agent-application.mdx +0 -148
- package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
- package/docs-site/example/claude-code-codex-skill.mdx +0 -147
- package/docs-site/example/showcase.mdx +0 -39
- package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
- package/docs-site/example/tier1-claude-sdk.mdx +0 -119
- package/docs-site/example/tier1-codex-sdk.mdx +0 -111
- package/docs-site/example/tier1-langgraph.mdx +0 -119
- package/docs-site/example/tier1-pi-sdk.mdx +0 -119
- package/docs-site/favicon.svg +0 -5
- package/docs-site/github-diff.css +0 -135
- package/docs-site/github-diff.js +0 -11
- package/docs-site/guides/authoring.mdx +0 -129
- package/docs-site/guides/ci-integration.mdx +0 -97
- package/docs-site/guides/connect-otel.mdx +0 -209
- package/docs-site/guides/connect-your-agent.mdx +0 -221
- package/docs-site/guides/dataset-fanout.mdx +0 -98
- package/docs-site/guides/experiments.mdx +0 -71
- package/docs-site/guides/fixtures.mdx +0 -147
- package/docs-site/guides/reporters.mdx +0 -113
- package/docs-site/guides/runner.mdx +0 -79
- package/docs-site/guides/sandbox-agent.mdx +0 -138
- package/docs-site/guides/sandbox-backends.mdx +0 -64
- package/docs-site/guides/scoring-guide.mdx +0 -92
- package/docs-site/guides/viewing-results.mdx +0 -195
- package/docs-site/guides/write-experiment.mdx +0 -81
- package/docs-site/guides/write-send.mdx +0 -347
- package/docs-site/index.mdx +0 -181
- package/docs-site/introduction.mdx +0 -141
- package/docs-site/quickstart.mdx +0 -136
- package/docs-site/reference/builtin-agents.mdx +0 -161
- package/docs-site/reference/capabilities.mdx +0 -76
- package/docs-site/reference/cli.mdx +0 -120
- package/docs-site/reference/define-agent.mdx +0 -168
- package/docs-site/reference/define-config.mdx +0 -41
- package/docs-site/reference/define-eval.mdx +0 -160
- package/docs-site/reference/events.mdx +0 -131
- package/docs-site/reference/expect.mdx +0 -112
- package/docs-site/tracker.js +0 -8
- package/src/report/react/data.ts +0 -17
- package/src/view/aggregate.ts +0 -103
- package/src/view/loader.test.ts +0 -61
- package/src/view/loader.ts +0 -257
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// --report 的装载:两个宿主(show / view)共用的中性入口。复用跑用户 .ts 配置的
|
|
2
|
+
// 同一 tsx 加载机制(bin 里已 register)。show 一进程一次装载;view 的 dev server
|
|
3
|
+
// 每次请求现读现渲染 —— ESM 模块缓存永不失效,所以按文件 mtime 做 cache-busting
|
|
4
|
+
// (freshImport):报告文件变更 → 下次请求拿到新模块,整页重算(docs/reports.md
|
|
5
|
+
// 裁决记录 6)。装载环境坑见 memory/tsx-dynamic-import-require-cycle.md。
|
|
6
|
+
|
|
7
|
+
import { existsSync, statSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { isReportDefinition, type ReportDefinition } from "./report.ts";
|
|
11
|
+
|
|
12
|
+
/** 可预期的装载错误:宿主打一句英文直说问题与下一步,不抛堆栈。 */
|
|
13
|
+
export class ReportLoadError extends Error {}
|
|
14
|
+
|
|
15
|
+
export interface LoadReportOptions {
|
|
16
|
+
/**
|
|
17
|
+
* 按文件 mtime 追加 query 绕开 ESM 模块缓存:同一文件未变时命中缓存,
|
|
18
|
+
* 变更后重新装载。dev server(view)传 true;一次性进程(show / --out)不需要。
|
|
19
|
+
*/
|
|
20
|
+
freshImport?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function loadReportFile(
|
|
24
|
+
cwd: string,
|
|
25
|
+
path: string,
|
|
26
|
+
options?: LoadReportOptions,
|
|
27
|
+
): Promise<ReportDefinition> {
|
|
28
|
+
const abs = resolve(cwd, path);
|
|
29
|
+
if (!existsSync(abs)) {
|
|
30
|
+
throw new ReportLoadError(
|
|
31
|
+
`Report file not found: ${abs}. Pass --report an explicit path to a module whose default export is defineReport(...).`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const plain = pathToFileURL(abs);
|
|
35
|
+
const url = new URL(plain.href);
|
|
36
|
+
if (options?.freshImport) {
|
|
37
|
+
// 只 bust 报告文件本体:它 import 的模块仍走缓存,与「报告文件变更整页重算」的
|
|
38
|
+
// 装载语义一致(依赖变更不追踪)。tsx 与 Node 原生 ESM 都认 file URL 的 query。
|
|
39
|
+
url.searchParams.set("mtime", String(statSync(abs).mtimeMs));
|
|
40
|
+
}
|
|
41
|
+
let mod: { default?: unknown };
|
|
42
|
+
try {
|
|
43
|
+
mod = (await import(url.href)) as { default?: unknown };
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// 个别装载环境(如 vitest 的 vite-node)按「扩展名 + query」误判文件类型;
|
|
46
|
+
// 退化为普通 import(失去变更重载,不失去功能)。仍失败才是真错误。
|
|
47
|
+
if (url.href !== plain.href) {
|
|
48
|
+
try {
|
|
49
|
+
mod = (await import(plain.href)) as { default?: unknown };
|
|
50
|
+
} catch {
|
|
51
|
+
throw new ReportLoadError(
|
|
52
|
+
`Cannot load report file ${abs}: ${e instanceof Error ? e.message : String(e)}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
throw new ReportLoadError(
|
|
57
|
+
`Cannot load report file ${abs}: ${e instanceof Error ? e.message : String(e)}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!isReportDefinition(mod.default)) {
|
|
62
|
+
throw new ReportLoadError(
|
|
63
|
+
`${path} does not default-export a report. Export default defineReport(async ({ selection, results }) => ...) from "niceeval/report".`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return mod.default;
|
|
67
|
+
}
|
package/src/report/metrics.ts
CHANGED
|
@@ -16,8 +16,9 @@ import type { Metric } from "./types.ts";
|
|
|
16
16
|
/**
|
|
17
17
|
* 定义一个指标。内置指标与自定义指标是同一个类型,没有特权;
|
|
18
18
|
* 校验只管「能进计算」的最低要求,聚合语义见 docs/reports.md。
|
|
19
|
+
* name 是 const 泛型:产物的 name 保持字面量,`row.cells[metric.name]` 才有编译期列键。
|
|
19
20
|
*/
|
|
20
|
-
export function defineMetric(def: Metric): Metric {
|
|
21
|
+
export function defineMetric<const Name extends string>(def: Metric<Name>): Metric<Name> {
|
|
21
22
|
if (typeof def.name !== "string" || def.name.length === 0) {
|
|
22
23
|
throw new Error("defineMetric: metric name must be a non-empty string.");
|
|
23
24
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// param():把 experiment 声明的 params 当维度或轴(docs/reports.md「params 与新摆法」)。
|
|
2
|
+
// 变量来自配置,不来自命名 —— 报告不解析 experiment id 字符串抠变量。
|
|
3
|
+
|
|
4
|
+
import type { ParamRef } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export function param(
|
|
7
|
+
name: string,
|
|
8
|
+
opts?: {
|
|
9
|
+
/** 组标签 / 轴标签;函数形态把声明值折成组名(如 `(v) => \`${v} agents\``)。 */
|
|
10
|
+
label?: string | ((value: string | number | boolean) => string);
|
|
11
|
+
unit?: string;
|
|
12
|
+
},
|
|
13
|
+
): ParamRef {
|
|
14
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
15
|
+
throw new Error("param: name must be a non-empty string (the key declared in the experiment's params).");
|
|
16
|
+
}
|
|
17
|
+
return { kind: "param", name, label: opts?.label, unit: opts?.unit };
|
|
18
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// 排版原语 Row / Col / Section / Text / Style:五个内置双面组件,没有特殊机制。
|
|
2
|
+
// web 面是普通 React 渲染;text 面用 ctx.render(child, 子宽) 显式传宽,
|
|
3
|
+
// Row 分栏、宽度不足降级纵向。Style 给自定义组件带样式:web 面吐 <style> 标签,
|
|
4
|
+
// text 面渲染为空 —— 静态导出不打包用户代码,className 引用的 CSS 靠它随树走。
|
|
5
|
+
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import { defineComponent, type ReportNode } from "./tree.ts";
|
|
8
|
+
import { indentBlock, joinColumns, wrapDisplay } from "./text/layout.ts";
|
|
9
|
+
|
|
10
|
+
function childArray(children: ReportNode): ReportNode[] {
|
|
11
|
+
if (children === null || children === undefined || typeof children === "boolean") return [];
|
|
12
|
+
return Array.isArray(children) ? children : [children];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cx(...parts: (string | undefined)[]): string {
|
|
16
|
+
return parts.filter(Boolean).join(" ");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LayoutProps {
|
|
20
|
+
children?: ReportNode;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 纵向依次排列:网页是块级堆叠,终端是逐块输出(块间空一行)。 */
|
|
25
|
+
export const Col = defineComponent<LayoutProps>({
|
|
26
|
+
web({ children, className }) {
|
|
27
|
+
return <div className={cx("nre", "nre-col", className)}>{children as ReactNode}</div>;
|
|
28
|
+
},
|
|
29
|
+
text({ children }, ctx) {
|
|
30
|
+
return childArray(children)
|
|
31
|
+
.map((child) => ctx.render(child))
|
|
32
|
+
.filter((block) => block.length > 0)
|
|
33
|
+
.join("\n\n");
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
Col.displayName = "Col";
|
|
37
|
+
|
|
38
|
+
// Row 的每栏至少留这个宽度,不硬挤;不够就降级纵向
|
|
39
|
+
const MIN_COLUMN_WIDTH = 24;
|
|
40
|
+
const COLUMN_SEPARATOR = " │ ";
|
|
41
|
+
|
|
42
|
+
/** 并排:网页横向排布,终端字符分栏;终端宽度不够时自动降级为纵向。 */
|
|
43
|
+
export const Row = defineComponent<LayoutProps>({
|
|
44
|
+
web({ children, className }) {
|
|
45
|
+
return <div className={cx("nre", "nre-row", className)}>{children as ReactNode}</div>;
|
|
46
|
+
},
|
|
47
|
+
text({ children }, ctx) {
|
|
48
|
+
const blocks = childArray(children).filter(
|
|
49
|
+
(child) => child !== null && child !== undefined && typeof child !== "boolean",
|
|
50
|
+
);
|
|
51
|
+
if (blocks.length === 0) return "";
|
|
52
|
+
if (blocks.length === 1) return ctx.render(blocks[0]);
|
|
53
|
+
const columnWidth = Math.floor(
|
|
54
|
+
(ctx.width - COLUMN_SEPARATOR.length * (blocks.length - 1)) / blocks.length,
|
|
55
|
+
);
|
|
56
|
+
if (columnWidth < MIN_COLUMN_WIDTH) {
|
|
57
|
+
// 宽度不足:降级纵向,与 Col 同一形态
|
|
58
|
+
return blocks
|
|
59
|
+
.map((child) => ctx.render(child))
|
|
60
|
+
.filter((block) => block.length > 0)
|
|
61
|
+
.join("\n\n");
|
|
62
|
+
}
|
|
63
|
+
const rendered = blocks.map((child) => ctx.render(child, columnWidth));
|
|
64
|
+
return joinColumns(rendered, rendered.map(() => columnWidth), COLUMN_SEPARATOR);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
Row.displayName = "Row";
|
|
68
|
+
|
|
69
|
+
export interface SectionProps extends LayoutProps {
|
|
70
|
+
title: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 带标题的块:网页是标题层级,终端是标题行加缩进。 */
|
|
74
|
+
export const Section = defineComponent<SectionProps>({
|
|
75
|
+
web({ title, children, className }) {
|
|
76
|
+
return (
|
|
77
|
+
<section className={cx("nre", "nre-section", className)}>
|
|
78
|
+
<h2 className="nre-section-title">{title}</h2>
|
|
79
|
+
{children as ReactNode}
|
|
80
|
+
</section>
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
text({ title, children }, ctx) {
|
|
84
|
+
const body = childArray(children)
|
|
85
|
+
.map((child) => ctx.render(child, ctx.width - 2))
|
|
86
|
+
.filter((block) => block.length > 0)
|
|
87
|
+
.join("\n\n");
|
|
88
|
+
return body.length > 0 ? `${title}\n${indentBlock(body, " ")}` : title;
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
Section.displayName = "Section";
|
|
92
|
+
|
|
93
|
+
/** 说明文字:网页是段落,终端是折行文本。 */
|
|
94
|
+
export const Text = defineComponent<LayoutProps>({
|
|
95
|
+
web({ children, className }) {
|
|
96
|
+
return <p className={cx("nre", "nre-text", className)}>{children as ReactNode}</p>;
|
|
97
|
+
},
|
|
98
|
+
text({ children }, ctx) {
|
|
99
|
+
return wrapDisplay(ctx.render(children), ctx.width).join("\n");
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
Text.displayName = "Text";
|
|
103
|
+
|
|
104
|
+
export interface StyleProps {
|
|
105
|
+
children?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 自定义组件的样式随树带走:web 面吐 <style> 标签,text 面渲染为空。 */
|
|
109
|
+
export const Style = defineComponent<StyleProps>({
|
|
110
|
+
web({ children }) {
|
|
111
|
+
return <style>{children}</style>;
|
|
112
|
+
},
|
|
113
|
+
text() {
|
|
114
|
+
return "";
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
Style.displayName = "Style";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// truncated 如实报「还有 n 条没列」。长文本收进 <details>,零 JS 也能展开。
|
|
4
4
|
|
|
5
5
|
import type { ReactElement } from "react";
|
|
6
|
-
import type { AttemptRef, CaseListData } from "
|
|
6
|
+
import type { AttemptRef, CaseListData } from "../types.ts";
|
|
7
7
|
import { colorClassForKey } from "./colors.ts";
|
|
8
8
|
import { cx, formatDurationMs, formatUSD } from "./format.ts";
|
|
9
9
|
|
|
@@ -32,7 +32,7 @@ export function CaseList({
|
|
|
32
32
|
{row.costUSD !== undefined && <span className="nre-case-cost">{formatUSD(row.costUSD)}</span>}
|
|
33
33
|
{attemptHref && (
|
|
34
34
|
<a className="nre-case-link" href={attemptHref(row.ref)}>
|
|
35
|
-
|
|
35
|
+
view attempt
|
|
36
36
|
</a>
|
|
37
37
|
)}
|
|
38
38
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 任一侧缺数据时 Δ 显示为缺,不硬算(数据侧已给 delta: null,这里只如实渲染)。
|
|
4
4
|
|
|
5
5
|
import type { ReactElement } from "react";
|
|
6
|
-
import type { DeltaData, MetricColumn } from "
|
|
6
|
+
import type { DeltaData, MetricColumn } from "../types.ts";
|
|
7
7
|
import { MetricCellView } from "./cell.tsx";
|
|
8
8
|
import { colorClassForKey } from "./colors.ts";
|
|
9
9
|
import { MISSING_TEXT, cx } from "./format.ts";
|
|
@@ -29,7 +29,7 @@ export function DeltaTable({
|
|
|
29
29
|
<thead>
|
|
30
30
|
<tr>
|
|
31
31
|
<th scope="col" className="nre-dimension">
|
|
32
|
-
|
|
32
|
+
pair (A → B)
|
|
33
33
|
</th>
|
|
34
34
|
{data.columns.map((col) => (
|
|
35
35
|
<th scope="col" key={col.key} className="nre-metric-col">
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// MetricBars:分组条形——同一份矩阵数据的另一种摆法(MetricBars.data = MetricMatrix.data)。
|
|
2
|
+
// 组维度一组条、系列维度一根条、条长是指标值;竖向分组柱,柱顶标数值,系列颜色与
|
|
3
|
+
// 其它组件的稳定配色一致,图例自动生成。组内按值排序,方向随 better;
|
|
4
|
+
// 缺数据的系列不画柱、不编 0(与 text 面的 — 同口径)。
|
|
5
|
+
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import type { AttemptRef, MatrixData, MetricCell } from "../types.ts";
|
|
8
|
+
import { colorHexForKey, colorClassForKey } from "./colors.ts";
|
|
9
|
+
import { cx } from "./format.ts";
|
|
10
|
+
|
|
11
|
+
const WIDTH = 640;
|
|
12
|
+
const HEIGHT = 320;
|
|
13
|
+
const PLOT = { left: 16, right: WIDTH - 16, top: 28, bottom: HEIGHT - 40 };
|
|
14
|
+
|
|
15
|
+
export function MetricBars({
|
|
16
|
+
data,
|
|
17
|
+
attemptHref,
|
|
18
|
+
className,
|
|
19
|
+
}: {
|
|
20
|
+
data: MatrixData;
|
|
21
|
+
attemptHref?: (ref: AttemptRef) => string;
|
|
22
|
+
className?: string;
|
|
23
|
+
}): ReactElement {
|
|
24
|
+
// 稀疏 cells → 首次出现顺序的组/系列键 + 查找表
|
|
25
|
+
const groupKeys: string[] = [];
|
|
26
|
+
const seriesKeys: string[] = [];
|
|
27
|
+
const byPosition = new Map<string, MetricCell>();
|
|
28
|
+
for (const entry of data.cells) {
|
|
29
|
+
if (!groupKeys.includes(entry.row)) groupKeys.push(entry.row);
|
|
30
|
+
if (!seriesKeys.includes(entry.column)) seriesKeys.push(entry.column);
|
|
31
|
+
byPosition.set(JSON.stringify([entry.row, entry.column]), entry.cell);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const better = data.metric.better ?? "higher";
|
|
35
|
+
const values = data.cells.map((c) => c.cell.value).filter((v): v is number => v !== null);
|
|
36
|
+
const maxValue = values.length > 0 ? Math.max(...values) : 0;
|
|
37
|
+
// 条高刻度:% 的天然域是 [0,1],其余以全图最大值为满条
|
|
38
|
+
const ratioOf = (value: number) => (data.metric.unit === "%" ? Math.min(1, Math.max(0, value)) : maxValue === 0 ? 0 : value / maxValue);
|
|
39
|
+
|
|
40
|
+
const groupWidth = (PLOT.right - PLOT.left) / Math.max(1, groupKeys.length);
|
|
41
|
+
const barWidth = Math.min(36, (groupWidth - 16) / Math.max(1, seriesKeys.length));
|
|
42
|
+
const plotHeight = PLOT.bottom - PLOT.top;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<figure className={cx("nre", "nre-metric-bars", className)}>
|
|
46
|
+
<svg
|
|
47
|
+
className="nre-bars-svg"
|
|
48
|
+
viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
|
|
49
|
+
role="img"
|
|
50
|
+
aria-label={`${data.metric.label} by ${data.rows} × ${data.columns}`}
|
|
51
|
+
>
|
|
52
|
+
{groupKeys.map((group, gi) => {
|
|
53
|
+
const x0 = PLOT.left + gi * groupWidth;
|
|
54
|
+
// 组内按值排序,方向随 better(缺数据的系列不画柱)
|
|
55
|
+
const entries = seriesKeys
|
|
56
|
+
.map((series) => ({ series, cell: byPosition.get(JSON.stringify([group, series])) }))
|
|
57
|
+
.filter((e): e is { series: string; cell: MetricCell } => e.cell !== undefined && e.cell.value !== null)
|
|
58
|
+
.sort((a, b) =>
|
|
59
|
+
better === "lower" ? (a.cell.value as number) - (b.cell.value as number) : (b.cell.value as number) - (a.cell.value as number),
|
|
60
|
+
);
|
|
61
|
+
const innerWidth = entries.length * barWidth;
|
|
62
|
+
const startX = x0 + (groupWidth - innerWidth) / 2;
|
|
63
|
+
return (
|
|
64
|
+
<g key={group} className="nre-bars-group" data-group={group}>
|
|
65
|
+
{entries.map(({ series, cell }, si) => {
|
|
66
|
+
const h = Math.max(1, ratioOf(cell.value as number) * plotHeight);
|
|
67
|
+
const x = startX + si * barWidth;
|
|
68
|
+
const y = PLOT.bottom - h;
|
|
69
|
+
const title = `${group} · ${series}: ${cell.display}(${cell.samples}/${cell.total})`;
|
|
70
|
+
const rect = (
|
|
71
|
+
<rect className="nre-bar" x={x + 2} y={y} width={barWidth - 4} height={h} fill={colorHexForKey(series)}>
|
|
72
|
+
<title>{title}</title>
|
|
73
|
+
</rect>
|
|
74
|
+
);
|
|
75
|
+
const ref = cell.refs[0];
|
|
76
|
+
return (
|
|
77
|
+
<g key={series}>
|
|
78
|
+
{attemptHref && ref ? <a href={attemptHref(ref)}>{rect}</a> : rect}
|
|
79
|
+
{/* 柱顶标数值;覆盖不全时把 samples/total 一并标出,不藏 */}
|
|
80
|
+
<text className="nre-bar-value" x={x + barWidth / 2} y={y - 4} textAnchor="middle" fontSize={10} fill="#525252">
|
|
81
|
+
{cell.samples < cell.total ? `${cell.display} ${cell.samples}/${cell.total}` : cell.display}
|
|
82
|
+
</text>
|
|
83
|
+
</g>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
<text className="nre-bars-group-label" x={x0 + groupWidth / 2} y={HEIGHT - 20} textAnchor="middle" fontSize={12} fill="#525252">
|
|
87
|
+
{group}
|
|
88
|
+
</text>
|
|
89
|
+
</g>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
{/* 基线 */}
|
|
93
|
+
<line x1={PLOT.left} y1={PLOT.bottom} x2={PLOT.right} y2={PLOT.bottom} stroke="#d4d4d4" />
|
|
94
|
+
</svg>
|
|
95
|
+
{/* 图例:系列 → 稳定配色,跨块同键同色 */}
|
|
96
|
+
<figcaption className="nre-bars-legend">
|
|
97
|
+
<span className="nre-bars-metric">
|
|
98
|
+
{data.metric.label}
|
|
99
|
+
{data.metric.unit && <span className="nre-unit">({data.metric.unit})</span>}
|
|
100
|
+
</span>
|
|
101
|
+
{seriesKeys.map((series) => (
|
|
102
|
+
<span key={series} className={cx("nre-legend-key", "nre-key", colorClassForKey(series))}>
|
|
103
|
+
{series}
|
|
104
|
+
</span>
|
|
105
|
+
))}
|
|
106
|
+
</figcaption>
|
|
107
|
+
</figure>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// MetricLine:趋势线——x 是 experiment 声明的 param(有序变量),每个系列一条线。
|
|
2
|
+
// 与 MetricScatter 的分工:scatter 的两轴都是测出来的指标(找 frontier),
|
|
3
|
+
// line 的 x 是你配置的变量(看趋势)。x 轴正常升序;y 轴向随 better;
|
|
4
|
+
// x 或 y 缺数据的点不画,注脚如实报数;每个点带 <title> hover,可经 pointHref 下钻。
|
|
5
|
+
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import type { LineData } from "../types.ts";
|
|
8
|
+
import { colorHexForKey } from "./colors.ts";
|
|
9
|
+
import { MISSING_TEXT, cx } from "./format.ts";
|
|
10
|
+
|
|
11
|
+
const WIDTH = 640;
|
|
12
|
+
const HEIGHT = 360;
|
|
13
|
+
const PLOT = { left: 64, right: WIDTH - 140, top: 24, bottom: HEIGHT - 56 };
|
|
14
|
+
|
|
15
|
+
interface DrawablePoint {
|
|
16
|
+
key: string;
|
|
17
|
+
series?: string;
|
|
18
|
+
xValue: number;
|
|
19
|
+
yValue: number;
|
|
20
|
+
title: string;
|
|
21
|
+
px: number;
|
|
22
|
+
py: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function linearScale(values: number[], pixelLo: number, pixelHi: number, invert: boolean) {
|
|
26
|
+
const lo = Math.min(...values);
|
|
27
|
+
const hi = Math.max(...values);
|
|
28
|
+
const span = hi - lo || 2;
|
|
29
|
+
const padded = { lo: lo - (hi - lo ? span * 0.08 : 1), hi: hi + (hi - lo ? span * 0.08 : 1) };
|
|
30
|
+
const scale = (v: number) => {
|
|
31
|
+
let t = (v - padded.lo) / (padded.hi - padded.lo);
|
|
32
|
+
if (invert) t = 1 - t;
|
|
33
|
+
return pixelLo + t * (pixelHi - pixelLo);
|
|
34
|
+
};
|
|
35
|
+
return { lo, hi, scale };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function MetricLine({
|
|
39
|
+
data,
|
|
40
|
+
pointHref,
|
|
41
|
+
className,
|
|
42
|
+
}: {
|
|
43
|
+
data: LineData;
|
|
44
|
+
pointHref?: (row: LineData["rows"][number]) => string;
|
|
45
|
+
className?: string;
|
|
46
|
+
}): ReactElement {
|
|
47
|
+
const missing = data.rows.filter((r) => r.x === null || r.y.value === null);
|
|
48
|
+
const drawableRows = data.rows.filter((r) => r.x !== null && r.y.value !== null);
|
|
49
|
+
|
|
50
|
+
const missingNote =
|
|
51
|
+
missing.length > 0 ? (
|
|
52
|
+
<p className="nre-line-missing" title={missing.map((r) => r.key).join(", ")}>
|
|
53
|
+
{missing.length} {missing.length === 1 ? "point" : "points"} missing data
|
|
54
|
+
</p>
|
|
55
|
+
) : null;
|
|
56
|
+
|
|
57
|
+
if (drawableRows.length === 0) {
|
|
58
|
+
return (
|
|
59
|
+
<figure className={cx("nre", "nre-metric-line", className)}>
|
|
60
|
+
<p className="nre-missing">{MISSING_TEXT}</p>
|
|
61
|
+
{missingNote}
|
|
62
|
+
</figure>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const xScale = linearScale(drawableRows.map((r) => r.x as number), PLOT.left, PLOT.right, false);
|
|
67
|
+
// y 像素轴向下增长:higher 高值在上;lower 反向后「好」的一端同样在上
|
|
68
|
+
const yScale = linearScale(
|
|
69
|
+
drawableRows.map((r) => r.y.value as number),
|
|
70
|
+
PLOT.bottom,
|
|
71
|
+
PLOT.top,
|
|
72
|
+
data.y.better === "lower",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const points: DrawablePoint[] = drawableRows.map((r) => ({
|
|
76
|
+
key: r.key,
|
|
77
|
+
series: r.series,
|
|
78
|
+
xValue: r.x as number,
|
|
79
|
+
yValue: r.y.value as number,
|
|
80
|
+
title: `${r.key}\n${data.x.label}: ${r.xDisplay}\n${data.y.label}: ${r.y.display}(${r.y.samples}/${r.y.total})`,
|
|
81
|
+
px: xScale.scale(r.x as number),
|
|
82
|
+
py: yScale.scale(r.y.value as number),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
// 同系列的点按 x 排序连线;系列名标在最右点旁
|
|
86
|
+
const seriesOrder: string[] = [];
|
|
87
|
+
const bySeries = new Map<string, DrawablePoint[]>();
|
|
88
|
+
for (const p of points) {
|
|
89
|
+
const key = p.series ?? "";
|
|
90
|
+
if (!bySeries.has(key)) {
|
|
91
|
+
bySeries.set(key, []);
|
|
92
|
+
seriesOrder.push(key);
|
|
93
|
+
}
|
|
94
|
+
bySeries.get(key)!.push(p);
|
|
95
|
+
}
|
|
96
|
+
for (const list of bySeries.values()) list.sort((a, b) => a.xValue - b.xValue);
|
|
97
|
+
|
|
98
|
+
const xTicks = xScale.lo === xScale.hi ? [xScale.lo] : [xScale.lo, xScale.hi];
|
|
99
|
+
const yTicks = yScale.lo === yScale.hi ? [yScale.lo] : [yScale.lo, yScale.hi];
|
|
100
|
+
const xDisplayFor = (value: number) =>
|
|
101
|
+
drawableRows.find((r) => r.x === value)?.xDisplay ?? String(value);
|
|
102
|
+
const yDisplayFor = (value: number) =>
|
|
103
|
+
drawableRows.find((r) => r.y.value === value)?.y.display ?? String(value);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<figure className={cx("nre", "nre-metric-line", className)}>
|
|
107
|
+
<svg
|
|
108
|
+
className="nre-line-svg"
|
|
109
|
+
viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
|
|
110
|
+
role="img"
|
|
111
|
+
aria-label={`${data.y.label} by ${data.x.label}`}
|
|
112
|
+
>
|
|
113
|
+
<rect
|
|
114
|
+
className="nre-line-plot"
|
|
115
|
+
x={PLOT.left}
|
|
116
|
+
y={PLOT.top}
|
|
117
|
+
width={PLOT.right - PLOT.left}
|
|
118
|
+
height={PLOT.bottom - PLOT.top}
|
|
119
|
+
fill="none"
|
|
120
|
+
stroke="#d4d4d4"
|
|
121
|
+
/>
|
|
122
|
+
<text className="nre-line-xlabel" x={(PLOT.left + PLOT.right) / 2} y={HEIGHT - 8} textAnchor="middle" fontSize={12} fill="#525252">
|
|
123
|
+
{data.x.label}
|
|
124
|
+
{data.x.unit ? `(${data.x.unit})` : ""}
|
|
125
|
+
</text>
|
|
126
|
+
<text
|
|
127
|
+
className="nre-line-ylabel"
|
|
128
|
+
x={16}
|
|
129
|
+
y={(PLOT.top + PLOT.bottom) / 2}
|
|
130
|
+
textAnchor="middle"
|
|
131
|
+
fontSize={12}
|
|
132
|
+
fill="#525252"
|
|
133
|
+
transform={`rotate(-90 16 ${(PLOT.top + PLOT.bottom) / 2})`}
|
|
134
|
+
>
|
|
135
|
+
{data.y.label}
|
|
136
|
+
{data.y.unit ? `(${data.y.unit})` : ""}
|
|
137
|
+
</text>
|
|
138
|
+
|
|
139
|
+
{xTicks.map((v) => (
|
|
140
|
+
<text key={`x${v}`} className="nre-line-tick" x={xScale.scale(v)} y={PLOT.bottom + 16} textAnchor="middle" fontSize={11} fill="#737373">
|
|
141
|
+
{xDisplayFor(v)}
|
|
142
|
+
</text>
|
|
143
|
+
))}
|
|
144
|
+
{yTicks.map((v) => (
|
|
145
|
+
<text key={`y${v}`} className="nre-line-tick" x={PLOT.left - 6} y={yScale.scale(v) + 4} textAnchor="end" fontSize={11} fill="#737373">
|
|
146
|
+
{yDisplayFor(v)}
|
|
147
|
+
</text>
|
|
148
|
+
))}
|
|
149
|
+
|
|
150
|
+
{seriesOrder.map((series) => {
|
|
151
|
+
const list = bySeries.get(series)!;
|
|
152
|
+
const color = series === "" ? "#525252" : colorHexForKey(series);
|
|
153
|
+
const labelAt = list.reduce((a, b) => (b.px > a.px ? b : a));
|
|
154
|
+
return (
|
|
155
|
+
<g key={series || "(single)"} className="nre-line-series" data-series={series || undefined}>
|
|
156
|
+
{list.length > 1 && (
|
|
157
|
+
<polyline
|
|
158
|
+
className="nre-line-path"
|
|
159
|
+
points={list.map((p) => `${p.px},${p.py}`).join(" ")}
|
|
160
|
+
fill="none"
|
|
161
|
+
stroke={color}
|
|
162
|
+
strokeWidth={1.5}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
{series !== "" && (
|
|
166
|
+
<text className="nre-line-series-label" x={labelAt.px + 8} y={labelAt.py + 4} fontSize={12} fill={color}>
|
|
167
|
+
{series}
|
|
168
|
+
</text>
|
|
169
|
+
)}
|
|
170
|
+
</g>
|
|
171
|
+
);
|
|
172
|
+
})}
|
|
173
|
+
|
|
174
|
+
{points.map((p, i) => {
|
|
175
|
+
const circle = (
|
|
176
|
+
<circle
|
|
177
|
+
className="nre-line-point"
|
|
178
|
+
data-key={p.key}
|
|
179
|
+
cx={p.px}
|
|
180
|
+
cy={p.py}
|
|
181
|
+
r={4}
|
|
182
|
+
fill={p.series !== undefined ? colorHexForKey(p.series) : "#525252"}
|
|
183
|
+
>
|
|
184
|
+
<title>{p.title}</title>
|
|
185
|
+
</circle>
|
|
186
|
+
);
|
|
187
|
+
const row = drawableRows[i];
|
|
188
|
+
return pointHref ? (
|
|
189
|
+
<a key={`${p.key}:${i}`} className="nre-line-point-link" href={pointHref(row)}>
|
|
190
|
+
{circle}
|
|
191
|
+
</a>
|
|
192
|
+
) : (
|
|
193
|
+
<g key={`${p.key}:${i}`}>{circle}</g>
|
|
194
|
+
);
|
|
195
|
+
})}
|
|
196
|
+
</svg>
|
|
197
|
+
{missingNote}
|
|
198
|
+
</figure>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 是格子里的普通 <a>,不 hydrate 也能点。
|
|
6
6
|
|
|
7
7
|
import type { ReactElement } from "react";
|
|
8
|
-
import type { AttemptRef, MatrixData } from "
|
|
8
|
+
import type { AttemptRef, MatrixData } from "../types.ts";
|
|
9
9
|
import { MetricCellView } from "./cell.tsx";
|
|
10
10
|
import { colorClassForKey } from "./colors.ts";
|
|
11
11
|
import { cx } from "./format.ts";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 底部注脚如实报「n 个点缺数据」;hover 信息退化为 SVG <title>,不 hydrate 也在。
|
|
5
5
|
|
|
6
6
|
import type { ReactElement } from "react";
|
|
7
|
-
import type { MetricColumn, ScatterData } from "
|
|
7
|
+
import type { MetricColumn, ScatterData } from "../types.ts";
|
|
8
8
|
import { colorHexForKey } from "./colors.ts";
|
|
9
9
|
import { MISSING_TEXT, cx } from "./format.ts";
|
|
10
10
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 一组全 null 渲染成「缺数据」,绝不画 0(逻辑在 MetricCellView)。
|
|
6
6
|
|
|
7
7
|
import type { ReactElement } from "react";
|
|
8
|
-
import type { AttemptRef, TableData } from "
|
|
8
|
+
import type { AttemptRef, TableData } from "../types.ts";
|
|
9
9
|
import { MetricCellView } from "./cell.tsx";
|
|
10
10
|
import { colorClassForKey } from "./colors.ts";
|
|
11
11
|
import { cx } from "./format.ts";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 诚实不靠使用者记得渲染(docs/reports.md「第一档」行为清单)。
|
|
4
4
|
|
|
5
5
|
import type { ReactElement } from "react";
|
|
6
|
-
import type { OverviewData } from "
|
|
6
|
+
import type { OverviewData } from "../types.ts";
|
|
7
7
|
import { MISSING_TEXT, cx, formatDurationMs, formatPercent, formatUSD } from "./format.ts";
|
|
8
8
|
|
|
9
9
|
export function RunOverview({
|
|
@@ -72,12 +72,12 @@ export function RunOverview({
|
|
|
72
72
|
))}
|
|
73
73
|
</p>
|
|
74
74
|
|
|
75
|
-
{/*
|
|
75
|
+
{/* 选集的警告(残缺快照等)直接渲染在条内,不静默;结构化字段供程序判断,这里打 message */}
|
|
76
76
|
{data.warnings.length > 0 && (
|
|
77
77
|
<ul className="nre-warnings">
|
|
78
78
|
{data.warnings.map((w, i) => (
|
|
79
|
-
<li key={i} className="nre-warning">
|
|
80
|
-
{w}
|
|
79
|
+
<li key={i} className="nre-warning" data-kind={w.kind}>
|
|
80
|
+
{w.message}
|
|
81
81
|
</li>
|
|
82
82
|
))}
|
|
83
83
|
</ul>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// weights 是「实际生效的权重表」,渲染出来让成绩单可审计(docs/reports.md scoreboard 公式一节)。
|
|
4
4
|
|
|
5
5
|
import type { ReactElement } from "react";
|
|
6
|
-
import type { ScoreboardData } from "
|
|
6
|
+
import type { ScoreboardData } from "../types.ts";
|
|
7
7
|
import { colorClassForKey } from "./colors.ts";
|
|
8
8
|
import { cx } from "./format.ts";
|
|
9
9
|
|
|
@@ -28,7 +28,7 @@ export function Scoreboard({
|
|
|
28
28
|
<thead>
|
|
29
29
|
<tr>
|
|
30
30
|
<th scope="col" className="nre-dimension">
|
|
31
|
-
{data.
|
|
31
|
+
{data.dimension}
|
|
32
32
|
</th>
|
|
33
33
|
<th scope="col" className="nre-total-col">
|
|
34
34
|
Total<span className="nre-full-marks">/ {data.fullMarks}</span>
|
|
@@ -72,7 +72,7 @@ export function Scoreboard({
|
|
|
72
72
|
</table>
|
|
73
73
|
{/* 实际生效的权重表:成绩单可审计 */}
|
|
74
74
|
<p className="nre-weights">
|
|
75
|
-
|
|
75
|
+
weights:{" "}
|
|
76
76
|
{data.weights.length === 0
|
|
77
77
|
? "all evals ×1"
|
|
78
78
|
: data.weights.map((w) => (
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 纯渲染、零 hooks;交互只有普通 <a>(下钻由使用者的 attemptHref 决定去处)。
|
|
4
4
|
|
|
5
5
|
import type { ReactElement } from "react";
|
|
6
|
-
import type { AttemptRef, MetricCell } from "
|
|
6
|
+
import type { AttemptRef, MetricCell } from "../types.ts";
|
|
7
7
|
import { MISSING_TEXT } from "./format.ts";
|
|
8
8
|
|
|
9
9
|
export function MetricCellView({
|