285 lines
9.1 KiB
TypeScript
285 lines
9.1 KiB
TypeScript
import type { AnswerUiBlock } from "@/features/agent";
|
||
import type { InvestigationPresentationCommand } from "../api/investigation-presentation";
|
||
import type { InvestigationMapTarget } from "../api/investigation-report";
|
||
import {
|
||
createInvestigationDevSample,
|
||
createPresentationRecord,
|
||
createUiRecord,
|
||
normalizeDevCenter,
|
||
serializeInvestigationDevJsonl
|
||
} from "./investigation-dev-preview";
|
||
|
||
export const investigationDevCases = [
|
||
{
|
||
id: "combined",
|
||
label: "组合联动",
|
||
expected: "合成空间点、压力曲线、候选表格与数据一同展示;有调查地图目标时同时高亮。"
|
||
},
|
||
{
|
||
id: "map_targets",
|
||
label: "地图定位与高亮",
|
||
expected: "显示地图中心附近的合成点,并高亮所选调查已有的地图目标;无目标时仅显示合成点。"
|
||
},
|
||
{
|
||
id: "pressure_line",
|
||
label: "压力趋势对比",
|
||
expected: "打开两条合成压力曲线,核对时间轴、单位、图例和数据表。"
|
||
},
|
||
{
|
||
id: "impact_bar",
|
||
label: "影响用户统计",
|
||
expected: "打开合成用户数柱状图;统计仅供展示测试,不是实际停水户数。"
|
||
},
|
||
{
|
||
id: "impact_area",
|
||
label: "合成影响范围",
|
||
expected: "在当前地图中心附近显示一个合成面;不代表真实隔离边界。"
|
||
},
|
||
{
|
||
id: "complaint_heatmap",
|
||
label: "投诉热力分布",
|
||
expected: "显示三个带非负权重的合成投诉点,按热力模式渲染。"
|
||
},
|
||
{
|
||
id: "candidates_table",
|
||
label: "候选管段对比",
|
||
expected: "展示两条合成候选数据和对比表格,不查询或高亮虚拟管段。"
|
||
},
|
||
{
|
||
id: "valves_table",
|
||
label: "隔离阀门清单",
|
||
expected: "展示两条合成阀门数据和核查表格,不执行关阀或发布处置方案。"
|
||
},
|
||
{
|
||
id: "data_only",
|
||
label: "纯 JSONL 数据",
|
||
expected: "仅展示候选与影响数据;数据行中出现 action 字段也不会触发地图动作。"
|
||
},
|
||
{
|
||
id: "invalid_json",
|
||
label: "错误:JSON 语法",
|
||
expected: "第 2 行报语法错误,整批不执行;可点击执行验证拦截结果。"
|
||
},
|
||
{
|
||
id: "invalid_command",
|
||
label: "错误:不支持的动作",
|
||
expected: "第 1 行展示指令被拒绝,不执行任意命令。"
|
||
},
|
||
{
|
||
id: "invalid_chart",
|
||
label: "错误:图表数据不齐",
|
||
expected: "第 1 行图表校验失败,时间轴和序列长度不一致时不能执行。"
|
||
}
|
||
] as const;
|
||
|
||
export type InvestigationDevCaseId = (typeof investigationDevCases)[number]["id"];
|
||
|
||
const jsonl = serializeInvestigationDevJsonl;
|
||
const presentation = createPresentationRecord;
|
||
// Invalid cases deliberately bypass the validated writer to exercise rejection.
|
||
const invalidJsonl = (records: unknown[]) =>
|
||
records.map((record) => JSON.stringify(record)).join("\n");
|
||
const data = (record_type: string, fields: Record<string, unknown>) => ({
|
||
record_type,
|
||
origin: "synthetic",
|
||
source_evidence_refs: [],
|
||
...fields
|
||
});
|
||
|
||
function table(id: string, title: string, columns: string[], rows: string[][]): unknown {
|
||
const block: AnswerUiBlock = {
|
||
version: "answer-ui@1",
|
||
block_id: `dev-case-${id}`,
|
||
fallback_text: `${title},仅供本地展示测试。`,
|
||
spec: {
|
||
root: "table",
|
||
elements: {
|
||
table: { type: "Table", props: { title, columns, rows }, children: [] }
|
||
}
|
||
}
|
||
};
|
||
return createUiRecord(block);
|
||
}
|
||
|
||
/** Fixtures use current map coordinates and existing task targets, never invented query IDs. */
|
||
export function createInvestigationDevCase(
|
||
caseId: InvestigationDevCaseId,
|
||
center: [number, number],
|
||
target?: InvestigationMapTarget
|
||
): string {
|
||
const [longitude, latitude] = normalizeDevCenter(center);
|
||
if (caseId === "combined") return createInvestigationDevSample([longitude, latitude], target);
|
||
const point = (dx = 0, dy = 0): [number, number] => [
|
||
Math.max(-180, Math.min(180, longitude + dx)),
|
||
latitude + dy
|
||
];
|
||
const overlay = (
|
||
mode: "geometry" | "heatmap",
|
||
title: string,
|
||
features: Extract<
|
||
InvestigationPresentationCommand,
|
||
{ action: "set_overlay" }
|
||
>["data"]["features"]
|
||
) =>
|
||
presentation({
|
||
action: "set_overlay",
|
||
overlay_id: `dev-case-${caseId}`,
|
||
title,
|
||
mode,
|
||
evidence_id: "dev:synthetic",
|
||
data: { type: "FeatureCollection", features }
|
||
});
|
||
|
||
switch (caseId) {
|
||
case "map_targets":
|
||
return jsonl([
|
||
...(target
|
||
? [
|
||
presentation({
|
||
action: "highlight_features",
|
||
layer: target.source_id,
|
||
feature_ids: [target.feature_id],
|
||
effect: "pulse"
|
||
})
|
||
]
|
||
: []),
|
||
overlay("geometry", "本地合成定位点", [
|
||
{
|
||
type: "Feature",
|
||
properties: { origin: "synthetic" },
|
||
geometry: { type: "Point", coordinates: point() }
|
||
}
|
||
])
|
||
]);
|
||
case "pressure_line":
|
||
return jsonl([
|
||
presentation({
|
||
action: "open_chart",
|
||
chart_id: "dev-case-pressure-line",
|
||
evidence_refs: ["dev:synthetic"],
|
||
chart: {
|
||
title: "本地合成压力对比",
|
||
chart_type: "line",
|
||
x_data: ["08:00", "08:15", "08:30", "08:45"],
|
||
series: [
|
||
{ name: "异常日压力水头(m)", data: [32, 18, 22, 30] },
|
||
{ name: "基线压力水头(m)", data: [33, 32, 32, 33] }
|
||
]
|
||
}
|
||
})
|
||
]);
|
||
case "impact_bar":
|
||
return jsonl([
|
||
presentation({
|
||
action: "open_chart",
|
||
chart_id: "dev-case-impact-bar",
|
||
evidence_refs: ["dev:synthetic"],
|
||
chart: {
|
||
title: "本地合成影响用户统计",
|
||
chart_type: "bar",
|
||
x_data: ["合成片区 A", "合成片区 B", "合成片区 C"],
|
||
series: [{ name: "合成用户数(户)", data: [120, 80, 45] }]
|
||
}
|
||
})
|
||
]);
|
||
case "impact_area":
|
||
return jsonl([
|
||
overlay("geometry", "本地合成影响范围", [
|
||
{
|
||
type: "Feature",
|
||
properties: { origin: "synthetic", purpose: "display_test_only" },
|
||
geometry: {
|
||
type: "Polygon",
|
||
coordinates: [
|
||
[
|
||
point(-0.002, -0.0015),
|
||
point(0.002, -0.0015),
|
||
point(0.002, 0.0015),
|
||
point(-0.002, 0.0015),
|
||
point(-0.002, -0.0015)
|
||
]
|
||
]
|
||
}
|
||
}
|
||
])
|
||
]);
|
||
case "complaint_heatmap":
|
||
return jsonl([
|
||
overlay(
|
||
"heatmap",
|
||
"本地合成投诉热力",
|
||
[-0.0015, 0, 0.0015].map((offset, index) => ({
|
||
type: "Feature",
|
||
properties: { origin: "synthetic", weight: [1, 3, 2][index] },
|
||
geometry: { type: "Point", coordinates: point(offset, offset / 2) }
|
||
}))
|
||
)
|
||
]);
|
||
case "candidates_table":
|
||
return jsonl([
|
||
table(
|
||
"candidates",
|
||
"合成候选管段对比",
|
||
["候选", "优先级", "核查依据"],
|
||
[
|
||
["DEMO-PIPE-01", "1", "合成压力降幅较大,待核查"],
|
||
["DEMO-PIPE-02", "2", "合成相关性较弱,待排除"]
|
||
]
|
||
),
|
||
data("candidate", { element_id: "DEMO-PIPE-01", rank: 1, status: "unverified" }),
|
||
data("candidate", { element_id: "DEMO-PIPE-02", rank: 2, status: "unverified" })
|
||
]);
|
||
case "valves_table":
|
||
return jsonl([
|
||
table(
|
||
"valves",
|
||
"合成隔离阀门清单",
|
||
["阀门", "操作建议", "核查要求"],
|
||
[
|
||
["DEMO-VALVE-01", "待评估", "位置与可操作性未核实"],
|
||
["DEMO-VALVE-02", "待评估", "连通性与影响范围未核实"]
|
||
]
|
||
),
|
||
data("valve", { element_id: "DEMO-VALVE-01", status: "unverified", executed: false }),
|
||
data("valve", { element_id: "DEMO-VALVE-02", status: "unverified", executed: false })
|
||
]);
|
||
case "data_only":
|
||
return jsonl([
|
||
data("candidate", {
|
||
element_id: "DEMO-PIPE-03",
|
||
action: "clear_highlights",
|
||
status: "data_only"
|
||
}),
|
||
data("impact", {
|
||
item: "合成片区 A",
|
||
estimated_users: 120,
|
||
unit: "户",
|
||
status: "example_only"
|
||
})
|
||
]);
|
||
case "invalid_json":
|
||
return `${jsonl([presentation({ action: "clear_highlights" })])}\n{"record_type":"candidate",`;
|
||
case "invalid_command":
|
||
return invalidJsonl([
|
||
{ record_type: "presentation", command: { action: "unsupported_test_action" } }
|
||
]);
|
||
case "invalid_chart":
|
||
return invalidJsonl([
|
||
{
|
||
record_type: "presentation",
|
||
command: {
|
||
action: "open_chart",
|
||
chart_id: "dev-invalid-chart",
|
||
evidence_refs: ["dev:synthetic"],
|
||
chart: {
|
||
title: "错误案例:数据长度不齐",
|
||
chart_type: "line",
|
||
x_data: ["08:00", "08:15"],
|
||
series: [{ name: "合成压力水头(m)", data: [32] }]
|
||
}
|
||
}
|
||
}
|
||
]);
|
||
}
|
||
}
|