feat(workbench): redesign investigation reports and diagrams
This commit is contained in:
@@ -0,0 +1,634 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { FileText, History, MessageSquare } from "lucide-react";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Disclosure } from "@/shared/ui/disclosure";
|
||||
import { StatusBadge } from "@/shared/ui/status";
|
||||
import type { InvestigationDiagram, InvestigationDiscussion } from "../api/investigation-report";
|
||||
import type {
|
||||
OperationalTask,
|
||||
OperationalTaskAction,
|
||||
OperationalTaskEvidence,
|
||||
OperationalTaskReport,
|
||||
OperationalTaskReportPage
|
||||
} from "../api/operational-task-client";
|
||||
import {
|
||||
investigationDiagram,
|
||||
investigationStatus,
|
||||
isPendingInvestigationAction
|
||||
} from "../utils/investigation-presentation";
|
||||
import { InvestigationDiagramView } from "./investigation-diagram-view";
|
||||
|
||||
type ReportSelection = { snapshot: OperationalTaskReport; diagram: InvestigationDiagram | null };
|
||||
type Props = {
|
||||
task: OperationalTask;
|
||||
onContinue: (discussion?: InvestigationDiscussion) => void;
|
||||
renderAction: (action: OperationalTaskAction) => ReactNode;
|
||||
renderEvidence: (evidence: OperationalTaskEvidence) => ReactNode;
|
||||
onLoadReports?: (offset: number) => Promise<OperationalTaskReportPage>;
|
||||
onReportViewed?: (generation: number, revision: number) => void;
|
||||
focusActionId?: string | null;
|
||||
};
|
||||
|
||||
const dateLabel = (value: string) => new Date(value).toLocaleString("zh-CN", { hour12: false });
|
||||
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.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,
|
||||
renderAction,
|
||||
renderEvidence,
|
||||
onLoadReports,
|
||||
onReportViewed,
|
||||
focusActionId
|
||||
}: Props) {
|
||||
const [selection, setSelection] = useState<ReportSelection | null>(() =>
|
||||
task.latest_report
|
||||
? { snapshot: task.latest_report, diagram: investigationDiagram(task, task.latest_report) }
|
||||
: null
|
||||
);
|
||||
const [history, setHistory] = useState<OperationalTaskReport[]>([]);
|
||||
const [historyOffset, setHistoryOffset] = useState<number | null>(0);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||
const snapshot = selection?.snapshot ?? null;
|
||||
const report = snapshot?.report;
|
||||
const diagram = selection?.diagram ?? (snapshot ? null : investigationDiagram(task, null));
|
||||
const previousRound = Boolean(
|
||||
snapshot && snapshot.execution_generation !== task.execution_generation
|
||||
);
|
||||
const olderRevision = Boolean(
|
||||
snapshot && task.latest_report && snapshot.revision !== task.latest_report.revision
|
||||
);
|
||||
const latestAvailable = Boolean(task.latest_report && (!snapshot || olderRevision));
|
||||
const insufficient = report?.outcome === "insufficient_data";
|
||||
const currentInsufficient = task.latest_report?.execution_generation === task.execution_generation
|
||||
&& task.latest_report?.report.outcome === "insufficient_data";
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selection && task.latest_report)
|
||||
setSelection({
|
||||
snapshot: task.latest_report,
|
||||
diagram: investigationDiagram(task, task.latest_report)
|
||||
});
|
||||
}, [selection, task]);
|
||||
|
||||
useEffect(() => {
|
||||
if (snapshot?.execution_generation == null || !onReportViewed) return;
|
||||
onReportViewed(snapshot.execution_generation, snapshot.revision);
|
||||
}, [snapshot, onReportViewed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusActionId) return;
|
||||
const details = [
|
||||
...document.querySelectorAll<HTMLDetailsElement>("details[data-review-action]")
|
||||
].find((item) => item.dataset.reviewAction === focusActionId);
|
||||
if (details) {
|
||||
details.open = true;
|
||||
details.scrollIntoView?.({ block: "nearest" });
|
||||
details.querySelector<HTMLElement>("summary")?.focus();
|
||||
}
|
||||
}, [focusActionId]);
|
||||
|
||||
const discuss = (prompt: string, extra?: { evidence_id?: string; gap_id?: string }) =>
|
||||
onContinue({
|
||||
prompt,
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article aria-label="调查任务报告" className="investigation-report surface-reading">
|
||||
<header className="investigation-report-header">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs text-[var(--ink-muted)]">
|
||||
调查任务 · 第 {task.execution_generation} 轮
|
||||
</p>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<h2 className="text-base font-semibold leading-6 text-[var(--ink)]">{task.title}</h2>
|
||||
<StatusBadge
|
||||
tone={
|
||||
task.lifecycle_status === "failed"
|
||||
? "danger"
|
||||
: currentInsufficient || pendingActions.length
|
||||
? "warning"
|
||||
: "neutral"
|
||||
}
|
||||
>
|
||||
{investigationStatus(task)}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-[var(--ink-muted)]">{task.summary}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
onContinue({ context: snapshot ? { report_revision: snapshot.revision } : {} })
|
||||
}
|
||||
>
|
||||
<MessageSquare size={14} aria-hidden />
|
||||
Agent 协作
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{latestAvailable ? (
|
||||
<div className="investigation-report-update" role="status">
|
||||
<span>第 {task.latest_report!.execution_generation ?? "未知"} 轮有新报告</span>
|
||||
<Button variant="outline" size="sm" onClick={() => selectReport(task.latest_report!)}>
|
||||
查看新报告
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section aria-label="任务报告" className="investigation-report-section">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold text-[var(--ink)]">
|
||||
<FileText size={15} aria-hidden />
|
||||
当前结果
|
||||
</h3>
|
||||
{report ? (
|
||||
<StatusBadge tone={insufficient ? "warning" : "neutral"}>
|
||||
{insufficient
|
||||
? "证据不足"
|
||||
: report.outcome === "concluded"
|
||||
? "已形成判断"
|
||||
: "结果未记录"}
|
||||
</StatusBadge>
|
||||
) : null}
|
||||
</div>
|
||||
{report && snapshot ? (
|
||||
<>
|
||||
<p className="mt-3 text-sm font-semibold leading-6 text-[var(--ink)]">
|
||||
{previousRound
|
||||
? task.status === "queued" || task.status === "running"
|
||||
? "上一轮报告,本轮分析中"
|
||||
: "上一轮报告"
|
||||
: olderRevision
|
||||
? "本轮历史报告"
|
||||
: insufficient
|
||||
? task.lifecycle_status === "completed"
|
||||
? "本轮已结束 · 无法形成结论"
|
||||
: task.lifecycle_status === "failed" || task.lifecycle_status === "cancelled"
|
||||
? "无法形成结论 · 本轮运行已停止"
|
||||
: "无法形成结论 · 正在结束本轮"
|
||||
: report.stage === "interim"
|
||||
? "阶段性判断"
|
||||
: report.outcome === "concluded"
|
||||
? "调查结论"
|
||||
: "历史报告内容"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm leading-6 text-[var(--ink)]">{report.current_conclusion}</p>
|
||||
<p className="mt-2 text-xs leading-5 text-[var(--ink-muted)]">
|
||||
结论边界:{report.conclusion_boundary}
|
||||
</p>
|
||||
{insufficient ? (
|
||||
<p className="mt-2 text-sm leading-6 text-[var(--status-warning-foreground)]">
|
||||
结束原因:{report.stop_reason}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="mt-2 text-xs text-[var(--ink-muted)]">
|
||||
正在阅读:{roundLabel(snapshot)} · 报告第 {snapshot.revision} 版 ·{" "}
|
||||
{dateLabel(snapshot.published_at)}
|
||||
</p>
|
||||
{!insufficient &&
|
||||
(report.recommendations.length > 0 || report.pending_items.length > 0) ? (
|
||||
<Disclosure label="判断说明与建议" className="mt-2">
|
||||
{report.recommendations.length > 0 ? (
|
||||
<ReportItems title="处置建议" items={report.recommendations} />
|
||||
) : null}
|
||||
{report.pending_items.length > 0 ? (
|
||||
<ReportItems title="待核实问题" items={report.pending_items} />
|
||||
) : null}
|
||||
</Disclosure>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-3 text-sm leading-6 text-[var(--ink-muted)]">
|
||||
尚未发布本轮调查报告。已保存的调查资料可在下方查看。
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{pendingActions.length > 0 ? (
|
||||
<section aria-label="当前需处理" className="investigation-report-section">
|
||||
<h3 className="text-sm font-semibold text-[var(--ink)]">
|
||||
当前需处理{" "}
|
||||
<span className="font-normal text-[var(--ink-muted)]">
|
||||
· {pendingActions.length} 项
|
||||
</span>
|
||||
</h3>
|
||||
{previousRound || olderRevision ? (
|
||||
<p className="mt-1 text-xs leading-5 text-[var(--ink-muted)]">
|
||||
下列操作使用当前草稿与审批状态。
|
||||
</p>
|
||||
) : null}
|
||||
{pendingActions.map((action) => (
|
||||
<Disclosure
|
||||
key={action.action_id}
|
||||
data-review-action={action.action_id}
|
||||
initiallyOpen={action.action_id === focusActionId}
|
||||
label={
|
||||
<span>
|
||||
{actionTitle(action)} ·{" "}
|
||||
{action.approval_status === "pending"
|
||||
? "待审核"
|
||||
: action.execution_status === "not_submitted"
|
||||
? "待执行"
|
||||
: "待核对回执"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{renderAction(action)}
|
||||
</Disclosure>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{completedActions.length > 0 ? (
|
||||
<section aria-label="处置进展" className="investigation-report-section">
|
||||
<h3 className="text-sm font-semibold text-[var(--ink)]">
|
||||
处置进展{" "}
|
||||
<span className="font-normal text-[var(--ink-muted)]">
|
||||
· {completedActions.length} 项
|
||||
</span>
|
||||
</h3>
|
||||
<p className="mt-1 text-xs leading-5 text-[var(--ink-muted)]">
|
||||
{previousRound || olderRevision
|
||||
? "以下为任务当前动作状态,与所选历史报告分开记录。"
|
||||
: "动作回执只说明对应系统操作结果,不替代后续现场事实。"}
|
||||
</p>
|
||||
{completedActions.map((action) => (
|
||||
<Disclosure
|
||||
key={action.action_id}
|
||||
label={
|
||||
<span className="investigation-action-summary">
|
||||
<span>{actionTitle(action)} · {actionProgressLabel(action)}</span>
|
||||
<span className="investigation-action-summary-detail">
|
||||
{action.receipt?.detail ?? action.execution_detail ?? action.reason}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="pb-2">{renderAction(action)}</div>
|
||||
</Disclosure>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{report?.data_gaps?.length ? (
|
||||
<section aria-label="数据缺口" className="investigation-report-section">
|
||||
<h3 className="text-sm font-semibold text-[var(--ink)]">
|
||||
{insufficient ? "关键数据缺口" : "数据范围限制"}
|
||||
</h3>
|
||||
{report.data_gaps.map((gap) => (
|
||||
<div key={gap.gap_id} className="mt-3">
|
||||
<p className="text-sm font-medium leading-6 text-[var(--ink)]">{gap.missing_data}</p>
|
||||
<p className="text-sm leading-6 text-[var(--ink-muted)]">判断影响:{gap.impact}</p>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-4">
|
||||
<Disclosure label="查看查询依据">
|
||||
<div className="space-y-1 py-2 text-xs leading-5 text-[var(--ink-muted)]">
|
||||
<p>查询范围:{gap.scope}</p>
|
||||
<p>需补齐:{gap.required_data}</p>
|
||||
{gap.evidence_refs.map((id) => (
|
||||
<EvidenceReference
|
||||
key={id}
|
||||
id={id}
|
||||
task={task}
|
||||
renderEvidence={renderEvidence}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Disclosure>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
discuss(
|
||||
`我要补充“${gap.missing_data}”相关的现场事实。请说明需要的记录范围和时间信息。`,
|
||||
{ gap_id: gap.gap_id }
|
||||
)
|
||||
}
|
||||
>
|
||||
补充事实
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{insufficient ? (
|
||||
<p className="mt-3 text-xs leading-5 text-[var(--ink-muted)]">
|
||||
{previousRound || olderRevision
|
||||
? "此处展示所选历史报告的数据缺口,当前调查状态见顶部。"
|
||||
: "补齐事实后发起新一轮调查。原投诉工单与现场处置进度独立记录。"}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
) : report?.data_gaps == null && report ? (
|
||||
<p className="px-4 py-2 text-xs text-[var(--ink-muted)]">历史记录未提供结构化数据缺口。</p>
|
||||
) : null}
|
||||
|
||||
{diagram ? (
|
||||
<InvestigationDiagramView
|
||||
key={`${snapshot?.revision ?? "plan"}:${diagram.mermaid}`}
|
||||
diagram={diagram}
|
||||
renderDetails={(node) => (
|
||||
<>
|
||||
{node.evidence_refs.map((id) => (
|
||||
<EvidenceReference key={id} id={id} task={task} renderEvidence={renderEvidence} />
|
||||
))}
|
||||
{node.action_refs.map((id) => {
|
||||
const action = task.actions.find((item) => item.action_id === id);
|
||||
return action ? (
|
||||
<p key={id} className="text-xs leading-5 text-[var(--ink-muted)]">
|
||||
关联动作:{actionTitle(action)} ·{" "}
|
||||
{action.approval_status === "approved"
|
||||
? "已批准"
|
||||
: action.approval_status === "rejected"
|
||||
? "已拒绝"
|
||||
: "待审核"}{" "}
|
||||
·{" "}
|
||||
{action.execution_status === "succeeded"
|
||||
? "已有执行回执"
|
||||
: "当前执行结果未确认成功"}
|
||||
</p>
|
||||
) : null;
|
||||
})}
|
||||
{!node.evidence_refs.length && !node.action_refs.length ? (
|
||||
<p className="text-xs leading-5 text-[var(--ink-muted)]">
|
||||
此处说明核查方向,尚无关联证据或业务动作记录。
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
onAsk={(node) =>
|
||||
discuss(
|
||||
`请解释调查脉络中的“${node.label}”及其关联依据。`,
|
||||
node.evidence_refs[0] ? { evidence_id: node.evidence_refs[0] } : undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<section aria-label="主要依据" className="investigation-report-section">
|
||||
<h3 className="text-sm font-semibold text-[var(--ink)]">主要依据</h3>
|
||||
{majorEvidence.map((item) => (
|
||||
<div key={item.evidence_id} className="mt-2">
|
||||
{renderEvidence(item)}
|
||||
<button
|
||||
type="button"
|
||||
className="investigation-text-action"
|
||||
onClick={() =>
|
||||
discuss(`请解释“${item.summary}”这条证据的范围、来源和判断影响。`, {
|
||||
evidence_id: item.evidence_id
|
||||
})
|
||||
}
|
||||
>
|
||||
围绕此证据提问
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{(snapshot ? evidence : task.evidence).length === 0 ? (
|
||||
<p className="mt-2 text-sm text-[var(--ink-muted)]">尚无可展示的引用证据。</p>
|
||||
) : null}
|
||||
{remainingEvidence.length > 0 ? (
|
||||
<Disclosure label={`更多引用依据 · ${remainingEvidence.length} 条`} className="mt-2">
|
||||
{remainingEvidence.map((item) => (
|
||||
<div key={item.evidence_id}>
|
||||
{renderEvidence(item)}
|
||||
<button
|
||||
type="button"
|
||||
className="investigation-text-action"
|
||||
onClick={() =>
|
||||
discuss(`请解释“${item.summary}”的来源和判断影响。`, {
|
||||
evidence_id: item.evidence_id
|
||||
})
|
||||
}
|
||||
>
|
||||
围绕此证据提问
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</Disclosure>
|
||||
) : null}
|
||||
{snapshot &&
|
||||
task.evidence.some((item) => !report?.evidence_refs.includes(item.evidence_id)) ? (
|
||||
<Disclosure label="其他已保存资料" className="mt-2">
|
||||
{task.evidence
|
||||
.filter((item) => !report?.evidence_refs.includes(item.evidence_id))
|
||||
.map(renderEvidence)}
|
||||
</Disclosure>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="investigation-report-section">
|
||||
<Disclosure label="任务时间线">
|
||||
<p className="pb-2 text-xs leading-5 text-[var(--ink-muted)]">
|
||||
按时间倒序汇总当前任务记录;报告事件对应当前正在阅读的版本。
|
||||
</p>
|
||||
<ol aria-label="任务时间线记录" className="investigation-timeline">
|
||||
{timeline.map((item) => (
|
||||
<li key={item.id} className="investigation-timeline-item">
|
||||
<time dateTime={item.occurredAt} className="block text-[11px] text-[var(--ink-muted)]">
|
||||
{dateLabel(item.occurredAt)}
|
||||
</time>
|
||||
<p className="text-xs font-medium leading-5 text-[var(--ink)]">{item.title}</p>
|
||||
<p className="text-xs leading-5 text-[var(--ink-muted)]">{item.detail}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Disclosure>
|
||||
{onLoadReports ? (
|
||||
<Disclosure
|
||||
label={
|
||||
<span className="flex items-center gap-2">
|
||||
<History size={14} aria-hidden />
|
||||
报告版本
|
||||
</span>
|
||||
}
|
||||
onToggle={(event) => {
|
||||
if (event.currentTarget.open && history.length === 0 && !historyLoading)
|
||||
void loadHistory(0);
|
||||
}}
|
||||
>
|
||||
{history.map((item) => (
|
||||
<button
|
||||
key={item.revision}
|
||||
type="button"
|
||||
className="investigation-history-row"
|
||||
aria-pressed={snapshot?.revision === item.revision}
|
||||
onClick={() => selectReport(item)}
|
||||
>
|
||||
<span>
|
||||
{roundLabel(item)} · 报告第 {item.revision} 版
|
||||
{item.revision === task.latest_report?.revision ? " · 最新版" : ""}
|
||||
</span>
|
||||
<span className="text-xs text-[var(--ink-muted)]">
|
||||
{item.report.outcome === "insufficient_data"
|
||||
? "证据不足"
|
||||
: item.report.stage === "interim"
|
||||
? "阶段性判断"
|
||||
: item.report.outcome === "concluded"
|
||||
? "已形成判断"
|
||||
: "历史记录"}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
{historyError ? (
|
||||
<p role="alert" className="text-xs leading-5 text-[var(--status-warning-foreground)]">
|
||||
{historyError}
|
||||
</p>
|
||||
) : null}
|
||||
{historyLoading ? (
|
||||
<p className="py-2 text-xs text-[var(--ink-muted)]">正在读取报告版本。</p>
|
||||
) : historyOffset != null || historyError ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void loadHistory(historyError ? 0 : historyOffset!)}
|
||||
>
|
||||
{historyError ? "重新加载" : "更多报告"}
|
||||
</Button>
|
||||
) : null}
|
||||
</Disclosure>
|
||||
) : null}
|
||||
<p className="mt-3 text-xs text-[var(--ink-muted)]">
|
||||
任务更新于 {dateLabel(task.updated_at)}
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
) : (
|
||||
<p className="text-xs text-[var(--ink-muted)]">关联证据当前不可用。</p>
|
||||
);
|
||||
}
|
||||
|
||||
function ReportItems({ title, items }: { title: string; items: string[] }) {
|
||||
return (
|
||||
<div className="py-2 text-sm leading-6 text-[var(--ink)]">
|
||||
<p className="font-medium">{title}</p>
|
||||
<ul className="mt-1 list-disc space-y-1 pl-4">
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user