niceeval 0.1.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.
Files changed (94) hide show
  1. package/README.md +164 -0
  2. package/README.zh.md +160 -0
  3. package/bin/niceeval.js +11 -0
  4. package/package.json +96 -0
  5. package/src/agents/bub.ts +223 -0
  6. package/src/agents/builtin.ts +6 -0
  7. package/src/agents/claude-code.ts +96 -0
  8. package/src/agents/codex.ts +122 -0
  9. package/src/agents/index.ts +25 -0
  10. package/src/agents/shared.ts +145 -0
  11. package/src/cli.ts +421 -0
  12. package/src/context/context.test.ts +96 -0
  13. package/src/context/context.ts +427 -0
  14. package/src/context/control-flow.ts +27 -0
  15. package/src/context/session.ts +124 -0
  16. package/src/define.ts +112 -0
  17. package/src/expect/index.ts +243 -0
  18. package/src/i18n/en.ts +131 -0
  19. package/src/i18n/index.ts +39 -0
  20. package/src/i18n/zh-CN.ts +132 -0
  21. package/src/index.ts +39 -0
  22. package/src/loaders/index.ts +56 -0
  23. package/src/o11y/cost.ts +57 -0
  24. package/src/o11y/derive.ts +304 -0
  25. package/src/o11y/otlp/canonical.ts +112 -0
  26. package/src/o11y/otlp/mappers/bub.ts +30 -0
  27. package/src/o11y/otlp/mappers/codex.ts +38 -0
  28. package/src/o11y/otlp/mappers/index.ts +25 -0
  29. package/src/o11y/otlp/parse.ts +330 -0
  30. package/src/o11y/otlp/receiver.ts +100 -0
  31. package/src/o11y/otlp/sandbox-receiver.ts +113 -0
  32. package/src/o11y/otlp/select.ts +82 -0
  33. package/src/o11y/parsers/bub.ts +240 -0
  34. package/src/o11y/parsers/claude-code.ts +270 -0
  35. package/src/o11y/parsers/codex.ts +480 -0
  36. package/src/o11y/parsers/index.ts +37 -0
  37. package/src/o11y/prices.json +9873 -0
  38. package/src/runner/discover.ts +77 -0
  39. package/src/runner/reporters/artifacts.ts +81 -0
  40. package/src/runner/reporters/console.ts +76 -0
  41. package/src/runner/reporters/index.ts +5 -0
  42. package/src/runner/reporters/json.ts +52 -0
  43. package/src/runner/reporters/live.ts +178 -0
  44. package/src/runner/reporters/table.ts +328 -0
  45. package/src/runner/run.ts +860 -0
  46. package/src/runner/sandbox-prep.ts +91 -0
  47. package/src/sandbox/checkpoint.ts +39 -0
  48. package/src/sandbox/docker.ts +539 -0
  49. package/src/sandbox/e2b.ts +203 -0
  50. package/src/sandbox/index.ts +25 -0
  51. package/src/sandbox/registry.ts +60 -0
  52. package/src/sandbox/resolve.ts +131 -0
  53. package/src/sandbox/source-files.ts +30 -0
  54. package/src/sandbox/vercel.ts +236 -0
  55. package/src/scoring/collector.ts +113 -0
  56. package/src/scoring/judge.ts +236 -0
  57. package/src/scoring/scoped.ts +289 -0
  58. package/src/scoring/verdict.ts +20 -0
  59. package/src/source-loc.ts +53 -0
  60. package/src/types.ts +913 -0
  61. package/src/util.test.ts +31 -0
  62. package/src/util.ts +50 -0
  63. package/src/view/app/App.tsx +189 -0
  64. package/src/view/app/components/AttemptModal.tsx +66 -0
  65. package/src/view/app/components/CodeView.tsx +272 -0
  66. package/src/view/app/components/CopyControls.tsx +89 -0
  67. package/src/view/app/components/ExperimentTable.tsx +266 -0
  68. package/src/view/app/components/GroupSelector.tsx +61 -0
  69. package/src/view/app/components/LazyArtifact.tsx +50 -0
  70. package/src/view/app/components/Trace.tsx +100 -0
  71. package/src/view/app/components/Transcript.tsx +130 -0
  72. package/src/view/app/components/primitives.tsx +43 -0
  73. package/src/view/app/components/ui/badge.tsx +21 -0
  74. package/src/view/app/components/ui/dialog.tsx +34 -0
  75. package/src/view/app/components/ui/tabs.tsx +30 -0
  76. package/src/view/app/i18n.ts +341 -0
  77. package/src/view/app/index.html +13 -0
  78. package/src/view/app/lib/cn.ts +7 -0
  79. package/src/view/app/lib/format.ts +73 -0
  80. package/src/view/app/lib/guards.ts +61 -0
  81. package/src/view/app/lib/outcome.ts +96 -0
  82. package/src/view/app/lib/rows.ts +63 -0
  83. package/src/view/app/lib/transcript-data.tsx +121 -0
  84. package/src/view/app/main.tsx +17 -0
  85. package/src/view/app/pages/RunsPage.tsx +83 -0
  86. package/src/view/app/pages/TracesPage.tsx +40 -0
  87. package/src/view/app/shared.ts +10 -0
  88. package/src/view/app/types.ts +114 -0
  89. package/src/view/app/vite.config.ts +26 -0
  90. package/src/view/client-dist/app.css +2 -0
  91. package/src/view/client-dist/app.js +56 -0
  92. package/src/view/index.ts +406 -0
  93. package/src/view/styles.css +1074 -0
  94. package/src/view/template.html +15 -0
