Files
zclaw_openfang/desktop/src/components/CloneManager.tsx
iven 1cf3f585d3 refactor(store): split gatewayStore into specialized domain stores
Major restructuring:
- Split monolithic gatewayStore into 5 focused stores:
  - connectionStore: WebSocket connection and gateway lifecycle
  - configStore: quickConfig, workspaceInfo, MCP services
  - agentStore: clones, usage stats, agent management
  - handStore: hands, approvals, triggers, hand runs
  - workflowStore: workflows, workflow runs, execution

- Update all components to use new stores with selector pattern
- Remove
2026-03-20 22:14:13 +08:00

166 lines
6.6 KiB
TypeScript

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>
);
}