niceeval 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/report/index.d.ts +1 -1
- package/dist/report/report.d.ts +23 -1
- package/dist/report/report.js +82 -5
- package/docs-site/zh/how-to/custom-reports.mdx +3 -3
- package/docs-site/zh/how-to/publish-report.mdx +42 -5
- package/docs-site/zh/how-to/viewing-results.mdx +4 -4
- package/docs-site/zh/reference/cli.mdx +2 -2
- package/docs-site/zh/reference/results-data.mdx +1 -1
- package/docs-site/zh/troubleshooting/debugging.mdx +2 -2
- package/package.json +7 -6
- package/src/report/index.ts +1 -0
- package/src/report/report.ts +128 -6
- package/src/report/shell-head.test.ts +102 -0
- package/src/show/report-host.ts +13 -0
- package/src/view/app/components/CodeView.test.tsx +142 -0
- package/src/view/app/components/CodeView.tsx +15 -1
- package/src/view/app/components/Transcript.tsx +28 -1
- package/src/view/app/i18n.ts +6 -0
- package/src/view/app/lib/guards.test.ts +108 -0
- package/src/view/app/lib/guards.ts +13 -3
- package/src/view/app/lib/transcript-data.tsx +14 -0
- package/src/view/app/types.ts +17 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.ts +54 -3
- package/src/view/index.ts +18 -47
- package/src/view/server.ts +49 -142
- package/src/view/site-head.test.ts +177 -0
- package/src/view/site-parity.test.ts +117 -0
- package/src/view/site.ts +209 -0
- package/src/view/styles.css +10 -0
- package/src/view/view-report.test.ts +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/reports/cases.md
|
|
2
|
+
// 「show/view 宿主等价与选择」分区——
|
|
3
|
+
// 本地 server 与 --out 消费同一份站点产物:同一路径两宿主逐字节一致;server 不提供产物清单
|
|
4
|
+
// 之外的路径。server 打开首页触发产物整份重建(盘上新数据无需重启),artifact 未命中重建一次。
|
|
5
|
+
// 契约:docs/feature/reports/view.md 开篇「同一条站点管线」。
|
|
6
|
+
|
|
7
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
11
|
+
import { createResultsWriter } from "../results/index.ts";
|
|
12
|
+
import { buildView } from "./index.ts";
|
|
13
|
+
import { planSite } from "./site.ts";
|
|
14
|
+
import { startViewServer, type ViewServer } from "./server.ts";
|
|
15
|
+
|
|
16
|
+
const roots: string[] = [];
|
|
17
|
+
async function makeDir(prefix: string): Promise<string> {
|
|
18
|
+
const dir = await mkdtemp(join(tmpdir(), prefix));
|
|
19
|
+
roots.push(dir);
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await Promise.all(roots.splice(0).map((r) => rm(r, { recursive: true, force: true })));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** 真实 writer 落一份含 events / trace / diff / 去重 sources 的快照(不手写落盘格式)。 */
|
|
27
|
+
async function seedSnapshot(root: string, experimentId: string, evalId: string): Promise<void> {
|
|
28
|
+
const writer = createResultsWriter(root, { producer: { name: "niceeval", version: "1.0.0" } });
|
|
29
|
+
const snap = await writer.snapshot({ experimentId, agent: "bub", startedAt: "2026-07-01T08:00:00.000Z" });
|
|
30
|
+
await snap.writeAttempt(
|
|
31
|
+
{ id: evalId, verdict: "passed", attempt: 0, durationMs: 1, assertions: [] },
|
|
32
|
+
{
|
|
33
|
+
sources: [{ path: "evals/a.eval.ts", content: "export default 1;\n" }],
|
|
34
|
+
events: [
|
|
35
|
+
{ type: "message", role: "user", text: "hi", loc: { file: "evals/a.eval.ts", line: 3 } },
|
|
36
|
+
{ type: "message", role: "assistant", text: "hello there" },
|
|
37
|
+
],
|
|
38
|
+
trace: [{ name: "turn", kind: "turn" } as never],
|
|
39
|
+
diff: [{ window: "s1/t1", changes: { "a.txt": { status: "added", after: "1" } } }] as never,
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
await writer.finish();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("站点管线奇偶:server 与 --out 是同一份产物", () => {
|
|
46
|
+
let server: ViewServer | undefined;
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
await server?.close();
|
|
49
|
+
server = undefined;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("导出目录的每个文件与 server 对同路径的响应逐字节一致;server 不提供清单之外的路径", async () => {
|
|
53
|
+
const root = await makeDir("niceeval-parity-root-");
|
|
54
|
+
await seedSnapshot(root, "exp", "q1");
|
|
55
|
+
|
|
56
|
+
const out = await makeDir("niceeval-parity-out-");
|
|
57
|
+
await buildView({ input: root, out, allowSensitiveArtifacts: true });
|
|
58
|
+
server = await startViewServer({ input: root });
|
|
59
|
+
|
|
60
|
+
// 清单驱动遍历:计划里的每一个路径,写盘字节 ≡ 服务字节。
|
|
61
|
+
const plan = await planSite(root);
|
|
62
|
+
expect(plan.files.size).toBeGreaterThanOrEqual(5); // index.html + sources/events/trace/diff
|
|
63
|
+
for (const file of plan.files.values()) {
|
|
64
|
+
const written = await readFile(join(out, file.path), "utf-8");
|
|
65
|
+
const res = await fetch(new URL(file.path === "index.html" ? "/" : `/${file.path}`, server.url));
|
|
66
|
+
expect(res.status, file.path).toBe(200);
|
|
67
|
+
expect(await res.text(), file.path).toBe(written);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 产物清单之外的路径不存在旁路取数:即便文件真实在结果根里(o11y.json),也不服务。
|
|
71
|
+
const base = [...plan.files.keys()].find((p) => p.endsWith("/events.json"))!.replace(/\/events\.json$/, "");
|
|
72
|
+
for (const path of [`${base}/o11y.json`, `${base}/result.json`, "artifact/../package.json", "no-such-file"]) {
|
|
73
|
+
const res = await fetch(new URL(`/${path}`, server.url));
|
|
74
|
+
expect(res.status, path).toBe(404);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("首页请求触发整份重建:server 启动后新落盘的快照,刷新即可见、其证据无需重启可 fetch", async () => {
|
|
79
|
+
const root = await makeDir("niceeval-parity-fresh-");
|
|
80
|
+
await seedSnapshot(root, "exp-a", "q1");
|
|
81
|
+
server = await startViewServer({ input: root });
|
|
82
|
+
|
|
83
|
+
const before = await (await fetch(server.url)).text();
|
|
84
|
+
expect(before).toContain("exp-a");
|
|
85
|
+
expect(before).not.toContain("exp-b");
|
|
86
|
+
|
|
87
|
+
await seedSnapshot(root, "exp-b", "q2");
|
|
88
|
+
|
|
89
|
+
// 刷新首页:整份产物重建,新实验进入 viewData。
|
|
90
|
+
const after = await (await fetch(server.url)).text();
|
|
91
|
+
expect(after).toContain("exp-b");
|
|
92
|
+
|
|
93
|
+
// 新快照的证据文件不重启 server 就能取到(artifact 未命中时管线重建一次再查)。
|
|
94
|
+
const plan = await planSite(root);
|
|
95
|
+
const eventsPath = [...plan.files.keys()].find((p) => p.includes("exp-b") && p.endsWith("/events.json"))!;
|
|
96
|
+
const res = await fetch(new URL(`/${eventsPath}`, server.url));
|
|
97
|
+
expect(res.status).toBe(200);
|
|
98
|
+
expect(await res.json()).toEqual([
|
|
99
|
+
{ type: "message", role: "user", text: "hi", loc: { file: "evals/a.eval.ts", line: 3 } },
|
|
100
|
+
{ type: "message", role: "assistant", text: "hello there" },
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("legacy /artifact?p= query 形式仍服务同一份产物", async () => {
|
|
105
|
+
const root = await makeDir("niceeval-parity-legacy-");
|
|
106
|
+
await seedSnapshot(root, "exp", "q1");
|
|
107
|
+
server = await startViewServer({ input: root });
|
|
108
|
+
|
|
109
|
+
const plan = await planSite(root);
|
|
110
|
+
const eventsPath = [...plan.files.keys()].find((p) => p.endsWith("/events.json"))!;
|
|
111
|
+
const rel = eventsPath.replace(/^artifact\//, "");
|
|
112
|
+
const res = await fetch(new URL(`/artifact?p=${encodeURIComponent(rel)}`, server.url));
|
|
113
|
+
expect(res.status).toBe(200);
|
|
114
|
+
const direct = await (await fetch(new URL(`/${eventsPath}`, server.url))).text();
|
|
115
|
+
expect(await res.text()).toBe(direct);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/view/site.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// 站点管线:本地 server 与 `--out` 的唯一联系面(docs/feature/reports/view.md 开篇)。
|
|
2
|
+
// planSite 把结果根物化成一份站点产物清单(index.html + artifact 证据树),writeSite 把清单
|
|
3
|
+
// 写盘,server 按路径服务同一份清单——布局与取数知识(artifact 相对路径、sources.json 解引用)
|
|
4
|
+
// 只住在这里,两个宿主都是哑消费者,同一路径两边逐字节一致(site-parity 测试守护)。
|
|
5
|
+
|
|
6
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { loadViewScan, type ResolvedHeadTag, type ViewScan, type ViewScanOptions } from "./data.ts";
|
|
11
|
+
import { localizeText } from "../show/report-host.ts";
|
|
12
|
+
|
|
13
|
+
/** 站点产物清单里的一个文件:现算内容,或指向结果根内的原文件。 */
|
|
14
|
+
export interface SiteFile {
|
|
15
|
+
/** 站点相对路径(posix),如 `index.html`、`artifact/<base>/events.json`。 */
|
|
16
|
+
path: string;
|
|
17
|
+
contentType: string;
|
|
18
|
+
source: { kind: "content"; body: string } | { kind: "file"; abs: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SitePlan {
|
|
22
|
+
/** path → SiteFile;写盘按它遍历,server 按它查表(查不到即 404,不存在旁路取数)。 */
|
|
23
|
+
files: Map<string, SiteFile>;
|
|
24
|
+
/** 构建这份产物用的扫描结果(publishState 等宿主前置校验用;不进产物)。 */
|
|
25
|
+
scan: ViewScan;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const JSON_TYPE = "application/json; charset=utf-8";
|
|
29
|
+
const HTML_TYPE = "text/html; charset=utf-8";
|
|
30
|
+
|
|
31
|
+
// 前端会 fetch 的原字节证据文件(docs/feature/reports/view.md「静态导出」:有就带,缺时前端
|
|
32
|
+
// 在证据位置如实显示缺失;o11y.json 永不进产物——报告数字已烘进 HTML,浏览器不读它)。
|
|
33
|
+
const RAW_COPY_ARTIFACTS = ["events.json", "trace.json", "diff.json"];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 把结果根物化成站点产物清单。sources.json 是唯一的格式例外——盘上是去重后的引用
|
|
37
|
+
* (`{path, sha256}[]`),必须经 `AttemptHandle.sources()` 解引用出完整内容(`{path, content}[]`)
|
|
38
|
+
* 才能给浏览器用,解引用只发生在这里这一处。
|
|
39
|
+
*/
|
|
40
|
+
export async function planSite(input?: string, opts: ViewScanOptions = {}): Promise<SitePlan> {
|
|
41
|
+
const scan = await loadViewScan(input, opts);
|
|
42
|
+
const files = new Map<string, SiteFile>();
|
|
43
|
+
// head 标签的本地 src/href 资产按内容哈希物化进 assets/(同内容同扩展名去重,
|
|
44
|
+
// 同名文件不冲突;shell.md「行为约束」),回填后的标签渲染进 <head>。
|
|
45
|
+
const headHtml = await materializeHeadAssets(scan.shellAssets.head, files);
|
|
46
|
+
files.set("index.html", {
|
|
47
|
+
path: "index.html",
|
|
48
|
+
contentType: HTML_TYPE,
|
|
49
|
+
source: { kind: "content", body: await renderHtml(scan, headHtml) },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
for (const [base, srcDir] of scan.artifactDirs) {
|
|
53
|
+
for (const name of RAW_COPY_ARTIFACTS) {
|
|
54
|
+
const abs = join(srcDir, name);
|
|
55
|
+
if (!existsSync(abs)) continue;
|
|
56
|
+
const path = `artifact/${base}/${name}`;
|
|
57
|
+
files.set(path, { path, contentType: JSON_TYPE, source: { kind: "file", abs } });
|
|
58
|
+
}
|
|
59
|
+
if (existsSync(join(srcDir, "sources.json"))) {
|
|
60
|
+
const attempt = scan.attemptsByBase.get(base);
|
|
61
|
+
const sources = attempt ? await attempt.sources() : null;
|
|
62
|
+
const path = `artifact/${base}/sources.json`;
|
|
63
|
+
files.set(path, { path, contentType: JSON_TYPE, source: { kind: "content", body: JSON.stringify(sources ?? []) } });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { files, scan };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 把站点产物清单写盘(`--out`)。输入本身已是导出布局(对上次导出的目录重新生成)时原文件不自拷。 */
|
|
70
|
+
export async function writeSite(plan: SitePlan, outDir: string): Promise<void> {
|
|
71
|
+
for (const file of plan.files.values()) {
|
|
72
|
+
const dest = join(outDir, file.path);
|
|
73
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
74
|
+
if (file.source.kind === "content") {
|
|
75
|
+
await writeFile(dest, file.source.body, "utf-8");
|
|
76
|
+
} else if (resolve(file.source.abs) !== resolve(dest)) {
|
|
77
|
+
await copyFile(file.source.abs, dest);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 取清单中一个文件的字节(server 响应体与写盘内容同源;file 类缺失时返回 undefined,由宿主 404)。 */
|
|
83
|
+
export async function readSiteFile(file: SiteFile): Promise<string | Buffer | undefined> {
|
|
84
|
+
if (file.source.kind === "content") return file.source.body;
|
|
85
|
+
try {
|
|
86
|
+
// 原字节读取:assets/ 里的 head 资产可以是二进制(favicon、字体),不能按 utf-8 解码。
|
|
87
|
+
return await readFile(file.source.abs);
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** head 资产的响应 content-type;命不中的按二进制下发(写盘路径不受影响)。 */
|
|
94
|
+
const ASSET_CONTENT_TYPES: Record<string, string> = {
|
|
95
|
+
".js": "text/javascript; charset=utf-8",
|
|
96
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
97
|
+
".css": "text/css; charset=utf-8",
|
|
98
|
+
".svg": "image/svg+xml",
|
|
99
|
+
".png": "image/png",
|
|
100
|
+
".jpg": "image/jpeg",
|
|
101
|
+
".jpeg": "image/jpeg",
|
|
102
|
+
".gif": "image/gif",
|
|
103
|
+
".webp": "image/webp",
|
|
104
|
+
".ico": "image/x-icon",
|
|
105
|
+
".woff": "font/woff",
|
|
106
|
+
".woff2": "font/woff2",
|
|
107
|
+
".json": JSON_TYPE,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function escapeAttrValue(value: string): string {
|
|
111
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 单个 head 标签 → HTML:attrs 值 true 渲染裸布尔属性,字符串转义后渲染 `key="value"`;script/style 的 children 原样落进标签(闭合序列已在装载期拒绝)。 */
|
|
115
|
+
function renderHeadTagHtml(tag: ResolvedHeadTag, attrs: Record<string, string | true>): string {
|
|
116
|
+
const attrHtml = Object.entries(attrs)
|
|
117
|
+
.map(([name, value]) => (value === true ? ` ${name}` : ` ${name}="${escapeAttrValue(value)}"`))
|
|
118
|
+
.join("");
|
|
119
|
+
if (tag.tag === "meta" || tag.tag === "link") return `<${tag.tag}${attrHtml}>`;
|
|
120
|
+
return `<${tag.tag}${attrHtml}>${tag.children ?? ""}</${tag.tag}>`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* head 标签落成 HTML,本地 src/href 资产进站点清单:`assets/<sha256><ext>`,
|
|
125
|
+
* source 指向原文件(写盘 copyFile、server 原字节下发都二进制安全),标签属性回填该相对路径。
|
|
126
|
+
* 外链(http(s)://)原样透传,不进 assets/。
|
|
127
|
+
*/
|
|
128
|
+
async function materializeHeadAssets(head: ResolvedHeadTag[], files: Map<string, SiteFile>): Promise<string> {
|
|
129
|
+
const rendered: string[] = [];
|
|
130
|
+
for (const tag of head) {
|
|
131
|
+
const attrs = { ...tag.attrs };
|
|
132
|
+
if (tag.localAsset) {
|
|
133
|
+
const bytes = await readFile(tag.localAsset.abs);
|
|
134
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
135
|
+
const path = `assets/${sha256}${tag.localAsset.ext}`;
|
|
136
|
+
files.set(path, {
|
|
137
|
+
path,
|
|
138
|
+
contentType: ASSET_CONTENT_TYPES[tag.localAsset.ext.toLowerCase()] ?? "application/octet-stream",
|
|
139
|
+
source: { kind: "file", abs: tag.localAsset.abs },
|
|
140
|
+
});
|
|
141
|
+
attrs[tag.localAsset.attr] = path;
|
|
142
|
+
}
|
|
143
|
+
rendered.push(renderHeadTagHtml(tag, attrs));
|
|
144
|
+
}
|
|
145
|
+
return rendered.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const TEMPLATE_PLACEHOLDERS = {
|
|
149
|
+
styles: "<!-- __NICEEVAL_STYLES__ -->",
|
|
150
|
+
appCode: "__NICEEVAL_APP_CODE__",
|
|
151
|
+
viewData: "__NICEEVAL_VIEW_DATA_JSON__",
|
|
152
|
+
reportSlot: "<!-- __NICEEVAL_REPORT_SLOT__ -->",
|
|
153
|
+
} as const;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 把 viewData(只含原始值与相对路径,不含宿主机绝对路径)和前端产物烘焙进单个 HTML。
|
|
157
|
+
* 报告槽恒在:每页报告 HTML 作为 <template id="niceeval-report-<pageId>-<locale>"> 静态块
|
|
158
|
+
* 烘在 __NICEEVAL_VIEW_DATA__ 旁(不 hydrate,自定义组件的 <Style> 产物已内联其中),
|
|
159
|
+
* 并恒内联官方组件样式(report/react/styles.css)与渐进增强 runtime(report/react/enhance.js,
|
|
160
|
+
* 内联 <script>:排序 / 过滤 / tooltip,document 级事件委托,报告块被前端搬进槽位也无需重绑;
|
|
161
|
+
* 无 JS 时报告内容依旧完整);外壳声明的 styles 注入在官方样式之后、head 标签(headHtml,
|
|
162
|
+
* 由站点管线物化本地资产后渲染)在外壳 styles 之后、scripts 注入在官方增强脚本之后
|
|
163
|
+
* </body> 前,均按声明顺序(docs/feature/reports/library/shell.md)。
|
|
164
|
+
* 前端只把当前页 / 当前界面语言对应的块摆进报告槽位置,不解析。
|
|
165
|
+
*/
|
|
166
|
+
export async function renderHtml(scan: ViewScan, headHtml = ""): Promise<string> {
|
|
167
|
+
const template = await readViewAsset("template.html");
|
|
168
|
+
const styles = await readViewAsset("client-dist/app.css");
|
|
169
|
+
const app = await readViewAsset("client-dist/app.js");
|
|
170
|
+
const [reportStyles, reportEnhance] = await Promise.all([
|
|
171
|
+
readFile(new URL("../report/react/styles.css", import.meta.url), "utf-8"),
|
|
172
|
+
readFile(new URL("../report/react/enhance.js", import.meta.url), "utf-8"),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const shellStyles = scan.shellAssets.styles.map((css) => `\n<style>\n${css}\n</style>`).join("");
|
|
176
|
+
const shellScripts = scan.shellAssets.scripts.map((js) => `<script>\n${js}\n</script>\n`).join("");
|
|
177
|
+
|
|
178
|
+
const pageTemplates = scan.reportPages
|
|
179
|
+
.flatMap((page) => [
|
|
180
|
+
`<template id="niceeval-report-${page.id}-en">${page.html.en}</template>`,
|
|
181
|
+
`<template id="niceeval-report-${page.id}-zh-CN">${page.html["zh-CN"]}</template>`,
|
|
182
|
+
])
|
|
183
|
+
.join("\n");
|
|
184
|
+
|
|
185
|
+
// 初始 <title> 与 hero 同源:走完回退链的报告标题(viewData.report.title;终点是内置文案
|
|
186
|
+
// 「Eval 运行结果 / Eval Results」)。模板 lang="en",初始按 en 解析;前端按界面语言更新。
|
|
187
|
+
const title = localizeText(scan.viewData.report?.title, "en") ?? "Eval Results";
|
|
188
|
+
|
|
189
|
+
return template
|
|
190
|
+
.replace(
|
|
191
|
+
/<title>[^<]*<\/title>/,
|
|
192
|
+
() => `<title>${title.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</title>`,
|
|
193
|
+
)
|
|
194
|
+
.replace(
|
|
195
|
+
TEMPLATE_PLACEHOLDERS.styles,
|
|
196
|
+
() =>
|
|
197
|
+
`<style>\n${styles}\n</style>\n<style>\n${reportStyles}\n</style>\n<script>\n${reportEnhance}\n</script>` +
|
|
198
|
+
shellStyles +
|
|
199
|
+
(headHtml ? `\n${headHtml}` : ""),
|
|
200
|
+
)
|
|
201
|
+
.replace(TEMPLATE_PLACEHOLDERS.reportSlot, () => pageTemplates)
|
|
202
|
+
.replace(TEMPLATE_PLACEHOLDERS.viewData, () => JSON.stringify(scan.viewData).replace(/</g, "\\u003c"))
|
|
203
|
+
.replace(TEMPLATE_PLACEHOLDERS.appCode, () => JSON.stringify(app).replace(/</g, "\\u003c"))
|
|
204
|
+
.replace("</body>", () => `${shellScripts}</body>`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function readViewAsset(name: string): Promise<string> {
|
|
208
|
+
return readFile(new URL(name, import.meta.url), "utf-8");
|
|
209
|
+
}
|
package/src/view/styles.css
CHANGED
|
@@ -673,6 +673,14 @@ details.span-d > summary.trace-row:hover { background: color-mix(in oklch, var(-
|
|
|
673
673
|
color: var(--bad);
|
|
674
674
|
font-size: 12.5px;
|
|
675
675
|
}
|
|
676
|
+
.ts-skill {
|
|
677
|
+
font-size: 12px;
|
|
678
|
+
}
|
|
679
|
+
.ts-raw .ts-tool { color: var(--soft); }
|
|
680
|
+
.ts-skill .ts-text {
|
|
681
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
682
|
+
color: var(--muted);
|
|
683
|
+
}
|
|
676
684
|
.ts-compaction {
|
|
677
685
|
text-align: center;
|
|
678
686
|
font-size: 11px;
|
|
@@ -925,6 +933,8 @@ a.brand:hover { opacity: 0.75; }
|
|
|
925
933
|
.reply-tool-name { color: var(--warn); }
|
|
926
934
|
.reply-tool-out { color: var(--muted); }
|
|
927
935
|
.reply-err { color: var(--bad); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
936
|
+
.reply-skill { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); }
|
|
937
|
+
.reply-user .reply-text { color: var(--muted); white-space: pre-wrap; word-break: break-word; }
|
|
928
938
|
.reply-empty { color: var(--soft); }
|
|
929
939
|
|
|
930
940
|
.assert-detail { display: flex; flex-direction: column; gap: 9px; }
|
|
@@ -23,7 +23,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
23
23
|
// `instanceof`-incompatible class.
|
|
24
24
|
import { ReportLoadError } from "../../dist/report/load.js";
|
|
25
25
|
import { ViewInputError, loadViewScan, type ViewScan } from "./data.ts";
|
|
26
|
-
import { renderHtml } from "./
|
|
26
|
+
import { renderHtml } from "./site.ts";
|
|
27
27
|
import { buildView, resolveViewInput } from "./index.ts";
|
|
28
28
|
import { runShow } from "../show/index.ts";
|
|
29
29
|
import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type Verdict } from "../types.ts";
|