样式修改
This commit is contained in:
1 parent
3b5525a0a5
commit
6521218308
12 files changed
+1435
-163
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -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: "把枯燥空洞的日常工作记录,精炼美化为重点突出、职业素养爆表的职场汇报"
|
||||
}
|
||||
];
|
||||
@@ -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>,
|
||||
);
|
||||
Reference in new issue
Block a user