样式修改
This commit is contained in:
1 parent
3b5525a0a5
commit
6521218308
12 files changed
+1435
-163
No files matched your search
@@ -5,15 +5,16 @@
|
||||
* - 左栏:候选智能体列表(`listAgents()` + `filterRecommendCandidates` 复用,剔除
|
||||
* intent/recommender/coordinator),支持**名称检索 + 标签筛选**(`TagSearchBar`,
|
||||
* 服务端过滤),竖排卡片,可拖到右栏、也可点击快捷加入。
|
||||
* - 右栏:当前链条的**有序**长条卡片(序号 + 名称 + 描述),可拖拽排序、删除单项;
|
||||
* 左栏拖入时按落点插入到对应位置。
|
||||
* - 右栏:当前链条的**有序**长条卡片(序号 + 名称 + 描述),可拖拽排序、上/下移、
|
||||
* 删除单项;左栏拖入时按落点插入到对应位置。
|
||||
*
|
||||
* 拖拽用 `@dnd-kit`:右栏是 `SortableContext`(席位互相排序),左栏卡片是
|
||||
* `useDraggable`(pool→右栏插入)。落点计算在 `onDragEnd`。
|
||||
*
|
||||
* 视觉:直接遵循《浅红色 UI 规范》/《深蓝色 UI 规范》—— 用 app 语义 token(光红暗蓝
|
||||
* 已内置)+ 规范小圆角(输入/按钮/tag 2px、卡片/分栏 4px)+ 克制阴影。**不**套圆桌页
|
||||
* 的 `roundtable-planning` scope。
|
||||
* 视觉:对齐设计稿 `yewuliantiao`(浅红 / 绛红风格)。统一走 app 语义 token
|
||||
* (`primary` 在浅色主题即设计稿的绛红 #c25555,深色主题自动切蓝),配规范小圆角
|
||||
* (输入/按钮/tag 2px、卡片/分栏 4px)+ 克制阴影。**不**套圆桌页的
|
||||
* `roundtable-planning` scope。
|
||||
*
|
||||
* 持久化走 `api/chains.ts`(后端 MySQL `/api/roundtable-chains`)。席位数约束
|
||||
* 2 ≤ N ≤ 8、agent 去重,保存时校验(后端 `_validate_seats` 也兜底)。
|
||||
@@ -26,14 +27,16 @@ import { toast } from "sonner";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
GripVertical,
|
||||
Layers,
|
||||
ListOrdered,
|
||||
Loader2,
|
||||
Plus,
|
||||
Save,
|
||||
Trash2,
|
||||
Users,
|
||||
UserPlus,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@@ -57,7 +60,6 @@ import {
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||
import { listAgents } from "@/core/agents";
|
||||
@@ -116,7 +118,7 @@ export default function BusinessChainEditorPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [activeDrag, setActiveDrag] = useState<ActiveDrag>(null);
|
||||
const selectValue = currentChainId ?? "";
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
@@ -214,6 +216,14 @@ export default function BusinessChainEditorPage() {
|
||||
setSeats((prev) => prev.filter((s) => s.agent_id !== agentId));
|
||||
}, []);
|
||||
|
||||
const moveSeat = useCallback((index: number, dir: -1 | 1) => {
|
||||
setSeats((prev) => {
|
||||
const target = index + dir;
|
||||
if (target < 0 || target >= prev.length) return prev;
|
||||
return arrayMove(prev, index, target);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ─── DnD ─────────────────────────────────────────────────────────────
|
||||
const onDragStart = useCallback(
|
||||
(e: DragStartEvent) => {
|
||||
@@ -344,108 +354,196 @@ export default function BusinessChainEditorPage() {
|
||||
|
||||
// ─── Render ──────────────────────────────────────────────────────────
|
||||
const seatItemIds = seats.map((s) => SEAT_PREFIX + s.agent_id);
|
||||
const triggerLabel = currentChainId ? title || "未命名链条" : "+ 新建链条…";
|
||||
|
||||
return (
|
||||
<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-2.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<header className="flex shrink-0 flex-wrap items-center gap-3 border-b border-border bg-card px-6 py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/page/workspace/roundtable/planning")}
|
||||
className={cn("h-8 gap-1.5 px-2 text-muted-foreground hover:text-foreground", R_CTRL)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 border border-border bg-card px-3 py-1.5 text-sm font-medium text-muted-foreground transition hover:bg-muted hover:text-foreground active:scale-[0.98]",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
返回圆桌
|
||||
</Button>
|
||||
<div className="h-4 w-px bg-border" />
|
||||
</button>
|
||||
|
||||
{/* 品牌标识 */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className={cn("flex size-8 items-center justify-center bg-primary/10 text-primary", R_CARD)}>
|
||||
<Layers className="size-4" />
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-8 select-none items-center justify-center bg-gradient-to-tr from-primary to-primary/40 text-sm font-bold text-primary-foreground shadow-sm",
|
||||
R_CARD,
|
||||
)}
|
||||
>
|
||||
🪶
|
||||
</div>
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold">业务链条配置</div>
|
||||
<div className="text-xs text-muted-foreground">编排有序的智能体研讨链路</div>
|
||||
<div className="text-base font-bold text-foreground">业务链条配置</div>
|
||||
<div className="text-[11px] font-medium tracking-wide text-muted-foreground">
|
||||
编排有序的智能体研讨链路
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 链条选择 + 新建 */}
|
||||
<div className="ml-2 flex items-center gap-2">
|
||||
<select
|
||||
value={selectValue}
|
||||
onChange={(e) => {
|
||||
const id = e.target.value;
|
||||
if (!id) {
|
||||
startNewChain();
|
||||
return;
|
||||
}
|
||||
const chain = chains.find((c) => c.id === id);
|
||||
if (chain) loadChain(chain);
|
||||
}}
|
||||
{/* 链条选择下拉 */}
|
||||
<div className="relative ml-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDropdownOpen((o) => !o)}
|
||||
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",
|
||||
"flex min-w-[150px] items-center justify-between gap-3 border border-border bg-card px-3 py-1.5 text-sm text-foreground transition hover:border-primary/50 focus:outline-none focus:ring-1 focus:ring-primary/30",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
<option value="">+ 新建链条…</option>
|
||||
{chains.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.title || "未命名"}({c.seats.length} 席)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button variant="outline" size="sm" onClick={startNewChain} className={cn("h-8 gap-1.5", R_CTRL)}>
|
||||
<Plus className="size-4" />
|
||||
新建
|
||||
</Button>
|
||||
<span className="truncate font-medium">{triggerLabel}</span>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{dropdownOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-30" onClick={() => setDropdownOpen(false)} />
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 z-40 mt-1 max-h-[280px] w-[260px] overflow-y-auto border border-border bg-card py-1 shadow-lg",
|
||||
R_CARD,
|
||||
)}
|
||||
>
|
||||
<div className="border-b border-border px-3 py-1 text-xs font-semibold text-muted-foreground">
|
||||
切换已有链条
|
||||
</div>
|
||||
{chains.length === 0 ? (
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground">暂无已保存链条</div>
|
||||
) : (
|
||||
chains.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
loadChain(c);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between px-3 py-2 text-xs transition hover:bg-primary/5",
|
||||
c.id === currentChainId
|
||||
? "bg-primary/5 font-bold text-primary"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{c.title || "未命名链条"}({c.seats.length} 席)
|
||||
</span>
|
||||
{c.id === currentChainId && (
|
||||
<span className={cn("ml-2 shrink-0 bg-primary/15 px-1.5 py-0.5 text-[10px] text-primary", R_CTRL)}>
|
||||
当前
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
<div className="mt-1 border-t border-border pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
startNewChain();
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
className="flex w-full items-center gap-1.5 px-3 py-2 text-xs font-medium text-primary transition hover:bg-primary/5"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
新建另一条链路
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 快捷新建 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={startNewChain}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 border border-border bg-card px-3 py-1.5 text-sm text-muted-foreground transition hover:bg-muted hover:text-foreground active:scale-[0.98]",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
新建
|
||||
</button>
|
||||
|
||||
{/* 右侧操作 */}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={!currentChainId}
|
||||
className={cn("h-8 gap-1.5 text-destructive hover:text-destructive", R_CTRL)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 border border-primary/30 bg-card px-3 py-1.5 text-sm font-medium text-primary transition hover:bg-primary/5 active:scale-[0.98]",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
删除
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={!canSave}
|
||||
className={cn("h-8 gap-1.5", R_CTRL)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground shadow-sm transition hover:bg-primary/90 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
{saving ? <Loader2 className="size-4 animate-spin" /> : <Save className="size-4" />}
|
||||
保存
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 标题 + 描述 */}
|
||||
<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={cn("h-8 bg-background", R_CTRL)}
|
||||
maxLength={120}
|
||||
/>
|
||||
<section className="shrink-0 border-b border-border bg-card px-6 py-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-stretch overflow-hidden border border-border bg-card transition focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
<span className="flex min-w-[64px] select-none items-center justify-center border-r border-border bg-muted/50 px-4 text-sm font-medium text-muted-foreground">
|
||||
标题
|
||||
</span>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="如:应急方案三段链"
|
||||
maxLength={120}
|
||||
className="h-auto rounded-none border-0 bg-transparent px-4 py-2.5 text-sm font-medium shadow-none focus-visible:ring-0 dark:border-0 dark:bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-stretch overflow-hidden border border-border bg-card transition focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
<span className="flex min-w-[64px] select-none items-center justify-center border-r border-border bg-muted/50 px-4 text-sm font-medium text-muted-foreground">
|
||||
描述
|
||||
</span>
|
||||
<Input
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="链条用途说明(可选)"
|
||||
maxLength={500}
|
||||
className="h-auto rounded-none border-0 bg-transparent px-4 py-2.5 text-sm font-medium shadow-none focus-visible:ring-0 dark:border-0 dark:bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<span className="shrink-0 text-xs font-medium text-muted-foreground">描述</span>
|
||||
<Input
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="链条用途说明(可选)"
|
||||
className={cn("h-8 bg-background", R_CTRL)}
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 双栏拖拽编辑器 */}
|
||||
<DndContext
|
||||
@@ -454,20 +552,34 @@ export default function BusinessChainEditorPage() {
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<div className="grid min-h-0 flex-1 grid-cols-1 gap-4 overflow-hidden p-4 md:grid-cols-[360px_1fr]">
|
||||
<main className="grid min-h-0 flex-1 grid-cols-1 gap-6 overflow-hidden px-6 py-6 lg:grid-cols-12">
|
||||
{/* 左栏:候选智能体 */}
|
||||
<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={cn("bg-primary/10 px-1.5 py-0.5 text-[11px] font-medium text-primary tabular-nums", R_CTRL)}>
|
||||
<section
|
||||
className={cn(
|
||||
"flex min-h-0 flex-col overflow-hidden border border-border bg-card lg:col-span-4",
|
||||
R_CARD,
|
||||
PANEL_SHADOW,
|
||||
)}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-border bg-muted/40 px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("flex size-5 items-center justify-center bg-primary/10", R_CTRL)}>
|
||||
<UserPlus className="size-3.5 text-primary" />
|
||||
</span>
|
||||
<h2 className="text-sm font-bold tracking-wide text-foreground">候选智能体</h2>
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-[20px] border border-primary/30 bg-primary/10 px-2 py-0.5 text-center text-xs font-bold text-primary tabular-nums",
|
||||
"rounded-full",
|
||||
)}
|
||||
>
|
||||
{pool.length}
|
||||
</span>
|
||||
<span className="ml-auto text-[11px] font-normal text-muted-foreground">
|
||||
拖到右侧 / 点击加入
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-muted-foreground">拖到右侧 / 点击加入</span>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 border-b border-border px-4 py-3">
|
||||
<TagSearchBar
|
||||
targetType="agent"
|
||||
onSearchChange={setSearch}
|
||||
@@ -475,14 +587,15 @@ export default function BusinessChainEditorPage() {
|
||||
onTagIdsChange={setFilterTagIds}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 space-y-1.5 overflow-y-auto p-2.5">
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-2.5 overflow-y-auto p-4">
|
||||
{poolLoading ? (
|
||||
<div className="flex items-center gap-2 px-2 py-6 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
正在加载候选智能体…
|
||||
</div>
|
||||
) : pool.length === 0 ? (
|
||||
<div className="px-2 py-6 text-center text-sm text-muted-foreground">
|
||||
<div className="px-2 py-12 text-center text-sm text-muted-foreground">
|
||||
{search || filterTagIds.length
|
||||
? "没有匹配的智能体,换个关键词或标签试试。"
|
||||
: `候选池为空,请先在「智能体」里创建至少 ${CHAIN_MIN_SEATS} 个智能体。`}
|
||||
@@ -506,18 +619,29 @@ export default function BusinessChainEditorPage() {
|
||||
seatItemIds={seatItemIds}
|
||||
seatCountInvalid={seatCountInvalid}
|
||||
onRemove={removeSeat}
|
||||
onMove={moveSeat}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<DragOverlay>
|
||||
{activeDrag ? (
|
||||
<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)}>
|
||||
<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>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
|
||||
{/* 页脚 */}
|
||||
<footer className="shrink-0 select-none border-t border-border bg-card px-6 py-3 text-center text-[11px] text-muted-foreground">
|
||||
编排有序的业务链条,让智能体决策更有深度。保存后,配置将自动同步至圆桌。
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -540,43 +664,57 @@ function PoolAgentCard({
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
onClick={inChain ? undefined : onAdd}
|
||||
className={cn(
|
||||
"group flex items-start gap-2 border p-2.5 transition-colors",
|
||||
R_CTRL,
|
||||
"group relative flex items-start gap-3 border p-3.5 transition-all duration-150",
|
||||
R_CARD,
|
||||
inChain
|
||||
? "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",
|
||||
? "cursor-default border-primary/30 bg-primary/[0.03]"
|
||||
: "cursor-pointer border-border bg-card hover:border-primary hover:shadow-[0_2px_8px_rgba(0,0,0,0.04)]",
|
||||
isDragging && "opacity-30",
|
||||
)}
|
||||
{...(inChain ? {} : listeners)}
|
||||
{...attributes}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">{agent.name || agent.agent_id}</div>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex size-6 shrink-0 items-center justify-center transition-colors",
|
||||
R_CARD,
|
||||
inChain
|
||||
? "bg-primary/10 text-primary"
|
||||
: "bg-muted text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary",
|
||||
)}
|
||||
>
|
||||
<Bot className="size-3.5" />
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 pr-16">
|
||||
<div className="truncate text-xs font-bold leading-tight text-foreground transition-colors group-hover:text-primary">
|
||||
{agent.name || agent.agent_id}
|
||||
</div>
|
||||
{agent.description && (
|
||||
<div className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
|
||||
<div className="mt-1 line-clamp-2 text-[11px] font-medium leading-normal text-muted-foreground">
|
||||
{agent.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{inChain ? (
|
||||
<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>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
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" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="absolute right-3.5 top-3.5 shrink-0 select-none">
|
||||
{inChain ? (
|
||||
<span className={cn("border border-primary/30 bg-primary/10 px-2 py-1 text-[10px] font-bold text-primary", R_CTRL)}>
|
||||
已加入
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
"border border-transparent bg-muted px-2 py-1 text-[10px] font-semibold text-muted-foreground transition-all group-hover:border-primary/30 group-hover:bg-primary/10 group-hover:text-primary",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
点击加入
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -588,45 +726,72 @@ function SeatsColumn({
|
||||
seatItemIds,
|
||||
seatCountInvalid,
|
||||
onRemove,
|
||||
onMove,
|
||||
}: {
|
||||
seats: ChainSeat[];
|
||||
seatItemIds: string[];
|
||||
seatCountInvalid: boolean;
|
||||
onRemove: (agentId: string) => void;
|
||||
onMove: (index: number, dir: -1 | 1) => void;
|
||||
}) {
|
||||
const { setNodeRef, isOver } = useDroppable({ id: SEATS_DROPPABLE_ID });
|
||||
return (
|
||||
<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={cn("bg-muted px-1.5 py-0.5 text-[11px] font-normal text-muted-foreground", R_CTRL)}>
|
||||
按顺序发言
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
{seats.length} / {CHAIN_MAX_SEATS}
|
||||
{seats.length < CHAIN_MIN_SEATS && ` · 至少 ${CHAIN_MIN_SEATS}`}
|
||||
</span>
|
||||
<section
|
||||
className={cn(
|
||||
"flex min-h-0 flex-col overflow-hidden border border-border bg-card lg:col-span-8",
|
||||
R_CARD,
|
||||
PANEL_SHADOW,
|
||||
)}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-border bg-muted/40 px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("flex size-5 items-center justify-center bg-primary/10", R_CTRL)}>
|
||||
<ListOrdered className="size-3.5 text-primary" />
|
||||
</span>
|
||||
<h2 className="text-sm font-bold tracking-wide text-foreground">链条席位</h2>
|
||||
<span className={cn("border border-border bg-muted px-2 py-0.5 text-[10px] font-bold text-muted-foreground", R_CTRL)}>
|
||||
按顺序发言
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex select-none items-center gap-1 text-xs">
|
||||
<span className="font-medium text-muted-foreground">配置席位</span>
|
||||
<span
|
||||
className={cn(
|
||||
"border px-2 py-0.5 font-bold tabular-nums",
|
||||
R_CTRL,
|
||||
seatCountInvalid
|
||||
? "border-destructive/30 bg-destructive/10 text-destructive"
|
||||
: "border-primary/30 bg-primary/10 text-primary",
|
||||
)}
|
||||
>
|
||||
{seats.length} / {CHAIN_MAX_SEATS}
|
||||
{seats.length < CHAIN_MIN_SEATS && ` · 至少 ${CHAIN_MIN_SEATS}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
"min-h-0 flex-1 space-y-2 overflow-y-auto p-3.5 transition-colors",
|
||||
"min-h-0 flex-1 space-y-3 overflow-y-auto p-5 transition-colors",
|
||||
isOver && "bg-primary/[0.03]",
|
||||
)}
|
||||
>
|
||||
{seats.length === 0 ? (
|
||||
<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
|
||||
className={cn(
|
||||
"m-1 flex h-full flex-col items-center justify-center border border-dashed border-border bg-card/50 px-8 py-16 text-center",
|
||||
R_CARD,
|
||||
)}
|
||||
>
|
||||
<div className="mb-3 flex size-12 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<ListOrdered className="size-6" />
|
||||
</div>
|
||||
<h3 className="mb-1 text-sm font-bold text-foreground">暂无席位</h3>
|
||||
<p className="max-w-sm text-xs leading-relaxed text-muted-foreground">
|
||||
该业务链条当前为空。请从左侧「候选智能体」点击或拖拽,将其按顺序编排加入此处。
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<SortableContext items={seatItemIds} strategy={verticalListSortingStrategy}>
|
||||
@@ -635,7 +800,9 @@ function SeatsColumn({
|
||||
key={seat.agent_id}
|
||||
seat={seat}
|
||||
index={index}
|
||||
total={seats.length}
|
||||
onRemove={() => onRemove(seat.agent_id)}
|
||||
onMove={(dir) => onMove(index, dir)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
@@ -650,59 +817,110 @@ function SeatsColumn({
|
||||
function SeatStrip({
|
||||
seat,
|
||||
index,
|
||||
total,
|
||||
onRemove,
|
||||
onMove,
|
||||
}: {
|
||||
seat: ChainSeat;
|
||||
index: number;
|
||||
total: number;
|
||||
onRemove: () => void;
|
||||
onMove: (dir: -1 | 1) => void;
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
||||
useSortable({ id: SEAT_PREFIX + seat.agent_id });
|
||||
const avatarType = getAvatarTypeFor(seat.agent_id, index);
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === total - 1;
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||
className={cn(
|
||||
"flex items-center gap-3 border border-border bg-card p-2.5",
|
||||
R_CTRL,
|
||||
"flex items-stretch overflow-hidden border border-border bg-card",
|
||||
R_CARD,
|
||||
PANEL_SHADOW,
|
||||
isDragging && "z-10 opacity-70 shadow-[0_4px_12px_rgba(15,23,42,0.12)]",
|
||||
)}
|
||||
>
|
||||
{/* 拖拽手柄 */}
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 cursor-grab touch-none text-muted-foreground/60 hover:text-foreground active:cursor-grabbing"
|
||||
className="flex w-7 shrink-0 cursor-grab touch-none items-center justify-center border-r border-border bg-muted/40 text-muted-foreground/60 transition-colors hover:text-foreground active:cursor-grabbing"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
title="拖动排序"
|
||||
>
|
||||
<GripVertical className="size-4" />
|
||||
<GripVertical className="size-3.5" />
|
||||
</button>
|
||||
<span
|
||||
className={cn(
|
||||
"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-medium text-foreground">{seat.name || seat.agent_id}</div>
|
||||
|
||||
{/* 序号 */}
|
||||
<div className="flex shrink-0 select-none items-center pl-2.5">
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-6 items-center justify-center border text-xs font-bold tabular-nums",
|
||||
R_CTRL,
|
||||
chainTagClasses(avatarType),
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 详情 */}
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-center px-2.5 py-2">
|
||||
<div className="truncate text-xs font-bold text-foreground">{seat.name || seat.agent_id}</div>
|
||||
{seat.description && (
|
||||
<div className="mt-0.5 line-clamp-1 text-xs text-muted-foreground">
|
||||
<div className="mt-0.5 line-clamp-1 text-[11px] font-medium text-muted-foreground">
|
||||
{seat.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
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" />
|
||||
</button>
|
||||
|
||||
{/* 控制:上移 / 下移 / 移除 */}
|
||||
<div className="flex shrink-0 items-center gap-0.5 border-l border-border px-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMove(-1)}
|
||||
disabled={isFirst}
|
||||
title="上移席位"
|
||||
className={cn(
|
||||
"p-1 transition",
|
||||
R_CTRL,
|
||||
isFirst
|
||||
? "pointer-events-none text-muted-foreground/30"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<ChevronUp className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMove(1)}
|
||||
disabled={isLast}
|
||||
title="下移席位"
|
||||
className={cn(
|
||||
"p-1 transition",
|
||||
R_CTRL,
|
||||
isLast
|
||||
? "pointer-events-none text-muted-foreground/30"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
title="移出此席位"
|
||||
className={cn(
|
||||
"ml-0.5 p-1 text-muted-foreground/60 transition-colors hover:bg-primary/10 hover:text-primary",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user