样式修改

This commit is contained in:
董宏凯 committed 2026-06-03 09:54:11 +08:00
1 parent 3b5525a0a5
commit 6521218308
12 files changed
+1435 -163

No files matched your search

+833
View File
@@ -0,0 +1,833 @@
import React, { useState, useEffect, useMemo } from "react";
import {
ArrowLeft,
Trash2,
Save,
Plus,
ChevronDown,
Search,
GripVertical,
X,
ListOrdered,
Languages,
Sparkles,
ChevronUp,
UserPlus,
Bot
} from "lucide-react";
import { INITIAL_AGENTS, Agent } from "./initialAgents";
interface ChainConfig {
id: string;
title: string;
description: string;
seats: string[]; // Holds reference to Agent IDs in order
}
const STORAGE_CHANNELS_KEY = "business_decision_chains";
const STORAGE_CURRENT_KEY = "business_decision_current_id";
const STORAGE_CUSTOM_AGENTS_KEY = "business_decision_custom_agents";
// Preset colors for the seat index badges
const SEAT_COLORS = [
{ bg: "bg-blue-50 border border-blue-200 text-blue-600", label: "蓝色" },
{ bg: "bg-purple-50 border border-purple-200 text-purple-600", label: "紫色" },
{ bg: "bg-emerald-50 border border-emerald-200 text-emerald-600", label: "绿色" },
{ bg: "bg-orange-50 border border-orange-200 text-orange-600", label: "橙色" },
{ bg: "bg-indigo-50 border border-indigo-200 text-indigo-600", label: "靛蓝" },
{ bg: "bg-pink-50 border border-pink-200 text-pink-600", label: "粉色" },
{ bg: "bg-amber-50 border border-amber-200 text-amber-600", label: "琥珀" },
{ bg: "bg-sky-50 border border-sky-200 text-sky-600", label: "天空蓝" },
];
export default function App() {
// 1. Storage & Config states
const [chains, setChains] = useState<ChainConfig[]>([]);
const [currentChainId, setCurrentChainId] = useState<string>("");
const [customAgents, setCustomAgents] = useState<Agent[]>([]);
// 2. Search & UI states
const [searchTerm, setSearchTerm] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
// Custom Agent Form Modal
const [showAgentModal, setShowAgentModal] = useState(false);
const [newAgentName, setNewAgentName] = useState("");
const [newAgentDesc, setNewAgentDesc] = useState("");
// Toast feedback
const [toast, setToast] = useState<{ message: string; type: "success" | "error" | "info" } | null>(null);
// Initialize data on load
useEffect(() => {
// Load custom agents
const savedCustom = localStorage.getItem(STORAGE_CUSTOM_AGENTS_KEY);
let loadedCustom: Agent[] = [];
if (savedCustom) {
try {
loadedCustom = JSON.parse(savedCustom);
setCustomAgents(loadedCustom);
} catch (e) {
console.error("Failed to parse custom agents", e);
}
}
// Load chains
const savedChains = localStorage.getItem(STORAGE_CHANNELS_KEY);
const savedCurrentId = localStorage.getItem(STORAGE_CURRENT_KEY);
let loadedChains: ChainConfig[] = [];
if (savedChains) {
try {
loadedChains = JSON.parse(savedChains);
} catch (e) {
console.error("Failed to parse chains from localStorage", e);
}
}
// Default configuration if empty
if (loadedChains.length === 0) {
loadedChains = [
{
id: "chain-1",
title: "应急方案三段链",
description: "链条用途说明(可选)",
seats: ["agent-1", "agent-2", "agent-3"],
}
];
}
setChains(loadedChains);
// Set current active id
if (savedCurrentId && loadedChains.some(c => c.id === savedCurrentId)) {
setCurrentChainId(savedCurrentId);
} else {
setCurrentChainId(loadedChains[0].id);
}
}, []);
// Save chains list to storage when it updates
const saveAllToLocalStorage = (newChainsList: ChainConfig[]) => {
localStorage.setItem(STORAGE_CHANNELS_KEY, JSON.stringify(newChainsList));
};
// Toast helper
const triggerToast = (message: string, type: "success" | "error" | "info" = "success") => {
setToast({ message, type });
setTimeout(() => {
setToast(null);
}, 2500);
};
// Complete list of agents (built-in + custom)
const allAgents = useMemo(() => {
return [...INITIAL_AGENTS, ...customAgents];
}, [customAgents]);
const activeChain = useMemo(() => {
return chains.find(c => c.id === currentChainId) || {
id: "fallback",
title: "",
description: "",
seats: []
} as ChainConfig;
}, [chains, currentChainId]);
// Handle text field updates
const handleTitleChange = (val: string) => {
setChains(prev => {
const updated = prev.map(c => c.id === currentChainId ? { ...c, title: val } : c);
saveAllToLocalStorage(updated);
return updated;
});
};
const handleDescChange = (val: string) => {
setChains(prev => {
const updated = prev.map(c => c.id === currentChainId ? { ...c, description: val } : c);
saveAllToLocalStorage(updated);
return updated;
});
};
// Add / Toggle seat inclusion
const handleToggleAgent = (agentId: string) => {
const isAlreadyIn = activeChain.seats.includes(agentId);
if (isAlreadyIn) {
// Remove it
const newSeats = activeChain.seats.filter(id => id !== agentId);
updateActiveSeats(newSeats);
triggerToast("已将智能体移出席位");
} else {
// Add it
if (activeChain.seats.length >= 8) {
triggerToast("席位已满,每个链条最多配置 8 个席位!", "error");
return;
}
const newSeats = [...activeChain.seats, agentId];
updateActiveSeats(newSeats);
triggerToast("成功添加到链条席位");
}
};
const updateActiveSeats = (newSeats: string[]) => {
setChains(prev => {
const updated = prev.map(c => c.id === currentChainId ? { ...c, seats: newSeats } : c);
saveAllToLocalStorage(updated);
return updated;
});
};
// Multi-chain operations
const createNewChain = () => {
const nextId = `chain-${Date.now()}`;
const newChainItem: ChainConfig = {
id: nextId,
title: `空白新链条 ${chains.length + 1}`,
description: "",
seats: []
};
const updated = [...chains, newChainItem];
setChains(updated);
setCurrentChainId(nextId);
localStorage.setItem(STORAGE_CURRENT_KEY, nextId);
saveAllToLocalStorage(updated);
triggerToast("已创建新的空白链条", "success");
setShowDropdown(false);
};
const handleSelectChain = (id: string) => {
setCurrentChainId(id);
localStorage.setItem(STORAGE_CURRENT_KEY, id);
setShowDropdown(false);
triggerToast(`切换到: ${chains.find(c => c.id === id)?.title || "新链条"}`);
};
const handleSaveChain = () => {
saveAllToLocalStorage(chains);
triggerToast("保存配置成功", "success");
};
const handleDeleteChain = () => {
if (chains.length <= 1) {
// Create a blank placeholder instead of locking
const nextId = `chain-${Date.now()}`;
const placeholder: ChainConfig = {
id: nextId,
title: "新配置链条",
description: "",
seats: []
};
setChains([placeholder]);
setCurrentChainId(nextId);
localStorage.setItem(STORAGE_CURRENT_KEY, nextId);
saveAllToLocalStorage([placeholder]);
triggerToast("重置当前链条");
return;
}
const filtered = chains.filter(c => c.id !== currentChainId);
setChains(filtered);
const nextId = filtered[0].id;
setCurrentChainId(nextId);
localStorage.setItem(STORAGE_CURRENT_KEY, nextId);
saveAllToLocalStorage(filtered);
triggerToast("删除成功", "success");
};
// Reorder commands: Drag & Drop
const handleDragStart = (index: number) => {
setDraggedIndex(index);
};
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
setDragOverIndex(index);
};
const handleDrop = (targetIndex: number) => {
if (draggedIndex === null) return;
const reordered = [...activeChain.seats];
const [movedItem] = reordered.splice(draggedIndex, 1);
reordered.splice(targetIndex, 0, movedItem);
updateActiveSeats(reordered);
setDraggedIndex(null);
setDragOverIndex(null);
};
// Fallback reordering commands: Move Up/Down button click
const moveDirection = (index: number, direction: "up" | "down") => {
const targetIdx = direction === "up" ? index - 1 : index + 1;
if (targetIdx < 0 || targetIdx >= activeChain.seats.length) return;
const items = [...activeChain.seats];
const temp = items[index];
items[index] = items[targetIdx];
items[targetIdx] = temp;
updateActiveSeats(items);
};
// Create customized user agents
const handleAddCustomAgent = (e: React.FormEvent) => {
e.preventDefault();
if (!newAgentName.trim()) {
triggerToast("请输入智能体名称", "error");
return;
}
const newAgent: Agent = {
id: `custom-agent-${Date.now()}`,
name: newAgentName,
description: newAgentDesc || "自定义创建的专属智能体角色。",
isCustom: true
};
const updatedCustom = [...customAgents, newAgent];
setCustomAgents(updatedCustom);
localStorage.setItem(STORAGE_CUSTOM_AGENTS_KEY, JSON.stringify(updatedCustom));
setNewAgentName("");
setNewAgentDesc("");
setShowAgentModal(false);
triggerToast(`智能体 '${newAgent.name}' 创建成功!`);
// Optionally auto-add to the chain seats if space available
if (activeChain.seats.length < 8) {
updateActiveSeats([...activeChain.seats, newAgent.id]);
}
};
// Clear a custom agent
const handleDeleteCustomAgent = (e: React.MouseEvent, agentId: string) => {
e.stopPropagation(); // Avoid triggering seat addition
const updatedCustom = customAgents.filter(a => a.id !== agentId);
setCustomAgents(updatedCustom);
localStorage.setItem(STORAGE_CUSTOM_AGENTS_KEY, JSON.stringify(updatedCustom));
// Also remove from any seats as well
if (activeChain.seats.includes(agentId)) {
updateActiveSeats(activeChain.seats.filter(id => id !== agentId));
}
triggerToast("已彻底删除该自定义智能体");
};
// Filter conditions
const filteredCandidates = allAgents.filter(agent => {
const text = (agent.name + " " + agent.description).toLowerCase();
return text.includes(searchTerm.toLowerCase());
});
return (
<div className="min-h-screen bg-[#faf9f9] text-[#1f2937] flex flex-col font-sans transition-all duration-150 antialiased" id="main-container">
{/* 1. Header Toolbar */}
<header className="bg-white border-b border-[#ede9e9] sticky top-0 z-20 px-6 py-3 shrink-0" id="header">
<div className="max-w-[1600px] mx-auto flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
{/* Left Controls */}
<div className="flex flex-wrap items-center gap-4">
{/* Back Button */}
<button
id="back-roundtable-btn"
onClick={() => triggerToast("页面状态已保持。")}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-[#4b5563] border border-[#ede9e9] rounded-md bg-white hover:bg-[#f6f4f4] active:scale-[0.98] transition"
>
<ArrowLeft className="w-4 h-4" />
<span>返回圆桌</span>
</button>
{/* Custom Brand Icon */}
<div className="flex items-center gap-2" id="brand-indicator">
<div className="w-8 h-8 rounded-lg bg-gradient-to-tr from-[#c25555] to-[#fcdbda] flex items-center justify-center text-white font-bold text-sm shadow-sm select-none">
🪶
</div>
<div className="flex flex-col">
<h1 className="text-base font-bold text-[#1f2937] leading-tight">业务链条配置</h1>
<p className="text-[11px] text-[#9ca3af] tracking-wide font-medium">编排有序的智能体研讨链路</p>
</div>
</div>
{/* Dropdown config selection */}
<div className="relative ml-2" id="chain-selector-dropdown">
<button
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center justify-between gap-3 min-w-[140px] px-3 py-1.5 text-sm bg-white border border-[#ede9e9] rounded-md text-[#374151] hover:border-[#dbcbcb] focus:outline-none focus:ring-1 focus:ring-[#c25555] transition"
>
<span className="font-medium truncate">{activeChain.title || "选择一个配置"}</span>
<ChevronDown className="w-4 h-4 text-[#9ca3af]" />
</button>
{showDropdown && (
<>
<div className="fixed inset-0 z-30" onClick={() => setShowDropdown(false)} />
<div className="absolute left-0 mt-1 w-[240px] bg-white border border-[#ede9e9] rounded-md shadow-lg py-1 z-40 max-h-[280px] overflow-y-auto animate-in fade-in slide-in-from-top-2 duration-150">
<div className="px-3 py-1 text-xs text-slate-400 font-semibold border-b border-[#f5f2f2]">
切换已有链条
</div>
{chains.map((c) => (
<button
key={c.id}
onClick={() => handleSelectChain(c.id)}
className={`w-full text-left px-3 py-2 text-xs hover:bg-[#fef4f3] flex items-center justify-between transition ${
c.id === currentChainId ? "text-[#c25555] font-bold bg-[#fef4f3]" : "text-[#4b5563]"
}`}
>
<span className="truncate">{c.title || "未命名链条"}</span>
{c.id === currentChainId && <span className="text-[10px] bg-[#fcdbda] px-1.5 py-0.5 rounded text-[#c25555]">当前</span>}
</button>
))}
<div className="border-t border-[#f5f2f2] mt-1 pt-1">
<button
onClick={createNewChain}
className="w-full text-left px-3 py-2 text-xs text-[#c25555] hover:bg-[#fef4f3] font-medium flex items-center gap-1.5 transition"
>
<Plus className="w-3.5 h-3.5" />
<span>新建另一条链路</span>
</button>
</div>
</div>
</>
)}
</div>
{/* Quick New Button */}
<button
id="new-chain-btn"
onClick={createNewChain}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-[#4b5563] border border-[#ede9e9] rounded-md bg-white hover:bg-[#f6f4f4] active:scale-[0.98] transition"
>
<Plus className="w-4 h-4 text-[#9ca3af]" />
<span>新建</span>
</button>
</div>
{/* Right Action buttons */}
<div className="flex items-center gap-2 ml-auto sm:ml-0">
<button
id="delete-chain-btn"
onClick={handleDeleteChain}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-[#c25555] font-medium border border-[#fcdbda] rounded-md bg-white hover:bg-[#fef4f3] active:scale-[0.98] transition"
>
<Trash2 className="w-4 h-4" />
<span>删除</span>
</button>
<button
id="save-chain-btn"
onClick={handleSaveChain}
className="flex items-center gap-1.5 px-4 py-1.5 text-sm text-white font-medium bg-[#c25555] hover:bg-[#b54545] border border-[#b24848] rounded-md shadow-sm active:scale-[0.98] transition"
>
<Save className="w-4 h-4" />
<span>保存</span>
</button>
</div>
</div>
</header>
{/* 2. Top Config Form Cards (Title, Description) */}
<section className="bg-white border-b border-[#ede9e9] px-6 py-4 shrink-0 shadow-[0_1px_2px_0_rgba(0,0,0,0.02)]" id="form-container">
<div className="max-w-[1600px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Title input field */}
<div className="flex items-stretch border border-[#ede9e9] rounded-md overflow-hidden bg-white group focus-within:ring-1 focus-within:ring-[#c25555] focus-within:border-[#c25555] transition" id="title-field">
<span className="bg-[#faf9f9] text-[#767e89] font-medium text-sm px-4 flex items-center justify-center border-r border-[#ede9e9] select-none min-w-[70px]">
标题
</span>
<input
type="text"
value={activeChain.title}
onChange={(e) => handleTitleChange(e.target.value)}
placeholder="如:应急方案三段链"
className="w-full px-4 py-2.5 text-sm text-[#1f2937] focus:outline-none placeholder-[#9ca3af] font-medium"
/>
</div>
{/* Description input field */}
<div className="flex items-stretch border border-[#ede9e9] rounded-md overflow-hidden bg-white group focus-within:ring-1 focus-within:ring-[#c25555] focus-within:border-[#c25555] transition" id="desc-field">
<span className="bg-[#faf9f9] text-[#767e89] font-medium text-sm px-4 flex items-center justify-center border-r border-[#ede9e9] select-none min-w-[70px]">
描述
</span>
<input
type="text"
value={activeChain.description}
onChange={(e) => handleDescChange(e.target.value)}
placeholder="链条用途说明(可选)"
className="w-full px-4 py-2.5 text-sm text-[#1f2937] focus:outline-none placeholder-[#9ca3af] font-medium"
/>
</div>
</div>
</section>
{/* 3. Main Split Workspace Grid */}
<main className="flex-1 max-w-[1600px] w-full mx-auto px-6 py-6 grid grid-cols-1 lg:grid-cols-12 gap-6 min-h-0" id="workspace">
{/* Column Left: 候选智能体 (Grid weight 4 of 12) */}
<section className="lg:col-span-4 bg-white border border-[#ede9e9] rounded-xl flex flex-col overflow-hidden shadow-sm" id="candidate-column">
{/* Candidate Header */}
<div className="px-5 py-4 border-b border-[#ede9e9] flex items-center justify-between shrink-0 bg-[#faf9f9]">
<div className="flex items-center gap-2">
<span className="flex items-center justify-center w-5 h-5 rounded bg-[#fef4f3]">
<UserPlus className="w-3.5 h-3.5 text-[#c25555]" />
</span>
<h2 className="font-bold text-[#1f2937] text-sm tracking-wide">候选智能体</h2>
<span className="bg-[#fef4f3] text-[#c25555] text-xs font-bold px-2 py-0.5 rounded-full min-w-[20px] text-center select-none border border-[#fcdbda]">
{filteredCandidates.length}
</span>
</div>
<span className="text-[11px] text-[#8c96a3] font-medium tracking-tight">拖到右侧 / 点击加入</span>
</div>
{/* Search Bar with Creation Utility */}
<div className="p-4 border-b border-[#f4f1f1] flex items-center gap-2 shrink-0 bg-[#fdfdfd]">
<div className="relative flex-1">
<span className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Search className="w-4 h-4 text-slate-400" />
</span>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="按名称搜索智能体..."
className="w-full pl-9 pr-3 py-2 text-xs border border-[#ede9e9] rounded-md bg-white focus:outline-none focus:ring-1 focus:ring-[#c25555] focus:border-[#c25555] placeholder-slate-400 font-medium"
/>
{searchTerm && (
<button
onClick={() => setSearchTerm("")}
className="absolute inset-y-0 right-0 flex items-center pr-2.5 text-slate-300 hover:text-slate-500"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
{/* Inline Custom Agent Launcher */}
<button
onClick={() => setShowAgentModal(true)}
title="创建自定义智能体角色"
className="flex items-center justify-center p-2 text-xs border border-dashed border-[#ede9e9] rounded-md hover:border-[#c25555] hover:text-[#c25555] hover:bg-[#fef4f3] transition shrink-0"
>
<Plus className="w-4 h-4" />
</button>
</div>
{/* Candidates Scroll List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2.5 scrollbar-thin" id="candidates-scroll">
{filteredCandidates.length === 0 ? (
<div className="py-12 px-4 text-center">
<p className="text-xs text-slate-400 font-medium">未找到符合搜索条件的智能体</p>
{searchTerm && (
<button
onClick={() => setSearchTerm("")}
className="mt-2 text-xs font-semibold text-[#c25555] underline hover:text-[#b44545]"
>
清除搜索
</button>
)}
</div>
) : (
filteredCandidates.map((agent) => {
const isAdded = activeChain.seats.includes(agent.id);
return (
<div
key={agent.id}
id={`candidate-card-${agent.id}`}
onClick={() => handleToggleAgent(agent.id)}
className={`group relative flex items-start gap-3 p-3.5 bg-white border rounded-lg cursor-pointer transition-all duration-150 ${
isAdded
? "border-[#fcdbda] bg-[#fdfdfd] shadow-[0_1px_3px_rgba(194,85,85,0.02)]"
: "border-[#ede9e9] hover:border-[#c25555] hover:shadow-[0_2px_8px_rgba(0,0,0,0.03)]"
}`}
>
{/* Left Icon status badge */}
<div className="mt-1 shrink-0">
<div className={`w-6 h-6 rounded-md flex items-center justify-center ${
isAdded ? "bg-[#fef4f3] text-[#c25555]" : "bg-slate-50 text-slate-400 group-hover:bg-[#fef4f3] group-hover:text-[#c25555]"
} transition-colors`}>
<Bot className="w-3.5 h-3.5" />
</div>
</div>
{/* Agent Details */}
<div className="flex-1 pr-14 min-w-0">
<div className="flex items-center gap-1.5 mb-1">
<h3 className="font-bold text-xs text-[#1f2937] leading-tight truncate group-hover:text-[#c25555] transition-colors">{agent.name}</h3>
{agent.isCustom && (
<span className="text-[10px] scale-95 font-medium px-1 bg-slate-50 text-slate-400 rounded border border-[#ede9e9]">自定义</span>
)}
</div>
<p className="text-[11px] text-[#717b88] leading-normal font-medium line-clamp-2">
{agent.description}
</p>
</div>
{/* Status Badge Action on Far Right */}
<div className="absolute top-3.5 right-3.5 select-none shrink-0 text-right">
{isAdded ? (
<span className="text-[10px] font-bold px-2 py-1 border border-[#fcdbda] bg-[#fef4f3] text-[#c25555] rounded">
已加入
</span>
) : (
<span className="text-[10px] font-semibold px-2 py-1 border border-transparent bg-slate-50 text-[#717b88] group-hover:bg-[#fef4f3] group-hover:text-[#c25555] group-hover:border-[#fcdbda] rounded transition-all">
点击加入
</span>
)}
{/* Custom agents get an extra trash deletion trigger */}
{agent.isCustom && (
<button
onClick={(e) => handleDeleteCustomAgent(e, agent.id)}
title="彻底删除此自定义智能体"
className="mt-1 block opacity-0 group-hover:opacity-100 hover:text-red-500 text-slate-300 transition-opacity p-1 ml-auto"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
);
})
)}
</div>
</section>
{/* Column Right: 链条席位 (Grid weight 8 of 12) */}
<section className="lg:col-span-8 bg-white border border-[#ede9e9] rounded-xl flex flex-col overflow-hidden shadow-sm" id="seats-column">
{/* Seats Header */}
<div className="px-5 py-4 border-b border-[#ede9e9] flex items-center justify-between shrink-0 bg-[#faf9f9]">
<div className="flex items-center gap-2">
<span className="flex items-center justify-center w-5 h-5 rounded bg-[#fef4f3]">
<ListOrdered className="w-3.5 h-3.5 text-[#c25555]" />
</span>
<h2 className="font-bold text-[#1f2937] text-sm tracking-wide">链条席位</h2>
<span className="inline-block px-2 py-0.5 text-[10px] font-bold bg-[#f5f2f2] text-[#717b88] rounded border border-[#ede9e9]">按顺序发言</span>
</div>
{/* Limit counter badge */}
<div className="flex items-center gap-1 text-xs select-none">
<span className="text-slate-400 font-medium">配置席位</span>
<span className={`px-2 py-0.5 font-bold rounded-md border ${
activeChain.seats.length > 0
? "bg-[#fef4f3] border-[#fcdbda] text-[#c25555]"
: "bg-slate-50 border-slate-200 text-slate-400"
}`}>
{activeChain.seats.length} / 8
</span>
</div>
</div>
{/* Seats Content Canvas */}
<div className="flex-1 overflow-y-auto p-5 space-y-3.5 scrollbar-thin" id="seats-scroll">
{activeChain.seats.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center py-20 px-8 border border-dashed border-[#ede9e9] rounded-xl bg-[#fdfdfd] m-1" id="empty-seats-place">
<div className="w-12 h-12 rounded-full bg-[#fef4f3] flex items-center justify-center text-[#c25555] mb-3">
<ListOrdered className="w-6 h-6" />
</div>
<h3 className="text-sm font-bold text-slate-700 mb-1">暂无席位</h3>
<p className="text-xs text-slate-400 text-center max-w-sm leading-relaxed mb-4">
该业务链条当前为空。请从左侧<b>“候选智能体”</b>列表中点击或者选择,将其按顺序编排加入此处。
</p>
<div className="flex flex-wrap justify-center gap-2 max-w-md">
{allAgents.slice(0, 3).map(agent => (
<button
key={agent.id}
onClick={() => handleToggleAgent(agent.id)}
className="px-2.5 py-1 text-[11px] font-semibold border border-[#ede9e9] hover:border-[#c25555] roundedbg-white hover:bg-slate-50 flex items-center gap-1 transition"
>
<span>💡</span>
<span>加入{agent.name}</span>
</button>
))}
</div>
</div>
) : (
<div className="space-y-3" id="seats-container-list">
{activeChain.seats.map((agentId, index) => {
const agent = allAgents.find(a => a.id === agentId);
if (!agent) return null;
const theme = SEAT_COLORS[index] || { bg: "bg-slate-50 border border-slate-200 text-slate-600" };
const isDragged = index === draggedIndex;
const isHoveredDrop = index === dragOverIndex;
return (
<div
key={`${agentId}-${index}`}
id={`seat-card-${index}`}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => handleDragOver(e, index)}
onDragEnd={() => { setDraggedIndex(null); setDragOverIndex(null); }}
onDrop={() => handleDrop(index)}
className={`flex items-stretch bg-white border rounded-lg shadow-sm transition-all duration-200 select-none ${
isDragged ? "opacity-35 scale-[0.98] border-dashed border-[#c25555]" : ""
} ${
isHoveredDrop ? "border-2 border-[#c25555] bg-[#fef4f3] scale-[1.01]" : "border-[#ede9e9]"
}`}
>
{/* Left Grip Handle */}
<div className="w-7 shrink-0 bg-[#faf9f9] border-r border-[#ede9e9] flex items-center justify-center rounded-l-lg select-none" id="grip-box">
<div
className="cursor-grab active:cursor-grabbing text-slate-400 hover:text-slate-600 p-1"
title="拖拽排序"
>
<GripVertical className="w-3.5 h-3.5" />
</div>
</div>
{/* Number Indicator Badge - Placed to the LEFT of details */}
<div className="flex items-center justify-center pl-2.5 shrink-0 select-none" id="seat-number-box">
<div className={`w-5.5 h-5.5 rounded flex items-center justify-center font-bold text-xs shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] select-none ${theme.bg}`}>
{index + 1}
</div>
</div>
{/* Seat info details */}
<div className="flex-1 py-2 pl-2.5 pr-2 min-w-0 flex flex-col justify-center">
<h4 className="font-bold text-xs text-[#1f2937] mb-0.5 flex items-center gap-1.5">
<span>{agent.name}</span>
{agent.isCustom && (
<span className="text-[9px] uppercase font-bold tracking-wider px-1 bg-slate-100 text-slate-400 rounded">Custom</span>
)}
</h4>
<p className="text-[11px] text-[#717b88] leading-tight font-medium line-clamp-1 truncate">
{agent.description}
</p>
</div>
{/* Seat controller Actions (Move up/down & close delete) */}
<div className="px-2 py-1.5 flex items-center gap-0.5 border-l border-[#f5f2f2] shrink-0" id="seat-controls">
{/* Up button */}
<button
onClick={() => moveDirection(index, "up")}
disabled={index === 0}
title="上移席位"
className={`p-1 rounded hover:bg-slate-100 transition ${
index === 0 ? "text-slate-200 pointer-events-none" : "text-slate-400 hover:text-[#1f2937]"
}`}
>
<ChevronUp className="w-3.5 h-3.5" />
</button>
{/* Down button */}
<button
onClick={() => moveDirection(index, "down")}
disabled={index === activeChain.seats.length - 1}
title="下移席位"
className={`p-1 rounded hover:bg-slate-100 transition ${
index === activeChain.seats.length - 1 ? "text-slate-200 pointer-events-none" : "text-slate-400 hover:text-[#1f2937]"
}`}
>
<ChevronDown className="w-3.5 h-3.5" />
</button>
{/* Cross Delete button */}
<button
onClick={() => handleToggleAgent(agent.id)}
title="移出此席位"
className="p-1 text-slate-300 hover:text-[#c25555] hover:bg-[#fef4f3] rounded transition-colors ml-0.5"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
</div>
);
})}
</div>
)}
</div>
</section>
</main>
{/* 4. Float Helper Footer decoration */}
<footer className="py-3 px-6 border-t border-[#ede9e9] bg-white text-center text-[11px] text-slate-400 select-none shrink-0" id="footer-section">
编排有序的业务链条,让智能体决策更有深度。保存后,配置将自动同步至圆桌。
</footer>
{/* 5. Custom Agent Modal (Popup Card) */}
{showAgentModal && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-[1px] flex items-center justify-center z-50 animate-in fade-in duration-200" id="custom-agent-modal">
<div className="bg-white border border-[#ede9e9] rounded-xl shadow-xl max-w-md w-full mx-4 overflow-hidden animate-in zoom-in-95 duration-200">
{/* Modal Header */}
<div className="px-5 py-4 bg-[#faf9f9] border-b border-[#ede9e9] flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="text-base">🧠</span>
<h3 className="font-bold text-sm text-[#1f2937]">添加自定义候选智能体</h3>
</div>
<button
onClick={() => setShowAgentModal(false)}
className="p-1 text-slate-400 hover:text-slate-600 rounded-md hover:bg-slate-100"
>
<X className="w-4.5 h-4.5" />
</button>
</div>
{/* Modal Form */}
<form onSubmit={handleAddCustomAgent} className="p-5 space-y-4">
<div>
<label className="block text-xs font-bold text-slate-600 mb-1.5">智能体名称 <span className="text-red-500">*</span></label>
<input
type="text"
required
placeholder="例如:市场公关专家"
value={newAgentName}
onChange={(e) => setNewAgentName(e.target.value)}
className="w-full px-3 py-2 text-xs border border-[#ede9e9] rounded-md focus:outline-none focus:ring-1 focus:ring-[#c25555] focus:border-[#c25555] placeholder-slate-400 font-medium"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1.5">职能介绍与描述</label>
<textarea
rows={3}
placeholder="描述其专业领域、面对对话时的应对策略及人格定位等"
value={newAgentDesc}
onChange={(e) => setNewAgentDesc(e.target.value)}
className="w-full px-3 py-2 text-xs border border-[#ede9e9] rounded-md focus:outline-none focus:ring-1 focus:ring-[#c25555] focus:border-[#c25555] placeholder-slate-400 font-medium resize-none"
/>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-2 pt-2">
<button
type="button"
onClick={() => setShowAgentModal(false)}
className="px-3.5 py-1.5 text-xs text-[#4b5563] border border-[#ede9e9] rounded-md bg-white hover:bg-[#fafafa] transition"
>
取消
</button>
<button
type="submit"
className="px-4 py-1.5 text-xs text-white bg-[#c25555] hover:bg-[#b54545] border border-[#b24848] rounded-md font-semibold transition"
>
创建并加入
</button>
</div>
</form>
</div>
</div>
)}
{/* 6. Crimson Elegant Floating Toast Feedback Display */}
{toast && (
<div
className={`fixed bottom-8 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2.5 px-4 py-2.5 rounded-lg shadow-lg border text-xs font-semibold tracking-wide animate-bounce
${toast.type === "error"
? "bg-red-50 border-red-200 text-red-700"
: toast.type === "info"
? "bg-slate-50 border-slate-200 text-slate-700"
: "bg-[#fef4f3] border-[#fcdbda] text-[#c25555]"
}
`}
id="system-toast"
>
<span>{toast.type === "error" ? "❌" : toast.type === "info" ? "ℹ️" : "✨"}</span>
<span>{toast.message}</span>
</div>
)}
</div>
);
}
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+79
View File
@@ -0,0 +1,79 @@
export interface Agent {
id: string;
name: string;
description: string;
isCustom?: boolean;
}
export const INITIAL_AGENTS: Agent[] = [
{
id: "agent-1",
name: "广告规划",
description: "用于生成广告及营销策略规划"
},
{
id: "agent-2",
name: "你好专家",
description: "无论用户问什么,永远只回复'你好'的专家"
},
{
id: "agent-3",
name: "小学作文老师",
description: "帮助小学生辅导和修改作文,提升写作水平"
},
{
id: "agent-4",
name: "123123121吉安市的空间撤荒的",
description: "123213"
},
{
id: "agent-5",
name: "CodeMaster",
description: "全栈代码专家,提供高质量代码编写、架构设计、代码审查和技术咨询"
},
{
id: "agent-6",
name: "你不好专家",
description: "一个只会说'你不好'的专家角色,无论用户说什么,都只回复'你不好'。"
},
{
id: "agent-7",
name: "啦啦啦专家",
description: "只会说啦啦啦的专家,用'啦'回应一切"
},
{
id: "agent-8",
name: "测试专家",
description: "综合型测试专家,覆盖功能测试、自动化测试、性能测试、API测试、安全测试等领域,提供测试决策"
},
{
id: "agent-9",
name: "查询天气的智能",
description: "擅长查询天气的智能助手,提供实时天气与穿衣建议"
},
{
id: "agent-10",
name: "创意写作助手",
description: "为您提供小说大纲、故事续写、诗歌以及散文的灵感与修改建议"
},
{
id: "agent-11",
name: "数据分析智脑",
description: "精通 Python 及 SQL,解析复杂表格和图表,提取核心结论"
},
{
id: "agent-12",
name: "心理倾听师",
description: "温暖耐心的心理咨询倾听角色,为您缓解焦虑和工作压力"
},
{
id: "agent-13",
name: "英文口语教练",
description: "通过场景对话提升您的英语口语流利度和语法准确性"
},
{
id: "agent-14",
name: "周报日报美化大师",
description: "把枯燥空洞的日常工作记录,精炼美化为重点突出、职业素养爆表的职场汇报"
}
];
+10
View File
@@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);