refactor: simplify evidence workspace presentation
This commit is contained in:
@@ -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} />)}
|
||||
|
||||
Reference in New Issue
Block a user