68 lines
4.2 KiB
TypeScript
68 lines
4.2 KiB
TypeScript
import { AnswerUiView, parseAnswerUiBlock, type AnswerUiBlock, type AnswerUiSelection } from "@/features/agent";
|
||
import type { OperationalTask, OperationalTaskReport } from "../api/operational-task-client";
|
||
import type { InvestigationDiscussion, InvestigationMapTarget } from "../api/investigation-report";
|
||
import { formatInvestigationText } from "../utils/investigation-text";
|
||
|
||
export function InvestigationReportBlocks({ task, snapshot, onContinue, onLocate }: {
|
||
task: Pick<OperationalTask, "evidence">;
|
||
snapshot: OperationalTaskReport;
|
||
onContinue: (discussion?: InvestigationDiscussion) => Promise<boolean | void> | boolean | void;
|
||
onLocate?: (targets: InvestigationMapTarget[]) => Promise<unknown> | void;
|
||
}) {
|
||
const blocks = snapshot.report.ui_blocks ?? [];
|
||
if (!blocks.length) return null;
|
||
const ask = async (selection: AnswerUiSelection) => {
|
||
const result = await onContinue({
|
||
reference: {
|
||
title: `${snapshot.report.title} · 报告第 ${snapshot.revision} 版 · ${selection.title}`,
|
||
text: `${selection.text}\n报告第 ${snapshot.revision} 版${snapshot.execution_generation ? ` · 第 ${snapshot.execution_generation} 轮` : ""}`
|
||
},
|
||
context: { report_revision: snapshot.revision },
|
||
reportGeneration: snapshot.execution_generation
|
||
});
|
||
if (result === false) throw new Error("引用暂未带入对话,请重试。");
|
||
};
|
||
return <section className="investigation-report-section space-y-4" aria-label="交互分析">
|
||
<div>
|
||
<h3 className="text-sm font-semibold">数据与分析</h3>
|
||
<p className="mt-1 text-xs text-muted-foreground">查看报告中的数据与依据;定位和补充资料使用各自入口。</p>
|
||
</div>
|
||
{blocks.map((item) => {
|
||
const parsed = parseAnswerUiBlock(item.block);
|
||
const evidence = task.evidence.filter((entry) => item.evidence_refs.includes(entry.evidence_id));
|
||
const quote = (selection: AnswerUiSelection) => ask({
|
||
...selection,
|
||
text: `${selection.text}\n引用依据:${item.evidence_refs.map((id) => {
|
||
const record = evidence.find((entry) => entry.evidence_id === id);
|
||
return record ? `${record.source_label}(${id})` : `${id}(记录当前不可用)`;
|
||
}).join("、")}`
|
||
});
|
||
// Evidence facts are supplied by the saved task record, not generated presentation props.
|
||
const block: AnswerUiBlock | null = parsed ? { ...parsed, spec: {
|
||
...parsed.spec,
|
||
elements: Object.fromEntries(Object.entries(parsed.spec.elements).map(([id, node]) => {
|
||
if (node.type !== "Evidence") return [id, node];
|
||
const record = evidence.find((entry) => entry.evidence_id === node.props.evidence_id);
|
||
return [id, record ? { ...node, props: {
|
||
...node.props, summary: record.summary, source: record.source_label,
|
||
observed_at: record.observed_at,
|
||
kind: record.kind === "synthetic" ? "demo" : record.kind === "human_input" ? "human_input" : "observed"
|
||
} } : { type: "Text", children: [], props: { text: "这条引用证据当前不可用。", tone: "warning" } }];
|
||
}))
|
||
} } : null;
|
||
const shownEvidence = new Set(block ? Object.values(block.spec.elements)
|
||
.filter((node) => node.type === "Evidence").map((node) => node.props.evidence_id) : []);
|
||
const remainingRefs = item.evidence_refs.filter((id) => !shownEvidence.has(id));
|
||
return <div key={`${snapshot.revision}:${item.block.block_id}`} className="space-y-2">
|
||
{block ? <AnswerUiView block={block} submitMode="draft"
|
||
onSubmit={(text) => quote({ title: "补充资料", text })}
|
||
onLocate={onLocate ? (target) => onLocate(target.feature_ids.map((id) => ({ source_id: target.layer, feature_id: id }))) : undefined} />
|
||
: <p className="whitespace-pre-wrap rounded-lg border p-3 text-sm leading-6">{formatInvestigationText(item.block.fallback_text)}</p>}
|
||
{remainingRefs.length ? <p className="text-[11px] leading-5 text-muted-foreground">
|
||
引用依据:{[...new Set(remainingRefs.map((id) => evidence.find((entry) => entry.evidence_id === id)?.source_label ?? `${id}(记录当前不可用)`))].join("、")}
|
||
</p> : null}
|
||
</div>;
|
||
})}
|
||
</section>;
|
||
}
|