feat: expand intelligent investigation workspace
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent ddd03dbff6
commit c0e4231b83
223 changed files with 13155 additions and 5304 deletions
@@ -0,0 +1,410 @@
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 {
investigationActionLabels,
investigationReportJsonl,
investigationReportUiSummary,
parseInvestigationDevJsonl,
type InvestigationDevPreview,
type InvestigationPresentationActivity
} from "../utils/investigation-dev-preview";
export type InvestigationDevToolsProps = {
task: OperationalTask | null;
mapReady: boolean;
charts: number;
overlay: string | null;
activity: InvestigationPresentationActivity[];
onSample: (caseId?: InvestigationDevCaseId) => string;
onPreview: (preview: InvestigationDevPreview, label?: string) => Promise<void>;
onClear: () => void;
};
export function InvestigationDevTools({
task,
mapReady,
charts,
overlay,
activity,
onSample,
onPreview,
onClear
}: InvestigationDevToolsProps) {
const [open, setOpen] = useState(true);
const [input, setInput] = useState("");
const [caseId, setCaseId] = useState<InvestigationDevCaseId | undefined>();
const [checked, setChecked] = useState<InvestigationDevPreview | null>(null);
const [preview, setPreview] = useState<InvestigationDevPreview | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [running, setRunning] = useState(false);
const [selectedRecord, setSelectedRecord] = useState(0);
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);
setPreview(null);
setMessage(null);
setError(null);
setSelectedRecord(0);
}
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);
setPreview(parsed);
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 (
<div className="min-w-0 space-y-4" data-testid="investigation-dev-tools">
<div className="space-y-1 border-b border-border pb-4 text-xs leading-5">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] font-medium text-muted-foreground">当前调查</p>
<Button
type="button"
variant="ghost"
className="h-8 px-0 text-xs"
aria-expanded={open}
aria-controls="investigation-dev-lab"
onClick={() => setOpen(!open)}
>
<FlaskConical size={14} />
{open ? "收起动作测试" : "测试地图、图表与 JSONL"}
</Button>
</div>
<p className="break-words text-sm font-semibold text-foreground">
{task ? task.title : "尚未选择调查"}
</p>
<p className="text-muted-foreground">
{task
? `第 ${task.execution_generation} 轮 · 实时图表 ${charts} · 报告图表 ${summary.charts} · 表格 ${summary.tables} · 地图入口 ${summary.maps}`
: "可直接载入、校验和执行本地案例;选择调查后可观察实时展示。"}
</p>
{task?.latest_report ? (
<p className="text-muted-foreground">
已保存报告第 {task.latest_report.revision} 版 · 第{" "}
{task.latest_report.execution_generation ?? "未知"} 轮
{task.latest_report.execution_generation !== task.execution_generation
? ",上一轮内容"
: ""}
</p>
) : null}
{overlay ? <p className="break-words text-muted-foreground">覆盖层:{overlay}</p> : null}
{summary.invalid ? (
<p className="text-[var(--status-warning-foreground)]">
{summary.invalid} 个报告界面块校验未通过。
</p>
) : null}
{task && !task.latest_report ? (
<p className="text-muted-foreground">本轮尚无报告,实时联动只处理选中后的新指令。</p>
) : null}
</div>
{open ? (
<div id="investigation-dev-lab" className="min-w-0 space-y-3">
<h3 className="text-xs font-semibold">01 准备数据</h3>
<p className="text-[11px] leading-5 text-muted-foreground">
仅在当前浏览器预览,不写入任务或触发调查。合成样例不代表现场事实。
</p>
<FieldSelect
label="JSONL 测试案例"
placeholder="选择案例,立即载入 JSONL"
value={caseId ?? ""}
onValueChange={(value) => {
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 ? (
<p className="text-[11px] text-muted-foreground">地图正在初始化,就绪后可载入案例。</p>
) : null}
{selectedCase ? (
<p aria-label="案例预期结果" className="text-xs leading-5 text-muted-foreground">
预期:{selectedCase.expected}
</p>
) : null}
<p className="text-[11px] leading-5 text-muted-foreground">
切换即载入,执行时自动校验;已执行的本地预览会清除。
</p>
<div className="grid grid-cols-2 gap-2">
<Button
type="button"
variant="outline"
className="text-xs"
disabled={!mapReady || running}
onClick={() => loadCase(caseId ?? "combined")}
>
载入合成样例
</Button>
<Button
type="button"
variant="outline"
className="text-xs"
disabled={!task || !summary.blocks || running}
onClick={() => {
if (task) {
clearLoadedPreview();
replaceInput(investigationReportJsonl(task));
}
}}
>
载入当前报告
</Button>
</div>
<div className="space-y-1.5 border-t border-border pt-4">
<h3 className="pb-2 text-xs font-semibold">02 校验与执行</h3>
<label htmlFor={inputId} className="text-xs font-medium">
调查展示 JSONL
</label>
<Textarea
id={inputId}
value={input}
disabled={running}
onChange={(event) => replaceInput(event.target.value)}
rows={5}
maxLength={200_000}
spellCheck={false}
className="min-h-28 resize-y font-mono text-[11px] leading-5"
placeholder="每行一条展示指令、界面块或带来源的调查数据"
/>
</div>
<Disclosure
label="JSONL 格式说明"
className="[&>summary]:min-h-8 [&>summary]:text-muted-foreground"
>
<p className="pb-2 text-[11px] leading-5 text-muted-foreground">
presentation 执行受控展示,ui_block 预览界面;candidate
等调查数据仅展示,不自动操作地图。
</p>
</Disclosure>
<div className="grid grid-cols-2 gap-2">
<Button
type="button"
variant="outline"
className="text-xs"
disabled={!input.trim() || running}
onClick={validate}
>
<Check size={14} />
校验 JSONL
</Button>
<Button
type="button"
className="text-xs"
disabled={!input.trim() || running}
onClick={() => void run()}
>
<Play size={14} />
{running ? "正在预览" : "执行本地预览"}
</Button>
</div>
<p className="text-[11px] text-muted-foreground">
点击执行会自动校验当前 JSONL,整批校验通过后才预览;也可单独校验。
</p>
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-border pt-4">
<h3 className="text-xs font-semibold">03 检查结果</h3>
<div className="flex flex-wrap gap-2">
<Button
type="button"
size="sm"
variant="ghost"
className="text-xs"
disabled={!checked || running}
onClick={download}
>
<Download size={14} />
下载测试 JSONL
</Button>
<Button
type="button"
size="sm"
variant="ghost"
className="text-xs"
onClick={() => {
onClear();
previewActive.current = false;
setPreview(null);
setMessage("已清除当前浏览器的临时展示。");
setError(null);
}}
>
<RotateCcw size={14} />
清除临时展示
</Button>
</div>
</div>
{message ? (
<p role="status" className="break-words text-xs leading-5 text-muted-foreground">
{message}
</p>
) : null}
{error ? (
<p
role="alert"
className="break-words text-xs leading-5 text-[var(--status-danger-foreground)]"
>
{error}
</p>
) : null}
{preview ? (
<section aria-label="JSONL 检查结果" className="min-w-0 space-y-3 overflow-x-auto">
<p className="text-xs text-muted-foreground">
业务结果已在展示面板打开,可切换动作历史回看。
</p>
<Disclosure label="原始数据检查">
{preview.records.length ? (
<div className="max-h-48 overflow-auto">
<table className="w-full text-left text-[11px] leading-5">
<caption className="sr-only">已识别的调查数据,仅供预览</caption>
<thead>
<tr className="border-b">
<th className="py-1 pr-3 font-medium">类型</th>
<th className="py-1 pr-3 font-medium">对象</th>
<th className="py-1 pr-3 font-medium">来源</th>
<th className="py-1 text-right font-medium">证据</th>
</tr>
</thead>
<tbody>
{preview.records.map((record, index) => (
<tr key={index} className="border-b last:border-0">
<td className="break-all py-1.5 pr-3">{String(record.record_type)}</td>
<td className="py-1.5 pr-3">
<button
type="button"
className="break-all text-left underline underline-offset-2 focus-visible:outline-2 focus-visible:outline-[var(--action-focus-ring)]"
aria-label={`查看第 ${index + 1} 条调查数据`}
aria-pressed={selectedRecord === index}
onClick={() => setSelectedRecord(index)}
>
{String(
record.element_id ??
record.device_id ??
record.action_id ??
record.item ??
`记录 ${index + 1}`
)}
</button>
</td>
<td className="py-1.5 pr-3">{String(record.origin)}</td>
<td className="py-1.5 text-right tabular-nums">
{(record.source_evidence_refs as string[]).length}
</td>
</tr>
))}
</tbody>
</table>
<pre
aria-label="调查数据详情"
className="mt-2 max-h-36 overflow-auto whitespace-pre-wrap break-all rounded-md bg-muted p-2 font-mono text-[11px] leading-5"
>
{JSON.stringify(preview.records[selectedRecord], null, 2)}
</pre>
</div>
) : null}
</Disclosure>
</section>
) : null}
</div>
) : null}
{activity.length ? (
<section aria-label="最近展示动作" className="space-y-2 border-t border-border pt-4">
<h3 className="text-xs font-semibold">最近展示动作</h3>
<ol
aria-label="动作识别记录"
className="space-y-1 border-l border-border pl-3 text-[11px] leading-5"
>
{activity.slice(-3).map((item) => (
<li key={`${item.source}-${item.id}`} className="break-words">
<span className="text-muted-foreground">
{item.source === "preview" ? "本地测试" : "实时指令"} ·{" "}
</span>
{investigationActionLabels[item.action]} ·{" "}
{item.status === "completed"
? "已应用"
: item.status === "error"
? "未完成"
: "展示中"}
{item.message ? (
<span className="text-[var(--status-warning-foreground)]">:{item.message}</span>
) : null}
</li>
))}
</ol>
</section>
) : null}
</div>
);
}