Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | import { useState, useEffect } from 'react'; import { useAgentStore } from '../store/agentStore'; import { useConnectionStore } from '../store/connectionStore'; import { useConfigStore } from '../store/configStore'; import { toChatAgent, useChatStore } from '../store/chatStore'; import { Bot, Plus, X, Globe, Cat, Search, BarChart2, Sparkles } from 'lucide-react'; import { AgentOnboardingWizard } from './AgentOnboardingWizard'; import type { Clone } from '../store/agentStore'; export function CloneManager() { const clones = useAgentStore((s) => s.clones); const loadClones = useAgentStore((s) => s.loadClones); const deleteClone = useAgentStore((s) => s.deleteClone); const connectionState = useConnectionStore((s) => s.connectionState); const quickConfig = useConfigStore((s) => s.quickConfig); const { agents, currentAgent, setCurrentAgent } = useChatStore(); const [showWizard, setShowWizard] = useState(false); const connected = connectionState === 'connected'; useEffect(() => { if (connected) { loadClones(); } }, [connected, loadClones]); const handleDelete = async (id: string) => { if (confirm('确定删除该分身?')) { await deleteClone(id); } }; const handleWizardSuccess = (clone: Clone) => { setCurrentAgent(toChatAgent(clone)); setShowWizard(false); }; // Merge gateway clones with local agents for display const displayClones = clones.length > 0 ? clones : agents.map(a => ({ id: a.id, name: a.name, role: '默认助手', nickname: a.name, scenarios: [] as string[], workspaceDir: '~/.openfang/zclaw-workspace', userName: quickConfig.userName || '未设置', userRole: '', restrictFiles: true, privacyOptIn: false, createdAt: '', onboardingCompleted: true, emoji: undefined as string | undefined, personality: undefined as string | undefined, })); // Function to get display emoji or icon for clone const getCloneDisplay = (clone: typeof displayClones[0]) => { // If clone has emoji, use it if (clone.emoji) { return { emoji: clone.emoji, icon: null, bg: 'bg-gradient-to-br from-orange-400 to-red-500', }; } // Fallback to icon based on name if (clone.name.includes('Browser') || clone.name.includes('浏览器')) { return { emoji: null, icon: <Globe className="w-5 h-5" />, bg: 'bg-blue-500 text-white' }; } if (clone.name.includes('AutoClaw') || clone.name.includes('ZCLAW')) { return { emoji: null, icon: <Cat className="w-6 h-6" />, bg: 'bg-gradient-to-br from-orange-400 to-red-500 text-white' }; } if (clone.name.includes('沉思')) { return { emoji: null, icon: <Search className="w-5 h-5" />, bg: 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-300' }; } if (clone.name.includes('监控')) { return { emoji: null, icon: <BarChart2 className="w-5 h-5" />, bg: 'bg-orange-100 text-orange-600 dark:bg-orange-900 dark:text-orange-300' }; } return { emoji: null, icon: <Bot className="w-5 h-5" />, bg: 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300' }; }; return ( <div className="h-full flex flex-col py-2"> {/* Clone list */} <div className="flex-1 overflow-y-auto custom-scrollbar"> {displayClones.map((clone, idx) => { const { emoji, icon, bg } = getCloneDisplay(clone); const isActive = currentAgent ? currentAgent.id === clone.id : idx === 0; const canDelete = clones.length > 0; return ( <div key={clone.id} onClick={() => setCurrentAgent(toChatAgent(clone))} className={`group sidebar-item mx-2 px-3 py-3 rounded-lg cursor-pointer mb-1 flex items-start gap-3 transition-colors ${ isActive ? 'bg-white dark:bg-gray-800 shadow-sm border border-gray-100 dark:border-gray-700' : 'hover:bg-black/5 dark:hover:bg-white/5' }`} > <div className={`w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 ${emoji ? bg : bg}`}> {emoji ? ( <span className="text-xl">{emoji}</span> ) : ( icon )} </div> <div className="flex-1 min-w-0"> <div className="flex justify-between items-center mb-0.5"> <span className={`truncate ${isActive ? 'font-semibold text-gray-900 dark:text-white' : 'font-medium text-gray-900 dark:text-white'}`}> {clone.name} </span> {isActive ? <span className="text-xs text-orange-500">当前</span> : null} </div> <p className="text-xs text-gray-500 dark:text-gray-400 truncate"> {clone.role || clone.personality || '新分身'} </p> </div> {canDelete && ( <button onClick={(e) => { e.stopPropagation(); handleDelete(clone.id); }} className="pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 focus:pointer-events-auto focus:opacity-100 p-1 mt-1 text-gray-300 hover:text-red-500 transition-opacity" title="删除" > <X className="w-3.5 h-3.5" /> </button> )} </div> ); })} {/* Add new clone button */} <div onClick={() => { if (connected) { setShowWizard(true); } }} className={`sidebar-item mx-2 px-3 py-3 rounded-lg mb-1 flex items-center gap-3 transition-colors border border-dashed border-gray-300 dark:border-gray-600 ${ connected ? 'cursor-pointer text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5' : 'cursor-not-allowed text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-800/50' }`} > <div className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 bg-gray-50 dark:bg-gray-800"> {connected ? ( <Sparkles className="w-5 h-5 text-primary" /> ) : ( <Plus className="w-5 h-5" /> )} </div> <span className="text-sm font-medium"> {connected ? '创建新 Agent' : '连接 Gateway 后创建'} </span> </div> </div> {/* Onboarding Wizard Modal */} <AgentOnboardingWizard isOpen={showWizard} onClose={() => setShowWizard(false)} onSuccess={handleWizardSuccess} /> </div> ); } |