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
166 lines
6.6 KiB
TypeScript
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>
|
|
);
|
|
}
|