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,
Map as MapIcon,
Minimize2,
PanelRightClose,
PanelRightOpen,
ShieldCheck,
Sparkles,
Trash2,
X
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import { showMapNotice } from "@/features/map/core";
import { cn } from "@/shared/ui/cn";
import type {
@@ -38,6 +36,8 @@ type AnalysisArtifactWorkspaceProps = {
onDestroy: () => void;
};
type EvidenceBlock = Exclude<AnalysisBlock, { kind: "metric-grid" }>;
export function AnalysisArtifactWorkspace({
artifact,
surfaceMode,
@@ -48,11 +48,10 @@ export function AnalysisArtifactWorkspace({
}: AnalysisArtifactWorkspaceProps) {
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
const metricBlock = artifact.blocks.find((block) => block.kind === "metric-grid");
const evidenceBlocks = artifact.blocks.filter((block) => block.kind !== "metric-grid");
const selectedBlock = useMemo(
() => artifact.blocks.find((block) => block.id === selectedBlockId) ?? null,
[artifact.blocks, selectedBlockId]
const evidenceBlocks = artifact.blocks.filter(
(block): block is EvidenceBlock => block.kind !== "metric-grid"
);
const selectedBlock = evidenceBlocks.find((block) => block.id === selectedBlockId) ?? null;
useEffect(() => {
setSelectedBlockId(null);
@@ -118,8 +117,6 @@ export function AnalysisArtifactWorkspace({
artifact={artifact}
surfaceMode={surfaceMode}
mapSplitAvailable={mapSplitAvailable}
detailOpen={Boolean(selectedBlock)}
onToggleDetail={() => setSelectedBlockId((current) => current ? null : evidenceBlocks[0]?.id ?? null)}
onSetView={onSetView}
onCollapse={onCollapse}
onDestroy={onDestroy}
@@ -176,7 +173,7 @@ function AnalysisBlockView({
selected,
onSelect
}: {
block: Exclude<AnalysisBlock, { kind: "metric-grid" }>;
block: EvidenceBlock;
selected: boolean;
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") {
return (
<div className="h-[258px] min-h-0">
@@ -286,9 +283,9 @@ function ArtifactProvenance({ artifact }: { artifact: AnalysisArtifact }) {
<Database size={14} aria-hidden="true" />
</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) => (
<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>
</div>
@@ -313,7 +310,7 @@ function ArtifactDetailDrawer({
onClose
}: {
artifact: AnalysisArtifact;
block: AnalysisBlock;
block: EvidenceBlock;
onClose: () => void;
}) {
const blockFacts = getEvidenceBlockFacts(block);
@@ -321,13 +318,11 @@ function ArtifactDetailDrawer({
return (
<aside
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="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} aria-hidden="true" />
</span>
<FileSearch size={17} className="mt-0.5 shrink-0 text-blue-700" aria-hidden="true" />
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold text-slate-500">
<span className="uppercase tracking-[0.12em]"></span>
@@ -346,32 +341,24 @@ function ArtifactDetailDrawer({
<X size={17} aria-hidden="true" />
</button>
</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">
<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">
{"description" in block ? block.description : "该模块汇总了当前成果中的关键判断依据。"}
{evidenceBlockDescription(block)}
</p>
</section>
<section className="material-tone-agent rounded-xl border px-4 py-3.5" aria-labelledby="evidence-relation-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">
<section className="border-t border-slate-200 pt-5" aria-labelledby="evidence-data-heading">
<div className="flex items-center justify-between gap-3">
<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>
</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) => (
<div key={fact.label} className="surface-reading min-w-0 px-3.5 py-3">
<dt className="text-[11px] 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>
<div key={fact.label} className="flex items-center justify-between gap-4 py-2 text-xs">
<dt className="text-slate-500">{fact.label}</dt>
<dd className="font-medium text-slate-800 tabular-nums">{fact.value}</dd>
</div>
))}
</dl>
@@ -388,80 +375,46 @@ function ArtifactDetailDrawer({
<dd className="text-right font-medium text-slate-800 tabular-nums">{artifact.confidence}</dd>
</dl>
</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>
</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 === "process-flow") 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") {
return [
{ label: "数据序列", value: `${block.series.length}` },
{ label: "时间 / 类别点", value: `${block.categories.length}` },
{ label: "计量单位", value: block.unit },
{ label: "图表类型", value: block.chartType === "line" ? "趋势曲线" : "对比柱图" }
{ label: "计量单位", value: block.unit }
];
}
if (block.kind === "data-table") {
return [
{ label: "证据记录", value: `${block.rows.length}` },
{ label: "数据字段", value: `${block.columns.length}` },
{ label: "展示方式", value: "明细表格" },
{ label: "证据状态", value: "已纳入结论" }
{ label: "数据字段", value: `${block.columns.length}` }
];
}
if (block.kind === "process-flow") {
return [
{ label: "流程步骤", value: `${block.nodes.length}` },
{ label: "已完成", value: `${block.nodes.filter((node) => node.status === "complete").length}` },
{ label: "当前进行", value: `${block.nodes.filter((node) => node.status === "active").length}` },
{ label: "展示方式", value: "处置流程" }
{ label: "当前进行", value: `${block.nodes.filter((node) => node.status === "active").length}` }
];
}
if (block.kind === "narrative") {
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 生成" }
];
return [{ label: "分析段落", value: `${block.paragraphs.length}` }];
}
function formatAnalyticalTimeRange(range: AnalysisArtifact["analyticalTimeRange"]) {
@@ -479,8 +432,6 @@ function ArtifactActionBar({
artifact,
surfaceMode,
mapSplitAvailable,
detailOpen,
onToggleDetail,
onSetView,
onCollapse,
onDestroy
@@ -488,8 +439,6 @@ function ArtifactActionBar({
artifact: AnalysisArtifact;
surfaceMode: WorkbenchSurfaceMode;
mapSplitAvailable: boolean;
detailOpen: boolean;
onToggleDetail: () => void;
onSetView: (view: ArtifactRequestedView) => void;
onCollapse: () => void;
onDestroy: () => void;
@@ -503,10 +452,6 @@ function ArtifactActionBar({
{surfaceMode === "map_split" ? "专注分析" : "显示地图"}
</button>
) : 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 className="flex shrink-0 items-center gap-1">
{artifact.actions.slice(0, 1).map((action) => <ArtifactPrimaryAction key={action.id} action={action} artifact={artifact} />)}