页面修改

This commit is contained in:
fengxiangdebaozi committed 2026-06-02 21:25:44 +08:00
1 parent fb43c036f6
commit 02eee4bd3e
2 files changed
+63 -62

No files matched your search

@@ -299,6 +299,7 @@ chain 模式的 Step 2 init **完全复用** `initMultiAgent`:`agentNames = ch
- **验收**:✅ typecheck 0 新增错误(76 个预存量错误均在无关模块)。**人工验收待跑**:新建「情报→方案→风险→总结」链保存 → 刷新后下拉重载 → 右栏拖拽排序 → 左栏拖入指定位 → 2–8 边界禁用保存。
- **切换成本备注**:若改全屏 Dialog 方案,编辑器组件原样复用,仅去掉路由、外面套 `Dialog`。
- **入口说明**:圆桌本身未在侧栏(`sidebar-menu.ts` 入口被注释),本页暂经直链 `/page/workspace/roundtable/chains` 或顶栏「返回圆桌」往返;阶段 3 推荐弹窗「去配置」按钮会导航到此。
- **视觉(后续重画)**:本页**不**套圆桌 `roundtable-planning` scope,直接遵循《浅红色/深蓝色 UI 规范》——用 app 语义 token(光 `--primary` 红 hue27 / 暗 蓝 hue259,已内置自适应)+ 规范小圆角(输入/按钮/tag/卡片内项 2px = `R_CTRL`,分栏/面板 4px = `R_CARD`)+ 克制阴影(`PANEL_SHADOW`)。筛选框复用通用 `TagSearchBar`,配色随 app 中性 token,不再被红 scope 染色。
### 阶段 2 · 推荐弹窗「模式选择」外壳(1 天) ✅ 已完成
**目标**:弹窗顶部模式切换,智能推荐行为不变。
@@ -11,8 +11,9 @@
* 拖拽用 `@dnd-kit`:右栏是 `SortableContext`(席位互相排序),左栏卡片是
* `useDraggable`(pool→右栏插入)。落点计算在 `onDragEnd`。
*
* 视觉:套 `roundtable-planning`(+`--dark`) 作用域,与圆桌主页面同一套浅红/深蓝皮肤
* (`styles/roundtable-planning.css`,对齐 docs/浅红色UI规范 + ui规范-深蓝)。
* 视觉:直接遵循《浅红色 UI 规范》/《深蓝色 UI 规范》—— 用 app 语义 token(光红暗蓝
* 已内置)+ 规范小圆角(输入/按钮/tag 2px、卡片/分栏 4px)+ 克制阴影。**不**套圆桌页
* 的 `roundtable-planning` scope。
*
* 持久化走 `api/chains.ts`(后端 MySQL `/api/roundtable-chains`)。席位数约束
* 2 ≤ N ≤ 8、agent 去重,保存时校验(后端 `_validate_seats` 也兜底)。
@@ -56,7 +57,6 @@ import {
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@/lib/utils";
import { useTheme } from "@/contexts/ThemeContext";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
@@ -85,6 +85,13 @@ const POOL_PREFIX = "pool::";
const SEAT_PREFIX = "seat::";
const SEATS_DROPPABLE_ID = "seats-droppable";
// 规范圆角(§4):输入/按钮/tag 2px,卡片/分栏 4px。app 默认 --radius=12px 偏大,
// 这里用 arbitrary 值精确命中规范。
const R_CARD = "rounded-[4px]";
const R_CTRL = "rounded-[2px]";
// 规范阴影(§3):克制的浅投影,深色下几乎不可见。
const PANEL_SHADOW = "shadow-[0_1px_2px_rgba(15,23,42,0.05)]";
type ActiveDrag =
| { kind: "pool"; agentId: string; name: string }
| { kind: "seat"; agentId: string; name: string }
@@ -92,8 +99,6 @@ type ActiveDrag =
export default function BusinessChainEditorPage() {
const navigate = useNavigate();
const { theme } = useTheme();
const isDark = theme === "dark";
// ─── 数据 ────────────────────────────────────────────────────────────
const [pool, setPool] = useState<RecommendCandidate[]>([]);
@@ -117,8 +122,6 @@ export default function BusinessChainEditorPage() {
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
);
// 候选池按 agent_id 取元数据。注意 pool 是「当前筛选视图」——已加入但被筛掉
// 的席位名称回退用 seat 自带快照(loadChain 写入),所以 poolById 缺项不影响保存。
const poolById = useMemo(() => {
const m = new Map<string, RecommendCandidate>();
for (const a of pool) m.set(a.agent_id, a);
@@ -235,7 +238,6 @@ export default function BusinessChainEditorPage() {
const overId = e.over ? String(e.over.id) : null;
if (!overId) return;
// over 落在第几个席位之前(over 是席位则取其索引,否则追加到末尾)。
const overSeatIndex = overId.startsWith(SEAT_PREFIX)
? seats.findIndex((s) => SEAT_PREFIX + s.agent_id === overId)
: -1;
@@ -344,31 +346,26 @@ export default function BusinessChainEditorPage() {
const seatItemIds = seats.map((s) => SEAT_PREFIX + s.agent_id);
return (
<div
className={cn(
"roundtable-planning theme-transition flex h-full min-h-0 flex-col bg-[var(--rt-page-bg)] font-sans text-foreground",
isDark && "roundtable-planning--dark",
)}
>
<div className="flex h-full min-h-0 flex-col bg-muted/40 text-foreground">
{/* 顶栏 */}
<header className="flex shrink-0 flex-wrap items-center gap-3 border-b border-border bg-card px-5 py-3 shadow-[0_1px_3px_rgba(0,0,0,0.03)]">
<header className="flex shrink-0 flex-wrap items-center gap-3 border-b border-border bg-card px-5 py-2.5">
<Button
variant="ghost"
size="sm"
onClick={() => navigate("/page/workspace/roundtable/planning")}
className="gap-1.5 text-muted-foreground hover:text-foreground"
className={cn("h-8 gap-1.5 px-2 text-muted-foreground hover:text-foreground", R_CTRL)}
>
<ArrowLeft className="size-4" />
返回圆桌
</Button>
<div className="h-5 w-px bg-border" />
<div className="h-4 w-px bg-border" />
<div className="flex items-center gap-2.5">
<span className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
<span className={cn("flex size-8 items-center justify-center bg-primary/10 text-primary", R_CARD)}>
<Layers className="size-4" />
</span>
<div className="leading-tight">
<div className="text-sm font-bold">业务链条配置</div>
<div className="text-[11px] text-muted-foreground">编排有序的智能体研讨链路</div>
<div className="text-sm font-semibold">业务链条配置</div>
<div className="text-xs text-muted-foreground">编排有序的智能体研讨链路</div>
</div>
</div>
@@ -385,7 +382,10 @@ export default function BusinessChainEditorPage() {
const chain = chains.find((c) => c.id === id);
if (chain) loadChain(chain);
}}
className="h-8 rounded-md border border-input bg-card px-2.5 text-sm font-medium shadow-sm outline-none focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
className={cn(
"h-8 border border-input bg-card px-2.5 text-sm text-foreground outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/30",
R_CTRL,
)}
>
<option value="">+ 新建链条…</option>
{chains.map((c) => (
@@ -394,7 +394,7 @@ export default function BusinessChainEditorPage() {
</option>
))}
</select>
<Button variant="outline" size="sm" onClick={startNewChain} className="gap-1.5">
<Button variant="outline" size="sm" onClick={startNewChain} className={cn("h-8 gap-1.5", R_CTRL)}>
<Plus className="size-4" />
新建
</Button>
@@ -406,7 +406,7 @@ export default function BusinessChainEditorPage() {
size="sm"
onClick={handleDelete}
disabled={!currentChainId}
className="gap-1.5 text-destructive hover:text-destructive"
className={cn("h-8 gap-1.5 text-destructive hover:text-destructive", R_CTRL)}
>
<Trash2 className="size-4" />
删除
@@ -415,7 +415,7 @@ export default function BusinessChainEditorPage() {
size="sm"
onClick={handleSave}
disabled={!canSave}
className="gap-1.5 bg-primary text-primary-foreground hover:opacity-90"
className={cn("h-8 gap-1.5", R_CTRL)}
>
{saving ? <Loader2 className="size-4 animate-spin" /> : <Save className="size-4" />}
保存
@@ -424,24 +424,24 @@ export default function BusinessChainEditorPage() {
</header>
{/* 标题 + 描述 */}
<div className="flex shrink-0 flex-col gap-2 border-b border-border bg-card/60 px-5 py-3 sm:flex-row sm:items-center">
<div className="flex items-center gap-2 sm:w-72">
<span className="shrink-0 text-xs font-semibold text-muted-foreground">标题</span>
<div className="flex shrink-0 flex-col gap-2.5 border-b border-border bg-card px-5 py-3 sm:flex-row sm:items-center">
<div className="flex items-center gap-2 sm:w-80">
<span className="shrink-0 text-xs font-medium text-muted-foreground">标题</span>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="如:应急方案三段链"
className="bg-card"
className={cn("h-8 bg-background", R_CTRL)}
maxLength={120}
/>
</div>
<div className="flex flex-1 items-center gap-2">
<span className="shrink-0 text-xs font-semibold text-muted-foreground">描述</span>
<span className="shrink-0 text-xs font-medium text-muted-foreground">描述</span>
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="链条用途说明(可选)"
className="bg-card"
className={cn("h-8 bg-background", R_CTRL)}
maxLength={500}
/>
</div>
@@ -456,15 +456,15 @@ export default function BusinessChainEditorPage() {
>
<div className="grid min-h-0 flex-1 grid-cols-1 gap-4 overflow-hidden p-4 md:grid-cols-[360px_1fr]">
{/* 左栏:候选智能体 */}
<section className="flex min-h-0 flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-sm">
<div className="shrink-0 space-y-2.5 border-b border-border px-3.5 pb-3 pt-3">
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
<section className={cn("flex min-h-0 flex-col overflow-hidden border border-border bg-card", R_CARD, PANEL_SHADOW)}>
<div className="shrink-0 space-y-2.5 border-b border-border px-3.5 py-3">
<div className="flex items-center gap-2 text-xs font-semibold text-foreground">
<Users className="size-3.5 text-primary" />
候选智能体
<span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold text-primary tabular-nums">
<span className={cn("bg-primary/10 px-1.5 py-0.5 text-[11px] font-medium text-primary tabular-nums", R_CTRL)}>
{pool.length}
</span>
<span className="ml-auto text-[10px] font-normal normal-case text-muted-foreground/70">
<span className="ml-auto text-[11px] font-normal text-muted-foreground">
拖到右侧 / 点击加入
</span>
</div>
@@ -475,7 +475,7 @@ export default function BusinessChainEditorPage() {
onTagIdsChange={setFilterTagIds}
/>
</div>
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto p-2.5 custom-scrollbar">
<div className="min-h-0 flex-1 space-y-1.5 overflow-y-auto p-2.5">
{poolLoading ? (
<div className="flex items-center gap-2 px-2 py-6 text-sm text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
@@ -511,7 +511,7 @@ export default function BusinessChainEditorPage() {
<DragOverlay>
{activeDrag ? (
<div className="flex items-center gap-2 rounded-lg border border-primary/40 bg-card px-3 py-2 text-sm font-semibold shadow-xl">
<div className={cn("flex items-center gap-2 border border-primary/50 bg-card px-3 py-2 text-sm font-medium text-foreground shadow-[0_4px_12px_rgba(15,23,42,0.12)]", R_CTRL)}>
<GripVertical className="size-4 text-primary" />
{activeDrag.name}
</div>
@@ -541,17 +541,18 @@ function PoolAgentCard({
<div
ref={setNodeRef}
className={cn(
"group flex items-start gap-2 rounded-xl border p-2.5 transition-all",
"group flex items-start gap-2 border p-2.5 transition-colors",
R_CTRL,
inChain
? "cursor-not-allowed border-dashed border-border bg-muted/30 opacity-60"
: "cursor-grab border-border bg-card hover:border-primary/40 hover:bg-primary/[0.04] hover:shadow-sm active:cursor-grabbing",
? "cursor-not-allowed border-dashed border-border bg-muted/40 opacity-60"
: "cursor-grab border-border bg-card hover:border-primary/50 hover:bg-primary/[0.03] active:cursor-grabbing",
isDragging && "opacity-30",
)}
{...(inChain ? {} : listeners)}
{...attributes}
>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold">{agent.name || agent.agent_id}</div>
<div className="truncate text-sm font-medium text-foreground">{agent.name || agent.agent_id}</div>
{agent.description && (
<div className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
{agent.description}
@@ -559,7 +560,7 @@ function PoolAgentCard({
)}
</div>
{inChain ? (
<span className="shrink-0 self-center rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary">
<span className={cn("shrink-0 self-center bg-primary/10 px-1.5 py-0.5 text-[11px] font-medium text-primary", R_CTRL)}>
已加入
</span>
) : (
@@ -567,7 +568,10 @@ function PoolAgentCard({
type="button"
onClick={onAdd}
onPointerDown={(e) => e.stopPropagation()}
className="shrink-0 self-center rounded-lg p-1.5 text-muted-foreground opacity-0 transition-all hover:bg-primary hover:text-primary-foreground group-hover:opacity-100"
className={cn(
"shrink-0 self-center p-1.5 text-muted-foreground opacity-0 transition-all hover:bg-primary hover:text-primary-foreground group-hover:opacity-100",
R_CTRL,
)}
title="加入链条"
>
<Plus className="size-4" />
@@ -592,16 +596,17 @@ function SeatsColumn({
}) {
const { setNodeRef, isOver } = useDroppable({ id: SEATS_DROPPABLE_ID });
return (
<section className="flex min-h-0 flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-sm">
<div className="flex shrink-0 items-center gap-2 border-b border-border px-4 py-3 text-xs font-bold uppercase tracking-wide text-muted-foreground">
<section className={cn("flex min-h-0 flex-col overflow-hidden border border-border bg-card", R_CARD, PANEL_SHADOW)}>
<div className="flex shrink-0 items-center gap-2 border-b border-border px-4 py-3 text-xs font-semibold text-foreground">
<ListOrdered className="size-3.5 text-primary" />
链条席位
<span className="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium normal-case text-muted-foreground">
<span className={cn("bg-muted px-1.5 py-0.5 text-[11px] font-normal text-muted-foreground", R_CTRL)}>
按顺序发言
</span>
<span
className={cn(
"ml-auto rounded-full px-2 py-0.5 text-[11px] font-bold tabular-nums normal-case",
"ml-auto px-1.5 py-0.5 text-[11px] font-medium tabular-nums",
R_CTRL,
seatCountInvalid
? "bg-destructive/10 text-destructive"
: "bg-primary/10 text-primary",
@@ -614,12 +619,12 @@ function SeatsColumn({
<div
ref={setNodeRef}
className={cn(
"min-h-0 flex-1 space-y-2 overflow-y-auto p-3.5 custom-scrollbar transition-colors",
isOver && "bg-primary/[0.04]",
"min-h-0 flex-1 space-y-2 overflow-y-auto p-3.5 transition-colors",
isOver && "bg-primary/[0.03]",
)}
>
{seats.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-border text-sm text-muted-foreground">
<div className={cn("flex h-full flex-col items-center justify-center gap-2 border border-dashed border-border text-sm text-muted-foreground", R_CARD)}>
<Layers className="size-7 opacity-40" />
从左侧把智能体拖到这里,组成有序业务链条
</div>
@@ -630,7 +635,6 @@ function SeatsColumn({
key={seat.agent_id}
seat={seat}
index={index}
isLast={index === seats.length - 1}
onRemove={() => onRemove(seat.agent_id)}
/>
))}
@@ -646,12 +650,10 @@ function SeatsColumn({
function SeatStrip({
seat,
index,
isLast,
onRemove,
}: {
seat: ChainSeat;
index: number;
isLast: boolean;
onRemove: () => void;
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
@@ -662,14 +664,11 @@ function SeatStrip({
ref={setNodeRef}
style={{ transform: CSS.Transform.toString(transform), transition }}
className={cn(
"relative flex items-center gap-3 rounded-xl border border-border bg-card p-3 shadow-sm transition-shadow",
isDragging && "z-10 opacity-70 shadow-lg ring-1 ring-primary/30",
"flex items-center gap-3 border border-border bg-card p-2.5",
R_CTRL,
isDragging && "z-10 opacity-70 shadow-[0_4px_12px_rgba(15,23,42,0.12)]",
)}
>
{/* 顺序连接线(最后一个不画) */}
{!isLast && (
<span className="pointer-events-none absolute -bottom-2 left-[26px] h-2 w-px bg-border" />
)}
<button
type="button"
className="shrink-0 cursor-grab touch-none text-muted-foreground/60 hover:text-foreground active:cursor-grabbing"
@@ -681,14 +680,15 @@ function SeatStrip({
</button>
<span
className={cn(
"flex size-7 shrink-0 items-center justify-center rounded-full border text-[12px] font-bold tabular-nums",
"flex size-6 shrink-0 items-center justify-center border text-[12px] font-semibold tabular-nums",
R_CTRL,
chainTagClasses(avatarType),
)}
>
{index + 1}
</span>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold">{seat.name || seat.agent_id}</div>
<div className="truncate text-sm font-medium text-foreground">{seat.name || seat.agent_id}</div>
{seat.description && (
<div className="mt-0.5 line-clamp-1 text-xs text-muted-foreground">
{seat.description}
@@ -698,7 +698,7 @@ function SeatStrip({
<button
type="button"
onClick={onRemove}
className="shrink-0 rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
className={cn("shrink-0 p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive", R_CTRL)}
title="移除"
>
<X className="size-4" />