feat: add project-aware agent workspace
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s

This commit is contained in:
2026-10-22 18:52:18 +08:00
parent c6a0e74e72
commit 2d8506fea0
191 changed files with 12373 additions and 3556 deletions
@@ -0,0 +1,63 @@
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";
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;
return <div key={`${snapshot.revision}:${item.block.block_id}`} className="space-y-2">
{block ? <AnswerUiView block={block} onAsk={quote} 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">{item.block.fallback_text}</p>}
<p className="text-[11px] leading-5 text-muted-foreground">
引用依据:{evidence.map((entry) => entry.source_label).join("、") || "关联记录当前不可用"}
</p>
</div>;
})}
</section>;
}