feat: add adaptive agent workspace windows
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import ReactECharts from "echarts-for-react";
|
||||
import { Activity, Check, Clock3, FileSearch, Sparkles } from "lucide-react";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import type {
|
||||
AnalysisBlock,
|
||||
AnalysisDocument,
|
||||
AnalysisMetric
|
||||
} from "./workspace-model";
|
||||
|
||||
export function AnalysisDocumentView({ document }: { document: AnalysisDocument }) {
|
||||
return (
|
||||
<article className="h-full overflow-y-auto bg-[#f4f7fa] text-slate-900" lang="zh-CN">
|
||||
<header className="sticky top-0 z-10 flex items-start justify-between gap-5 border-b border-slate-200 bg-white/95 px-5 py-4 shadow-[0_1px_3px_rgba(15,23,42,0.08)] backdrop-blur-sm">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-blue-700">
|
||||
<Sparkles size={14} aria-hidden="true" />
|
||||
Agent 临时分析文档
|
||||
</div>
|
||||
<h2 className="mt-1 text-xl font-semibold leading-7 text-slate-950">{document.title}</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-slate-600">{document.subtitle}</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5 rounded-md bg-slate-100 px-2.5 py-1.5 text-xs text-slate-600">
|
||||
<Clock3 size={13} aria-hidden="true" />
|
||||
{document.generatedAt}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 p-4 xl:p-5">
|
||||
{document.blocks.map((block) => (
|
||||
<AnalysisBlockView key={block.id} block={block} />
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function AnalysisBlockView({ block }: { block: AnalysisBlock }) {
|
||||
const spanClass = block.span === 5 ? "col-span-12 xl:col-span-5" : block.span === 7 ? "col-span-12 xl:col-span-7" : "col-span-12";
|
||||
|
||||
if (block.kind === "metric-grid") {
|
||||
return (
|
||||
<section className={spanClass} aria-labelledby={`${block.id}-title`}>
|
||||
<h3 id={`${block.id}-title`} className="sr-only">{block.title}</h3>
|
||||
<div className="grid grid-cols-2 gap-3 2xl:grid-cols-4">
|
||||
{block.metrics.map((metric) => (
|
||||
<MetricTile key={metric.label} metric={metric} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (block.kind === "chart") {
|
||||
return (
|
||||
<AnalysisSection block={block} className={spanClass}>
|
||||
<div className="h-[258px] min-h-0">
|
||||
<ReactECharts
|
||||
notMerge
|
||||
lazyUpdate
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
option={createChartOption(block)}
|
||||
/>
|
||||
</div>
|
||||
</AnalysisSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (block.kind === "process-flow") {
|
||||
return (
|
||||
<AnalysisSection block={block} className={spanClass}>
|
||||
<ol className="space-y-0" aria-label={block.title}>
|
||||
{block.nodes.map((node, index) => (
|
||||
<li key={node.id} className="relative flex gap-3 pb-4 last:pb-0">
|
||||
{index < block.nodes.length - 1 ? (
|
||||
<span className="absolute left-[15px] top-8 h-[calc(100%-1rem)] w-px bg-slate-200" aria-hidden="true" />
|
||||
) : null}
|
||||
<span
|
||||
className={cn(
|
||||
"relative z-[1] grid h-8 w-8 shrink-0 place-items-center rounded-full",
|
||||
node.status === "complete" && "bg-emerald-100 text-emerald-700",
|
||||
node.status === "active" && "bg-blue-600 text-white shadow-[0_0_0_4px_rgba(37,99,235,0.12)]",
|
||||
node.status === "pending" && "bg-slate-100 text-slate-500"
|
||||
)}
|
||||
>
|
||||
{node.status === "complete" ? <Check size={15} aria-hidden="true" /> : node.status === "active" ? <Activity size={15} aria-hidden="true" /> : <Clock3 size={14} aria-hidden="true" />}
|
||||
</span>
|
||||
<div className="min-w-0 pt-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-semibold text-slate-900">{node.label}</p>
|
||||
{node.status === "active" ? <span className="rounded-full bg-blue-50 px-2 py-0.5 text-xs font-semibold text-blue-700">当前</span> : null}
|
||||
</div>
|
||||
<p className="mt-1 text-sm leading-5 text-slate-600">{node.detail}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</AnalysisSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (block.kind === "data-table") {
|
||||
return (
|
||||
<AnalysisSection block={block} className={spanClass}>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[520px] border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50 text-xs font-semibold text-slate-500">
|
||||
{block.columns.map((column) => (
|
||||
<th key={column.key} scope="col" className={cn("px-3 py-2.5 text-left", column.numeric && "text-right")}>{column.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{block.rows.map((row, index) => (
|
||||
<tr key={`${block.id}-${index}`} className="border-b border-slate-100 last:border-0">
|
||||
{block.columns.map((column) => (
|
||||
<td key={column.key} className={cn("px-3 py-3 text-slate-700", column.numeric && "text-right tabular-nums")}>{row[column.key]}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</AnalysisSection>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AnalysisSection block={block} className={spanClass}>
|
||||
<div className="space-y-3 text-sm leading-7 text-slate-700">
|
||||
{block.paragraphs.map((paragraph) => <p key={paragraph}>{paragraph}</p>)}
|
||||
</div>
|
||||
</AnalysisSection>
|
||||
);
|
||||
}
|
||||
|
||||
function AnalysisSection({
|
||||
block,
|
||||
className,
|
||||
children
|
||||
}: {
|
||||
block: Exclude<AnalysisBlock, { kind: "metric-grid" }>;
|
||||
className: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className={cn("min-w-0 overflow-hidden rounded-lg bg-white shadow-[0_1px_4px_rgba(15,23,42,0.12)]", className)} aria-labelledby={`${block.id}-title`}>
|
||||
<header className="flex items-start gap-3 border-b border-slate-100 px-4 py-3.5">
|
||||
<span className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-md bg-blue-50 text-blue-700"><FileSearch size={16} aria-hidden="true" /></span>
|
||||
<div className="min-w-0">
|
||||
<h3 id={`${block.id}-title`} className="text-sm font-semibold text-slate-950">{block.title}</h3>
|
||||
{"description" in block ? <p className="mt-1 text-xs leading-5 text-slate-500">{block.description}</p> : null}
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-4">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricTile({ metric }: { metric: AnalysisMetric }) {
|
||||
return (
|
||||
<div className="min-w-0 rounded-lg bg-white px-4 py-3.5 shadow-[0_1px_4px_rgba(15,23,42,0.12)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("h-2 w-2 rounded-full", metricToneClass(metric.tone))} aria-hidden="true" />
|
||||
<p className="truncate text-xs font-medium text-slate-500">{metric.label}</p>
|
||||
</div>
|
||||
<p className="mt-2 text-xl font-semibold text-slate-950 tabular-nums">{metric.value}</p>
|
||||
<p className="mt-1 truncate text-xs text-slate-500" title={metric.detail}>{metric.detail}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function createChartOption(block: Extract<AnalysisBlock, { kind: "chart" }>) {
|
||||
return {
|
||||
animationDuration: 260,
|
||||
animationEasing: "cubicOut",
|
||||
color: block.series.map((series) => series.color),
|
||||
grid: { top: 40, right: 18, bottom: 34, left: 52, containLabel: false },
|
||||
legend: { top: 0, right: 0, itemWidth: 12, itemHeight: 7, textStyle: { color: "#475569", fontSize: 11 } },
|
||||
tooltip: { trigger: "axis", confine: true, borderWidth: 0, backgroundColor: "rgba(15,23,42,0.92)", textStyle: { color: "#f8fafc", fontSize: 12 } },
|
||||
xAxis: { type: "category", data: block.categories, boundaryGap: block.chartType === "bar", axisLine: { lineStyle: { color: "#cbd5e1" } }, axisTick: { show: false }, axisLabel: { color: "#64748b", fontSize: 11 } },
|
||||
yAxis: { type: "value", name: block.unit, nameTextStyle: { color: "#64748b", fontSize: 11, padding: [0, 0, 0, -26] }, splitLine: { lineStyle: { color: "rgba(148,163,184,0.18)" } }, axisLabel: { color: "#64748b", fontSize: 11 } },
|
||||
series: block.series.map((series) => ({
|
||||
name: series.name,
|
||||
type: series.type ?? block.chartType,
|
||||
data: series.values,
|
||||
smooth: (series.type ?? block.chartType) === "line" ? 0.28 : undefined,
|
||||
symbol: "circle",
|
||||
symbolSize: 6,
|
||||
barMaxWidth: 24,
|
||||
lineStyle: { width: 2.5 },
|
||||
itemStyle: { borderRadius: (series.type ?? block.chartType) === "bar" ? [3, 3, 0, 0] : undefined }
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function metricToneClass(tone: AnalysisMetric["tone"]) {
|
||||
if (tone === "danger") return "bg-rose-500";
|
||||
if (tone === "warning") return "bg-amber-500";
|
||||
if (tone === "success") return "bg-emerald-500";
|
||||
if (tone === "info") return "bg-blue-600";
|
||||
return "bg-slate-400";
|
||||
}
|
||||
Reference in New Issue
Block a user