import { getReportPresentationState } from "../utils/report-presentation-state";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { InvestigationQuestions } from "./investigation-questions";
import { History, LoaderCircle, MessageSquare } from "lucide-react";
import { Button } from "@/shared/ui/button";
import { Disclosure } from "@/shared/ui/disclosure";
import { StatusBadge } from "@/shared/ui/status";
import { formatDateTime as dateLabel } from "@/shared/utils/date-time";
import { formatInvestigationText } from "../utils/investigation-text";
import type { InvestigationDiagram, InvestigationDiscussion, InvestigationMapTarget } from "../api/investigation-report";
import type {
OperationalTask,
OperationalTaskAction,
OperationalTaskEvidence,
OperationalTaskReport,
OperationalTaskReportPage,
TaskExecutionHistoryTarget
} from "../api/operational-task-client";
import {
investigationDiagram,
investigationStatus,
isPendingInvestigationAction,
isRetryableInvestigationAction
} from "../utils/investigation-presentation";
import { InvestigationDiagramView } from "./investigation-diagram-view";
import { InvestigationExpandCard } from "./investigation-expand-card";
import { useReportUpdateMotion } from "../hooks/use-report-update-motion";
import { ExpandableReading } from "@/shared/ui/reading-workspace";
import { InvestigationReportBlocks } from "./investigation-report-blocks";
import { InvestigationReportNavigation, type InvestigationReportSection } from "./investigation-report-navigation";
import { showMapNotice } from "@/features/map/core";
type ReportSelection = { snapshot: OperationalTaskReport; diagram: InvestigationDiagram | null };
type Props = {
task: OperationalTask;
onContinue: (discussion?: InvestigationDiscussion) => Promise | boolean | void;
onViewExecutionHistory?: (target: TaskExecutionHistoryTarget, knownRunId?: string) => Promise | void;
renderAction: (action: OperationalTaskAction) => ReactNode;
renderEvidence: (evidence: OperationalTaskEvidence) => ReactNode;
onAnswerQuestion?: import("../api/operational-task-client").AnswerTaskQuestion;
onLoadReports?: (offset: number) => Promise;
onReportViewed?: (generation: number, revision: number) => void;
focusActionId?: string | null;
onLocate?: (targets: InvestigationMapTarget[]) => Promise | void;
};
const roundLabel = (snapshot: OperationalTaskReport) =>
snapshot.execution_generation == null ? "历史报告" : `第 ${snapshot.execution_generation} 轮`;
const actionTitle = (action: OperationalTaskAction) =>
String(action.payload.title ?? (action.kind === "issue_command" ? "设备命令" : "工单草稿"));
const actionProgressLabel = (action: OperationalTaskAction) => {
if (action.approval_status === "rejected") return "已拒绝";
if (action.approval_status === "pending") return "待审核";
return {
not_submitted: "待执行",
submitted: "已提交",
succeeded: action.kind === "create_work_order" ? "工单已创建" : "执行成功",
failed: "执行失败",
unsupported: "无法执行",
unknown: "待核对回执"
}[action.execution_status];
};
type InvestigationTimelineItem = {
id: string;
occurredAt: string;
title: string;
detail: string;
};
function investigationTimeline(
task: OperationalTask,
snapshot: OperationalTaskReport | null
): InvestigationTimelineItem[] {
const items: InvestigationTimelineItem[] = task.evidence.map((evidence) => ({
id: `evidence:${evidence.evidence_id}`,
occurredAt: evidence.recorded_at,
title: evidence.kind === "human_input" ? "已记录人工事实" : "已保存调查资料",
detail: evidence.summary
}));
if (snapshot) {
items.push({
id: `report:${snapshot.revision}`,
occurredAt: snapshot.published_at,
title: `${roundLabel(snapshot)}报告第 ${snapshot.revision} 版已发布`,
detail: snapshot.report.current_conclusion
});
}
task.questions?.forEach((question) => {
items.push({ id: `question:${question.question_request_id}`, occurredAt: question.created_at,
title: "调查提出补充问题", detail: question.questions.map((item) => item.title).join(";") });
if (question.answered_at) items.push({ id: `answer:${question.question_request_id}`, occurredAt: question.answered_at,
title: "已收到调查回答", detail: "回答已保存为用户补充信息,调查接续处理。" });
});
task.actions.forEach((action) => {
const title = actionTitle(action);
items.push({
id: `action:${action.action_id}:proposed`,
occurredAt: action.proposed_at,
title: `${title}已提出`,
detail: action.reason
});
if (action.edited_at) {
items.push({
id: `action:${action.action_id}:edited`,
occurredAt: action.edited_at,
title: `${title}草稿已更新`,
detail: "后续审核与执行以更新后的版本为准。"
});
}
if (action.decided_at && action.approval_status !== "pending") {
items.push({
id: `action:${action.action_id}:decided`,
occurredAt: action.decided_at,
title: `${title}${action.approval_status === "approved" ? "已批准" : "已拒绝"}`,
detail: action.decision_comment ?? "已记录人工审核结果。"
});
}
if (action.receipt) {
items.push({
id: `action:${action.action_id}:receipt`,
occurredAt:
action.receipt.completed_at ?? action.executed_at ?? action.receipt.submitted_at,
title: `${title}已有执行回执`,
detail: action.receipt.detail
});
} else if (action.executed_at) {
items.push({
id: `action:${action.action_id}:executed`,
occurredAt: action.executed_at,
title: `${title}${actionProgressLabel(action)}`,
detail: action.execution_detail ?? "已记录动作执行状态。"
});
}
});
return items.sort((left, right) => Date.parse(right.occurredAt) - Date.parse(left.occurredAt));
}
export function InvestigationTaskReport({
task,
onContinue,
onViewExecutionHistory,
renderAction,
renderEvidence,
onLoadReports,
onAnswerQuestion,
onReportViewed,
focusActionId,
onLocate
}: Props) {
const questionsRef = useRef(null);
const hasPendingQuestions = task.questions?.some((request) => request.status === "pending"
&& request.execution_generation === task.execution_generation) ?? false;
// Null follows live task content. Only a deliberate history selection freezes a report.
const [selection, setSelection] = useState(null);
const [history, setHistory] = useState([]);
const [historyOffset, setHistoryOffset] = useState(0);
const [historyLoading, setHistoryLoading] = useState(false);
const [historyError, setHistoryError] = useState(null);
const [openingExecutionHistory, setOpeningExecutionHistory] = useState(false);
const live = selection === null;
const snapshot = selection?.snapshot ?? task.latest_report;
const displayedGeneration = live ? task.execution_generation : snapshot?.execution_generation;
const fallbackExecutionRun = task.latest_run ?? task.active_run;
const executionHistoryTarget: TaskExecutionHistoryTarget | null = snapshot ? {
executionGeneration: snapshot.execution_generation ?? null,
sessionId: snapshot.source_session_id
} : live && fallbackExecutionRun?.agent_session_id ? {
executionGeneration: fallbackExecutionRun.generation,
sessionId: fallbackExecutionRun.agent_session_id
} : null;
const knownExecutionRun = executionHistoryTarget
? [task.active_run, task.latest_run].find((run) => run
&& (executionHistoryTarget.executionGeneration == null
|| run.generation === executionHistoryTarget.executionGeneration)
&& run.agent_session_id === executionHistoryTarget.sessionId) ?? null
: null;
const report = snapshot?.report;
const progress = task.latest_progress?.execution_generation === task.execution_generation ? task.latest_progress : null;
// A current final report owns its archived graph. During investigation, a
// progress snapshot owns its whole graph, including an explicit null. A prior
// round's final report must never hide the new round's progress.
const { currentFinalReport, previousRound, olderRevision, insufficient } = getReportPresentationState(task, snapshot);
const savedDiagram = task.latest_progress?.progress.diagram;
const cumulativeDiagram = savedDiagram?.edges != null ? savedDiagram : null;
const diagram = live
? cumulativeDiagram ?? (progress && !currentFinalReport ? progress.progress.diagram : investigationDiagram(task, snapshot))
: selection.diagram;
const latestAvailable = !live;
const { currentInsufficient } = getReportPresentationState(task, task.latest_report);
const pendingActions = task.actions.filter(isPendingInvestigationAction);
const completedActions = task.actions.filter((action) => !isPendingInvestigationAction(action));
const evidence = task.evidence.filter((item) => report?.evidence_refs.includes(item.evidence_id));
const majorEvidence = (snapshot ? evidence : task.evidence).slice(0, 3);
const remainingEvidence = (snapshot ? evidence : task.evidence).slice(3);
const timeline = investigationTimeline(task, snapshot);
const reportRef = useReportUpdateMotion(task.task_id,
`${snapshot?.execution_generation}:${snapshot?.revision}:${progress?.run_id}:${progress?.revision}`, live);
const sections: InvestigationReportSection[] = [
"任务报告",
...(report?.ui_blocks?.length ? ["交互分析" as const] : []),
...(live ? ["当前调查进展" as const] : []),
...(pendingActions.length ? ["当前需处理" as const] : []),
...(completedActions.length ? ["处置进展" as const] : []),
...(report?.data_gaps?.length ? ["数据缺口" as const] : []),
...(diagram ? ["调查脉络" as const] : []),
"主要依据", "任务时间线",
...(onLoadReports ? ["报告版本" as const] : []),
];
useEffect(() => {
if (snapshot?.execution_generation == null || !onReportViewed) return;
onReportViewed(snapshot.execution_generation, snapshot.revision);
}, [snapshot, onReportViewed]);
useEffect(() => {
if (!focusActionId) return;
const details = [
...document.querySelectorAll("details[data-review-action]")
].find((item) => item.dataset.reviewAction === focusActionId);
if (details) {
details.open = true;
details.scrollIntoView?.({ block: "nearest" });
details.querySelector("summary")?.focus();
}
}, [focusActionId]);
const discuss = (prompt: string, extra?: { evidence_id?: string; gap_id?: string }) =>
onContinue({
prompt,
reportGeneration: snapshot?.execution_generation,
context: { ...(snapshot ? { report_revision: snapshot.revision } : {}), ...extra }
});
const selectReport = (item: OperationalTaskReport) =>
setSelection({ snapshot: item, diagram: investigationDiagram(task, item) });
const loadHistory = async (offset: number) => {
if (!onLoadReports || historyLoading) return;
setHistoryLoading(true);
setHistoryError(null);
try {
const page = await onLoadReports(offset);
setHistory((current) => (offset === 0 ? page.items : [...current, ...page.items]));
setHistoryOffset(page.next_offset);
} catch (error) {
setHistoryError(error instanceof Error ? error.message : "报告版本加载失败");
} finally {
setHistoryLoading(false);
}
};
const openExecutionHistory = async () => {
if (!onViewExecutionHistory || !executionHistoryTarget || openingExecutionHistory) return;
setOpeningExecutionHistory(true);
try {
if (knownExecutionRun) {
await onViewExecutionHistory(executionHistoryTarget, knownExecutionRun.run_id);
} else {
await onViewExecutionHistory(executionHistoryTarget);
}
} catch (error) {
showMapNotice({
tone: "error",
title: "调查执行记录打开失败",
message: error instanceof Error ? error.message : "请稍后重试。"
});
} finally {
setOpeningExecutionHistory(false);
}
};
return (
{snapshot?.report.current_conclusion ?? task.summary}
}>
{displayedGeneration == null ? "调查任务 · 历史轮次" : `调查任务 · 第 ${displayedGeneration} 轮`}
{task.title}
{investigationStatus(task)}
{task.summary}
{latestAvailable ? (
正在阅读历史报告{olderRevision ? ",已有新报告" : ""}
) : null}
{insufficient
? "证据不足"
: report.outcome === "concluded"
? "已形成判断"
: "结果未记录"}
) : null}
preview={<>
{report && snapshot ? (
<>
{previousRound || olderRevision || insufficient || report.stage !== "final" || report.outcome !== "concluded" ? (
{previousRound
? task.status === "queued" || task.status === "running"
? "上一轮报告,本轮分析中"
: "上一轮报告"
: olderRevision
? "本轮历史报告"
: insufficient
? task.lifecycle_status === "completed"
? "本轮已结束 · 无法形成结论"
: task.lifecycle_status === "failed" || task.lifecycle_status === "cancelled"
? "无法形成结论 · 本轮运行已停止"
: "无法形成结论 · 正在结束本轮"
: report.stage === "interim"
? "阶段性判断"
: "历史报告内容"}
) : null}
{formatInvestigationText(report.current_conclusion)}
结论边界:{formatInvestigationText(report.conclusion_boundary)}
{insufficient ? (
结束原因:{formatInvestigationText(report.stop_reason)}
) : null}
正在阅读:{roundLabel(snapshot)} · 报告第 {snapshot.revision} 版 ·{" "}
{dateLabel(snapshot.published_at)}
>
) : (
尚未发布本轮调查报告。已保存的调查资料可在下方查看。
)}
>}>
{report && snapshot ? <>
{roundLabel(snapshot)} · 报告第 {snapshot.revision} 版 · {dateLabel(snapshot.published_at)}
{formatInvestigationText(report.current_conclusion)}
结论边界:{formatInvestigationText(report.conclusion_boundary)}
{insufficient ? 结束原因:{formatInvestigationText(report.stop_reason)}
: <>
{report.recommendations.length > 0 ? : null}
{report.pending_items.length > 0 ? : null}
>}
> : 尚未发布本轮调查报告。
}
{snapshot ? : null}
{live ?
当前调查进展
{formatInvestigationText(progress?.progress.summary ?? "尚未发布本轮调查进展。")}
{progress ? 实时内容 · 第 {progress.execution_generation} 轮 · 更新 {dateLabel(progress.published_at)}
: null}
: null}
{pendingActions.length > 0 ? (
当前需处理{" "}
· {pendingActions.length} 项
{previousRound || olderRevision ? (
下列操作使用当前草稿与审批状态。
) : null}
{pendingActions.map((action) => (
{actionTitle(action)} ·{" "}
{action.approval_status === "pending"
? "待审核"
: isRetryableInvestigationAction(action)
? "待重试"
: action.execution_status === "not_submitted"
? "待执行"
: "待核对回执"}
}
>
{renderAction(action)}
))}
) : null}
{completedActions.length > 0 ? (
处置进展{" "}
· {completedActions.length} 项
{previousRound || olderRevision
? "以下为任务当前动作状态,与所选历史报告分开记录。"
: "动作回执只说明对应系统操作结果,不替代后续现场事实。"}
{completedActions.map((action) => (
{actionTitle(action)} · {actionProgressLabel(action)}
{action.receipt?.detail ?? action.execution_detail ?? action.reason}
}
>
{renderAction(action)}
))}
) : null}
{report?.data_gaps?.length ? (
{insufficient ? "关键数据缺口" : "数据范围限制"}
{report.data_gaps.map((gap) => (
{formatInvestigationText(gap.missing_data)}
判断影响:{formatInvestigationText(gap.impact)}
查询范围:{formatInvestigationText(gap.scope)}
需补齐:{formatInvestigationText(gap.required_data)}
{gap.evidence_refs.map((id) => (
))}
))}
{insufficient ? (
{previousRound || olderRevision
? "此处展示所选历史报告的数据缺口,当前调查状态见顶部。"
: "补齐事实后发起新一轮调查。原投诉工单与现场处置进度独立记录。"}
) : null}
) : report?.data_gaps == null && report ? (
历史记录未提供结构化数据缺口。
) : null}
{diagram ? (
(
<>
{node.evidence_refs.map((id) => (
))}
{node.action_refs.map((id) => {
const action = task.actions.find((item) => item.action_id === id);
return action ? (
关联动作:{actionTitle(action)} ·{" "}
{action.approval_status === "approved"
? "已批准"
: action.approval_status === "rejected"
? "已拒绝"
: "待审核"}{" "}
·{" "}
{action.execution_status === "succeeded"
? "已有执行回执"
: "当前执行结果未确认成功"}
) : null;
})}
{!node.evidence_refs.length && !node.action_refs.length ? (
此处说明核查方向,尚无关联证据或业务动作记录。
) : null}
>
)}
/>
) : null}
{majorEvidence.map((item) =>
{formatInvestigationText(item.summary)}
{item.source_label} · {dateLabel(item.observed_at ?? item.recorded_at)}
)}
{(snapshot ? evidence : task.evidence).length > 0
? `共 ${(snapshot ? evidence : task.evidence).length} 条引用依据,展开查看来源与原始记录。`
: "尚无可展示的引用证据。"}
>}>
{(close) => <>
{majorEvidence.map((item) => (
{renderEvidence(item)}
{onLocate && diagram?.node_refs.some((node) => node.evidence_refs.includes(item.evidence_id) && node.map_targets?.length) ? (
) : null}
))}
{(snapshot ? evidence : task.evidence).length === 0 ? (
尚无可展示的引用证据。
) : null}
{remainingEvidence.length > 0 ? (
{remainingEvidence.map((item) => (
{renderEvidence(item)}
))}
) : null}
{snapshot &&
task.evidence.some((item) => !report?.evidence_refs.includes(item.evidence_id)) ? (
{task.evidence
.filter((item) => !report?.evidence_refs.includes(item.evidence_id))
.map(renderEvidence)}
) : null}
>}
{timeline[0] ? `${timeline[0].title} · ${dateLabel(timeline[0].occurredAt)},共 ${timeline.length} 条记录。` : "尚无调查记录。"}
}>
{onLoadReports ? (
{snapshot ? `${roundLabel(snapshot)} · 报告第 ${snapshot.revision} 版 · ${dateLabel(snapshot.published_at)}` : "尚未发布报告。"}
}
onOpen={() => { if (history.length === 0 && !historyLoading) void loadHistory(0); }}
>
{(close) => <>
{history.map((item) => (
))}
{historyError ? (
{historyError}
) : null}
{historyLoading ? (
正在读取报告版本。
) : historyOffset != null || historyError ? (
) : null}
>}
) : null}
任务更新于 {dateLabel(task.updated_at)}
);
}
function InvestigationTimeline({ items }: { items: InvestigationTimelineItem[] }) {
return <>
按时间倒序汇总当前任务记录;报告事件对应当前正在阅读的版本。
{items.map((item) => -
{item.title}
{formatInvestigationText(item.detail)}
)}
{items.length === 0 ? 尚无调查记录。
: null}
>;
}
function EvidenceReference({
id,
task,
renderEvidence
}: {
id: string;
task: OperationalTask;
renderEvidence: Props["renderEvidence"];
}) {
const item = task.evidence.find((evidence) => evidence.evidence_id === id);
return item ? (
renderEvidence(item)
) : (
关联证据当前不可用。
);
}
function ReportItems({ title, items }: { title: string; items: string[] }) {
return (
{title}
{items.map((item, index) => (
- {formatInvestigationText(item)}
))}
);
}