import { useState, useEffect, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import './index.css'; import { Sidebar, MainViewType } from './components/Sidebar'; import { ChatArea } from './components/ChatArea'; import { RightPanel } from './components/RightPanel'; import { SettingsLayout } from './components/Settings/SettingsLayout'; import { AutomationPanel } from './components/Automation'; import { SkillMarket } from './components/SkillMarket'; import { AgentOnboardingWizard } from './components/AgentOnboardingWizard'; import { HandApprovalModal } from './components/HandApprovalModal'; import { TopBar } from './components/TopBar'; import { DetailDrawer } from './components/DetailDrawer'; import { useConnectionStore } from './store/connectionStore'; import { useSaaSStore } from './store/saasStore'; import { useHandStore, type HandRun } from './store/handStore'; import { useChatStore } from './store/chatStore'; import { initializeStores } from './store'; import { getStoredGatewayToken } from './lib/gateway-client'; import { pageVariants, defaultTransition, fadeInVariants } from './lib/animations'; import { Loader2 } from 'lucide-react'; import { isTauriRuntime, getLocalGatewayStatus, startLocalGateway } from './lib/tauri-gateway'; import { LoginPage } from './components/LoginPage'; import { useOnboarding } from './lib/use-onboarding'; import { intelligenceClient } from './lib/intelligence-client'; import { loadEmbeddingConfig, loadEmbeddingApiKey } from './lib/embedding-client'; import { invoke } from '@tauri-apps/api/core'; import { useProposalNotifications, ProposalNotificationHandler } from './lib/useProposalNotifications'; import { useToast } from './components/ui/Toast'; import type { Clone } from './store/agentStore'; import { createLogger } from './lib/logger'; const log = createLogger('App'); type View = 'main' | 'settings'; // Bootstrap component that ensures ZCLAW is running before rendering main UI function BootstrapScreen({ status }: { status: string }) { return (

{status}

); } function App() { const [view, setView] = useState('main'); const [mainContentView, setMainContentView] = useState('chat'); const [bootstrapping, setBootstrapping] = useState(true); const [bootstrapStatus, setBootstrapStatus] = useState('Initializing...'); const [showOnboarding, setShowOnboarding] = useState(false); const [showDetailDrawer, setShowDetailDrawer] = useState(false); // Hand Approval state const [pendingApprovalRun, setPendingApprovalRun] = useState(null); const [showApprovalModal, setShowApprovalModal] = useState(false); const connect = useConnectionStore((s) => s.connect); const isLoggedIn = useSaaSStore((s) => s.isLoggedIn); const hands = useHandStore((s) => s.hands); const approveHand = useHandStore((s) => s.approveHand); const loadHands = useHandStore((s) => s.loadHands); const { setCurrentAgent } = useChatStore(); const { isNeeded: onboardingNeeded, isLoading: onboardingLoading, markCompleted } = useOnboarding(); // Proposal notifications const { toast } = useToast(); useProposalNotifications(); // Sets up polling for pending proposals // Show toast when new proposals are available useEffect(() => { const handleProposalAvailable = (event: Event) => { const customEvent = event as CustomEvent<{ count: number }>; const { count } = customEvent.detail; toast(`${count} 个新的人格变更提案待审批`, 'info'); }; window.addEventListener('zclaw:proposal-available', handleProposalAvailable); return () => { window.removeEventListener('zclaw:proposal-available', handleProposalAvailable); }; }, [toast]); useEffect(() => { document.title = 'ZCLAW'; }, []); // Restore SaaS session from OS keyring on startup (before auth gate) const [isRestoring, setIsRestoring] = useState(true); useEffect(() => { const restore = async () => { try { await useSaaSStore.getState().restoreSession(); } catch { // Session restore failed — user will see login page } setIsRestoring(false); }; restore(); }, []); // Watch for Hands that need approval useEffect(() => { const handsNeedingApproval = hands.filter(h => h.status === 'needs_approval'); if (handsNeedingApproval.length > 0 && !showApprovalModal) { // Find the first hand with needs_approval and create a pending run const hand = handsNeedingApproval[0]; if (hand.currentRunId) { setPendingApprovalRun({ runId: hand.currentRunId, status: 'needs_approval', startedAt: new Date().toISOString(), }); setShowApprovalModal(true); } } }, [hands, showApprovalModal]); // Handle approval/rejection of Hand runs const handleApproveHand = useCallback(async (runId: string) => { // Find the hand that owns this run const hand = hands.find(h => h.currentRunId === runId); if (!hand) return; await approveHand(hand.id, runId, true); await loadHands(); setShowApprovalModal(false); setPendingApprovalRun(null); }, [hands, approveHand, loadHands]); const handleRejectHand = useCallback(async (runId: string, reason: string) => { // Find the hand that owns this run const hand = hands.find(h => h.currentRunId === runId); if (!hand) return; await approveHand(hand.id, runId, false, reason); await loadHands(); setShowApprovalModal(false); setPendingApprovalRun(null); }, [hands, approveHand, loadHands]); const handleCloseApprovalModal = useCallback(() => { setShowApprovalModal(false); // Don't clear pendingApprovalRun - keep it for reference }, []); // Bootstrap: Start ZCLAW Gateway before rendering main UI useEffect(() => { let mounted = true; const bootstrap = async () => { // 未登录时不启动 bootstrap if (!useSaaSStore.getState().isLoggedIn) return; try { // Step 1: Check and start local gateway in Tauri environment if (isTauriRuntime()) { setBootstrapStatus('Checking gateway status...'); try { const status = await getLocalGatewayStatus(); const isRunning = status.portStatus === 'busy' || status.listenerPids.length > 0; if (!isRunning && status.cliAvailable) { setBootstrapStatus('Starting ZCLAW Gateway...'); log.debug('Local gateway not running, auto-starting...'); await startLocalGateway(); // Wait for gateway to be ready await new Promise(resolve => setTimeout(resolve, 2000)); log.debug('Local gateway started'); } else if (isRunning) { log.debug('Local gateway already running'); } } catch (err) { log.warn('Failed to check/start local gateway:', err); } } if (!mounted) return; // Step 2: Connect to gateway setBootstrapStatus('Connecting to gateway...'); const gatewayToken = getStoredGatewayToken(); await connect(undefined, gatewayToken); if (!mounted) return; // Step 3: Check if onboarding is needed if (onboardingNeeded && !onboardingLoading) { setShowOnboarding(true); } // Step 4: Initialize stores with gateway client initializeStores(); // Step 4.5: Auto-start heartbeat engine for self-evolution try { const defaultAgentId = 'zclaw-main'; await intelligenceClient.heartbeat.init(defaultAgentId, { enabled: true, interval_minutes: 30, quiet_hours_start: '22:00', quiet_hours_end: '08:00', notify_channel: 'ui', proactivity_level: 'standard', max_alerts_per_tick: 5, }); // Sync memory stats to heartbeat engine try { const stats = await intelligenceClient.memory.stats(); const taskCount = stats.byType?.['task'] || 0; await intelligenceClient.heartbeat.updateMemoryStats( defaultAgentId, taskCount, stats.totalEntries, stats.storageSizeBytes ); log.debug('Memory stats synced to heartbeat engine'); } catch (statsErr) { log.warn('Failed to sync memory stats:', statsErr); } await intelligenceClient.heartbeat.start(defaultAgentId); log.debug('Heartbeat engine started for self-evolution'); // Set up periodic memory stats sync (every 5 minutes) const MEMORY_STATS_SYNC_INTERVAL = 5 * 60 * 1000; const statsSyncInterval = setInterval(async () => { try { const stats = await intelligenceClient.memory.stats(); const taskCount = stats.byType?.['task'] || 0; await intelligenceClient.heartbeat.updateMemoryStats( defaultAgentId, taskCount, stats.totalEntries, stats.storageSizeBytes ); log.debug('Memory stats synced (periodic)'); } catch (err) { log.warn('Periodic memory stats sync failed:', err); } }, MEMORY_STATS_SYNC_INTERVAL); // Store interval for cleanup // @ts-expect-error - Global cleanup reference window.__ZCLAW_STATS_SYNC_INTERVAL__ = statsSyncInterval; } catch (err) { log.warn('Failed to start heartbeat engine:', err); // Non-critical, continue without heartbeat } // Step 5: Restore embedding config to Rust backend try { // Migrate plaintext embedding apiKey to secure storage if present try { const embStored = localStorage.getItem('zclaw-embedding-config'); if (embStored) { const embParsed = JSON.parse(embStored); if (embParsed.apiKey && embParsed.apiKey.trim()) { const { saveEmbeddingApiKey } = await import('./lib/embedding-client'); const { secureStorage } = await import('./lib/secure-storage'); // Only migrate if not already in secure storage const existing = await secureStorage.get('zclaw-secure-embedding-apikey'); if (!existing) { await saveEmbeddingApiKey(embParsed.apiKey); } // Strip apiKey from localStorage const { apiKey: _, ...stripped } = embParsed; localStorage.setItem('zclaw-embedding-config', JSON.stringify(stripped)); } } } catch { /* migration failure is non-critical */ } const embConfig = loadEmbeddingConfig(); const embApiKey = await loadEmbeddingApiKey(); if (embConfig.enabled && embConfig.provider !== 'local' && embApiKey) { setBootstrapStatus('Restoring embedding configuration...'); await invoke('viking_configure_embedding', { provider: embConfig.provider, apiKey: embApiKey, model: embConfig.model || undefined, endpoint: embConfig.endpoint || undefined, }); log.debug('Embedding configuration restored to backend'); } } catch (embErr) { log.warn('Failed to restore embedding config:', embErr); // Non-critical, semantic search will fall back to TF-IDF } // Step 5b: Configure summary driver using active LLM (for L0/L1 generation) try { const { getDefaultModelConfigAsync, migrateModelApiKeysToSecureStorage } = await import('./store/connectionStore'); // Migrate any plaintext API keys to secure storage (idempotent) await migrateModelApiKeysToSecureStorage(); const modelConfig = await getDefaultModelConfigAsync(); if (modelConfig && modelConfig.apiKey && modelConfig.baseUrl) { setBootstrapStatus('Configuring summary driver...'); await invoke('viking_configure_summary_driver', { endpoint: modelConfig.baseUrl, apiKey: modelConfig.apiKey, model: modelConfig.model || undefined, }); log.debug('Summary driver configured with active LLM'); } } catch (sumErr) { log.warn('Failed to configure summary driver:', sumErr); // Non-critical, summaries won't be auto-generated } // Step 6: Bootstrap complete setBootstrapping(false); } catch (err) { log.error('Bootstrap failed:', err); // Still allow app to load, connection status will show error setBootstrapping(false); } }; bootstrap(); return () => { mounted = false; // Clean up periodic stats sync interval // @ts-expect-error - Global cleanup reference if (window.__ZCLAW_STATS_SYNC_INTERVAL__) { // @ts-expect-error - Global cleanup reference clearInterval(window.__ZCLAW_STATS_SYNC_INTERVAL__); } }; }, [connect, onboardingNeeded, onboardingLoading, isLoggedIn]); // Handle onboarding completion const handleOnboardingSuccess = (clone: Clone) => { markCompleted({ userName: clone.userName || 'User', userRole: clone.userRole, }); setCurrentAgent({ id: clone.id, name: clone.name, icon: clone.emoji || '🦞', color: 'bg-gradient-to-br from-orange-500 to-red-500', lastMessage: clone.role || 'New Agent', time: '', }); setShowOnboarding(false); }; // 处理主视图切换 const handleMainViewChange = (view: MainViewType) => { setMainContentView(view); }; // 登录门禁 — 必须登录才能使用 if (isRestoring) { return ; } if (!isLoggedIn) { return ; } if (view === 'settings') { return setView('main')} />; } // Show bootstrap screen while starting gateway if (bootstrapping) { return ; } // Show onboarding wizard for first-time users if (showOnboarding) { return ( { // Skip onboarding but still create a default agent with default personality try { const { getGatewayClient } = await import('./lib/gateway-client'); const client = getGatewayClient(); if (client) { // Create default agent with versatile assistant personality const defaultAgent = await client.createClone({ name: '全能助手', role: '全能型 AI 助手', nickname: '小龙', emoji: '🦞', personality: 'friendly', scenarios: ['coding', 'writing', 'research', 'product', 'data'], userName: 'User', userRole: 'user', communicationStyle: '亲切、耐心、善解人意,用易懂的语言解释复杂概念', }); if (defaultAgent?.clone) { setCurrentAgent({ id: defaultAgent.clone.id, name: defaultAgent.clone.name, icon: defaultAgent.clone.emoji || '🦞', color: 'bg-gradient-to-br from-orange-500 to-red-500', lastMessage: defaultAgent.clone.role || '全能型 AI 助手', time: '', }); } } } catch (err) { log.warn('Failed to create default agent on skip:', err); } // Mark onboarding as completed markCompleted({ userName: 'User', userRole: 'user', }); setShowOnboarding(false); }} onSuccess={handleOnboardingSuccess} /> ); } return (
{/* 左侧边栏 */} setView('settings')} onMainViewChange={handleMainViewChange} /> {/* 主内容区 */}
{/* 顶部工具栏 */} setShowDetailDrawer(true)} /> {/* 内容区域 */} {mainContentView === 'automation' ? ( ) : mainContentView === 'skills' ? ( ) : ( )}
{/* 详情抽屉 - 按需显示 */} setShowDetailDrawer(false)} title="详情" > {/* Hand Approval Modal (global) */} {/* Proposal Notifications Handler */}
); } export default App;