feat: 圆桌会商 Step1 左栏样式优化(业务链关系图/分析记录卡片)

- 业务链:红色主题卡片头+单选圆点+席位胶囊+描述;选中可展开关系图,
  席位按 stages 分组用彩色圆形小图标排列,悬停浮层显示名称/描述
- 分析记录:圆角改 5px,新增已完成/未完成标签与「第 N 步」进度,
  标题/状态/时间/步骤四角布局,加选中高亮与 hover 阴影
- 修复滚动容器 overflow 裁剪导致卡片左边框被切

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-17 17:31:24 +08:00
1 parent ef1a12ab74
commit fc7576fea3
1 file changed
+260 -52
@@ -1595,6 +1595,7 @@ export default function RoundtablePlanningPage() {
selectedChainId={autoChainResult?.chain?.id ?? null}
onSelectChain={handleSelectChainFromSidebar}
onOpenDraft={drafts.handleLoadDraft}
activeDraftId={drafts.currentDraftId}
/>
{/* MIDDLE VIEWPORT */}
@@ -2100,6 +2101,8 @@ interface LeftSidebarProps {
onSelectChain: (chain: BusinessChain) => void;
/** 需求 0616 #2:Step1 落地左栏「分析记录」点击 → 打开历史会商分析(加载草稿)。 */
onOpenDraft: (draftId: string) => void;
/** 当前已加载草稿 id(分析记录选中高亮用),来自 drafts.currentDraftId。 */
activeDraftId: string | null;
}
function Step2SandboxSidebarSync({
@@ -2144,6 +2147,7 @@ function LeftSidebar({
selectedChainId,
onSelectChain,
onOpenDraft,
activeDraftId,
}: LeftSidebarProps) {
// 整片脱敏:左侧面板(任务概览/参与角色/人工参与等)的所有文本——标题、动态内容、
// 嵌套子组件文本一并脱敏。消息列表与沙箱是另外的容器,不挂此 Hook,保持原文。
@@ -2279,12 +2283,41 @@ function LeftSidebar({
selectedChainId={selectedChainId}
onSelectChain={onSelectChain}
onOpenDraft={onOpenDraft}
activeDraftId={activeDraftId}
/>
)}
</section>
);
}
/** 「展开关系图」示例芯片的轮换配色(仅作视觉区分,无业务语义)。 */
const SEAT_CHIP_PALETTE = [
"bg-rose-50 text-rose-700 border-rose-200",
"bg-amber-50 text-amber-700 border-amber-200",
"bg-teal-50 text-teal-700 border-teal-200",
"bg-purple-50 text-purple-700 border-purple-200",
"bg-sky-50 text-sky-700 border-sky-200",
"bg-emerald-50 text-emerald-700 border-emerald-200",
];
/**
* 把业务链席位按 `stages`(分层并行编排)分组:组内并行、组间串行;无 `stages`
* (线性链)时每个席位单独成一层。仅供左栏「展开关系图」做编排示意展示。
*/
function getChainStageGroups(chain: BusinessChain): BusinessChain["seats"][] {
const byId = new Map(chain.seats.map((s) => [s.agent_id, s]));
if (chain.stages && chain.stages.length > 0) {
return chain.stages
.map((stage) =>
stage
.map((id) => byId.get(id))
.filter((s): s is BusinessChain["seats"][number] => Boolean(s)),
)
.filter((stage) => stage.length > 0);
}
return chain.seats.map((s) => [s]);
}
/**
* 需求 0616 #2:多智能体会商 Step1(落地)左栏入口栏。
* - 业务链:仅公共链条(``listChains("public")``)前 3 个;点击以该链席位启动会商。
@@ -2295,12 +2328,15 @@ function RoundtableEntryRail({
selectedChainId,
onSelectChain,
onOpenDraft,
activeDraftId,
}: {
selectedChainId: string | null;
onSelectChain: (chain: BusinessChain) => void;
onOpenDraft: (draftId: string) => void;
activeDraftId: string | null;
}) {
const [chains, setChains] = useState<BusinessChain[]>([]);
const [expandedChainId, setExpandedChainId] = useState<string | null>(null);
const { drafts: records, refresh } = useRoundtableDrafts();
useEffect(() => {
@@ -2333,51 +2369,170 @@ function RoundtableEntryRail({
return (
<>
{/* 业务链(公共,前 3 个) */}
<div className="bg-white border border-slate-200/50 rounded-2xl p-4 shadow-[0_2px_8px_rgba(0,0,0,0.01)]">
<div className="flex items-center gap-2 border-b border-slate-100 pb-2.5 mb-3">
<Layers className="w-4 h-4 text-primary" strokeWidth={2.5} />
<h2 className="text-xs font-bold text-slate-900 uppercase tracking-widest">业务链</h2>
<div className="bg-white border border-[#ECE7E4] rounded-2xl p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] transition-shadow duration-300 hover:shadow-[0_4px_24px_rgba(45,37,34,0.04)]">
<div className="flex items-center gap-2 mb-3">
<span className="flex items-center justify-center w-5 h-5 rounded-md bg-primary text-primary-foreground">
<Layers className="w-3 h-3" strokeWidth={2.5} />
</span>
<h2 className="text-sm font-semibold tracking-wide text-slate-900">业务链</h2>
</div>
{chains.length === 0 ? (
<p className="text-xs text-slate-400 py-1.5">暂无公共业务链</p>
) : (
<div className="space-y-2">
<div className="flex flex-col gap-2.5">
{chains.map((chain) => {
const selected = selectedChainId === chain.id;
const graphOpen = expandedChainId === chain.id;
const stageGroups = getChainStageGroups(chain);
let seatColorIndex = 0;
return (
<button
<div
key={chain.id}
type="button"
onClick={() => onSelectChain(chain)}
title={selected ? "再次点击取消勾选" : `勾选业务链「${chain.title}」`}
aria-pressed={selected}
className={cn(
"w-full rounded-xl border p-2.5 text-left transition-colors",
"rounded-xl border transition-all duration-200",
selected
? "border-primary/40 bg-primary/5"
: "border-slate-100 bg-slate-50/60 hover:border-primary/30 hover:bg-white",
? "border-primary/25 bg-primary/5 ring-1 ring-primary/20 shadow-sm"
: "border-[#ECE7E4] bg-neutral-50/50 hover:border-neutral-300 hover:bg-neutral-50",
)}
>
<div className="flex items-center justify-between gap-2">
<span
className={cn(
"truncate text-xs font-extrabold",
selected ? "text-primary" : "text-slate-800",
)}
>
{chain.title}
</span>
{selected ? (
<Check className="h-3.5 w-3.5 shrink-0 text-primary" />
<button
type="button"
onClick={() => onSelectChain(chain)}
title={selected ? "再次点击取消勾选" : `勾选业务链「${chain.title}」`}
aria-pressed={selected}
className="w-full rounded-xl p-3 text-left"
>
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
{/* 单选圆点 */}
<span
className={cn(
"flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full border transition-all",
selected
? "border-primary bg-primary"
: "border-neutral-300 bg-white",
)}
>
{selected && <span className="h-1.5 w-1.5 rounded-full bg-white" />}
</span>
<span
className={cn(
"truncate text-xs font-semibold",
selected ? "text-primary" : "text-neutral-800",
)}
>
{chain.title}
</span>
</div>
<span
className={cn(
"shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-semibold",
selected
? "bg-primary/10 text-primary"
: "bg-neutral-100 text-neutral-500",
)}
>
{chain.seats.length} 席
</span>
</div>
{chain.description ? (
<p className="mt-1.5 line-clamp-2 text-[10.5px] leading-relaxed text-neutral-500">
{chain.description}
</p>
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-slate-400" />
<p className="mt-1.5 text-[10px] font-medium text-neutral-400">
{selected ? "已选 · 再次点击取消" : "点击勾选该业务链"}
</p>
)}
</div>
<div className="mt-1 text-[10px] font-semibold text-slate-400">
{chain.seats.length} 个席位
{selected ? " · 已选" : ""}
</div>
</button>
</button>
{/* 关系图示例:选中后可展开,把该链席位智能体用小图标排成编排示意 */}
{selected && (
<div className="mx-3 mb-3 border-t border-dashed border-[#E8D0CC]/60 pt-2.5">
<button
type="button"
onClick={() =>
setExpandedChainId(graphOpen ? null : chain.id)
}
className="flex w-full items-center justify-center gap-1 rounded-full border border-primary/15 bg-primary/5 px-3 py-1 text-[10.5px] font-semibold text-primary transition-colors hover:bg-primary/10"
>
<Layers className="h-3 w-3" />
<span>{graphOpen ? "收起关系图" : "展开关系图"}</span>
{graphOpen ? (
<ChevronUp className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)}
</button>
{graphOpen && (
<div className="mt-2 rounded-xl border border-dashed border-[#E8D0CC]/60 bg-[#FAF6F5] p-2.5">
{stageGroups.length === 0 ? (
<p className="py-2 text-center text-[10px] text-neutral-400">
该业务链暂无智能体席位
</p>
) : (
<div className="flex flex-col items-center gap-1.5">
{stageGroups.map((stage, si) => (
<div
key={si}
className="flex flex-col items-center gap-1"
>
{si > 0 && (
<ChevronDown className="h-3 w-3 text-neutral-300" />
)}
{stage.length > 1 && (
<span className="rounded-full bg-primary/5 px-1.5 text-[8.5px] font-semibold text-primary">
并行 {stage.length}
</span>
)}
<div className="flex flex-wrap items-center justify-center gap-1.5">
{stage.map((seat, sj) => {
const palette =
SEAT_CHIP_PALETTE[
seatColorIndex++ %
SEAT_CHIP_PALETTE.length
];
return (
<div
key={`${si}-${sj}-${seat.agent_id}`}
className="group/seat relative"
>
<span
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full border",
palette,
)}
>
<Cpu className="h-3 w-3" />
</span>
{/* 悬停浮层显示名称 / 描述 */}
<div className="pointer-events-none absolute bottom-full left-1/2 z-50 mb-1.5 hidden -translate-x-1/2 group-hover/seat:block">
<div className="max-w-[180px] rounded-lg bg-neutral-900 px-2 py-1 text-white shadow-lg">
<div className="whitespace-nowrap text-[10px] font-semibold">
{seat.name}
</div>
{seat.description && (
<div className="mt-0.5 whitespace-normal text-[9px] leading-snug text-neutral-300">
{seat.description}
</div>
)}
</div>
<div className="absolute left-1/2 top-full h-0 w-0 -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-neutral-900" />
</div>
</div>
);
})}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
)}
</div>
);
})}
</div>
@@ -2385,30 +2540,83 @@ function RoundtableEntryRail({
</div>
{/* 分析记录(圆桌历史草稿) */}
<div className="bg-white border border-slate-200/50 rounded-2xl p-4 shadow-[0_2px_8px_rgba(0,0,0,0.01)] flex-1 flex flex-col min-h-[200px]">
<div className="flex items-center gap-2 border-b border-slate-100 pb-2.5 mb-3">
<History className="w-4 h-4 text-primary" strokeWidth={2.5} />
<h2 className="text-xs font-bold text-slate-900 uppercase tracking-widest">分析记录</h2>
<div className="bg-white border border-[#ECE7E4] rounded-2xl p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] flex-1 flex flex-col min-h-[200px] overflow-hidden">
<div className="flex items-center gap-2 mb-3">
<span className="flex items-center justify-center w-5 h-5 rounded-md bg-primary text-primary-foreground">
<History className="w-3 h-3" strokeWidth={2.5} />
</span>
<h2 className="text-sm font-semibold tracking-wide text-slate-900">分析记录</h2>
</div>
{records.length === 0 ? (
<p className="text-xs text-slate-400 py-1.5">暂无分析记录</p>
<p className="flex-1 text-xs text-neutral-400 text-center py-8">暂无分析记录</p>
) : (
<div className="space-y-2 overflow-y-auto flex-1 pr-1 custom-scrollbar">
{records.map((rec) => (
<button
key={rec.id}
type="button"
onClick={() => onOpenDraft(rec.id)}
title={rec.title}
className="w-full rounded-xl border border-slate-100 bg-slate-50/60 p-2.5 text-left transition-colors hover:border-primary/20 hover:bg-white"
>
<div className="truncate text-xs font-bold text-slate-700">{rec.title}</div>
<div className="mt-1 text-[10px] font-semibold text-slate-400">
{formatTime(rec.updatedAt)}
</div>
</button>
))}
</div>
<>
<div className="space-y-2 overflow-y-auto flex-1 px-1 py-0.5 custom-scrollbar">
{records.map((rec) => {
// 已完成:进行到第 3 步(执行过方案总结报告)
const completed = rec.furthestStep === 3;
// 选中:当前已加载的草稿(正在查看的那条)
const active = activeDraftId === rec.id;
return (
<button
key={rec.id}
type="button"
onClick={() => onOpenDraft(rec.id)}
title={rec.title}
aria-current={active}
className={cn(
"group/rec w-full rounded-[5px] border px-3 py-2.5 text-left transition-all duration-200",
active
? "border-primary/40 bg-primary/5 ring-1 ring-primary/20 shadow-[0_2px_10px_rgba(45,37,34,0.05)]"
: "border-[#ECE7E4] bg-white hover:border-primary/30 hover:bg-neutral-50 hover:shadow-[0_2px_10px_rgba(45,37,34,0.05)]",
)}
>
{/* 上:标题(左) + 状态标签(右上、放大) */}
<div className="flex items-start justify-between gap-2">
<span
className={cn(
"truncate text-[13px] font-semibold",
active
? "text-primary"
: "text-neutral-800 group-hover/rec:text-primary",
)}
>
{rec.title}
</span>
<span
className={cn(
"inline-flex shrink-0 items-center gap-1 rounded-[5px] px-2 py-1 text-[11px] font-semibold",
completed
? "bg-emerald-50 text-emerald-600"
: "bg-amber-50 text-amber-600",
)}
>
<span
className={cn(
"h-1.5 w-1.5 rounded-full",
completed ? "bg-emerald-500" : "bg-amber-500",
)}
/>
{completed ? "已完成" : "未完成"}
</span>
</div>
{/* 下:时间(左下) + 步骤(右下) */}
<div className="mt-2 flex items-center justify-between">
<span className="font-mono text-[10px] text-neutral-400">
{formatTime(rec.updatedAt)}
</span>
<span className="rounded-[5px] bg-neutral-100 px-1.5 py-0.5 text-[10px] font-medium text-neutral-500">
第 {rec.furthestStep} 步
</span>
</div>
</button>
);
})}
</div>
<div className="mt-2 pt-2 border-t border-neutral-100 text-center text-[11px] text-neutral-400">
共 {records.length} 条分析记录
</div>
</>
)}
</div>
</>