refactor: simplify evidence workspace presentation

This commit is contained in:
2026-08-19 17:31:21 +08:00
parent 4a589d7a43
commit 57c494e8bd
@@ -10,14 +10,12 @@ import {
Focus, Focus,
Map as MapIcon, Map as MapIcon,
Minimize2, Minimize2,
PanelRightClose,
PanelRightOpen,
ShieldCheck, ShieldCheck,
Sparkles, Sparkles,
Trash2, Trash2,
X X
} from "lucide-react"; } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useState } from "react";
import { showMapNotice } from "@/features/map/core"; import { showMapNotice } from "@/features/map/core";
import { cn } from "@/shared/ui/cn"; import { cn } from "@/shared/ui/cn";
import type { import type {
@@ -38,6 +36,8 @@ type AnalysisArtifactWorkspaceProps = {
onDestroy: () => void; onDestroy: () => void;
}; };
type EvidenceBlock = Exclude<AnalysisBlock, { kind: "metric-grid" }>;
export function AnalysisArtifactWorkspace({ export function AnalysisArtifactWorkspace({
artifact, artifact,
surfaceMode, surfaceMode,
@@ -48,11 +48,10 @@ export function AnalysisArtifactWorkspace({
}: AnalysisArtifactWorkspaceProps) { }: AnalysisArtifactWorkspaceProps) {
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null); const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
const metricBlock = artifact.blocks.find((block) => block.kind === "metric-grid"); const metricBlock = artifact.blocks.find((block) => block.kind === "metric-grid");
const evidenceBlocks = artifact.blocks.filter((block) => block.kind !== "metric-grid"); const evidenceBlocks = artifact.blocks.filter(
const selectedBlock = useMemo( (block): block is EvidenceBlock => block.kind !== "metric-grid"
() => artifact.blocks.find((block) => block.id === selectedBlockId) ?? null,
[artifact.blocks, selectedBlockId]
); );
const selectedBlock = evidenceBlocks.find((block) => block.id === selectedBlockId) ?? null;
useEffect(() => { useEffect(() => {
setSelectedBlockId(null); setSelectedBlockId(null);
@@ -118,8 +117,6 @@ export function AnalysisArtifactWorkspace({
artifact={artifact} artifact={artifact}
surfaceMode={surfaceMode} surfaceMode={surfaceMode}
mapSplitAvailable={mapSplitAvailable} mapSplitAvailable={mapSplitAvailable}
detailOpen={Boolean(selectedBlock)}
onToggleDetail={() => setSelectedBlockId((current) => current ? null : evidenceBlocks[0]?.id ?? null)}
onSetView={onSetView} onSetView={onSetView}
onCollapse={onCollapse} onCollapse={onCollapse}
onDestroy={onDestroy} onDestroy={onDestroy}
@@ -176,7 +173,7 @@ function AnalysisBlockView({
selected, selected,
onSelect onSelect
}: { }: {
block: Exclude<AnalysisBlock, { kind: "metric-grid" }>; block: EvidenceBlock;
selected: boolean; selected: boolean;
onSelect: () => void; onSelect: () => void;
}) { }) {
@@ -218,7 +215,7 @@ function AnalysisBlockView({
); );
} }
function AnalysisBlockBody({ block }: { block: Exclude<AnalysisBlock, { kind: "metric-grid" }> }) { function AnalysisBlockBody({ block }: { block: EvidenceBlock }) {
if (block.kind === "chart") { if (block.kind === "chart") {
return ( return (
<div className="h-[258px] min-h-0"> <div className="h-[258px] min-h-0">
@@ -286,9 +283,9 @@ function ArtifactProvenance({ artifact }: { artifact: AnalysisArtifact }) {
<Database size={14} aria-hidden="true" /> <Database size={14} aria-hidden="true" />
</h3> </h3>
<ul className="flex min-w-0 flex-wrap gap-1.5"> <ul className="flex min-w-0 flex-wrap gap-x-2 gap-y-1 pt-1 text-xs text-slate-600">
{artifact.sources.map((source) => ( {artifact.sources.map((source) => (
<li key={source} className="surface-reading rounded-md border px-2.5 py-1 text-xs text-slate-600">{source}</li> <li key={source} className="after:ml-2 after:text-slate-300 after:content-['/'] last:after:hidden">{source}</li>
))} ))}
</ul> </ul>
</div> </div>
@@ -313,7 +310,7 @@ function ArtifactDetailDrawer({
onClose onClose
}: { }: {
artifact: AnalysisArtifact; artifact: AnalysisArtifact;
block: AnalysisBlock; block: EvidenceBlock;
onClose: () => void; onClose: () => void;
}) { }) {
const blockFacts = getEvidenceBlockFacts(block); const blockFacts = getEvidenceBlockFacts(block);
@@ -321,13 +318,11 @@ function ArtifactDetailDrawer({
return ( return (
<aside <aside
aria-label="证据详情" aria-label="证据详情"
className="surface-reading absolute bottom-[57px] right-0 top-0 z-30 w-full max-w-[440px] overflow-y-auto overscroll-contain border-l border-slate-300/80 shadow-[-18px_0_44px_rgba(15,23,42,0.14)]" className="surface-reading absolute bottom-[57px] right-0 top-0 z-30 w-full max-w-[420px] overflow-y-auto overscroll-contain border-l border-slate-300/80 shadow-[-12px_0_32px_rgba(15,23,42,0.12)]"
> >
<div className="surface-control sticky top-0 z-10 flex items-start justify-between gap-4 border-b border-slate-300/70 px-5 py-4"> <div className="surface-control sticky top-0 z-10 flex items-start justify-between gap-4 border-b border-slate-300/70 px-5 py-4">
<div className="flex min-w-0 items-start gap-3"> <div className="flex min-w-0 items-start gap-3">
<span className="surface-reading grid h-9 w-9 shrink-0 place-items-center rounded-lg border text-blue-700 shadow-[0_1px_3px_rgba(15,23,42,0.08)]"> <FileSearch size={17} className="mt-0.5 shrink-0 text-blue-700" aria-hidden="true" />
<FileSearch size={17} aria-hidden="true" />
</span>
<div className="min-w-0"> <div className="min-w-0">
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold text-slate-500"> <div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold text-slate-500">
<span className="uppercase tracking-[0.12em]"></span> <span className="uppercase tracking-[0.12em]"></span>
@@ -346,32 +341,24 @@ function ArtifactDetailDrawer({
<X size={17} aria-hidden="true" /> <X size={17} aria-hidden="true" />
</button> </button>
</div> </div>
<div className="space-y-6 px-5 py-5"> <div className="space-y-5 px-5 py-5">
<section aria-labelledby="evidence-purpose-heading"> <section aria-labelledby="evidence-purpose-heading">
<p id="evidence-purpose-heading" className="text-xs font-semibold text-slate-500"></p> <p id="evidence-purpose-heading" className="text-xs font-semibold text-slate-500"></p>
<p className="mt-2 text-sm leading-6 text-slate-700"> <p className="mt-2 text-sm leading-6 text-slate-700">
{"description" in block ? block.description : "该模块汇总了当前成果中的关键判断依据。"} {evidenceBlockDescription(block)}
</p> </p>
</section> </section>
<section className="material-tone-agent rounded-xl border px-4 py-3.5" aria-labelledby="evidence-relation-heading"> <section className="border-t border-slate-200 pt-5" aria-labelledby="evidence-data-heading">
<div className="flex items-center gap-2 text-xs font-semibold text-blue-900">
<Sparkles size={14} aria-hidden="true" />
<h4 id="evidence-relation-heading">Agent </h4>
</div>
<p className="mt-2 text-sm leading-6 text-slate-700">{artifact.summary}</p>
</section>
<section aria-labelledby="evidence-data-heading">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<h4 id="evidence-data-heading" className="text-xs font-semibold text-slate-500"></h4> <h4 id="evidence-data-heading" className="text-xs font-semibold text-slate-500"></h4>
<span className="text-[11px] font-medium text-slate-400">Artifact R{artifact.revision}</span> <span className="text-[11px] font-medium text-slate-400">Artifact R{artifact.revision}</span>
</div> </div>
<dl className="surface-well mt-2 grid grid-cols-2 gap-px overflow-hidden rounded-xl border bg-slate-300/70"> <dl className="mt-2 divide-y divide-slate-200">
{blockFacts.map((fact) => ( {blockFacts.map((fact) => (
<div key={fact.label} className="surface-reading min-w-0 px-3.5 py-3"> <div key={fact.label} className="flex items-center justify-between gap-4 py-2 text-xs">
<dt className="text-[11px] text-slate-500">{fact.label}</dt> <dt className="text-slate-500">{fact.label}</dt>
<dd className="mt-1 truncate text-sm font-semibold text-slate-900 tabular-nums" title={fact.value}>{fact.value}</dd> <dd className="font-medium text-slate-800 tabular-nums">{fact.value}</dd>
</div> </div>
))} ))}
</dl> </dl>
@@ -388,80 +375,46 @@ function ArtifactDetailDrawer({
<dd className="text-right font-medium text-slate-800 tabular-nums">{artifact.confidence}</dd> <dd className="text-right font-medium text-slate-800 tabular-nums">{artifact.confidence}</dd>
</dl> </dl>
</section> </section>
<section className="border-t border-slate-200 pt-5" aria-labelledby="evidence-source-heading">
<div className="flex items-center gap-2">
<Database size={14} className="text-slate-500" aria-hidden="true" />
<h4 id="evidence-source-heading" className="text-xs font-semibold text-slate-700"></h4>
</div>
<ul className="mt-3 flex flex-wrap gap-2">
{artifact.sources.map((source) => (
<li key={source} className="surface-control rounded-md border px-2.5 py-1.5 text-xs text-slate-600">{source}</li>
))}
</ul>
</section>
<section className="material-tone-warning rounded-xl border px-4 py-3.5" aria-labelledby="evidence-limit-heading">
<div className="flex items-center gap-2 text-xs font-semibold text-amber-900">
<AlertTriangle size={14} aria-hidden="true" />
<h4 id="evidence-limit-heading">使</h4>
</div>
<ul className="mt-2 space-y-1.5 text-xs leading-5 text-amber-950/75">
{artifact.limitations.map((item) => <li key={item}> {item}</li>)}
</ul>
</section>
</div> </div>
</aside> </aside>
); );
} }
function evidenceBlockKindLabel(block: AnalysisBlock) { function evidenceBlockDescription(block: EvidenceBlock) {
return "description" in block
? block.description
: block.paragraphs[0] ?? "该模块汇总了当前成果中的关键判断依据。";
}
function evidenceBlockKindLabel(block: EvidenceBlock) {
if (block.kind === "chart") return block.chartType === "line" ? "趋势图" : "对比图"; if (block.kind === "chart") return block.chartType === "line" ? "趋势图" : "对比图";
if (block.kind === "process-flow") return "流程证据"; if (block.kind === "process-flow") return "流程证据";
if (block.kind === "data-table") return "明细数据"; if (block.kind === "data-table") return "明细数据";
if (block.kind === "narrative") return "分析说明"; return "分析说明";
return "指标摘要";
} }
function getEvidenceBlockFacts(block: AnalysisBlock): Array<{ label: string; value: string }> { function getEvidenceBlockFacts(block: EvidenceBlock): Array<{ label: string; value: string }> {
if (block.kind === "chart") { if (block.kind === "chart") {
return [ return [
{ label: "数据序列", value: `${block.series.length}` }, { label: "数据序列", value: `${block.series.length}` },
{ label: "时间 / 类别点", value: `${block.categories.length}` }, { label: "时间 / 类别点", value: `${block.categories.length}` },
{ label: "计量单位", value: block.unit }, { label: "计量单位", value: block.unit }
{ label: "图表类型", value: block.chartType === "line" ? "趋势曲线" : "对比柱图" }
]; ];
} }
if (block.kind === "data-table") { if (block.kind === "data-table") {
return [ return [
{ label: "证据记录", value: `${block.rows.length}` }, { label: "证据记录", value: `${block.rows.length}` },
{ label: "数据字段", value: `${block.columns.length}` }, { label: "数据字段", value: `${block.columns.length}` }
{ label: "展示方式", value: "明细表格" },
{ label: "证据状态", value: "已纳入结论" }
]; ];
} }
if (block.kind === "process-flow") { if (block.kind === "process-flow") {
return [ return [
{ label: "流程步骤", value: `${block.nodes.length}` }, { label: "流程步骤", value: `${block.nodes.length}` },
{ label: "已完成", value: `${block.nodes.filter((node) => node.status === "complete").length}` }, { label: "已完成", value: `${block.nodes.filter((node) => node.status === "complete").length}` },
{ label: "当前进行", value: `${block.nodes.filter((node) => node.status === "active").length}` }, { label: "当前进行", value: `${block.nodes.filter((node) => node.status === "active").length}` }
{ label: "展示方式", value: "处置流程" }
]; ];
} }
if (block.kind === "narrative") { return [{ label: "分析段落", value: `${block.paragraphs.length}` }];
return [
{ label: "分析段落", value: `${block.paragraphs.length}` },
{ label: "展示方式", value: "分析说明" },
{ label: "证据状态", value: "已纳入结论" },
{ label: "内容来源", value: "Agent 生成" }
];
}
return [
{ label: "关键指标", value: `${block.metrics.length}` },
{ label: "展示方式", value: "指标摘要" },
{ label: "证据状态", value: "已纳入结论" },
{ label: "内容来源", value: "Agent 生成" }
];
} }
function formatAnalyticalTimeRange(range: AnalysisArtifact["analyticalTimeRange"]) { function formatAnalyticalTimeRange(range: AnalysisArtifact["analyticalTimeRange"]) {
@@ -479,8 +432,6 @@ function ArtifactActionBar({
artifact, artifact,
surfaceMode, surfaceMode,
mapSplitAvailable, mapSplitAvailable,
detailOpen,
onToggleDetail,
onSetView, onSetView,
onCollapse, onCollapse,
onDestroy onDestroy
@@ -488,8 +439,6 @@ function ArtifactActionBar({
artifact: AnalysisArtifact; artifact: AnalysisArtifact;
surfaceMode: WorkbenchSurfaceMode; surfaceMode: WorkbenchSurfaceMode;
mapSplitAvailable: boolean; mapSplitAvailable: boolean;
detailOpen: boolean;
onToggleDetail: () => void;
onSetView: (view: ArtifactRequestedView) => void; onSetView: (view: ArtifactRequestedView) => void;
onCollapse: () => void; onCollapse: () => void;
onDestroy: () => void; onDestroy: () => void;
@@ -503,10 +452,6 @@ function ArtifactActionBar({
{surfaceMode === "map_split" ? "专注分析" : "显示地图"} {surfaceMode === "map_split" ? "专注分析" : "显示地图"}
</button> </button>
) : null} ) : null}
<button type="button" aria-pressed={detailOpen} onClick={onToggleDetail} className="inline-flex h-10 items-center gap-2 rounded-md px-3 text-xs font-semibold text-slate-700 hover:bg-slate-100 active:scale-95">
{detailOpen ? <PanelRightClose size={15} aria-hidden="true" /> : <PanelRightOpen size={15} aria-hidden="true" />}
</button>
</div> </div>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
{artifact.actions.slice(0, 1).map((action) => <ArtifactPrimaryAction key={action.id} action={action} artifact={artifact} />)} {artifact.actions.slice(0, 1).map((action) => <ArtifactPrimaryAction key={action.id} action={action} artifact={artifact} />)}