@@ -0,0 +1,73 @@
1
+ import type { ViewUsage } from "../types.ts";
2
+
3
+ export function prettyJson(value: unknown): string {
4
+ if (typeof value === "string") return value;
5
+ try {
6
+ return JSON.stringify(value, null, 2);
7
+ } catch {
8
+ return String(value);
9
+ }
10
+ }
11
+
12
+ export function previewText(value: string): string {
13
+ return String(value).split("\n").find((line) => line.trim()) || "";
14
+ }
15
+
16
+ export function truncate(value: unknown, n: number): string {
17
+ const str = String(value);
18
+ return str.length > n ? str.slice(0, n) + " ... [+" + (str.length - n) + " chars]" : str;
19
+ }
20
+
21
+ export function formatConfigValue(value: unknown): string {
22
+ if (value === null) return "null";
23
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
24
+ return JSON.stringify(value);
25
+ }
26
+
27
+ export function totalTokens(usage?: ViewUsage): number {
28
+ return (usage?.inputTokens || 0) + (usage?.outputTokens || 0) + (usage?.cacheReadTokens || 0) + (usage?.cacheWriteTokens || 0);
29
+ }
30
+
31
+ export function formatPercent(value?: number): string {
32
+ if (typeof value !== "number" || !Number.isFinite(value)) return "0%";
33
+ return Math.round(value * 100) + "%";
34
+ }
35
+
36
+ /** 断言 / judge 分数本就是 0–1,直接展示原值(去掉末尾零),不转百分比。pass-rate 之类的「比率」仍用 formatPercent。 */
37
+ export function formatScore(value?: number): string {
38
+ if (typeof value !== "number" || !Number.isFinite(value)) return "0";
39
+ return String(Number(value.toFixed(2)));
40
+ }
41
+
42
+ export function formatDuration(ms?: number): string {
43
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "0ms";
44
+ if (ms >= 60000) return (ms / 60000).toFixed(1) + "m";
45
+ if (ms >= 1000) return (ms / 1000).toFixed(2) + "s";
46
+ return Math.round(ms) + "ms";
47
+ }
48
+
49
+ export function formatTokens(value: number): string {
50
+ if (!Number.isFinite(value) || value <= 0) return "0";
51
+ if (value >= 1000000) return (value / 1000000).toFixed(2) + "M";
52
+ if (value >= 1000) return (value / 1000).toFixed(1) + "k";
53
+ return String(Math.round(value));
54
+ }
55
+
56
+ export function formatCost(value?: number): string {
57
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return "$0";
58
+ return "$" + value.toFixed(value < 1 ? 3 : 2);
59
+ }
60
+
61
+ export function formatDateTime(iso?: string): string {
62
+ if (!iso) return "-";
63
+ const date = new Date(iso);
64
+ if (Number.isNaN(date.getTime())) return "-";
65
+ return date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
66
+ }
67
+
68
+ export function formatClock(iso?: string): string {
69
+ if (!iso) return "";
70
+ const date = new Date(iso);
71
+ if (Number.isNaN(date.getTime())) return "";
72
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
73
+ }
@@ -0,0 +1,61 @@
1
+ import type { CodeSource, ObjectRecord, Span, TranscriptEvent } from "../types.ts";
2
+
3
+ export function asSources(value: unknown): CodeSource[] | null {
4
+ if (!Array.isArray(value)) return null;
5
+ return value.every(isCodeSource) ? value : null;
6
+ }
7
+
8
+ export function isCodeSource(value: unknown): value is CodeSource {
9
+ return isObjectRecord(value) && typeof value.path === "string" && typeof value.content === "string";
10
+ }
11
+
12
+ export function asEvents(value: unknown): TranscriptEvent[] | null {
13
+ if (!Array.isArray(value)) return null;
14
+ return value.every(isTranscriptEvent) ? value : null;
15
+ }
16
+
17
+ export function asSpans(value: unknown): Span[] | null {
18
+ if (!Array.isArray(value)) return null;
19
+ return value.every(isSpan) ? value : null;
20
+ }
21
+
22
+ export function isTranscriptEvent(value: unknown): value is TranscriptEvent {
23
+ if (!isObjectRecord(value) || typeof value.type !== "string") return false;
24
+ switch (value.type) {
25
+ case "message":
26
+ return (value.role === "assistant" || value.role === "user") && typeof value.text === "string";
27
+ case "action.called":
28
+ return typeof value.callId === "string" && typeof value.name === "string";
29
+ case "action.result":
30
+ return typeof value.callId === "string";
31
+ case "subagent.called":
32
+ return typeof value.callId === "string" && typeof value.name === "string";
33
+ case "subagent.completed":
34
+ return typeof value.callId === "string";
35
+ case "input.requested":
36
+ return isObjectRecord(value.request);
37
+ case "thinking":
38
+ return typeof value.text === "string";
39
+ case "compaction":
40
+ return true;
41
+ case "error":
42
+ return typeof value.message === "string";
43
+ default:
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export function isSpan(value: unknown): value is Span {
49
+ return (
50
+ isObjectRecord(value) &&
51
+ typeof value.traceId === "string" &&
52
+ typeof value.spanId === "string" &&
53
+ typeof value.name === "string" &&
54
+ typeof value.startMs === "number" &&
55
+ typeof value.endMs === "number"
56
+ );
57
+ }
58
+
59
+ export function isObjectRecord(value: unknown): value is ObjectRecord {
60
+ return !!value && typeof value === "object" && !Array.isArray(value);
61
+ }
@@ -0,0 +1,96 @@
1
+ import type { Assertion, Outcome, ViewResult, ViewRow } from "../types.ts";
2
+ import type { T } from "../shared.ts";
3
+ import { formatScore } from "./format.ts";
4
+
5
+ export function outcomeOf(result: ViewResult): Outcome {
6
+ return result.outcome;
7
+ }
8
+
9
+ /**
10
+ * 同一个 eval 的多轮 attempt 折叠成单一判决:任一轮通过 → 通过(对齐 earlyExit「先过一次即停」),
11
+ * 否则按 failed > errored > skipped 取最严重的。后端 view/index.ts:foldEvalOutcome 用同样口径,
12
+ * 两边必须一致,否则折叠行的状态会和 KPI / 成功率对不上。
13
+ */
14
+ export function foldEvalOutcome(attempts: ViewResult[]): Outcome {
15
+ const outs = attempts.map(outcomeOf);
16
+ if (outs.some((o) => o === "passed")) return "passed";
17
+ if (outs.some((o) => o === "failed")) return "failed";
18
+ if (outs.some((o) => o === "errored")) return "errored";
19
+ return "skipped";
20
+ }
21
+
22
+ export interface EvalGroup {
23
+ id: string;
24
+ experimentId?: string;
25
+ outcome: Outcome;
26
+ attempts: ViewResult[];
27
+ passedAttempts: number;
28
+ }
29
+
30
+ /** 把一批 attempt 按 (experimentId, eval id) 折叠成「每个 eval 一行」,内部 attempt 按轮次排序。 */
31
+ export function groupByEval(results: ViewResult[]): EvalGroup[] {
32
+ const byEval = new Map<string, ViewResult[]>();
33
+ for (const r of results) {
34
+ const key = `${r.experimentId ?? ""}|||${r.id}`;
35
+ byEval.set(key, [...(byEval.get(key) ?? []), r]);
36
+ }
37
+ return [...byEval.values()].map((attempts) => {
38
+ const sorted = [...attempts].sort((a, b) => a.attempt - b.attempt);
39
+ return {
40
+ id: sorted[0]!.id,
41
+ experimentId: sorted[0]!.experimentId,
42
+ outcome: foldEvalOutcome(sorted),
43
+ attempts: sorted,
44
+ passedAttempts: sorted.filter((a) => outcomeOf(a) === "passed").length,
45
+ };
46
+ });
47
+ }
48
+
49
+ /** 成功率按 eval 计票:折叠后通过的 eval 占已跑(非 skipped)eval 的比例。 */
50
+ export function evalPassRate(results: ViewResult[]): number {
51
+ const ran = groupByEval(results).filter((g) => g.outcome !== "skipped");
52
+ return ran.length ? ran.filter((g) => g.outcome === "passed").length / ran.length : 0;
53
+ }
54
+
55
+ export function outcomeClass(outcome: Outcome): string {
56
+ return outcome === "passed" ? "good" : outcome === "errored" ? "infra-err" : outcome === "failed" ? "bad" : "warn";
57
+ }
58
+
59
+ export function outcomeLabel(outcome: Outcome, t: T): string {
60
+ if (outcome === "passed") return t("status.pass");
61
+ if (outcome === "failed") return t("status.fail");
62
+ if (outcome === "errored") return t("status.error");
63
+ if (outcome === "skipped") return t("status.skipped");
64
+ return outcome || "—";
65
+ }
66
+
67
+ // Only gate-severity failures are eval "failure reasons"; soft failures show as scores
68
+ export function failingAssertions(result: ViewResult): Assertion[] {
69
+ return (result.assertions || []).filter((a: Assertion) => !a.passed && a.severity === "gate");
70
+ }
71
+
72
+ export function reasonFor(result: ViewResult, failedGates: Assertion[]): string {
73
+ if (result.error) return result.error;
74
+ if (result.skipReason) return result.skipReason;
75
+ return failedGates.map((a: Assertion) => (a.detail ? `${a.name}: ${a.detail}` : a.name)).join(", ");
76
+ }
77
+
78
+ export function scoresSummary(assertions: Assertion[]): string {
79
+ const scored = (assertions || []).filter((a: Assertion) => a.score !== undefined && a.score !== null);
80
+ if (!scored.length) return "";
81
+ return scored
82
+ .map((a: Assertion) => {
83
+ const s = formatScore(a.score);
84
+ return a.threshold !== undefined ? `${a.name} ${s}/${formatScore(a.threshold)}` : `${a.name} ${s}`;
85
+ })
86
+ .join(" · ");
87
+ }
88
+
89
+ export function outcomeSummary(row: ViewRow, t: T): string {
90
+ // fold "scored" (soft-only) into passed count
91
+ const passed = (row.passed || 0) + (row.scored || 0);
92
+ const parts = [`${passed} ${t("outcome.passed")}`, `${row.failed} ${t("outcome.failed")}`];
93
+ if (row.errored) parts.push(`${row.errored} ${t("outcome.errored")}`);
94
+ if (row.skipped) parts.push(`${row.skipped} ${t("outcome.skipped")}`);
95
+ return parts.join(" / ");
96
+ }
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SortKey, ViewResult, ViewRow } from "../types.ts";
3
+ import type { T } from "../shared.ts";
4
+ import { formatConfigValue, totalTokens } from "./format.ts";
5
+
6
+ export function resultFromUrl(rows: ViewRow[]): ViewResult | null {
7
+ const p = new URLSearchParams(location.search);
8
+ const id = p.get("modal");
9
+ if (!id) return null;
10
+ const exp = p.get("exp");
11
+ const attempt = parseInt(p.get("a") ?? "0", 10);
12
+ for (const row of rows) {
13
+ for (const result of row.results ?? []) {
14
+ if (result.id === id && (!exp || result.experimentId === exp) && result.attempt === attempt) {
15
+ return result;
16
+ }
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ export function buildGroupMap(rows: ViewRow[]): Map<string, ViewRow[]> {
23
+ const map = new Map<string, ViewRow[]>();
24
+ for (const row of rows) {
25
+ if (!row.group) continue;
26
+ if (!map.has(row.group)) map.set(row.group, []);
27
+ map.get(row.group)?.push(row);
28
+ }
29
+ return map;
30
+ }
31
+
32
+ export function compareRows(a: ViewRow, b: ViewRow, key: SortKey): number {
33
+ const av = valueFor(a, key);
34
+ const bv = valueFor(b, key);
35
+ if (typeof av === "string" || typeof bv === "string") return String(av).localeCompare(String(bv));
36
+ return Number(av) - Number(bv);
37
+ }
38
+
39
+ export function valueFor(row: ViewRow, key: SortKey): string | number {
40
+ if (key === "experiment") return row.label;
41
+ if (key === "model") return row.model || "";
42
+ if (key === "agent") return row.agent;
43
+ if (key === "cost") return row.estimatedCostUSD || 0;
44
+ if (key === "tokens") return totalTokens(row.usage);
45
+ return row[key] || 0;
46
+ }
47
+
48
+ export function configChips(row: ViewRow, t: T): [string, ReactNode][] {
49
+ const exp = row.experiment || {};
50
+ const flags = exp.flags && Object.keys(exp.flags).length
51
+ ? Object.entries(exp.flags).map(([k, v]) => k + "=" + formatConfigValue(v)).join(", ")
52
+ : t("config.flagsNone");
53
+ return [
54
+ [t("config.experiment"), row.experimentId || row.label],
55
+ [t("table.model"), row.model || t("config.default")],
56
+ ["agent", row.agent],
57
+ ["runs", exp.runs ?? row.runs],
58
+ ["earlyExit", exp.earlyExit === undefined ? t("config.notApplicable") : String(exp.earlyExit)],
59
+ ["sandbox", exp.sandbox || t("config.default")],
60
+ ["budget", exp.budget === undefined ? t("config.none") : "$" + exp.budget],
61
+ ["flags", flags],
62
+ ];
63
+ }
@@ -0,0 +1,121 @@
1
+ import type { ReactNode } from "react";
2
+ import type { Assertion, Indexed, IndexedTurns, SourceTurn, TranscriptEvent, ViewJson } from "../types.ts";
3
+ import type { ToolBlockCall } from "../shared.ts";
4
+ import { isObjectRecord } from "./guards.ts";
5
+ import { prettyJson } from "./format.ts";
6
+
7
+ // ───────────────────────── 源码对齐的代码视图(github-diff 式)─────────────────────────
8
+ // 拿 sources.json(eval 源码)+ events.json(带 loc 的 send),把每条 send / 断言的运行结果
9
+ // 叠回真实源码行:send 行折叠→展开看回复;断言行绿(过)/红(不过),judge 行带分数,展开看 CoT。
10
+
11
+ export function locKey(file: string, line: number): string {
12
+ return `${file}:${line}`;
13
+ }
14
+
15
+ /** events → 按 send 的 loc 聚成「轮」:每轮含 sent 文本 + 后续 thinking/assistant/tool 回复。 */
16
+ export function indexTurns(events: TranscriptEvent[]): IndexedTurns {
17
+ const byKey = new Map<string, SourceTurn>();
18
+ const noloc: SourceTurn[] = [];
19
+ let cur: SourceTurn | null = null;
20
+ for (const ev of events || []) {
21
+ if (ev.type === "message" && ev.role === "user") {
22
+ cur = { loc: ev.loc, sent: ev.text || "", replies: [] };
23
+ if (ev.loc) byKey.set(locKey(ev.loc.file, ev.loc.line), cur);
24
+ else noloc.push(cur);
25
+ } else if (!cur) {
26
+ continue;
27
+ } else if (ev.type === "message" && ev.role === "assistant") {
28
+ cur.replies.push({ kind: "text", text: ev.text || "" });
29
+ } else if (ev.type === "thinking") {
30
+ cur.replies.push({ kind: "thinking", text: ev.text || "" });
31
+ } else if (ev.type === "action.called") {
32
+ cur.replies.push({ kind: "tool", ev });
33
+ } else if (ev.type === "action.result") {
34
+ const tool = [...cur.replies].reverse().find(
35
+ (r): r is Extract<SourceTurn["replies"][number], { kind: "tool" }> => r.kind === "tool" && r.ev.callId === ev.callId,
36
+ );
37
+ if (tool) tool.result = ev;
38
+ } else if (ev.type === "error") {
39
+ cur.replies.push({ kind: "error", text: ev.message || "error" });
40
+ }
41
+ }
42
+ return { byKey, noloc };
43
+ }
44
+
45
+ /** assertions → 按 loc 聚到行。有 loc 的进 byKey,没 loc 的进 noloc(底部兜底列)。 */
46
+ export function indexAsserts(assertions: Assertion[]): Indexed<Assertion> {
47
+ const byKey = new Map<string, Assertion[]>();
48
+ const noloc: Assertion[] = [];
49
+ for (const a of assertions || []) {
50
+ if (a.loc) {
51
+ const k = locKey(a.loc.file, a.loc.line);
52
+ if (!byKey.has(k)) byKey.set(k, []);
53
+ byKey.get(k)?.push(a);
54
+ } else {
55
+ noloc.push(a);
56
+ }
57
+ }
58
+ return { byKey, noloc };
59
+ }
60
+
61
+ export const TS_HL_RE =
62
+ /(\/\/[^\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;
63
+
64
+ /** 轻量 TS 着色(逐行,零依赖):注释 / 字符串 / 关键字 / 数字 / 函数名。 */
65
+ export function highlightTs(line: string): ReactNode[] {
66
+ const out: ReactNode[] = [];
67
+ let last = 0;
68
+ let i = 0;
69
+ let m: RegExpExecArray | null;
70
+ TS_HL_RE.lastIndex = 0;
71
+ while ((m = TS_HL_RE.exec(line))) {
72
+ if (m.index > last) out.push(line.slice(last, m.index));
73
+ const cls = m[1] || m[2] ? "tok-comment" : m[3] ? "tok-str" : m[4] ? "tok-kw" : m[5] ? "tok-num" : m[6] ? "tok-fn" : null;
74
+ out.push(cls ? <span key={i++} className={cls}>{m[0]}</span> : m[0]);
75
+ last = m.index + m[0].length;
76
+ if (m[0].length === 0) TS_HL_RE.lastIndex++;
77
+ }
78
+ if (last < line.length) out.push(line.slice(last));
79
+ return out;
80
+ }
81
+
82
+ export const TOOL_VERB: Record<string, string> = {
83
+ file_read: "Read",
84
+ file_write: "Write",
85
+ file_edit: "Edit",
86
+ shell: "Bash",
87
+ web_fetch: "Fetch",
88
+ web_search: "Search",
89
+ glob: "Glob",
90
+ grep: "Grep",
91
+ list_dir: "List",
92
+ agent_task: "Task",
93
+ };
94
+
95
+ export function toolPrimaryArg(call: ToolBlockCall): string {
96
+ const input = call.input;
97
+ if (typeof input === "string") return input;
98
+ if (!isObjectRecord(input)) return "";
99
+ if (call.tool === "shell") {
100
+ const command = input.command ?? input.cmd;
101
+ if (typeof command === "string") return command;
102
+ if (Array.isArray(command)) return command.filter((x: ViewJson) => typeof x === "string").join(" ");
103
+ }
104
+ for (const key of ["path", "file", "file_path", "filename", "pattern", "query", "url", "uri", "prompt", "description", "command", "remoteUrl"]) {
105
+ const value = input[key];
106
+ if (typeof value === "string" && value) return value;
107
+ }
108
+ return "";
109
+ }
110
+
111
+ export function resultBody(output: ViewJson | undefined): string {
112
+ if (output == null) return "";
113
+ if (typeof output === "string") return output;
114
+ if (isObjectRecord(output)) {
115
+ for (const key of ["output", "stdout", "content", "text", "result", "body"]) {
116
+ const value = output[key];
117
+ if (typeof value === "string") return value;
118
+ }
119
+ }
120
+ return prettyJson(output);
121
+ }
@@ -0,0 +1,17 @@
1
+ import { createRoot } from "react-dom/client";
2
+ import type { ViewData } from "./types.ts";
3
+ import { App } from "./App.tsx";
4
+ import "../styles.css";
5
+
6
+ const initialData: ViewData = window.__NICEEVAL_VIEW_DATA__ ?? {
7
+ rows: [],
8
+ lastRun: "No runs yet",
9
+ passRate: "0%",
10
+ resultCount: "0",
11
+ duration: "0ms",
12
+ cost: "$0",
13
+ };
14
+
15
+ const rootEl = document.getElementById("root");
16
+ if (!rootEl) throw new Error("Missing #root element");
17
+ createRoot(rootEl).render(<App data={initialData} />);
@@ -0,0 +1,83 @@
1
+ import { useMemo, useState } from "react";
2
+ import type { RowRun, T } from "../shared.ts";
3
+ import type { ViewResult, ViewRow } from "../types.ts";
4
+ import { outcomeClass, outcomeLabel, outcomeOf } from "../lib/outcome.ts";
5
+ import { formatCost, formatDateTime, formatDuration, formatTokens, totalTokens } from "../lib/format.ts";
6
+
7
+ export function RunsView({ rows, t }: { rows: ViewRow[]; t: T }) {
8
+ const [query, setQuery] = useState("");
9
+ const allRuns = useMemo(
10
+ () => rows.flatMap((row: ViewRow) => (row.results ?? []).map((r: ViewResult): RowRun => ({ ...r, rowLabel: row.label, rowAgent: row.agent, rowModel: row.model }))),
11
+ [rows],
12
+ );
13
+ const filtered = allRuns.filter((r: RowRun) => {
14
+ const q = query.trim().toLowerCase();
15
+ return !q || `${r.id} ${r.rowLabel} ${r.rowAgent} ${r.rowModel || ""}`.toLowerCase().includes(q);
16
+ });
17
+ return (
18
+ <section id="tab-runs">
19
+ <div className="section-head">
20
+ <h2>{t("section.individualRuns")}</h2>
21
+ <div className="controls">
22
+ <input
23
+ className="search"
24
+ type="search"
25
+ placeholder={t("search.runs")}
26
+ autoComplete="off"
27
+ value={query}
28
+ onChange={(e) => setQuery(e.target.value)}
29
+ />
30
+ </div>
31
+ </div>
32
+ {!allRuns.length ? (
33
+ <div className="empty">{t("empty.individualRuns")}</div>
34
+ ) : (
35
+ <div className="table-wrap">
36
+ <table>
37
+ <thead>
38
+ <tr>
39
+ <th>{t("table.evalId")}</th>
40
+ <th>{t("table.experiment")}</th>
41
+ <th>{t("table.outcome")}</th>
42
+ <th>{t("table.agent")}</th>
43
+ <th>{t("table.model")}</th>
44
+ <th>{t("metric.duration")}</th>
45
+ <th>{t("table.tokens")}</th>
46
+ <th>{t("table.estCost")}</th>
47
+ <th>{t("table.ranAt")}</th>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ {filtered.length ? (
52
+ filtered.map((r: RowRun) => {
53
+ const outcome = outcomeOf(r);
54
+ return (
55
+ <tr key={`${r.id}-${r.rowLabel}-${r.attempt}`}>
56
+ <td>
57
+ <span className="name">{r.id}</span>
58
+ </td>
59
+ <td>{r.rowLabel}</td>
60
+ <td className={outcomeClass(outcome)}>{outcomeLabel(outcome, t)}</td>
61
+ <td>{r.rowAgent}</td>
62
+ <td>{r.rowModel || t("config.default")}</td>
63
+ <td className="num">{formatDuration(r.durationMs)}</td>
64
+ <td className="num">{formatTokens(totalTokens(r.usage))}</td>
65
+ <td className="num">{formatCost(r.estimatedCostUSD)}</td>
66
+ <td className="num">{r.startedAt ? formatDateTime(r.startedAt) : "-"}</td>
67
+ </tr>
68
+ );
69
+ })
70
+ ) : (
71
+ <tr>
72
+ <td colSpan={9} style={{ textAlign: "center", color: "var(--muted)" }}>
73
+ {t("empty.runsFilter")}
74
+ </td>
75
+ </tr>
76
+ )}
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ )}
81
+ </section>
82
+ );
83
+ }
@@ -0,0 +1,40 @@
1
+ import { useMemo } from "react";
2
+ import type { RowRun, T } from "../shared.ts";
3
+ import type { ViewResult, ViewRow } from "../types.ts";
4
+ import { outcomeClass, outcomeLabel, outcomeOf } from "../lib/outcome.ts";
5
+ import { formatDuration } from "../lib/format.ts";
6
+ import { LazyArtifact } from "../components/LazyArtifact.tsx";
7
+
8
+ export function TracesView({ rows, t }: { rows: ViewRow[]; t: T }) {
9
+ const allRuns = useMemo(
10
+ () => rows.flatMap((row: ViewRow) => (row.results ?? []).map((r: ViewResult): RowRun => ({ ...r, rowLabel: row.label, rowAgent: row.agent, rowModel: row.model }))),
11
+ [rows],
12
+ );
13
+ const traceable = allRuns.filter((r: RowRun) => r.hasEvents || r.hasTrace);
14
+ return (
15
+ <section id="tab-traces">
16
+ <div className="section-head">
17
+ <h2>{t("section.traces")}</h2>
18
+ </div>
19
+ {!traceable.length ? (
20
+ <div className="empty">{t("empty.traces")}</div>
21
+ ) : (
22
+ traceable.map((r: RowRun) => {
23
+ const outcome = outcomeOf(r);
24
+ return (
25
+ <div className="traces-entry" key={`${r.id}-${r.rowLabel}-${r.attempt}`}>
26
+ <div className="traces-entry-head">
27
+ <span className={`${outcomeClass(outcome)} traces-verdict`}>{outcomeLabel(outcome, t)}</span>
28
+ <span className="eval-id">{r.id}</span>
29
+ <span className="traces-exp">{r.rowLabel}</span>
30
+ <span className="num traces-dur">{formatDuration(r.durationMs)}</span>
31
+ </div>
32
+ {r.hasEvents && r.artifactBase ? <LazyArtifact type="transcript" src={`${r.artifactBase}/events.json`} t={t} /> : null}
33
+ {r.hasTrace && r.artifactBase ? <LazyArtifact type="trace" src={`${r.artifactBase}/trace.json`} t={t} /> : null}
34
+ </div>
35
+ );
36
+ })
37
+ )}
38
+ </section>
39
+ );
40
+ }
@@ -0,0 +1,10 @@
1
+ import type { MessageKey } from "./i18n.ts";
2
+ import type { CodeSource, TranscriptEvent, ViewJson, ViewResult } from "./types.ts";
3
+
4
+ export type T = (key: MessageKey) => string;
5
+ export type OpenModal = (result: ViewResult) => void;
6
+ export type ArtifactLoadState =
7
+ | { sources: CodeSource[] | null; events: TranscriptEvent[] | null; status: "loading" | "ready" | "none" };
8
+ export type RowRun = ViewResult & { rowLabel: string; rowAgent: string; rowModel?: string };
9
+ export type LazyArtifactType = "trace" | "transcript";
10
+ export type ToolBlockCall = { tool?: string; name: string; input: ViewJson };