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) =>
  1. {item.title}

    {formatInvestigationText(item.detail)}

  2. )}
{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)}
  • ))}
); }