niceeval 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +77 -45
- package/dist/agents/types.d.ts +28 -6
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/zh-CN.d.ts +3 -1
- package/dist/report/built-in/index.d.ts +3 -2
- package/dist/report/built-in/index.js +7 -8
- package/dist/report/built-in/standard.d.ts +1 -0
- package/dist/report/built-in/standard.js +30 -0
- package/dist/report/components.d.ts +69 -2
- package/dist/report/components.js +152 -3
- package/dist/report/compute.d.ts +28 -1
- package/dist/report/compute.js +123 -0
- package/dist/report/index.d.ts +4 -4
- package/dist/report/index.js +3 -2
- package/dist/report/locale.d.ts +39 -1
- package/dist/report/locale.js +69 -0
- package/dist/report/react/AttemptList.d.ts +3 -1
- package/dist/report/react/AttemptList.js +3 -3
- package/dist/report/react/CopyFixPrompt.d.ts +12 -0
- package/dist/report/react/CopyFixPrompt.js +12 -0
- package/dist/report/react/HeroCard.d.ts +13 -0
- package/dist/report/react/HeroCard.js +35 -0
- package/dist/report/react/PoweredBy.d.ts +5 -0
- package/dist/report/react/PoweredBy.js +7 -0
- package/dist/report/react/ScopeWarnings.d.ts +12 -0
- package/dist/report/react/ScopeWarnings.js +18 -0
- package/dist/report/react/TraceWaterfall.d.ts +14 -0
- package/dist/report/react/TraceWaterfall.js +22 -0
- package/dist/report/react/index.d.ts +6 -1
- package/dist/report/react/index.js +6 -0
- package/dist/report/report.d.ts +22 -6
- package/dist/report/report.js +68 -55
- package/dist/report/scope-warnings.d.ts +28 -0
- package/dist/report/scope-warnings.js +101 -0
- package/dist/report/text/faces.d.ts +19 -1
- package/dist/report/text/faces.js +61 -0
- package/dist/report/tree.d.ts +1 -1
- package/dist/report/tree.js +7 -2
- package/dist/report/types.d.ts +45 -0
- package/dist/report/web.d.ts +5 -4
- package/dist/report/web.js +7 -22
- package/dist/results/select.d.ts +17 -4
- package/dist/results/select.js +76 -11
- package/dist/results/types.d.ts +26 -0
- package/dist/runner/fingerprint.d.ts +3 -3
- package/dist/runner/sandbox-selection.d.ts +12 -0
- package/dist/runner/types.d.ts +18 -6
- package/dist/sandbox/types.d.ts +12 -0
- package/dist/shared/aggregate.d.ts +1 -1
- package/dist/shared/aggregate.js +1 -1
- package/docs-site/zh/explanation/evals.mdx +2 -1
- package/docs-site/zh/explanation/experiment.mdx +2 -0
- package/docs-site/zh/how-to/custom-reports.mdx +23 -7
- package/docs-site/zh/how-to/experiments.mdx +2 -2
- package/docs-site/zh/how-to/publish-report.mdx +11 -4
- package/docs-site/zh/how-to/viewing-results.mdx +17 -8
- package/docs-site/zh/how-to/write-experiment.mdx +40 -1
- package/docs-site/zh/reference/builtin-agents.mdx +40 -3
- package/docs-site/zh/reference/cli.mdx +3 -3
- package/docs-site/zh/reference/define-eval.mdx +8 -0
- package/docs-site/zh/reference/official-adapters.mdx +10 -5
- package/docs-site/zh/troubleshooting/debugging.mdx +3 -3
- package/package.json +2 -1
- package/src/agents/bub.ts +13 -1
- package/src/agents/claude-code.test.ts +43 -1
- package/src/agents/claude-code.ts +32 -14
- package/src/agents/codex.test.ts +168 -1
- package/src/agents/codex.ts +51 -15
- package/src/agents/mcp.ts +31 -0
- package/src/agents/post-setup.ts +33 -0
- package/src/agents/types.ts +28 -7
- package/src/cli.ts +15 -14
- package/src/define.ts +3 -0
- package/src/i18n/en.ts +8 -5
- package/src/i18n/zh-CN.ts +8 -4
- package/src/index.ts +1 -0
- package/src/report/built-in/index.tsx +8 -7
- package/src/report/built-in/standard.tsx +59 -0
- package/src/report/components.tsx +218 -2
- package/src/report/compute.ts +138 -1
- package/src/report/dual-render.test.tsx +141 -14
- package/src/report/index.ts +20 -0
- package/src/report/locale.ts +83 -1
- package/src/report/react/AttemptList.tsx +13 -1
- package/src/report/react/CopyFixPrompt.tsx +37 -0
- package/src/report/react/HeroCard.tsx +59 -0
- package/src/report/react/PoweredBy.tsx +20 -0
- package/src/report/react/ScopeWarnings.tsx +74 -0
- package/src/report/react/TraceWaterfall.tsx +78 -0
- package/src/report/react/enhance.js +14 -0
- package/src/report/react/index.tsx +11 -0
- package/src/report/react/styles.css +193 -7
- package/src/report/report.ts +99 -64
- package/src/report/scope-warnings.ts +155 -0
- package/src/report/site-components.test.tsx +526 -0
- package/src/report/text/faces.ts +66 -0
- package/src/report/tree.ts +10 -3
- package/src/report/types.ts +51 -0
- package/src/report/web.ts +7 -40
- package/src/results/host-equivalence.test.ts +6 -2
- package/src/results/open.ts +5 -4
- package/src/results/results.test.ts +78 -1
- package/src/results/select.ts +80 -12
- package/src/results/types.ts +27 -0
- package/src/runner/attempt.ts +10 -9
- package/src/runner/discover.test.ts +9 -1
- package/src/runner/discover.ts +3 -3
- package/src/runner/fingerprint.ts +9 -4
- package/src/runner/ledger.test.ts +30 -1
- package/src/runner/ledger.ts +26 -4
- package/src/runner/run.ts +5 -1
- package/src/runner/sandbox-selection.test.ts +131 -0
- package/src/runner/sandbox-selection.ts +110 -0
- package/src/runner/types.ts +19 -2
- package/src/sandbox/types.ts +6 -0
- package/src/shared/aggregate.ts +1 -1
- package/src/show/index.ts +19 -12
- package/src/show/render.ts +12 -12
- package/src/show/report-host.test.ts +34 -17
- package/src/show/report-host.ts +6 -5
- package/src/show/show.test.ts +141 -4
- package/src/view/app/App.test.tsx +79 -17
- package/src/view/app/App.tsx +25 -74
- package/src/view/app/components/CopyControls.tsx +4 -42
- package/src/view/app/i18n.ts +5 -227
- package/src/view/app/lib/rows.ts +3 -21
- package/src/view/app/shared.ts +1 -3
- package/src/view/app/types.ts +2 -2
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.ts +21 -10
- package/src/view/index.ts +2 -12
- package/src/view/server.ts +4 -4
- package/src/view/shared/types.ts +11 -6
- package/src/view/styles.css +9 -252
- package/src/view/view-report.test.ts +99 -33
- package/src/view/app/components/LazyArtifact.tsx +0 -51
- package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
- package/src/view/app/pages/AttemptsPage.tsx +0 -80
- package/src/view/app/pages/TracesPage.tsx +0 -35
|
@@ -31,19 +31,23 @@ import {
|
|
|
31
31
|
import { renderReportToStaticHtml } from "./web.ts";
|
|
32
32
|
import {
|
|
33
33
|
AttemptList,
|
|
34
|
+
CopyFixPrompt,
|
|
34
35
|
ExperimentComparison,
|
|
35
36
|
ExperimentList,
|
|
36
37
|
FailureList,
|
|
38
|
+
Hero,
|
|
37
39
|
MetricBars,
|
|
38
40
|
MetricMatrix,
|
|
39
41
|
MetricScatter,
|
|
40
42
|
MetricTable,
|
|
41
43
|
ScopeSummary,
|
|
44
|
+
ScopeWarnings,
|
|
45
|
+
TraceWaterfall,
|
|
42
46
|
} from "./components.tsx";
|
|
43
47
|
import { Col, Row, Section, Style, Tab, Table, Tabs, Text } from "./primitives.tsx";
|
|
44
48
|
import { attemptListData, metricScatterData } from "./compute.ts";
|
|
45
49
|
import { costUSD, defineMetric, endToEndPassRate } from "./metrics.ts";
|
|
46
|
-
import builtInReport from "./built-in/index.tsx";
|
|
50
|
+
import builtInReport, { standard } from "./built-in/index.tsx";
|
|
47
51
|
|
|
48
52
|
// ───────────────────────── fake 数据 ─────────────────────────
|
|
49
53
|
|
|
@@ -437,7 +441,7 @@ describe("ReportNode 形状与非法节点", () => {
|
|
|
437
441
|
// ───────────────────────── 双面同源 ─────────────────────────
|
|
438
442
|
|
|
439
443
|
describe("text/web 双面同源", () => {
|
|
440
|
-
it("双面显示同一份解析终值、覆盖率与 warning
|
|
444
|
+
it("双面显示同一份解析终值、覆盖率与 warning;警告的呈现件是页内 ScopeWarnings 组件,宿主无树外通道", async () => {
|
|
441
445
|
const s = snap({ experimentId: "dual/a", results: [res("q1", "passed"), res("q2", "skipped")] });
|
|
442
446
|
const warning: ScopeWarning = {
|
|
443
447
|
kind: "unfinished-snapshot",
|
|
@@ -448,7 +452,12 @@ describe("text/web 双面同源", () => {
|
|
|
448
452
|
command: "niceeval exp dual/a",
|
|
449
453
|
};
|
|
450
454
|
const scope = scopeOf([s], [warning]);
|
|
451
|
-
const definition = defineReport(
|
|
455
|
+
const definition = defineReport(
|
|
456
|
+
<Col>
|
|
457
|
+
<ScopeWarnings />
|
|
458
|
+
<ScopeSummary />
|
|
459
|
+
</Col>,
|
|
460
|
+
);
|
|
452
461
|
const ctx = { scope, results: resultsOf([s]) };
|
|
453
462
|
const text = await renderReportToText(definition, ctx);
|
|
454
463
|
const html = await renderReportToStaticHtml(definition, ctx);
|
|
@@ -460,6 +469,10 @@ describe("text/web 双面同源", () => {
|
|
|
460
469
|
// web 面把警告的 command 渲染为可复制命令
|
|
461
470
|
expect(html).toContain("niceeval exp dual/a");
|
|
462
471
|
expect(html).toContain("nre-warning-command");
|
|
472
|
+
// 宿主不再前置树外警告块:没有 ScopeWarnings 的树两面都不出现警告
|
|
473
|
+
const bare = defineReport(<ScopeSummary />);
|
|
474
|
+
expect(await renderReportToText(bare, ctx)).not.toContain("snapshot is incomplete");
|
|
475
|
+
expect(await renderReportToStaticHtml(bare, ctx)).not.toContain("nre-warning");
|
|
463
476
|
});
|
|
464
477
|
|
|
465
478
|
it("web 排序/过滤只改变浏览状态:有无 filter prop 数值与行集合相同", async () => {
|
|
@@ -505,6 +518,9 @@ describe("text/web 双面同源", () => {
|
|
|
505
518
|
});
|
|
506
519
|
});
|
|
507
520
|
|
|
521
|
+
// ScopeWarnings 的聚合、排序、折叠与下一步行为在 site-components.test.tsx(组件版单源);
|
|
522
|
+
// 宿主不再有树外警告前置块,这里不重复宿主版。
|
|
523
|
+
|
|
508
524
|
// ───────────────────────── FailureList ─────────────────────────
|
|
509
525
|
|
|
510
526
|
describe("FailureList", () => {
|
|
@@ -710,9 +726,11 @@ describe("defineReport 装载规范化", () => {
|
|
|
710
726
|
expect(fromTree.pages[0]!.title).toEqual(fromContent.pages[0]!.title);
|
|
711
727
|
});
|
|
712
728
|
|
|
713
|
-
it("content 与 pages
|
|
714
|
-
expect(() => defineReport({ content: <ScopeSummary />, pages: [] } as never)).toThrow(
|
|
715
|
-
|
|
729
|
+
it("content 与 pages 同时声明或都省略,装载报错且文案给出 extends: standard 下一步", () => {
|
|
730
|
+
expect(() => defineReport({ content: <ScopeSummary />, pages: [] } as never)).toThrow(
|
|
731
|
+
/"content" and "pages" — declare exactly one/,
|
|
732
|
+
);
|
|
733
|
+
expect(() => defineReport({ title: "X" } as never)).toThrow(/niceeval\/report\/built-in/);
|
|
716
734
|
});
|
|
717
735
|
|
|
718
736
|
it("defineReport 产物不是 ReportNode:页里放产物装载报错;树校验同样拒绝", () => {
|
|
@@ -805,20 +823,72 @@ describe("defineReport 装载规范化", () => {
|
|
|
805
823
|
// ───────────────────────── 内建报告 ─────────────────────────
|
|
806
824
|
|
|
807
825
|
describe("内建报告", () => {
|
|
808
|
-
it("
|
|
826
|
+
it("三页普通 defineReport:页 id、页名与逐页组件构成和 built-in.md 全文一致", () => {
|
|
809
827
|
expect(builtInReport.kind).toBe("report");
|
|
810
|
-
expect(builtInReport.pages).
|
|
828
|
+
expect(builtInReport.pages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
|
|
829
|
+
expect(builtInReport.pages.map((p) => p.title)).toEqual([
|
|
830
|
+
{ en: "Report", "zh-CN": "报告" },
|
|
831
|
+
"Attempts",
|
|
832
|
+
{ en: "Traces", "zh-CN": "追踪" },
|
|
833
|
+
]);
|
|
834
|
+
// 逐页组件构成:每页一个 Col,children 按 built-in.md 全文的声明序,全部是公开组件。
|
|
835
|
+
const childTypes = (content: unknown) => {
|
|
836
|
+
const col = content as { type: unknown; props: { children: Array<{ type: unknown; props: Record<string, unknown> }> } };
|
|
837
|
+
expect(col.type).toBe(Col);
|
|
838
|
+
return col.props.children;
|
|
839
|
+
};
|
|
840
|
+
const [reportPage, attemptsPage, tracesPage] = builtInReport.pages;
|
|
841
|
+
expect(childTypes(reportPage!.content).map((c) => c.type)).toEqual([
|
|
842
|
+
Hero,
|
|
843
|
+
ScopeWarnings,
|
|
844
|
+
CopyFixPrompt,
|
|
845
|
+
ExperimentComparison,
|
|
846
|
+
]);
|
|
847
|
+
const attemptsChildren = childTypes(attemptsPage!.content);
|
|
848
|
+
expect(attemptsChildren.map((c) => c.type)).toEqual([Hero, ScopeWarnings, AttemptList]);
|
|
849
|
+
expect(attemptsChildren[2]!.props.filter).toBe(true);
|
|
850
|
+
expect(childTypes(tracesPage!.content).map((c) => c.type)).toEqual([Hero, ScopeWarnings, TraceWaterfall]);
|
|
851
|
+
});
|
|
811
852
|
|
|
853
|
+
it("与 --report 同内容文件完全等价:同一棵页树经同一条管线渲染出逐字节相同的两面", async () => {
|
|
812
854
|
const s1 = snap({ experimentId: "compare/a", agent: "bub", results: [res("q", "passed")] });
|
|
813
855
|
const s2 = snap({ experimentId: "compare/b", agent: "codex", results: [res("q", "failed")] });
|
|
814
856
|
const scope = scopeOf([s1, s2]);
|
|
815
857
|
const ctx = { scope, results: resultsOf([s1, s2]) };
|
|
816
|
-
|
|
817
|
-
const user =
|
|
818
|
-
|
|
858
|
+
// 用户按 built-in.md 全文自己写同内容的 defineReport(不 import 内建入口)。
|
|
859
|
+
const user = defineReport({
|
|
860
|
+
pages: [
|
|
861
|
+
{
|
|
862
|
+
id: "report",
|
|
863
|
+
title: { en: "Report", "zh-CN": "报告" },
|
|
864
|
+
content: (
|
|
865
|
+
<Col>
|
|
866
|
+
<Hero />
|
|
867
|
+
<ScopeWarnings />
|
|
868
|
+
<CopyFixPrompt />
|
|
869
|
+
<ExperimentComparison />
|
|
870
|
+
</Col>
|
|
871
|
+
),
|
|
872
|
+
},
|
|
873
|
+
{ id: "attempts", title: "Attempts", content: <Col><Hero /><ScopeWarnings /><AttemptList filter /></Col> },
|
|
874
|
+
{
|
|
875
|
+
id: "traces",
|
|
876
|
+
title: { en: "Traces", "zh-CN": "追踪" },
|
|
877
|
+
content: <Col><Hero /><ScopeWarnings /><TraceWaterfall /></Col>,
|
|
878
|
+
},
|
|
879
|
+
],
|
|
880
|
+
});
|
|
881
|
+
for (const pageId of ["report", "attempts", "traces"]) {
|
|
882
|
+
expect(await renderReportToText(builtInReport, ctx, { width: 120, pageId })).toBe(
|
|
883
|
+
await renderReportToText(user, ctx, { width: 120, pageId }),
|
|
884
|
+
);
|
|
885
|
+
expect(await renderReportToStaticHtml(builtInReport, ctx, { pageId })).toBe(
|
|
886
|
+
await renderReportToStaticHtml(user, ctx, { pageId }),
|
|
887
|
+
);
|
|
888
|
+
}
|
|
819
889
|
});
|
|
820
890
|
|
|
821
|
-
it("text
|
|
891
|
+
it("首页 text 面多组时只输出组索引与单组查看命令;单组时输出散点与实验列表;页首是 Hero 的标题与 meta 行", async () => {
|
|
822
892
|
const g1 = snap({ experimentId: "compare/a", results: [res("q", "passed")] });
|
|
823
893
|
const g2 = snap({ experimentId: "dev/b", results: [res("q", "failed")] });
|
|
824
894
|
const multi = await renderReportToText(
|
|
@@ -826,8 +896,11 @@ describe("内建报告", () => {
|
|
|
826
896
|
{ scope: scopeOf([g1, g2]), results: resultsOf([g1, g2]) },
|
|
827
897
|
{ width: 120 },
|
|
828
898
|
);
|
|
829
|
-
|
|
830
|
-
expect(multi).
|
|
899
|
+
// Hero text 面:标题行(回退链终点)+ 最后运行 meta 行在页首
|
|
900
|
+
expect(multi.split("\n")[0]).toBe("Eval Results");
|
|
901
|
+
expect(multi).toContain("Last run");
|
|
902
|
+
expect(multi).toContain("niceeval show --exp compare");
|
|
903
|
+
expect(multi).toContain("niceeval show --exp dev");
|
|
831
904
|
expect(multi).not.toContain("Eval / Attempt"); // 不倾倒组内 experiment 明细
|
|
832
905
|
|
|
833
906
|
const priced = snap({
|
|
@@ -844,3 +917,57 @@ describe("内建报告", () => {
|
|
|
844
917
|
expect(single).toContain("better → upper right");
|
|
845
918
|
});
|
|
846
919
|
});
|
|
920
|
+
|
|
921
|
+
// ───────────────────────── extends 与内建视图集合 ─────────────────────────
|
|
922
|
+
|
|
923
|
+
describe("extends 与内建视图集合", () => {
|
|
924
|
+
it("内建入口是视图集合:默认导出与具名导出 standard 同引用", () => {
|
|
925
|
+
expect(builtInReport).toBe(standard);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it("extends 叠外壳:页列表与 base 逐项同引用,声明整字段覆盖、未声明沿用 base", () => {
|
|
929
|
+
const branded = defineReport({
|
|
930
|
+
extends: standard,
|
|
931
|
+
title: "Memory Evals",
|
|
932
|
+
links: [{ label: "GitHub", href: "https://github.com/you/repo" }],
|
|
933
|
+
});
|
|
934
|
+
branded.pages.forEach((page, i) => expect(page).toBe(standard.pages[i]));
|
|
935
|
+
expect(branded.title).toBe("Memory Evals");
|
|
936
|
+
expect(branded.links).toEqual([{ label: "GitHub", href: "https://github.com/you/repo" }]);
|
|
937
|
+
expect(branded.head).toEqual([...standard.head]); // 未声明沿用 base
|
|
938
|
+
|
|
939
|
+
// 二级 extends 链:上一次折叠的产物就是下一次的 base
|
|
940
|
+
const chained = defineReport({ extends: branded, links: [] });
|
|
941
|
+
chained.pages.forEach((page, i) => expect(page).toBe(standard.pages[i]));
|
|
942
|
+
expect(chained.title).toBe("Memory Evals"); // 未声明沿用最近声明
|
|
943
|
+
expect(chained.links).toEqual([]); // 声明即整字段覆盖,不拼接
|
|
944
|
+
|
|
945
|
+
// ctx.report.title 取 extends 上声明的 title
|
|
946
|
+
const s = snap({ experimentId: "compare/a", results: [res("q", "passed")] });
|
|
947
|
+
expect(buildReportMeta(branded, scopeOf([s]), "report").title).toBe("Memory Evals");
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it("无外壳字段的 extends 与内建逐页两面渲染逐字节相同", async () => {
|
|
951
|
+
const s1 = snap({ experimentId: "compare/a", agent: "bub", results: [res("q", "passed")] });
|
|
952
|
+
const s2 = snap({ experimentId: "compare/b", agent: "codex", results: [res("q", "failed")] });
|
|
953
|
+
const ctx = { scope: scopeOf([s1, s2]), results: resultsOf([s1, s2]) };
|
|
954
|
+
const alias = defineReport({ extends: standard });
|
|
955
|
+
for (const pageId of ["report", "attempts", "traces"]) {
|
|
956
|
+
expect(await renderReportToText(alias, ctx, { width: 120, pageId })).toBe(
|
|
957
|
+
await renderReportToText(builtInReport, ctx, { width: 120, pageId }),
|
|
958
|
+
);
|
|
959
|
+
expect(await renderReportToStaticHtml(alias, ctx, { pageId })).toBe(
|
|
960
|
+
await renderReportToStaticHtml(builtInReport, ctx, { pageId }),
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
it("extends 只收 defineReport 产物;与 content/pages 多选或全省略按完整用户反馈报错", () => {
|
|
966
|
+
// @ts-expect-error 非 defineReport 产物,类型层已拒绝;这里模拟无类型 JS 输入
|
|
967
|
+
expect(() => defineReport({ extends: {} })).toThrow(/must be a defineReport\(\.\.\.\) product/);
|
|
968
|
+
// @ts-expect-error extends 与 pages 互斥
|
|
969
|
+
expect(() => defineReport({ extends: standard, pages: standard.pages })).toThrow(/declare exactly one/);
|
|
970
|
+
// @ts-expect-error content / pages / extends 至少声明一个
|
|
971
|
+
expect(() => defineReport({ title: "x" })).toThrow(/niceeval\/report\/built-in/);
|
|
972
|
+
});
|
|
973
|
+
});
|
package/src/report/index.ts
CHANGED
|
@@ -99,29 +99,39 @@ export { DEFAULT_REPORT_LOCALE, localizedTextEquals, resolveLocalizedText, resol
|
|
|
99
99
|
export type { LocalizedText, ReportLocale } from "./locale.ts";
|
|
100
100
|
|
|
101
101
|
// 官方双面组件(spec / data 双形态;配套 *Data 计算函数在下面成对导出)
|
|
102
|
+
// 与站点组件(Hero / HeroCard / PoweredBy / ScopeWarnings / CopyFixPrompt / TraceWaterfall)
|
|
102
103
|
export {
|
|
103
104
|
AttemptList,
|
|
105
|
+
CopyFixPrompt,
|
|
104
106
|
DeltaTable,
|
|
105
107
|
EvalList,
|
|
106
108
|
ExperimentComparison,
|
|
107
109
|
ExperimentList,
|
|
108
110
|
FailureList,
|
|
111
|
+
Hero,
|
|
112
|
+
HeroCard,
|
|
109
113
|
MetricBars,
|
|
110
114
|
MetricLine,
|
|
111
115
|
MetricMatrix,
|
|
112
116
|
MetricScatter,
|
|
113
117
|
MetricTable,
|
|
118
|
+
PoweredBy,
|
|
114
119
|
Scoreboard,
|
|
115
120
|
ScopeSummary,
|
|
121
|
+
ScopeWarnings,
|
|
122
|
+
TraceWaterfall,
|
|
116
123
|
} from "./components.tsx";
|
|
117
124
|
export type {
|
|
118
125
|
AttemptListProps,
|
|
126
|
+
CopyFixPromptProps,
|
|
119
127
|
DataProps,
|
|
120
128
|
DeltaTableProps,
|
|
121
129
|
EvalListProps,
|
|
122
130
|
ExperimentComparisonProps,
|
|
123
131
|
ExperimentListProps,
|
|
124
132
|
FailureListProps,
|
|
133
|
+
HeroCardProps,
|
|
134
|
+
HeroProps,
|
|
125
135
|
MetricBarsProps,
|
|
126
136
|
MetricLineProps,
|
|
127
137
|
MetricMatrixProps,
|
|
@@ -129,23 +139,29 @@ export type {
|
|
|
129
139
|
MetricTableProps,
|
|
130
140
|
ScoreboardProps,
|
|
131
141
|
ScopeSummaryProps,
|
|
142
|
+
ScopeWarningsProps,
|
|
143
|
+
TraceWaterfallProps,
|
|
132
144
|
} from "./components.tsx";
|
|
133
145
|
|
|
134
146
|
// 计算函数(组件解析面的具名形式,与组件成对;spec 形态下由管线代调,data 形态与
|
|
135
147
|
// 嵌入场景下由作者手工调)
|
|
136
148
|
export {
|
|
137
149
|
attemptListData,
|
|
150
|
+
copyFixPromptData,
|
|
138
151
|
deltaTableData,
|
|
139
152
|
evalListData,
|
|
140
153
|
experimentComparisonData,
|
|
141
154
|
experimentListData,
|
|
155
|
+
heroData,
|
|
142
156
|
metricLineData,
|
|
143
157
|
metricMatrixData,
|
|
144
158
|
metricScatterData,
|
|
145
159
|
metricTableData,
|
|
146
160
|
pairsByFlag,
|
|
147
161
|
scopeSummaryData,
|
|
162
|
+
scopeWarningsData,
|
|
148
163
|
scoreboardData,
|
|
164
|
+
traceWaterfallData,
|
|
149
165
|
} from "./compute.ts";
|
|
150
166
|
export type {
|
|
151
167
|
DeltaTableOptions,
|
|
@@ -162,6 +178,7 @@ export type {
|
|
|
162
178
|
AttemptListItem,
|
|
163
179
|
AttemptLocator,
|
|
164
180
|
BuiltInDimension,
|
|
181
|
+
CopyFixPromptData,
|
|
165
182
|
CustomDimension,
|
|
166
183
|
DeltaData,
|
|
167
184
|
DeltaPair,
|
|
@@ -174,6 +191,7 @@ export type {
|
|
|
174
191
|
ExperimentListEvalRow,
|
|
175
192
|
ExperimentListItem,
|
|
176
193
|
FlagPairs,
|
|
194
|
+
HeroData,
|
|
177
195
|
LineData,
|
|
178
196
|
MatrixData,
|
|
179
197
|
Metric,
|
|
@@ -190,6 +208,8 @@ export type {
|
|
|
190
208
|
ScopeWarning,
|
|
191
209
|
ScoreboardData,
|
|
192
210
|
TableData,
|
|
211
|
+
TraceSpanSummary,
|
|
212
|
+
TraceWaterfallRow,
|
|
193
213
|
VerdictTally,
|
|
194
214
|
} from "./types.ts";
|
|
195
215
|
|
package/src/report/locale.ts
CHANGED
|
@@ -146,6 +146,49 @@ const en = {
|
|
|
146
146
|
"delta.pairHeader": "pair (A → B)",
|
|
147
147
|
"delta.empty": "{experiments} experiments, 0 comparable pairs",
|
|
148
148
|
|
|
149
|
+
/** ScopeWarnings 聚合层的 chrome:汇总行、kind 徽标、组头与明细折叠标签;message 本体不经字典。 */
|
|
150
|
+
"warnings.summary.experiments.one": "{n} experiment flagged",
|
|
151
|
+
"warnings.summary.experiments.other": "{n} experiments flagged",
|
|
152
|
+
"warnings.group.unreadableSnapshot.one": "{n} snapshot skipped",
|
|
153
|
+
"warnings.group.unreadableSnapshot.other": "{n} snapshots skipped",
|
|
154
|
+
"warnings.details.one": "{n} warning",
|
|
155
|
+
"warnings.details.other": "{n} warnings",
|
|
156
|
+
"warnings.badge.partialCoverage": "coverage {covered}/{total}",
|
|
157
|
+
"warnings.badge.staleSnapshot": "{gap} behind",
|
|
158
|
+
"warnings.badge.unfinishedSnapshot": "unfinished",
|
|
159
|
+
"warnings.gap.second.one": "{n} second",
|
|
160
|
+
"warnings.gap.second.other": "{n} seconds",
|
|
161
|
+
"warnings.gap.minute.one": "{n} minute",
|
|
162
|
+
"warnings.gap.minute.other": "{n} minutes",
|
|
163
|
+
"warnings.gap.hour.one": "{n} hour",
|
|
164
|
+
"warnings.gap.hour.other": "{n} hours",
|
|
165
|
+
"warnings.gap.day.one": "{n} day",
|
|
166
|
+
"warnings.gap.day.other": "{n} days",
|
|
167
|
+
|
|
168
|
+
/** Hero / HeroCard 的运行 meta(hero.noRuns 是 latestStartedAt 为 null 时的内置文案)。 */
|
|
169
|
+
"hero.lastRun": "Last run {time}",
|
|
170
|
+
"hero.noRuns": "No runs yet",
|
|
171
|
+
/** web 面的合成来源标注(仅 snapshots > 1 时显示)。 */
|
|
172
|
+
"hero.composedRuns": "composed from {n} runs",
|
|
173
|
+
/** text 面的合成来源标注(show 页首 meta 行,仅 snapshots > 1 时显示)。 */
|
|
174
|
+
"hero.composedSnapshots": "composed from {n} snapshots",
|
|
175
|
+
|
|
176
|
+
/** CopyFixPrompt 的 web 面 chrome(prompt 本身面向 agent、固定英文,不经词典)。 */
|
|
177
|
+
"copyFixPrompt.summary.one": "Fix prompt · {n} failure",
|
|
178
|
+
"copyFixPrompt.summary.other": "Fix prompt · {n} failures",
|
|
179
|
+
"copyFixPrompt.copy": "Copy fix prompt",
|
|
180
|
+
|
|
181
|
+
/** TraceWaterfall 的 chrome。 */
|
|
182
|
+
"traceWaterfall.empty": "No attempts",
|
|
183
|
+
"traceWaterfall.noTrace": "no trace",
|
|
184
|
+
"traceWaterfall.spans.one": "{n} span",
|
|
185
|
+
"traceWaterfall.spans.other": "{n} spans",
|
|
186
|
+
"traceWaterfall.failedSpans.one": "{n} failed",
|
|
187
|
+
"traceWaterfall.failedSpans.other": "{n} failed",
|
|
188
|
+
|
|
189
|
+
/** AttemptList 的 web 面过滤框占位符(filter 渐进增强)。 */
|
|
190
|
+
"attemptList.filterPlaceholder": "Filter attempts…",
|
|
191
|
+
|
|
149
192
|
"tabs.tab": "Tab",
|
|
150
193
|
} as const;
|
|
151
194
|
|
|
@@ -245,6 +288,42 @@ const zhCN: Record<ReportMessageKey, string> = {
|
|
|
245
288
|
"delta.pairHeader": "对比 (A → B)",
|
|
246
289
|
"delta.empty": "{experiments} 个实验、0 个可配对",
|
|
247
290
|
|
|
291
|
+
"warnings.summary.experiments.one": "{n} 个实验的数字带警告",
|
|
292
|
+
"warnings.summary.experiments.other": "{n} 个实验的数字带警告",
|
|
293
|
+
"warnings.group.unreadableSnapshot.one": "{n} 个快照被跳过",
|
|
294
|
+
"warnings.group.unreadableSnapshot.other": "{n} 个快照被跳过",
|
|
295
|
+
"warnings.details.one": "{n} 条原始警告",
|
|
296
|
+
"warnings.details.other": "{n} 条原始警告",
|
|
297
|
+
"warnings.badge.partialCoverage": "覆盖 {covered}/{total}",
|
|
298
|
+
"warnings.badge.staleSnapshot": "落后 {gap}",
|
|
299
|
+
"warnings.badge.unfinishedSnapshot": "未收尾",
|
|
300
|
+
"warnings.gap.second.one": "{n} 秒",
|
|
301
|
+
"warnings.gap.second.other": "{n} 秒",
|
|
302
|
+
"warnings.gap.minute.one": "{n} 分钟",
|
|
303
|
+
"warnings.gap.minute.other": "{n} 分钟",
|
|
304
|
+
"warnings.gap.hour.one": "{n} 小时",
|
|
305
|
+
"warnings.gap.hour.other": "{n} 小时",
|
|
306
|
+
"warnings.gap.day.one": "{n} 天",
|
|
307
|
+
"warnings.gap.day.other": "{n} 天",
|
|
308
|
+
|
|
309
|
+
"hero.lastRun": "最后运行 {time}",
|
|
310
|
+
"hero.noRuns": "暂无运行",
|
|
311
|
+
"hero.composedRuns": "由 {n} 次运行合成",
|
|
312
|
+
"hero.composedSnapshots": "由 {n} 份快照合成",
|
|
313
|
+
|
|
314
|
+
"copyFixPrompt.summary.one": "修复 prompt · {n} 个失败",
|
|
315
|
+
"copyFixPrompt.summary.other": "修复 prompt · {n} 个失败",
|
|
316
|
+
"copyFixPrompt.copy": "复制修复 prompt",
|
|
317
|
+
|
|
318
|
+
"traceWaterfall.empty": "没有 attempt",
|
|
319
|
+
"traceWaterfall.noTrace": "无 trace",
|
|
320
|
+
"traceWaterfall.spans.one": "{n} 个 span",
|
|
321
|
+
"traceWaterfall.spans.other": "{n} 个 span",
|
|
322
|
+
"traceWaterfall.failedSpans.one": "{n} 个失败",
|
|
323
|
+
"traceWaterfall.failedSpans.other": "{n} 个失败",
|
|
324
|
+
|
|
325
|
+
"attemptList.filterPlaceholder": "筛选 attempt…",
|
|
326
|
+
|
|
248
327
|
"tabs.tab": "Tab",
|
|
249
328
|
};
|
|
250
329
|
|
|
@@ -276,7 +355,10 @@ export function countText(
|
|
|
276
355
|
| "scoreboard.unscorable"
|
|
277
356
|
| "scoreboard.ignored"
|
|
278
357
|
| "entityList.moreFailures"
|
|
279
|
-
| "table.columnsHidden"
|
|
358
|
+
| "table.columnsHidden"
|
|
359
|
+
| "copyFixPrompt.summary"
|
|
360
|
+
| "traceWaterfall.spans"
|
|
361
|
+
| "traceWaterfall.failedSpans",
|
|
280
362
|
n: number,
|
|
281
363
|
): string {
|
|
282
364
|
return localeText(locale, `${base}.${n === 1 ? "one" : "other"}` as ReportMessageKey, { n });
|
|
@@ -53,7 +53,8 @@ export function AttemptRow({
|
|
|
53
53
|
}): ReactElement {
|
|
54
54
|
const reason = failureSummaryText(item, locale);
|
|
55
55
|
return (
|
|
56
|
-
|
|
56
|
+
// data-nre-verdict:filter 增强按 verdict 词收窄行的匹配源(✓/✗ 判定符不含词面)
|
|
57
|
+
<li className={cx("nre-attempt", `nre-attempt-${item.verdict}`)} data-nre-verdict={item.verdict}>
|
|
57
58
|
<div className="nre-attempt-head">
|
|
58
59
|
<AttemptLocatorBadge item={item} attemptHref={attemptHref} />
|
|
59
60
|
<span className="nre-attempt-eval">{item.evalId}</span>
|
|
@@ -72,6 +73,7 @@ export function AttemptRow({
|
|
|
72
73
|
export function AttemptList({
|
|
73
74
|
data,
|
|
74
75
|
total,
|
|
76
|
+
filter = false,
|
|
75
77
|
attemptHref = DEFAULT_ATTEMPT_HREF,
|
|
76
78
|
className,
|
|
77
79
|
locale = DEFAULT_REPORT_LOCALE,
|
|
@@ -79,6 +81,8 @@ export function AttemptList({
|
|
|
79
81
|
data: readonly AttemptListItem[];
|
|
80
82
|
/** data 被 slice 时的原始数量;如实显示还剩多少条没展示。 */
|
|
81
83
|
total?: number;
|
|
84
|
+
/** web 面加过滤输入框(按 experiment、eval、agent、verdict 或摘要文本收窄行);渐进增强,不改变数据。 */
|
|
85
|
+
filter?: boolean;
|
|
82
86
|
attemptHref?: (locator: AttemptLocator) => string;
|
|
83
87
|
className?: string;
|
|
84
88
|
locale?: ReportLocale;
|
|
@@ -86,6 +90,14 @@ export function AttemptList({
|
|
|
86
90
|
const remaining = (total ?? data.length) - data.length;
|
|
87
91
|
return (
|
|
88
92
|
<section className={cx("nre", "nre-attempt-list", className)}>
|
|
93
|
+
{filter && (
|
|
94
|
+
<input
|
|
95
|
+
className="nre-filter"
|
|
96
|
+
data-nre-attempt-filter=""
|
|
97
|
+
type="search"
|
|
98
|
+
placeholder={localeText(locale, "attemptList.filterPlaceholder")}
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
89
101
|
{data.length === 0 && <p className="nre-attempt-list-empty">{localeText(locale, "attemptList.empty")}</p>}
|
|
90
102
|
<ul className="nre-attempts">
|
|
91
103
|
{data.map((item) => (
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// CopyFixPrompt:批量修复 prompt 的 web 面。prompt 在 resolve 阶段算好、烘进静态 HTML;
|
|
2
|
+
// 无 JS 时 prompt 全文在原生 <details> 折叠块里完整可读,「复制到剪贴板」是 enhance.js 的
|
|
3
|
+
// 增强行为(data-nre-copy),增强只加浏览行为、不改内容。failures 为 0 时零输出
|
|
4
|
+
// (docs/feature/reports/library/site-components.md「CopyFixPrompt」)。
|
|
5
|
+
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import type { CopyFixPromptData } from "../types.ts";
|
|
8
|
+
import { DEFAULT_REPORT_LOCALE, countText, localeText, type ReportLocale } from "../locale.ts";
|
|
9
|
+
import { cx } from "./format.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 批量修复 prompt(纯 web 渲染面):折叠块内完整 prompt 文本 + 复制按钮(增强层)。
|
|
13
|
+
* 嵌入自有 React 页面时配合 `copyFixPromptData()` 使用;failures 为 0 返回 null。
|
|
14
|
+
*/
|
|
15
|
+
export function CopyFixPrompt({
|
|
16
|
+
data,
|
|
17
|
+
className,
|
|
18
|
+
locale = DEFAULT_REPORT_LOCALE,
|
|
19
|
+
}: {
|
|
20
|
+
data: CopyFixPromptData;
|
|
21
|
+
className?: string;
|
|
22
|
+
locale?: ReportLocale;
|
|
23
|
+
}): ReactElement | null {
|
|
24
|
+
if (data.failures === 0) return null;
|
|
25
|
+
return (
|
|
26
|
+
<details className={cx("nre", "nre-copy-fix-prompt", className)}>
|
|
27
|
+
<summary className="nre-copy-fix-prompt-summary">
|
|
28
|
+
{countText(locale, "copyFixPrompt.summary", data.failures)}
|
|
29
|
+
</summary>
|
|
30
|
+
{/* 无 JS 时按钮静默无功能(与过滤框同一约定);prompt 文本本身始终完整可读 */}
|
|
31
|
+
<button type="button" className="nre-copy-fix-prompt-copy" data-nre-copy={data.prompt}>
|
|
32
|
+
{localeText(locale, "copyFixPrompt.copy")}
|
|
33
|
+
</button>
|
|
34
|
+
<pre className="nre-copy-fix-prompt-text">{data.prompt}</pre>
|
|
35
|
+
</details>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// HeroCard:站点标题区的 web 面——hero 标题(h1)、运行 meta(最后运行时间按渲染 locale
|
|
2
|
+
// 格式化;latestStartedAt 为 null 时内置「暂无运行」文案;snapshots > 1 时标注合成来源)
|
|
3
|
+
// 与品牌行(等同 PoweredBy,恒含、无拆除 prop)。标题输入是站点声明与 Scope 的合成物,
|
|
4
|
+
// 组件只收 data 形态(docs/feature/reports/library/site-components.md「HeroCard」)。
|
|
5
|
+
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import type { HeroData } from "../types.ts";
|
|
8
|
+
import { DEFAULT_REPORT_LOCALE, localeText, resolveLocalizedText, type LocalizedText, type ReportLocale } from "../locale.ts";
|
|
9
|
+
import { cx } from "./format.ts";
|
|
10
|
+
import { PoweredBy } from "./PoweredBy.tsx";
|
|
11
|
+
|
|
12
|
+
/** ISO 时间 → 按 locale 的「最后运行」显示(年月日 时:分);不可解析原样返回。 */
|
|
13
|
+
function formatLastRun(iso: string, locale: ReportLocale): string {
|
|
14
|
+
const date = new Date(iso);
|
|
15
|
+
if (Number.isNaN(date.valueOf())) return iso;
|
|
16
|
+
try {
|
|
17
|
+
return new Intl.DateTimeFormat(locale, {
|
|
18
|
+
year: "numeric",
|
|
19
|
+
month: "short",
|
|
20
|
+
day: "numeric",
|
|
21
|
+
hour: "2-digit",
|
|
22
|
+
minute: "2-digit",
|
|
23
|
+
}).format(date);
|
|
24
|
+
} catch {
|
|
25
|
+
return iso;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 站点标题区(纯 web 渲染面):`<h1>` 标题 + meta 行 + 品牌行。
|
|
31
|
+
* 嵌入自有 React 页面时配合 `heroData()` 使用。
|
|
32
|
+
*/
|
|
33
|
+
export function HeroCard({
|
|
34
|
+
title,
|
|
35
|
+
data,
|
|
36
|
+
className,
|
|
37
|
+
locale = DEFAULT_REPORT_LOCALE,
|
|
38
|
+
}: {
|
|
39
|
+
title: LocalizedText;
|
|
40
|
+
data: HeroData;
|
|
41
|
+
className?: string;
|
|
42
|
+
locale?: ReportLocale;
|
|
43
|
+
}): ReactElement {
|
|
44
|
+
const meta =
|
|
45
|
+
data.latestStartedAt === null
|
|
46
|
+
? localeText(locale, "hero.noRuns")
|
|
47
|
+
: [
|
|
48
|
+
localeText(locale, "hero.lastRun", { time: formatLastRun(data.latestStartedAt, locale) }),
|
|
49
|
+
...(data.snapshots > 1 ? [localeText(locale, "hero.composedRuns", { n: data.snapshots })] : []),
|
|
50
|
+
].join(" · ");
|
|
51
|
+
return (
|
|
52
|
+
<header className={cx("nre", "nre-hero", className)}>
|
|
53
|
+
<h1 className="nre-hero-title">{resolveLocalizedText(title, locale)}</h1>
|
|
54
|
+
<p className="nre-hero-meta">{meta}</p>
|
|
55
|
+
{/* 品牌行与 PoweredBy 同一渲染:品牌跟着组件走,不区分官方宿主与嵌入页面 */}
|
|
56
|
+
<PoweredBy />
|
|
57
|
+
</header>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// PoweredBy:唯一的品牌件的 web 面。品牌契约是「提供一个组件,不给开关」——无 props、
|
|
2
|
+
// 无关闭配置;不想要品牌 = 不用它(docs/feature/reports/library/site-components.md「PoweredBy」)。
|
|
3
|
+
// rel 只声明 noopener 以保留 Referer(默认策略只发 origin):官网统计由此得知点击来自哪个
|
|
4
|
+
// 报告站点;静态导出在构建期不知道自己托管在哪个域名,来源不进 URL 参数。
|
|
5
|
+
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
|
|
8
|
+
/** 品牌行的固定去处:niceeval 官网,utm 标记「来自报告的品牌行」。 */
|
|
9
|
+
export const POWERED_BY_HREF = "https://niceeval.com/?utm_source=report&utm_medium=powered-by";
|
|
10
|
+
|
|
11
|
+
/** 一行品牌色小字 `Powered by NiceEval`,链接官网;HeroCard 的品牌行与它同一渲染。 */
|
|
12
|
+
export function PoweredBy(): ReactElement {
|
|
13
|
+
return (
|
|
14
|
+
<p className="nre nre-powered-by">
|
|
15
|
+
<a href={POWERED_BY_HREF} target="_blank" rel="noopener">
|
|
16
|
+
Powered by NiceEval
|
|
17
|
+
</a>
|
|
18
|
+
</p>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ScopeWarnings:选择警告区的 web 面。按「下一步动作」聚合(../scope-warnings.ts,
|
|
2
|
+
// 与 text 面共用同一聚合层):整个警告区是默认折叠的原生 <details>,<summary> 是
|
|
3
|
+
// 分类计数汇总行(恒可见);展开后每组组头 = 标题 + kind 徽标 + 去重后恰一条的
|
|
4
|
+
// 可复制命令,逐条原始 message 收进第二层 <details>(总条数 ≤ 3 默认展开)。
|
|
5
|
+
// 空警告集零输出,不渲染空容器
|
|
6
|
+
// (docs/feature/reports/library/site-components.md「ScopeWarnings」)。
|
|
7
|
+
|
|
8
|
+
import type { ReactElement } from "react";
|
|
9
|
+
import type { ScopeWarning } from "../types.ts";
|
|
10
|
+
import { DEFAULT_REPORT_LOCALE, type ReportLocale } from "../locale.ts";
|
|
11
|
+
import { groupScopeWarnings, warningDetailsLabel } from "../scope-warnings.ts";
|
|
12
|
+
import { cx } from "./format.ts";
|
|
13
|
+
|
|
14
|
+
/** 命令的可复制块:无 JS 时点击全选(user-select: all),enhance.js 在场时点击复制。 */
|
|
15
|
+
function CommandBlock({ command }: { command: string }): ReactElement {
|
|
16
|
+
return (
|
|
17
|
+
<code className="nre-warning-command" data-nre-copy={command}>
|
|
18
|
+
{command}
|
|
19
|
+
</code>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 选择警告区(纯 web 渲染面):按动作聚合的警告组。嵌入自有 React 页面时传
|
|
25
|
+
* `data={scope.warnings}`;空集返回 null,不渲染空容器。
|
|
26
|
+
*/
|
|
27
|
+
export function ScopeWarnings({
|
|
28
|
+
data,
|
|
29
|
+
className,
|
|
30
|
+
locale = DEFAULT_REPORT_LOCALE,
|
|
31
|
+
}: {
|
|
32
|
+
data: readonly ScopeWarning[];
|
|
33
|
+
className?: string;
|
|
34
|
+
locale?: ReportLocale;
|
|
35
|
+
}): ReactElement | null {
|
|
36
|
+
if (data.length === 0) return null;
|
|
37
|
+
const { summary, groups, detailsOpen } = groupScopeWarnings(data, locale);
|
|
38
|
+
return (
|
|
39
|
+
<div className={cx("nre", "nre-scope-warnings", className)}>
|
|
40
|
+
<details className="nre-warnings">
|
|
41
|
+
<summary className="nre-warnings-summary">{summary}</summary>
|
|
42
|
+
<ul className="nre-warning-groups">
|
|
43
|
+
{groups.map((group, i) => (
|
|
44
|
+
<li key={i} className="nre-warning-group" data-category={group.category}>
|
|
45
|
+
<div className="nre-warning-head">
|
|
46
|
+
<span className="nre-warning-title">{group.title}</span>
|
|
47
|
+
{group.badges.map((badge, j) => (
|
|
48
|
+
<span key={j} className="nre-warning-badge" data-kind={badge.kind}>
|
|
49
|
+
{badge.text}
|
|
50
|
+
</span>
|
|
51
|
+
))}
|
|
52
|
+
{group.headCommand !== null && <CommandBlock command={group.headCommand} />}
|
|
53
|
+
</div>
|
|
54
|
+
<details className="nre-warning-details" open={detailsOpen || undefined}>
|
|
55
|
+
<summary>{warningDetailsLabel(locale, group.warnings.length)}</summary>
|
|
56
|
+
<ul>
|
|
57
|
+
{group.warnings.map((w, j) => (
|
|
58
|
+
<li key={j} className="nre-warning" data-kind={w.kind}>
|
|
59
|
+
{w.message}
|
|
60
|
+
{/* 组头命令的含义是「复制即推进整组」;去重后多于一条时命令随明细逐条走 */}
|
|
61
|
+
{group.headCommand === null && "command" in w && w.command !== undefined && (
|
|
62
|
+
<CommandBlock command={w.command} />
|
|
63
|
+
)}
|
|
64
|
+
</li>
|
|
65
|
+
))}
|
|
66
|
+
</ul>
|
|
67
|
+
</details>
|
|
68
|
+
</li>
|
|
69
|
+
))}
|
|
70
|
+
</ul>
|
|
71
|
+
</details>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|