import { useId, useMemo, useRef, useState } from "react"; import { Check, Download, FlaskConical, Play, RotateCcw } from "lucide-react"; import { Button } from "@/shared/ui/button"; import { Textarea } from "@/shared/ui/textarea"; import { Disclosure } from "@/shared/ui/disclosure"; import { FieldSelect } from "@/shared/ui/field-select"; import { investigationDevCases, type InvestigationDevCaseId } from "../utils/investigation-dev-cases"; import type { OperationalTask } from "../api/operational-task-client"; import { investigationReportJsonl, investigationReportUiSummary, parseInvestigationDevJsonl, type InvestigationDevPreview } from "../utils/investigation-dev-preview"; export type InvestigationDevToolsProps = { task: OperationalTask | null; mapReady: boolean; charts: number; overlay: string | null; onSample: (caseId?: InvestigationDevCaseId) => string; onPreview: (preview: InvestigationDevPreview, label?: string) => Promise; onClear: () => void; }; export function InvestigationDevTools({ task, mapReady, charts, overlay, onSample, onPreview, onClear }: InvestigationDevToolsProps) { const [open, setOpen] = useState(true); const [input, setInput] = useState(""); const [caseId, setCaseId] = useState(); const [checked, setChecked] = useState(null); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const [running, setRunning] = useState(false); const previewActive = useRef(false); const inputId = useId(); const summary = useMemo(() => investigationReportUiSummary(task), [task]); function replaceInput(value: string, nextCaseId?: InvestigationDevCaseId) { setInput(value); setCaseId(nextCaseId); setChecked(null); setMessage(null); setError(null); } function clearLoadedPreview() { if (!previewActive.current) return; onClear(); previewActive.current = false; } function loadCase(nextCaseId: InvestigationDevCaseId) { if (running || !mapReady) return; clearLoadedPreview(); replaceInput(onSample(nextCaseId), nextCaseId); } const selectedCase = investigationDevCases.find((item) => item.id === caseId); function validate() { try { const parsed = parseInvestigationDevJsonl(input); setChecked(parsed); setError(null); setMessage( `已识别 ${parsed.commands.length} 条展示指令、${parsed.blocks.length} 个界面块、${parsed.records.length} 条调查数据。` ); return parsed; } catch (cause) { setChecked(null); setError(cause instanceof Error ? cause.message : "JSONL 校验失败。"); setMessage(null); return null; } } async function run() { if (running || !input.trim()) return; const parsed = validate(); if (!parsed) return; if (parsed.commands.length > 0 && !mapReady) { setError("地图尚未就绪,请稍后再执行本地预览。"); setMessage(null); return; } setRunning(true); setError(null); previewActive.current = true; try { await onPreview(parsed, selectedCase?.label ?? "JSONL"); setMessage( `本地预览完成:${parsed.commands.length} 条展示指令、${parsed.blocks.length} 个界面块、${parsed.records.length} 条调查数据。` ); } catch (cause) { if (!(cause instanceof DOMException && cause.name === "AbortError")) setError(cause instanceof Error ? cause.message : "本地展示失败。"); } finally { setRunning(false); } } function download() { if (!checked) return; const url = URL.createObjectURL( new Blob([`${input.trim()}\n`], { type: "application/x-ndjson;charset=utf-8" }) ); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `investigation-preview-${task?.execution_generation ?? "local"}.jsonl`; anchor.click(); URL.revokeObjectURL(url); } return (

当前调查

{task ? task.title : "尚未选择调查"}

{task ? `第 ${task.execution_generation} 轮 · 实时图表 ${charts} · 报告图表 ${summary.charts} · 表格 ${summary.tables} · 地图入口 ${summary.maps}` : "可直接载入、校验和执行本地案例;选择调查后可观察实时展示。"}

{task?.latest_report ? (

已保存报告第 {task.latest_report.revision} 版 · 第{" "} {task.latest_report.execution_generation ?? "未知"} 轮 {task.latest_report.execution_generation !== task.execution_generation ? ",上一轮内容" : ""}

) : null} {overlay ?

覆盖层:{overlay}

: null} {summary.invalid ? (

{summary.invalid} 个报告界面块校验未通过。

) : null} {task && !task.latest_report ? (

本轮尚无报告,实时联动只处理选中后的新指令。

) : null}
{open ? (

01 准备数据

仅在当前浏览器预览,不写入任务或触发调查。空间位置与要素 ID 以 tjwater_v2 为基准;读数与方案为合成样例,不代表现场事实。

{ const nextCase = investigationDevCases.find((item) => item.id === value); if (nextCase) loadCase(nextCase.id); }} options={investigationDevCases.map((item) => ({ value: item.id, label: item.label }))} disabled={!mapReady || running} contentClassName="workbench-shadcn-surface" /> {!mapReady ? (

地图正在初始化,就绪后可载入案例。

) : null} {selectedCase ? (

预期:{selectedCase.expected}

) : null}

切换即载入,执行时自动校验;已执行的本地预览会清除。定位和高亮请使用“地图调试”页签的业务源与要素 ID。

02 校验与执行