Files
next-tjwater-frontend/src/features/workbench/utils/investigation-dev-preview.ts
T

230 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod";
import {
parseAnswerUiBlock,
presentationCommandSchema,
type AnswerUiBlock
} from "@/features/agent";
import { type InvestigationPresentationCommand } from "../api/investigation-presentation";
import { INVESTIGATION_DEV_NETWORK } from "./investigation-dev-network";
import type { OperationalTask } from "../api/operational-task-client";
export type InvestigationDevPreview = {
commands: InvestigationPresentationCommand[];
blocks: AnswerUiBlock[];
records: Array<Record<string, unknown>>;
};
export type InvestigationPresentationActivity = {
id: number;
action: InvestigationPresentationCommand["action"];
source: "live" | "preview" | "chat";
status: "running" | "completed" | "error";
message?: string;
};
export const investigationActionLabels: Record<InvestigationPresentationCommand["action"], string> =
{
locate_features: "地图定位",
zoom_to_map: "调整视角",
render_scada_analysis: "SCADA 分析",
clear_scada_analysis: "清除 SCADA 分析",
highlight_features: "地图高亮",
clear_highlights: "清除高亮",
open_chart: "打开图表",
close_chart: "关闭图表",
set_overlay: "显示覆盖层",
clear_overlay: "清除覆盖层"
};
const references = z.array(z.string().trim().min(1).max(128)).max(100);
const command = presentationCommandSchema;
const presentationRecord = z.object({ record_type: z.literal("presentation"), command }).strict();
const uiRecord = z
.object({
record_type: z.literal("ui_block"),
block: z.unknown(),
evidence_refs: references.optional()
})
.strict();
const dataRecord = z
.object({
record_type: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/),
origin: z.enum(["system_data", "synthetic", "derived", "human_input"]),
source_evidence_refs: references
})
.passthrough();
/** Only explicit presentation records execute. Skill data records remain data. */
export function parseInvestigationDevJsonl(input: string): InvestigationDevPreview {
if (new TextEncoder().encode(input).byteLength > 200_000)
throw new Error("JSONL 超过 200 KB,请缩小数据范围。");
const lines = input
.replace(/^\uFEFF/, "")
.split(/\r?\n/)
.map((text, index) => ({ text: text.trim(), line: index + 1 }))
.filter((item) => item.text);
if (!lines.length) throw new Error("请先载入样例、当前报告,或粘贴 JSONL。");
if (lines.length > 40) throw new Error("一次最多校验 40 行,请分批预览。");
const result: InvestigationDevPreview = { commands: [], blocks: [], records: [] };
const blockIds = new Set<string>();
for (const { text, line } of lines) {
let value: unknown;
try {
value = JSON.parse(text);
} catch {
throw new Error(`第 ${line} 行不是合法 JSON。`);
}
if (!value || typeof value !== "object" || Array.isArray(value))
throw new Error(`第 ${line} 行必须是 JSON 对象。`);
const record = value as Record<string, unknown>;
if (record.record_type === "presentation") {
const parsed = presentationRecord.safeParse(value);
if (!parsed.success) throw new Error(`第 ${line} 行展示指令不合法,请检查动作、参数和范围。`);
const data = parsed.data;
result.commands.push(data.command);
} else if (record.record_type === "ui_block") {
const wrapped = uiRecord.safeParse(value);
const block = parseAnswerUiBlock(wrapped.success ? wrapped.data.block : null);
if (!block || blockIds.has(block.block_id))
throw new Error(`第 ${line} 行界面块不合法或 block_id 重复。`);
blockIds.add(block.block_id);
result.blocks.push(block);
if (result.blocks.length > 8) throw new Error("一次最多预览 8 个界面块。");
} else {
const parsed = dataRecord.safeParse(value);
if (!parsed.success)
throw new Error(`第 ${line} 行不是已支持的展示指令、界面块或带来源的调查数据。`);
result.records.push(parsed.data);
}
}
return result;
}
/** Explicit records are the only export format; this never interprets legacy inputs. */
export function createPresentationRecord(command: InvestigationPresentationCommand) {
return presentationRecord.parse({ record_type: "presentation", command });
}
export function createUiRecord(block: AnswerUiBlock, evidenceRefs: string[] = []) {
const normalized = parseAnswerUiBlock({ ...block, phase: block.phase ?? "final" });
if (!normalized) throw new Error("界面块未通过当前组件协议校验。");
return uiRecord.parse({
record_type: "ui_block",
block: normalized,
evidence_refs: evidenceRefs
});
}
export function serializeInvestigationDevJsonl(records: unknown[]) {
const input = records.map((record) => JSON.stringify(record)).join("\n");
parseInvestigationDevJsonl(input);
return input;
}
export function investigationReportJsonl(task: OperationalTask) {
return (task.latest_report?.report.ui_blocks ?? [])
.map((item) =>
JSON.stringify({
record_type: "ui_block",
block: item.block,
evidence_refs: item.evidence_refs
})
)
.join("\n");
}
export function investigationReportUiSummary(task: OperationalTask | null) {
const counts = { blocks: 0, charts: 0, tables: 0, maps: 0, invalid: 0 };
for (const item of task?.latest_report?.report.ui_blocks ?? []) {
const block = parseAnswerUiBlock(item.block);
if (!block) {
counts.invalid += 1;
continue;
}
counts.blocks += 1;
for (const node of Object.values(block.spec.elements)) {
if (node.type === "Chart") counts.charts += 1;
if (node.type === "Table") counts.tables += 1;
if (node.type === "MapLocation") counts.maps += 1;
}
}
return counts;
}
export function createInvestigationDevSample(): string {
const center: [number, number] = [...INVESTIGATION_DEV_NETWORK.center];
const chart = {
title: "本地合成压力趋势",
chart_type: "line" as const,
x_data: ["08:00", "08:15", "08:30"],
series: [{ name: "压力水头(m)", data: [32, 28, 30] }]
};
const block: AnswerUiBlock = {
version: "answer-ui@1",
block_id: "dev-investigation-summary",
fallback_text: "仅供展示测试的合成候选对比,不是调查成果。",
spec: {
root: "summary",
elements: {
summary: {
type: "Stack",
props: { columns: "one", gap: "compact" },
children: ["notice", "candidates"]
},
notice: {
type: "Text",
props: { text: "本地合成样例,不代表真实故障、阀门或现场结果。", tone: "warning" },
children: []
},
candidates: {
type: "Table",
props: {
title: "合成候选对比",
columns: ["候选", "排序", "说明"],
rows: [
[INVESTIGATION_DEV_NETWORK.pipeIds[0], "1", "仅测试表格展示"],
[INVESTIGATION_DEV_NETWORK.pipeIds[1], "2", "未执行定位分析"]
]
},
children: []
}
}
}
};
const records: unknown[] = [];
records.push(
createPresentationRecord({
action: "set_overlay",
overlay_id: "dev-synthetic-area",
title: "本地合成空间样例",
mode: "geometry",
evidence_id: "dev:synthetic",
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: { project_code: INVESTIGATION_DEV_NETWORK.projectCode, origin: "synthetic" },
geometry: { type: "Point", coordinates: center }
}
]
}
}),
createPresentationRecord({
action: "open_chart",
chart_id: "dev-synthetic-pressure",
chart,
evidence_refs: ["dev:synthetic"]
}),
createUiRecord(block),
{
record_type: "candidate",
origin: "synthetic",
source_evidence_refs: [],
element_id: INVESTIGATION_DEV_NETWORK.pipeIds[0],
rank: 1,
status: "example_only"
}
);
return serializeInvestigationDevJsonl(records);
}