niceeval 0.10.0 → 0.10.2
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/dist/report/components/attempt-detail/AttemptConversation.d.ts +5 -1
- package/dist/report/components/attempt-detail/AttemptConversation.js +42 -2
- package/dist/report/components/attempt-detail/AttemptSource.js +83 -13
- package/dist/report/components/attempt-detail/compute.js +44 -1
- package/dist/report/components/attempt-detail/faces.js +4 -0
- package/dist/report/components/attempt-detail/index.d.ts +2 -2
- package/dist/report/components/attempt-detail/index.js +24 -4
- package/dist/report/components/entity-lists/ExperimentList.d.ts +1 -2
- package/dist/report/components/entity-lists/ExperimentList.js +6 -5
- package/dist/report/components/entity-lists/faces.d.ts +1 -1
- package/dist/report/components/entity-lists/faces.js +10 -9
- package/dist/report/components/entity-lists/index.d.ts +5 -7
- package/dist/report/components/entity-lists/index.js +7 -3
- package/dist/report/components/metric-views/MetricScatter.js +2 -38
- package/dist/report/index.d.ts +1 -1
- package/dist/report/model/format.d.ts +5 -5
- package/dist/report/model/format.js +34 -8
- package/dist/report/model/types.d.ts +16 -2
- package/dist/report/react/index.d.ts +1 -1
- package/dist/runner/types.d.ts +2 -1
- package/dist/sandbox/e2b.d.ts +0 -1
- package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +5 -5
- package/docs-site/zh/explanation/runner.mdx +6 -2
- package/docs-site/zh/reference/cli.mdx +2 -2
- package/docs-site/zh/reference/report-components.mdx +3 -1
- package/docs-site/zh/tutorials/experiments.mdx +1 -2
- package/docs-site/zh/tutorials/viewing-results.mdx +12 -9
- package/docs-site/zh/tutorials/write-experiment.mdx +1 -3
- package/package.json +3 -4
- package/src/cli.ts +4 -2
- package/src/o11y/prices.json +176 -99
- package/src/report/assets/styles.css +822 -0
- package/src/report/components/attempt-detail/AttemptConversation.tsx +55 -7
- package/src/report/components/attempt-detail/AttemptSource.tsx +186 -42
- package/src/report/components/attempt-detail/attempt-components.test.tsx +82 -4
- package/src/report/components/attempt-detail/compute.ts +50 -1
- package/src/report/components/attempt-detail/faces.ts +3 -0
- package/src/report/components/attempt-detail/index.tsx +33 -16
- package/src/report/components/attempt-detail/validate.test.ts +19 -2
- package/src/report/components/entity-lists/ExperimentList.tsx +13 -10
- package/src/report/components/entity-lists/faces.ts +10 -9
- package/src/report/components/entity-lists/index.tsx +7 -10
- package/src/report/components/metric-views/MetricScatter.tsx +2 -38
- package/src/report/components/render.test.tsx +19 -9
- package/src/report/index.ts +2 -0
- package/src/report/model/format.ts +33 -8
- package/src/report/model/types.ts +18 -2
- package/src/report/react/index.tsx +2 -0
- package/src/report/runtime/dual-render.test.tsx +43 -10
- package/src/runner/types.ts +2 -1
- package/src/sandbox/e2b-reconcile.test.ts +125 -0
- package/src/sandbox/e2b.ts +38 -2
- package/src/view/view-report.test.ts +3 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
-
import type { AttemptConversationData } from "../../model/types.ts";
|
|
2
|
+
import type { AttemptConversationData, AttemptConversationReply } from "../../model/types.ts";
|
|
3
|
+
/** AttemptSource 复用同一份回复 renderer,把一轮执行挂回对应的 send 源码行。 */
|
|
4
|
+
export declare function ConversationReplies({ replies }: {
|
|
5
|
+
replies: AttemptConversationReply[];
|
|
6
|
+
}): ReactElement;
|
|
3
7
|
export declare function AttemptConversation({ data, className, }: {
|
|
4
8
|
data: AttemptConversationData | null;
|
|
5
9
|
className?: string;
|
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../shared.js";
|
|
3
|
+
const TOOL_VERB = {
|
|
4
|
+
shell: "Bash",
|
|
5
|
+
file_read: "Read",
|
|
6
|
+
file_write: "Write",
|
|
7
|
+
file_edit: "Edit",
|
|
8
|
+
web_fetch: "Fetch",
|
|
9
|
+
web_search: "Search",
|
|
10
|
+
glob: "Glob",
|
|
11
|
+
grep: "Grep",
|
|
12
|
+
list_dir: "List",
|
|
13
|
+
agent_task: "Task",
|
|
14
|
+
};
|
|
15
|
+
function compact(value, max = 140) {
|
|
16
|
+
if (value === undefined || value === null)
|
|
17
|
+
return "";
|
|
18
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
19
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
20
|
+
return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;
|
|
21
|
+
}
|
|
22
|
+
function toolPrimaryArg(input) {
|
|
23
|
+
if (typeof input === "string")
|
|
24
|
+
return compact(input, 80);
|
|
25
|
+
if (input === null || Array.isArray(input) || typeof input !== "object")
|
|
26
|
+
return "";
|
|
27
|
+
for (const key of ["command", "cmd", "path", "file", "file_path", "pattern", "query", "url", "prompt", "description"]) {
|
|
28
|
+
const value = input[key];
|
|
29
|
+
if (typeof value === "string" && value)
|
|
30
|
+
return compact(value, 80);
|
|
31
|
+
if (key === "command" && Array.isArray(value))
|
|
32
|
+
return compact(value.filter((item) => typeof item === "string").join(" "), 80);
|
|
33
|
+
}
|
|
34
|
+
return compact(input, 80);
|
|
35
|
+
}
|
|
3
36
|
function ReplyRow({ reply }) {
|
|
4
37
|
switch (reply.kind) {
|
|
5
38
|
case "assistant":
|
|
@@ -13,7 +46,10 @@ function ReplyRow({ reply }) {
|
|
|
13
46
|
case "skill":
|
|
14
47
|
return (_jsxs("div", { className: "nre-conv-skill", children: [_jsx("span", { className: "nre-conv-role", children: "skill loaded" }), " ", reply.skill] }));
|
|
15
48
|
case "tool":
|
|
16
|
-
|
|
49
|
+
const verb = (reply.tool ? TOOL_VERB[reply.tool] : undefined) ?? reply.name;
|
|
50
|
+
const arg = toolPrimaryArg(reply.input);
|
|
51
|
+
const preview = compact(reply.output);
|
|
52
|
+
return (_jsxs("details", { className: "nre-conv-tool", children: [_jsxs("summary", { children: [_jsx("span", { className: cx("nre-conv-tool-dot", reply.status ? `nre-conv-tool-${reply.status}` : "nre-conv-tool-pending") }), _jsx("span", { className: "nre-conv-tool-name", title: arg ? `${verb}(${arg})` : verb, children: arg ? `${verb}(${arg})` : verb }), _jsxs("span", { className: "nre-conv-tool-preview", children: [reply.status ?? "pending", preview ? ` · ${preview}` : ""] })] }), _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.input, null, 2) }), reply.output !== undefined ? _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.output, null, 2) }) : null] }));
|
|
17
53
|
case "subagent":
|
|
18
54
|
return (_jsxs("details", { className: "nre-conv-subagent", children: [_jsxs("summary", { children: ["subagent ", reply.name, reply.status ? ` · ${reply.status}` : ""] }), reply.output !== undefined ? _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.output, null, 2) }) : null] }));
|
|
19
55
|
case "input":
|
|
@@ -24,8 +60,12 @@ function ReplyRow({ reply }) {
|
|
|
24
60
|
return (_jsxs("details", { className: "nre-conv-raw", children: [_jsx("summary", { children: "unrecognized event" }), _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.raw, null, 2) })] }));
|
|
25
61
|
}
|
|
26
62
|
}
|
|
63
|
+
/** AttemptSource 复用同一份回复 renderer,把一轮执行挂回对应的 send 源码行。 */
|
|
64
|
+
export function ConversationReplies({ replies }) {
|
|
65
|
+
return (_jsx("div", { className: "nre-conv-replies", children: replies.map((reply, i) => (_jsx(ReplyRow, { reply: reply }, i))) }));
|
|
66
|
+
}
|
|
27
67
|
function RoundCard({ round, index }) {
|
|
28
|
-
return (_jsxs("div", { className: "nre-conv-round", children: [_jsxs("div", { className: "nre-conv-round-head", children: ["round ", index + 1, round.loc ? (_jsxs("span", { className: "nre-conv-round-loc", title: `${round.loc.file}:${round.loc.line}`, children: [round.loc.file.split("/").pop(), ":", round.loc.line] })) : null] }), round.sentText ? _jsx("div", { className: "nre-conv-sent", children: round.sentText }) : null, _jsx(
|
|
68
|
+
return (_jsxs("div", { className: "nre-conv-round", children: [_jsxs("div", { className: "nre-conv-round-head", children: ["round ", index + 1, round.loc ? (_jsxs("span", { className: "nre-conv-round-loc", title: `${round.loc.file}:${round.loc.line}`, children: [round.loc.file.split("/").pop(), ":", round.loc.line] })) : null] }), round.sentText ? _jsx("div", { className: "nre-conv-sent", children: round.sentText }) : null, _jsx(ConversationReplies, { replies: round.replies })] }));
|
|
29
69
|
}
|
|
30
70
|
export function AttemptConversation({ data, className, }) {
|
|
31
71
|
if (data === null)
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../shared.js";
|
|
3
|
+
import { ConversationReplies } from "./AttemptConversation.js";
|
|
4
|
+
const TS_HL_RE = /(\/\/[^\n]*)|(\/\*[^]*?\*\/)|(`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|\b(import|from|export|default|const|let|var|async|await|function|return|if|else|for|of|in|new|class|extends|typeof|void|true|false|null|undefined)\b|\b(\d[\d_.]*)\b|([A-Za-z_$][\w$]*)(?=\s*\()/g;
|
|
5
|
+
/** 逐行零依赖 TS 高亮;token class 是稳定的 web 展示语义,不改源码文本。 */
|
|
6
|
+
function highlightTs(line) {
|
|
7
|
+
const out = [];
|
|
8
|
+
let last = 0;
|
|
9
|
+
let i = 0;
|
|
10
|
+
let match;
|
|
11
|
+
TS_HL_RE.lastIndex = 0;
|
|
12
|
+
while ((match = TS_HL_RE.exec(line))) {
|
|
13
|
+
if (match.index > last)
|
|
14
|
+
out.push(line.slice(last, match.index));
|
|
15
|
+
const tokenClass = match[1] || match[2] ? "tok-comment" : match[3] ? "tok-str" : match[4] ? "tok-kw" : match[5] ? "tok-num" : "tok-fn";
|
|
16
|
+
out.push(_jsx("span", { className: tokenClass, children: match[0] }, i++));
|
|
17
|
+
last = match.index + match[0].length;
|
|
18
|
+
if (match[0].length === 0)
|
|
19
|
+
TS_HL_RE.lastIndex++;
|
|
20
|
+
}
|
|
21
|
+
if (last < line.length)
|
|
22
|
+
out.push(line.slice(last));
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
3
25
|
function assertTone(a) {
|
|
4
26
|
if (a.outcome === "unavailable")
|
|
5
27
|
return "na";
|
|
@@ -7,21 +29,69 @@ function assertTone(a) {
|
|
|
7
29
|
return "good";
|
|
8
30
|
return a.severity === "soft" ? "warn" : "bad";
|
|
9
31
|
}
|
|
32
|
+
function lineTone(line) {
|
|
33
|
+
if (line.assertions.length === 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (line.assertions.some((a) => assertTone(a) === "bad"))
|
|
36
|
+
return "bad";
|
|
37
|
+
if (line.assertions.some((a) => assertTone(a) === "warn"))
|
|
38
|
+
return "warn";
|
|
39
|
+
if (line.assertions.some((a) => assertTone(a) === "na"))
|
|
40
|
+
return "na";
|
|
41
|
+
return "good";
|
|
42
|
+
}
|
|
43
|
+
function formatDuration(ms) {
|
|
44
|
+
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
|
|
45
|
+
}
|
|
46
|
+
function thresholdScores(assertions) {
|
|
47
|
+
const scores = [];
|
|
48
|
+
for (const assertion of assertions) {
|
|
49
|
+
if (assertion.outcome !== "unavailable" && assertion.threshold !== undefined) {
|
|
50
|
+
scores.push(`${assertion.score}/${assertion.threshold}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return scores;
|
|
54
|
+
}
|
|
55
|
+
/** 行号位标记(与产品站示例卡同语言:图标顶替行号,不加独立状态列);内联 SVG,零图标依赖。 */
|
|
56
|
+
const MARK_ICONS = {
|
|
57
|
+
send: _jsx("path", { d: "M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z" }),
|
|
58
|
+
good: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "m9 12 2 2 4-4" })] })),
|
|
59
|
+
bad: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "m15 9-6 6" }), _jsx("path", { d: "m9 9 6 6" })] })),
|
|
60
|
+
warn: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }), _jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })] })),
|
|
61
|
+
na: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }), _jsx("path", { d: "M12 17h.01" })] })),
|
|
62
|
+
};
|
|
63
|
+
function LineNo({ line, tone, send }) {
|
|
64
|
+
const kind = tone ?? (send ? "send" : null);
|
|
65
|
+
if (kind === null)
|
|
66
|
+
return _jsx("span", { className: "nre-source-ln", children: line });
|
|
67
|
+
const label = kind === "bad" ? "failed" : kind === "warn" ? "soft failed" : kind === "good" ? "passed" : kind === "na" ? "unavailable" : "send";
|
|
68
|
+
return (_jsx("span", { className: "nre-source-ln nre-source-ln-mark", role: "img", "aria-label": label, title: label, children: _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: MARK_ICONS[kind] }) }));
|
|
69
|
+
}
|
|
70
|
+
function LineSummary({ line, tone }) {
|
|
71
|
+
const interactive = line.assertions.length > 0 || line.sends.length > 0 || line.turns.length > 0;
|
|
72
|
+
const scores = thresholdScores(line.assertions);
|
|
73
|
+
return (_jsxs("span", { className: "nre-source-line-summary", children: [_jsx(LineNo, { line: line.line, tone: tone, send: line.sends.length > 0 || line.turns.length > 0 }), _jsx("code", { className: "nre-source-text", children: highlightTs(line.text) }), _jsxs("span", { className: "nre-source-line-meta", children: [scores.length > 0 ? _jsx("span", { className: "nre-source-score-badge", children: scores.join(", ") }) : null, interactive ? _jsx("span", { className: "nre-source-chevron", children: "\u203A" }) : null] })] }));
|
|
74
|
+
}
|
|
75
|
+
function TurnDetail({ turn, showMeta = false, showSent = false }) {
|
|
76
|
+
return (_jsxs("div", { className: cx("nre-source-turn", `nre-source-turn-${turn.status}`), children: [showMeta ? (_jsxs("div", { className: "nre-source-turn-head", children: [_jsx("span", { children: turn.label }), _jsx("span", { children: turn.status }), turn.durationMs === undefined ? null : _jsx("span", { children: formatDuration(turn.durationMs) })] })) : null, showSent && turn.sentText ? _jsx("div", { className: "nre-conv-sent", children: turn.sentText }) : null, _jsx(ConversationReplies, { replies: turn.replies })] }));
|
|
77
|
+
}
|
|
78
|
+
function AssertionDetail({ assertion }) {
|
|
79
|
+
return (_jsxs("div", { className: `nre-assertion-row nre-tone-${assertTone(assertion)}`, children: [_jsxs("div", { className: "nre-source-assertion-head", children: [_jsx("span", { className: "nre-assertion-badge", children: assertion.outcome }), _jsx("span", { className: "nre-assertion-name", children: assertion.name }), assertion.outcome !== "unavailable" ? _jsx("span", { className: "nre-source-assertion-score", children: assertion.score }) : null] }), assertion.detail ? _jsx("div", { className: "nre-assertion-detail", children: assertion.detail }) : null, assertion.outcome === "unavailable" ? (_jsxs("div", { className: "nre-assertion-body", children: ["reason: ", assertion.reason] })) : assertion.expected !== undefined || assertion.received !== undefined || assertion.evidence !== undefined ? (_jsxs("div", { className: "nre-assertion-body", children: [assertion.expected !== undefined ? _jsxs("span", { children: ["expected: ", assertion.expected] }) : null, assertion.received !== undefined ? _jsxs("span", { children: ["received: ", assertion.received] }) : null, assertion.evidence !== undefined ? _jsxs("span", { children: ["evidence: ", assertion.evidence] }) : null] })) : null] }));
|
|
80
|
+
}
|
|
10
81
|
export function AttemptSource({ data, className }) {
|
|
11
82
|
if (data === null)
|
|
12
83
|
return null;
|
|
84
|
+
const firstAttentionLine = data.lines.find((line) => {
|
|
85
|
+
const tone = lineTone(line);
|
|
86
|
+
return tone === "bad" || tone === "warn" || tone === "na";
|
|
87
|
+
})?.line;
|
|
13
88
|
return (_jsxs("div", { className: cx("nre", "nre-attempt-source", className), children: [_jsx("div", { className: "nre-attempt-source-head", children: data.sourcePath }), _jsx("div", { className: "nre-attempt-source-lines", children: data.lines.map((line) => {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
? "na"
|
|
23
|
-
: "good"
|
|
24
|
-
: undefined;
|
|
25
|
-
return (_jsxs("details", { className: cx("nre-source-line", worstTone ? `nre-tone-${worstTone}` : undefined, hasSends ? "nre-source-line-send" : undefined), open: hasAsserts && (worstTone === "bad" || worstTone === "warn"), children: [_jsxs("summary", { children: [_jsx("span", { className: "nre-source-ln", children: line.line }), _jsx("code", { className: "nre-source-text", children: line.text }), hasSends ? _jsxs("span", { className: "nre-source-send-mark", children: ["\u21B3 ", line.sends.map((s) => s.label).join(", ")] }) : null] }), hasAsserts ? (_jsx("div", { className: "nre-source-line-detail", children: line.assertions.map((a, i) => (_jsxs("div", { className: `nre-assertion-row nre-tone-${assertTone(a)}`, children: [_jsx("span", { className: "nre-assertion-name", children: a.name }), a.outcome === "unavailable" ? (_jsx("span", { children: a.reason })) : (_jsxs(_Fragment, { children: [a.expected !== undefined ? _jsxs("span", { children: ["expected: ", a.expected] }) : null, a.received !== undefined ? _jsxs("span", { children: ["received: ", a.received] }) : null] }))] }, i))) })) : null] }, line.line));
|
|
26
|
-
}) }), data.unmapped.length > 0 ? (_jsxs("div", { className: "nre-attempt-source-unmapped", children: [_jsx("div", { className: "nre-attempt-source-unmapped-head", children: "Other assertions" }), data.unmapped.map((a, i) => (_jsxs("div", { className: `nre-assertion-row nre-tone-${assertTone(a)}`, children: [a.groupPath?.length ? `${a.groupPath.join(" > ")} · ` : "", a.name] }, i)))] })) : null] }));
|
|
89
|
+
const tone = lineTone(line);
|
|
90
|
+
const interactive = line.assertions.length > 0 || line.sends.length > 0 || line.turns.length > 0;
|
|
91
|
+
const lineClass = cx("nre-source-line", tone ? `nre-tone-${tone}` : undefined, line.sends.length > 0 || line.turns.length > 0 ? "nre-source-line-send" : undefined);
|
|
92
|
+
if (!interactive) {
|
|
93
|
+
return (_jsx("div", { className: lineClass, children: _jsx(LineSummary, { line: line, tone: tone }) }, line.line));
|
|
94
|
+
}
|
|
95
|
+
return (_jsxs("details", { className: lineClass, open: line.line === firstAttentionLine, children: [_jsx("summary", { children: _jsx(LineSummary, { line: line, tone: tone }) }), _jsxs("div", { className: "nre-source-line-detail", children: [line.turns.map((turn, i) => (_jsx(TurnDetail, { turn: turn }, `${turn.label}-${i}`))), line.assertions.map((assertion, i) => (_jsx(AssertionDetail, { assertion: assertion }, i)))] })] }, line.line));
|
|
96
|
+
}) }), data.unmapped.length > 0 ? (_jsxs("div", { className: "nre-attempt-source-unmapped", children: [_jsx("div", { className: "nre-attempt-source-unmapped-head", children: "Other assertions" }), data.unmapped.map((a, i) => (_jsxs("div", { className: `nre-assertion-row nre-tone-${assertTone(a)}`, children: [a.groupPath?.length ? `${a.groupPath.join(" > ")} · ` : "", a.name] }, i)))] })) : null, data.unlocatedTurns.length > 0 ? (_jsxs("div", { className: "nre-attempt-source-unlocated", children: [_jsx("div", { className: "nre-attempt-source-unmapped-head", children: "Other conversation" }), data.unlocatedTurns.map((turn, i) => (_jsx(TurnDetail, { turn: turn, showMeta: true, showSent: true }, `${turn.label}-${i}`)))] })) : null] }));
|
|
27
97
|
}
|
|
@@ -48,7 +48,50 @@ export function attemptSourceData(evidence) {
|
|
|
48
48
|
if (!evidence.capabilities.source || evidence.evalSource === null)
|
|
49
49
|
return null;
|
|
50
50
|
const { sourcePath, lines, unmapped, summary } = evidence.evalSource;
|
|
51
|
-
|
|
51
|
+
const projectedLines = lines.map((line) => ({
|
|
52
|
+
...line,
|
|
53
|
+
turns: line.sends.map((send) => ({
|
|
54
|
+
label: send.label,
|
|
55
|
+
status: send.status,
|
|
56
|
+
...(send.durationMs === undefined ? {} : { durationMs: send.durationMs }),
|
|
57
|
+
sentText: "",
|
|
58
|
+
replies: [],
|
|
59
|
+
})),
|
|
60
|
+
}));
|
|
61
|
+
const usedTurns = new Map();
|
|
62
|
+
const unlocatedTurns = [];
|
|
63
|
+
const conversation = attemptConversationData(evidence);
|
|
64
|
+
for (const [roundIndex, round] of (conversation?.rounds ?? []).entries()) {
|
|
65
|
+
const status = round.replies.some((reply) => reply.kind === "error" ||
|
|
66
|
+
((reply.kind === "tool" || reply.kind === "subagent") && reply.status === "failed"))
|
|
67
|
+
? "failed"
|
|
68
|
+
: round.replies.some((reply) => reply.kind === "input")
|
|
69
|
+
? "waiting"
|
|
70
|
+
: "completed";
|
|
71
|
+
const fallback = {
|
|
72
|
+
label: `t${roundIndex + 1}`,
|
|
73
|
+
status,
|
|
74
|
+
sentText: round.sentText,
|
|
75
|
+
replies: round.replies,
|
|
76
|
+
};
|
|
77
|
+
const loc = round.loc;
|
|
78
|
+
if (!loc || loc.file !== sourcePath || loc.line < 1 || loc.line > projectedLines.length) {
|
|
79
|
+
unlocatedTurns.push(fallback);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const line = projectedLines[loc.line - 1];
|
|
83
|
+
const turnIndex = usedTurns.get(loc.line) ?? 0;
|
|
84
|
+
usedTurns.set(loc.line, turnIndex + 1);
|
|
85
|
+
const annotated = line.turns[turnIndex];
|
|
86
|
+
if (annotated) {
|
|
87
|
+
annotated.sentText = round.sentText;
|
|
88
|
+
annotated.replies = round.replies;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
line.turns.push(fallback);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { locator: evidence.locator, sourcePath, lines: projectedLines, unmapped, unlocatedTurns, summary };
|
|
52
95
|
}
|
|
53
96
|
// ───────────────────────── AttemptFixPrompt ─────────────────────────
|
|
54
97
|
/** 单条 attempt 版的批量修复 prompt(与 CopyFixPrompt 的多条版本同一份步骤文案)。 */
|
|
@@ -85,6 +85,10 @@ export function attemptSourceText(data, ctx) {
|
|
|
85
85
|
const headerParts = [`${data.sourcePath} · ${data.summary.annotatedLines}/${data.summary.totalLines} lines annotated`];
|
|
86
86
|
if (command)
|
|
87
87
|
headerParts.push(command);
|
|
88
|
+
const hasConversation = data.unlocatedTurns.length > 0 || data.lines.some((line) => line.turns.length > 0);
|
|
89
|
+
const executionCommand = hasConversation ? evidenceCommand(ctx, data.locator, "--execution") : null;
|
|
90
|
+
if (executionCommand)
|
|
91
|
+
headerParts.push(executionCommand);
|
|
88
92
|
// 源码锚由 assertionLine 自己按 a.loc 拼(与 AttemptAssertions 共用同一份逻辑);这里只负责
|
|
89
93
|
// 挑出非 passed 的条目,不重复算锚点。
|
|
90
94
|
const failed = data.lines.flatMap((line) => line.assertions.filter((a) => a.outcome !== "passed"));
|
|
@@ -34,5 +34,5 @@ export declare function validateDiffData(data: unknown): string | null;
|
|
|
34
34
|
export declare const AttemptDiff: ReportComponent<AttemptSectionProps<AttemptDiffData>>;
|
|
35
35
|
/** source / assertions fallback:有 source 放 AttemptSource,否则放 AttemptAssertions。 */
|
|
36
36
|
export declare const AttemptAssessment: ReportComponent<Record<string, never>>;
|
|
37
|
-
/**
|
|
38
|
-
export declare const AttemptDetail: ReportComponent<
|
|
37
|
+
/** 内建排列顺序;有 source 时回复已按 loc 展开在 AttemptSource 行内,不再重复一份 round 卡。 */
|
|
38
|
+
export declare const AttemptDetail: ReportComponent<Record<string, never>>;
|
|
@@ -205,7 +205,7 @@ export const AttemptAssertions = makeAttemptComponent({
|
|
|
205
205
|
/** AnnotatedSourceLine(src/results/annotated-source.ts):一行源码 + 映射到这一行的断言 / send 标注。 */
|
|
206
206
|
function annotatedSourceLineProblem(value, path) {
|
|
207
207
|
if (!isObject(value))
|
|
208
|
-
return `"${path}" must be an
|
|
208
|
+
return `"${path}" must be an AttemptSourceLineData { line, text, assertions, sends, turns }`;
|
|
209
209
|
if (typeof value.line !== "number")
|
|
210
210
|
return `"${path}.line" must be a number`;
|
|
211
211
|
if (typeof value.text !== "string")
|
|
@@ -215,7 +215,21 @@ function annotatedSourceLineProblem(value, path) {
|
|
|
215
215
|
return assertionsProblem;
|
|
216
216
|
if (!Array.isArray(value.sends))
|
|
217
217
|
return `"${path}.sends" must be an array`;
|
|
218
|
-
return
|
|
218
|
+
return arrayProblem(value.turns, `${path}.turns`, sourceTurnProblem);
|
|
219
|
+
}
|
|
220
|
+
function sourceTurnProblem(value, path) {
|
|
221
|
+
if (!isObject(value))
|
|
222
|
+
return `"${path}" must be an AttemptSourceTurn`;
|
|
223
|
+
if (typeof value.label !== "string")
|
|
224
|
+
return `"${path}.label" must be a string`;
|
|
225
|
+
if (value.status !== "completed" && value.status !== "failed" && value.status !== "waiting") {
|
|
226
|
+
return `"${path}.status" must be "completed", "failed", or "waiting"`;
|
|
227
|
+
}
|
|
228
|
+
if (value.durationMs !== undefined && typeof value.durationMs !== "number")
|
|
229
|
+
return `"${path}.durationMs" must be a number`;
|
|
230
|
+
if (typeof value.sentText !== "string")
|
|
231
|
+
return `"${path}.sentText" must be a string`;
|
|
232
|
+
return arrayProblem(value.replies, `${path}.replies`, conversationReplyProblem);
|
|
219
233
|
}
|
|
220
234
|
/** AnnotatedEvalSourceSummary(src/results/annotated-source.ts):全是计数字段。 */
|
|
221
235
|
function sourceSummaryProblem(value, path) {
|
|
@@ -250,6 +264,9 @@ export function validateSourceData(data) {
|
|
|
250
264
|
const unmappedProblem = arrayProblem(data.unmapped, "unmapped", assertionResultProblem);
|
|
251
265
|
if (unmappedProblem !== null)
|
|
252
266
|
return unmappedProblem;
|
|
267
|
+
const turnsProblem = arrayProblem(data.unlocatedTurns, "unlocatedTurns", sourceTurnProblem);
|
|
268
|
+
if (turnsProblem !== null)
|
|
269
|
+
return turnsProblem;
|
|
253
270
|
return sourceSummaryProblem(data.summary, "summary");
|
|
254
271
|
}
|
|
255
272
|
export const AttemptSource = makeAttemptComponent({
|
|
@@ -522,6 +539,9 @@ export const AttemptAssessment = defineComponent((_props, ctx) => {
|
|
|
522
539
|
return (_jsxs(Col, { children: [_jsx(AttemptError, {}), ctx.page.evidence.capabilities.source ? _jsx(AttemptSource, {}) : _jsx(AttemptAssertions, {})] }));
|
|
523
540
|
});
|
|
524
541
|
AttemptAssessment.displayName = "AttemptAssessment";
|
|
525
|
-
/**
|
|
526
|
-
export const AttemptDetail = defineComponent((
|
|
542
|
+
/** 内建排列顺序;有 source 时回复已按 loc 展开在 AttemptSource 行内,不再重复一份 round 卡。 */
|
|
543
|
+
export const AttemptDetail = defineComponent((_props, ctx) => {
|
|
544
|
+
const conversationLivesInSource = ctx.page.input === "attempt" && ctx.page.evidence.capabilities.source && ctx.page.evidence.evalSource !== null;
|
|
545
|
+
return (_jsxs(Col, { children: [_jsx(AttemptSummary, {}), _jsx(AttemptAssessment, {}), _jsx(AttemptFixPrompt, {}), _jsx(AttemptTimeline, {}), _jsx(AttemptDiagnostics, {}), _jsx(AttemptUsage, {}), conversationLivesInSource ? null : _jsx(AttemptConversation, {}), _jsx(AttemptTrace, {}), _jsx(AttemptDiff, {})] }));
|
|
546
|
+
});
|
|
527
547
|
AttemptDetail.displayName = "AttemptDetail";
|
|
@@ -2,11 +2,10 @@ import type { ReactElement } from "react";
|
|
|
2
2
|
import type { AttemptLocator } from "../../../results/locator.ts";
|
|
3
3
|
import type { ExperimentListItem } from "../../model/types.ts";
|
|
4
4
|
import { type ReportLocale } from "../../model/locale.ts";
|
|
5
|
-
export declare function ExperimentList({ data, attemptHref, filter, className, locale,
|
|
5
|
+
export declare function ExperimentList({ data, attemptHref, filter, className, locale, }: {
|
|
6
6
|
data: readonly ExperimentListItem[];
|
|
7
7
|
attemptHref?: (locator: AttemptLocator) => string;
|
|
8
8
|
filter?: boolean;
|
|
9
9
|
className?: string;
|
|
10
10
|
locale?: ReportLocale;
|
|
11
|
-
relativeTo?: string;
|
|
12
11
|
}): ReactElement;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { shortestUniqueLabels } from "../../model/format.js";
|
|
3
3
|
import { DEFAULT_REPORT_LOCALE, localeText } from "../../model/locale.js";
|
|
4
4
|
import { AttemptLocatorBadge, failureSummaryText } from "./AttemptList.js";
|
|
5
5
|
import { MetricCellView } from "../cell.js";
|
|
@@ -47,10 +47,11 @@ function Flags({ flags, locale }) {
|
|
|
47
47
|
return null;
|
|
48
48
|
return (_jsxs("div", { className: "nre-experiment-flags", children: [_jsx("span", { children: localeText(locale, "experimentList.flags") }), Object.entries(flags).map(([key, value]) => (_jsxs("b", { children: [key, "=", typeof value === "string" ? value : JSON.stringify(value)] }, key)))] }));
|
|
49
49
|
}
|
|
50
|
-
function ExperimentRow({ item, attemptHref, locale,
|
|
51
|
-
return (_jsxs("details", { className: "nre-experiment-entry", children: [_jsxs("summary", { className: "nre-experiment-summary", children: [_jsxs("span", { className: "nre-experiment-name", "data-sort-value": item.experimentId, children: [_jsx("b", { className: cx("nre-experiment-id", "nre-key"), children:
|
|
50
|
+
function ExperimentRow({ item, label, attemptHref, locale, }) {
|
|
51
|
+
return (_jsxs("details", { className: "nre-experiment-entry", children: [_jsxs("summary", { className: "nre-experiment-summary", children: [_jsxs("span", { className: "nre-experiment-name", "data-sort-value": item.experimentId, children: [_jsx("b", { className: cx("nre-experiment-id", "nre-key"), children: label }), _jsxs("small", { children: [localeText(locale, "overview.evalsCount", { n: item.evals }), item.attempts > item.evals ? ` · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })}` : "", ` · ${formatDate(item.lastRunAt, locale)}`] })] }), _jsx("span", { "data-sort-value": item.model ?? "", children: item.model ?? localeText(locale, "experimentList.defaultModel") }), _jsx("span", { "data-sort-value": item.agent, children: _jsx("span", { className: cx("nre-experiment-agent", "nre-key", colorClassForKey(item.agent)), children: item.agent }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.durationMs.value ?? "", children: _jsx(MetricCellView, { cell: item.durationMs, locale: locale }) }), _jsx("span", { className: cx("nre-num", passRateTone(item.endToEndPassRate.value)), "data-sort-value": item.endToEndPassRate.value ?? "", children: _jsx(MetricCellView, { cell: item.endToEndPassRate, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.tokens.value ?? "", children: _jsx(MetricCellView, { cell: item.tokens, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.costUSD.value ?? "", children: _jsx(MetricCellView, { cell: item.costUSD, locale: locale }) }), _jsx("span", { "data-sort-value": item.evalVerdicts.passed, children: _jsx(VerdictSummary, { item: item, locale: locale }) })] }), _jsxs("div", { className: "nre-experiment-detail", children: [_jsx(Flags, { flags: item.flags, locale: locale }), _jsx("ul", { className: "nre-experiment-evals", children: item.evalRows.map((row) => (_jsx(EvalAttempts, { row: row, attemptHref: attemptHref, locale: locale }, row.evalId))) })] })] }));
|
|
52
52
|
}
|
|
53
|
-
export function ExperimentList({ data, attemptHref, filter = false, className, locale = DEFAULT_REPORT_LOCALE,
|
|
53
|
+
export function ExperimentList({ data, attemptHref, filter = false, className, locale = DEFAULT_REPORT_LOCALE, }) {
|
|
54
|
+
const experimentLabels = shortestUniqueLabels(data.map((item) => item.experimentId));
|
|
54
55
|
const labels = [
|
|
55
56
|
localeText(locale, "experimentList.experiment"),
|
|
56
57
|
localeText(locale, "table.model"),
|
|
@@ -61,6 +62,6 @@ export function ExperimentList({ data, attemptHref, filter = false, className, l
|
|
|
61
62
|
localeText(locale, "experimentList.cost"),
|
|
62
63
|
localeText(locale, "experimentList.result"),
|
|
63
64
|
];
|
|
64
|
-
const board = (_jsxs("div", { className: "nre-experiment-table", children: [_jsx("div", { className: "nre-experiment-head", children: labels.map((label, index) => (_jsxs("button", { type: "button", "data-nre-experiment-sort": index, className: cx(index >= 3 && index <= 6 && "nre-num-head", index === 4 && "nre-sort-desc"), title: index === 4 ? localeText(locale, "experimentList.passRateDescription") : undefined, children: [_jsx("span", { className: "nre-sort-label", children: label }), _jsx("span", { className: "nre-sort-icon", "aria-hidden": "true" })] }, label))) }), data.length === 0 && _jsx("p", { className: "nre-experiment-list-empty", children: localeText(locale, "attemptList.empty") }), data.map((item) => (_jsx(ExperimentRow, { item: item, attemptHref: attemptHref, locale: locale
|
|
65
|
+
const board = (_jsxs("div", { className: "nre-experiment-table", children: [_jsx("div", { className: "nre-experiment-head", children: labels.map((label, index) => (_jsxs("button", { type: "button", "data-nre-experiment-sort": index, className: cx(index >= 3 && index <= 6 && "nre-num-head", index === 4 && "nre-sort-desc"), title: index === 4 ? localeText(locale, "experimentList.passRateDescription") : undefined, children: [_jsx("span", { className: "nre-sort-label", children: label }), _jsx("span", { className: "nre-sort-icon", "aria-hidden": "true" })] }, label))) }), data.length === 0 && _jsx("p", { className: "nre-experiment-list-empty", children: localeText(locale, "attemptList.empty") }), data.map((item) => (_jsx(ExperimentRow, { item: item, label: experimentLabels.get(item.experimentId) ?? item.experimentId, attemptHref: attemptHref, locale: locale }, item.experimentId)))] }));
|
|
65
66
|
return (_jsxs("div", { className: cx("nre", "nre-experiment-list", filter && "nre-experiment-table-wrap", className), children: [filter && (_jsx("input", { className: "nre-filter", "data-nre-experiment-filter": "", type: "search", placeholder: localeText(locale, "experimentList.filterPlaceholder") })), board] }));
|
|
66
67
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AttemptListItem, EvalListItem, ExperimentListItem } from "../../model/types.ts";
|
|
2
2
|
import type { TextContext } from "../../definition/tree.ts";
|
|
3
|
-
export declare function experimentListText(items: readonly ExperimentListItem[], ctx: TextContext
|
|
3
|
+
export declare function experimentListText(items: readonly ExperimentListItem[], ctx: TextContext): string;
|
|
4
4
|
export declare function evalListText(items: readonly EvalListItem[], ctx: TextContext): string;
|
|
5
5
|
export declare function attemptListText(items: readonly AttemptListItem[], total: number | undefined, ctx: TextContext): string;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// locator,中间不留空格)。ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的
|
|
4
4
|
// 摘要,不重复整段 niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼
|
|
5
5
|
// `niceeval show <locator>`。零 react、零 IO、纯同步。
|
|
6
|
-
import {
|
|
6
|
+
import { fitFailureSummary, formatDurationMs, formatUSD, shortestUniqueLabels, verdictMark, } from "../../model/format.js";
|
|
7
7
|
import { countText, localeText } from "../../model/locale.js";
|
|
8
8
|
import { stringWidth, wrapDisplay } from "../../model/text-layout.js";
|
|
9
9
|
import { renderTableText } from "../../definition/table-text.js";
|
|
@@ -29,7 +29,7 @@ function attemptReasonText(item, locale, maxChars) {
|
|
|
29
29
|
return fitFailureSummary(withMore, Math.max(24, maxChars));
|
|
30
30
|
}
|
|
31
31
|
// ── ExperimentList ──
|
|
32
|
-
function experimentSummaryTable(items, ctx,
|
|
32
|
+
function experimentSummaryTable(items, ctx, labels) {
|
|
33
33
|
const locale = ctx.locale;
|
|
34
34
|
const compact = ctx.width < 100;
|
|
35
35
|
const columns = [
|
|
@@ -45,7 +45,7 @@ function experimentSummaryTable(items, ctx, relativeTo) {
|
|
|
45
45
|
const rows = items.map((item) => ({
|
|
46
46
|
key: item.experimentId,
|
|
47
47
|
cells: {
|
|
48
|
-
experiment:
|
|
48
|
+
experiment: labels.get(item.experimentId) ?? item.experimentId,
|
|
49
49
|
model: item.model ?? localeText(locale, "experimentList.defaultModel"),
|
|
50
50
|
agent: item.agent,
|
|
51
51
|
duration: cellText(item.durationMs, locale),
|
|
@@ -55,10 +55,10 @@ function experimentSummaryTable(items, ctx, relativeTo) {
|
|
|
55
55
|
cost: cellText(item.costUSD, locale),
|
|
56
56
|
},
|
|
57
57
|
}));
|
|
58
|
-
const metadata = items.flatMap((item) => wrapDisplay(`${
|
|
58
|
+
const metadata = items.flatMap((item) => wrapDisplay(`${labels.get(item.experimentId) ?? item.experimentId}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`, Math.max(8, ctx.width - 2)).map((line) => ` ${line}`));
|
|
59
59
|
return [renderTableText({ columns: columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
|
|
60
60
|
}
|
|
61
|
-
function experimentDetailTable(item, ctx,
|
|
61
|
+
function experimentDetailTable(item, ctx, label) {
|
|
62
62
|
const locale = ctx.locale;
|
|
63
63
|
const columns = [
|
|
64
64
|
{ key: "status", header: localeText(locale, "experimentList.status") },
|
|
@@ -104,19 +104,20 @@ function experimentDetailTable(item, ctx, relativeTo) {
|
|
|
104
104
|
.join(" · ")}`
|
|
105
105
|
: undefined;
|
|
106
106
|
return [
|
|
107
|
-
|
|
107
|
+
label,
|
|
108
108
|
flags,
|
|
109
109
|
renderTableText({ columns: columns, rows, locale }, ctx),
|
|
110
110
|
]
|
|
111
111
|
.filter(Boolean)
|
|
112
112
|
.join("\n");
|
|
113
113
|
}
|
|
114
|
-
export function experimentListText(items, ctx
|
|
114
|
+
export function experimentListText(items, ctx) {
|
|
115
115
|
if (items.length === 0)
|
|
116
116
|
return localeText(ctx.locale, "attemptList.empty");
|
|
117
|
+
const labels = shortestUniqueLabels(items.map((item) => item.experimentId));
|
|
117
118
|
return [
|
|
118
|
-
experimentSummaryTable(items, ctx,
|
|
119
|
-
...items.map((item) => experimentDetailTable(item, ctx,
|
|
119
|
+
experimentSummaryTable(items, ctx, labels),
|
|
120
|
+
...items.map((item) => experimentDetailTable(item, ctx, labels.get(item.experimentId) ?? item.experimentId)),
|
|
120
121
|
].join("\n\n");
|
|
121
122
|
}
|
|
122
123
|
// ── EvalList ──
|
|
@@ -12,14 +12,12 @@ interface EntityListChrome extends ChromeProps {
|
|
|
12
12
|
export type ExperimentListProps = DataProps<readonly ExperimentListItem[], Record<never, never>, EntityListChrome & {
|
|
13
13
|
/** web 面在比较表前显示实验过滤框;text 面忽略。 */
|
|
14
14
|
filter?: boolean;
|
|
15
|
-
/**
|
|
16
|
-
* 可选父路径:两面的行标签去掉与它相同的前缀,只显示 experiment id 末段。自定义报告
|
|
17
|
-
* 显式传入使用;默认 `ExperimentComparison` 不传,完整 id 始终可见。完整 id 仍是
|
|
18
|
-
* 排序 / 着色 / 过滤 / 折叠的键。
|
|
19
|
-
*/
|
|
20
|
-
relativeTo?: string;
|
|
21
15
|
}>;
|
|
22
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* 实验列表:每项一个 experiment,固定八列比较表 + 展开到 Eval / Attempt。行标签是
|
|
18
|
+
* experiment id 在当前列表里的最短唯一后缀(与 MetricScatter 点标签同一算法,重名逐步
|
|
19
|
+
* 加长到能区分为止);完整 id 不受影响,仍是排序 / 过滤 / 折叠的键。
|
|
20
|
+
*/
|
|
23
21
|
export declare const ExperimentList: ReportComponent<ExperimentListProps>;
|
|
24
22
|
export type EvalListProps = DataProps<readonly EvalListItem[], Record<never, never>, EntityListChrome>;
|
|
25
23
|
/** Eval 列表:每项一个 experimentId + evalId,展开到这道题的 Attempt。 */
|
|
@@ -103,7 +103,11 @@ export const validateEvalListData = (data) => arrayProblem(data, "data", (item,
|
|
|
103
103
|
return arrayProblem(item.attempts, `${path}.attempts`, attemptListItemProblem);
|
|
104
104
|
});
|
|
105
105
|
export const validateAttemptListData = (data) => arrayProblem(data, "data", attemptListItemProblem);
|
|
106
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* 实验列表:每项一个 experiment,固定八列比较表 + 展开到 Eval / Attempt。行标签是
|
|
108
|
+
* experiment id 在当前列表里的最短唯一后缀(与 MetricScatter 点标签同一算法,重名逐步
|
|
109
|
+
* 加长到能区分为止);完整 id 不受影响,仍是排序 / 过滤 / 折叠的键。
|
|
110
|
+
*/
|
|
107
111
|
export const ExperimentList = makeDataComponent({
|
|
108
112
|
name: "ExperimentList",
|
|
109
113
|
dataFnName: "experimentListData",
|
|
@@ -111,8 +115,8 @@ export const ExperimentList = makeDataComponent({
|
|
|
111
115
|
dataFn: (input) => experimentListData(input),
|
|
112
116
|
specKeys: [],
|
|
113
117
|
validate: validateExperimentListData,
|
|
114
|
-
web: (props, ctx) => (_jsx(ExperimentListWeb, { data: props.data, filter: props.filter,
|
|
115
|
-
text: (props, ctx) => experimentListText(props.data, ctx
|
|
118
|
+
web: (props, ctx) => (_jsx(ExperimentListWeb, { data: props.data, filter: props.filter, locale: props.locale ?? ctx.locale, attemptHref: hrefOf(props, ctx), className: props.className })),
|
|
119
|
+
text: (props, ctx) => experimentListText(props.data, ctx),
|
|
116
120
|
});
|
|
117
121
|
/** Eval 列表:每项一个 experimentId + evalId,展开到这道题的 Attempt。 */
|
|
118
122
|
export const EvalList = makeDataComponent({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { formatMetricValue } from "../../model/format.js";
|
|
2
|
+
import { formatMetricValue, shortestUniqueLabels } from "../../model/format.js";
|
|
3
3
|
import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveLocalizedText, resolveMetricLabel } from "../../model/locale.js";
|
|
4
4
|
import { niceTicks, placePointLabels } from "./chart-math.js";
|
|
5
5
|
import { colorIndicesForKeys } from "../../assets/colors.js";
|
|
@@ -9,42 +9,6 @@ const HEIGHT = 400;
|
|
|
9
9
|
const MARGIN = { top: 28, right: 32, bottom: 48, left: 64 };
|
|
10
10
|
const PLOT_W = WIDTH - MARGIN.left - MARGIN.right;
|
|
11
11
|
const PLOT_H = HEIGHT - MARGIN.top - MARGIN.bottom;
|
|
12
|
-
/**
|
|
13
|
-
* 点的直接标签:末段在当前 data 中唯一才缩成末段;重名时逐步加长为能区分它们的最短
|
|
14
|
-
* 路径后缀(完整 id 与两轴值仍进 <title>)。
|
|
15
|
-
*/
|
|
16
|
-
function pointLabels(keys) {
|
|
17
|
-
const segsOf = (key) => key.split("/").filter(Boolean);
|
|
18
|
-
const depth = new Map(keys.map((key) => [key, 1]));
|
|
19
|
-
for (;;) {
|
|
20
|
-
const byLabel = new Map();
|
|
21
|
-
for (const key of keys) {
|
|
22
|
-
const segs = segsOf(key);
|
|
23
|
-
const label = segs.slice(-Math.min(depth.get(key), segs.length)).join("/") || key;
|
|
24
|
-
byLabel.set(label, [...(byLabel.get(label) ?? []), key]);
|
|
25
|
-
}
|
|
26
|
-
let grew = false;
|
|
27
|
-
for (const group of byLabel.values()) {
|
|
28
|
-
if (group.length < 2)
|
|
29
|
-
continue;
|
|
30
|
-
for (const key of group) {
|
|
31
|
-
const segs = segsOf(key);
|
|
32
|
-
if (depth.get(key) < segs.length) {
|
|
33
|
-
depth.set(key, depth.get(key) + 1);
|
|
34
|
-
grew = true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (!grew) {
|
|
39
|
-
const out = new Map();
|
|
40
|
-
for (const key of keys) {
|
|
41
|
-
const segs = segsOf(key);
|
|
42
|
-
out.set(key, segs.slice(-Math.min(depth.get(key), segs.length)).join("/") || key);
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
12
|
/**
|
|
49
13
|
* 一根轴:niceTicks 撑出整齐的值域,值 → 像素做线性映射。轴方向跟随指标的 `better`:
|
|
50
14
|
* `better: "lower"` 的轴反向渲染(值大的一端在左 / 下),「更好」因此恒指向右与上;
|
|
@@ -78,7 +42,7 @@ export function MetricScatter({ data, connect = false, pointHref, className, loc
|
|
|
78
42
|
const xScale = axisScale(drawableRows.map((r) => r.x.value), MARGIN.left, MARGIN.left + PLOT_W, data.x.better === "lower");
|
|
79
43
|
// y 像素轴向下增长:正向 = 高值在上 → 映射到 [bottom, top];lower 反向 = 高值在下。
|
|
80
44
|
const yScale = axisScale(drawableRows.map((r) => r.y.value), MARGIN.top + PLOT_H, MARGIN.top, data.y.better === "lower");
|
|
81
|
-
const labelByKey =
|
|
45
|
+
const labelByKey = shortestUniqueLabels(drawableRows.map((r) => r.key));
|
|
82
46
|
// 方向提示恒为「越靠右上越好」,仅当两轴都声明 better 时显示——任一轴未声明,
|
|
83
47
|
// 组件不猜「更好」朝哪边,整图无提示。
|
|
84
48
|
const showBetterHint = data.x.better !== undefined && data.y.better !== undefined;
|
package/dist/report/index.d.ts
CHANGED
|
@@ -30,5 +30,5 @@ export type { DeltaTableOptions, MetricLineOptions, MetricMatrixOptions, MetricS
|
|
|
30
30
|
export { copyFixPromptData, heroData, scopeWarningsData, traceWaterfallData, } from "./components/site-components/compute.ts";
|
|
31
31
|
export { attemptAssertionsData, attemptConversationData, attemptDiagnosticsData, attemptDiffData, attemptErrorData, attemptFixPromptData, attemptSourceData, attemptSummaryData, attemptTimelineData, attemptTraceData, attemptUsageData, } from "./components/attempt-detail/compute.ts";
|
|
32
32
|
export type { Aggregator, AttemptListItem, AttemptLocator, BuiltInDimension, CopyFixPromptData, CustomDimension, DeltaData, DeltaPair, DimensionInput, DimensionOptions, DimensionRef, EvalListItem, ExperimentListEvalRow, ExperimentListItem, FlagPairs, HeroData, LineData, MatrixData, Metric, MetricAggregate, MetricCell, MetricColumn, NumericAxis, NumericAxisOptions, NumericRunConfigAxisOptions, ReportInput, RunConfigKey, ScatterData, ScopeSummaryData, ScopeWarning, ScoreboardData, SeriesInput, TableData, TraceSpanSummary, TraceWaterfallRow, VerdictTally, } from "./model/types.ts";
|
|
33
|
-
export type { AttemptAssertionsData, AttemptConversationData, AttemptConversationReply, AttemptConversationRound, AttemptDiagnosticsData, AttemptDiffData, AttemptDiffFileEntry, AttemptErrorData, AttemptFixPromptData, AttemptSourceData, AttemptSummaryData, AttemptTimelineData, AttemptTraceData, AttemptUsageData, } from "./model/types.ts";
|
|
33
|
+
export type { AttemptAssertionsData, AttemptConversationData, AttemptConversationReply, AttemptConversationRound, AttemptDiagnosticsData, AttemptDiffData, AttemptDiffFileEntry, AttemptErrorData, AttemptFixPromptData, AttemptSourceData, AttemptSourceLineData, AttemptSourceTurn, AttemptSummaryData, AttemptTimelineData, AttemptTraceData, AttemptUsageData, } from "./model/types.ts";
|
|
34
34
|
export type { AttemptHandle, Results, Scope, Snapshot } from "../results/types.ts";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Verdict } from "../../types.ts";
|
|
2
2
|
import { type LocalizedText, type ReportLocale } from "./locale.ts";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 一组 id 的显示名:每个 id 缩成在这组里唯一的最短路径后缀,重名逐步加长到能区分为止
|
|
5
|
+
* (与 `MetricScatter` 点标签同一算法,两处共用本函数以保证同一份 experiment id 在散点和
|
|
6
|
+
* 列表里缩成同一个显示名)。单个 id、或所有 id 深度不同时也照常缩到各自的最短唯一后缀。
|
|
7
|
+
* 完整 id 不受影响,调用方仍用它做排序 / 过滤 / 折叠的身份键,这里只产出显示名。
|
|
8
8
|
*/
|
|
9
|
-
export declare function
|
|
9
|
+
export declare function shortestUniqueLabels(ids: readonly string[]): Map<string, string>;
|
|
10
10
|
export declare function formatMetricValue(value: number, unit?: string): string;
|
|
11
11
|
/**
|
|
12
12
|
* MetricCell.display 的生成:为官方生成面覆盖的每个 locale(DISPLAY_LOCALES)各生成一份;
|