feat: add adaptive agent workspace windows

This commit is contained in:
2026-08-19 12:57:37 +08:00
parent d08cf2abc1
commit f58e0a70f4
18 changed files with 1797 additions and 309 deletions
@@ -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";
}