/** * Setup Wizard Page * First-time setup experience for new users */ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Check, ChevronDown, ChevronLeft, ChevronRight, Loader2, AlertCircle, Eye, EyeOff, RefreshCw, CheckCircle2, XCircle, ExternalLink, BookOpen, Copy, } from 'lucide-react'; import { TitleBar } from '@/components/layout/TitleBar'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; import { useGatewayStore } from '@/stores/gateway'; import { useSettingsStore } from '@/stores/settings'; import { useTranslation } from 'react-i18next'; import { SUPPORTED_LANGUAGES } from '@/i18n'; import { toast } from 'sonner'; import { CHANNEL_META, getPrimaryChannels, type ChannelType, type ChannelMeta, type ChannelConfigField, } from '@/types/channel'; interface SetupStep { id: string; title: string; description: string; } const STEP = { WELCOME: 0, RUNTIME: 1, PROVIDER: 2, CHANNEL: 3, INSTALLING: 4, COMPLETE: 5, } as const; const steps: SetupStep[] = [ { id: 'welcome', title: 'Welcome to ClawX', description: 'Your AI assistant is ready to be configured', }, { id: 'runtime', title: 'Environment Check', description: 'Verifying system requirements', }, { id: 'provider', title: 'AI Provider', description: 'Configure your AI service', }, { id: 'channel', title: 'Connect a Channel', description: 'Connect a messaging platform (optional)', }, { id: 'installing', title: 'Setting Up', description: 'Installing essential components', }, { id: 'complete', title: 'All Set!', description: 'ClawX is ready to use', }, ]; // Default skills to auto-install (no additional API keys required) interface DefaultSkill { id: string; name: string; description: string; } const defaultSkills: DefaultSkill[] = [ { id: 'opencode', name: 'OpenCode', description: 'AI coding assistant backend' }, { id: 'python-env', name: 'Python Environment', description: 'Python runtime for skills' }, { id: 'code-assist', name: 'Code Assist', description: 'Code analysis and suggestions' }, { id: 'file-tools', name: 'File Tools', description: 'File operations and management' }, { id: 'terminal', name: 'Terminal', description: 'Shell command execution' }, ]; import { SETUP_PROVIDERS, type ProviderTypeInfo, getProviderIconUrl, shouldInvertInDark } from '@/lib/providers'; import clawxIcon from '@/assets/logo.svg'; // Use the shared provider registry for setup providers const providers = SETUP_PROVIDERS; // NOTE: Channel types moved to Settings > Channels page // NOTE: Skill bundles moved to Settings > Skills page - auto-install essential skills during setup export function Setup() { const { t } = useTranslation(['setup', 'channels']); const navigate = useNavigate(); const [currentStep, setCurrentStep] = useState(STEP.WELCOME); // Setup state const [selectedProvider, setSelectedProvider] = useState(null); const [providerConfigured, setProviderConfigured] = useState(false); const [apiKey, setApiKey] = useState(''); // Installation state for the Installing step const [installedSkills, setInstalledSkills] = useState([]); // Runtime check status const [runtimeChecksPassed, setRuntimeChecksPassed] = useState(false); const safeStepIndex = Number.isInteger(currentStep) ? Math.min(Math.max(currentStep, STEP.WELCOME), steps.length - 1) : STEP.WELCOME; const step = steps[safeStepIndex] ?? steps[STEP.WELCOME]; const isFirstStep = safeStepIndex === STEP.WELCOME; const isLastStep = safeStepIndex === steps.length - 1; const markSetupComplete = useSettingsStore((state) => state.markSetupComplete); // Derive canProceed based on current step - computed directly to avoid useEffect const canProceed = useMemo(() => { switch (safeStepIndex) { case STEP.WELCOME: return true; case STEP.RUNTIME: return runtimeChecksPassed; case STEP.PROVIDER: return providerConfigured; case STEP.CHANNEL: return true; // Always allow proceeding — channel step is optional case STEP.INSTALLING: return false; // Cannot manually proceed, auto-proceeds when done case STEP.COMPLETE: return true; default: return true; } }, [safeStepIndex, providerConfigured, runtimeChecksPassed]); const handleNext = async () => { if (isLastStep) { // Complete setup markSetupComplete(); toast.success(t('complete.title')); navigate('/'); } else { setCurrentStep((i) => i + 1); } }; const handleBack = () => { setCurrentStep((i) => Math.max(i - 1, 0)); }; const handleSkip = () => { markSetupComplete(); navigate('/'); }; // Auto-proceed when installation is complete const handleInstallationComplete = useCallback((skills: string[]) => { setInstalledSkills(skills); // Auto-proceed to next step after a short delay setTimeout(() => { setCurrentStep((i) => i + 1); }, 1000); }, []); return (
{/* Progress Indicator */}
{steps.map((s, i) => (
{i < safeStepIndex ? ( ) : ( {i + 1} )}
{i < steps.length - 1 && (
)}
))}
{/* Step Content */}

{t(`steps.${step.id}.title`)}

{t(`steps.${step.id}.description`)}

{/* Step-specific content */}
{safeStepIndex === STEP.WELCOME && } {safeStepIndex === STEP.RUNTIME && } {safeStepIndex === STEP.PROVIDER && ( )} {safeStepIndex === STEP.CHANNEL && } {safeStepIndex === STEP.INSTALLING && ( setCurrentStep((i) => i + 1)} /> )} {safeStepIndex === STEP.COMPLETE && ( )}
{/* Navigation - hidden during installation step */} {safeStepIndex !== STEP.INSTALLING && (
{!isFirstStep && ( )}
{safeStepIndex === STEP.CHANNEL && ( )} {!isLastStep && safeStepIndex !== STEP.RUNTIME && safeStepIndex !== STEP.CHANNEL && ( )}
)}
); } // ==================== Step Content Components ==================== function WelcomeContent() { const { t } = useTranslation(['setup', 'settings']); const { language, setLanguage } = useSettingsStore(); return (
ClawX

{t('welcome.title')}

{t('welcome.description')}

{/* Language Selector */}
{SUPPORTED_LANGUAGES.map((lang) => ( ))}
  • {t('welcome.features.noCommand')}
  • {t('welcome.features.modernUI')}
  • {t('welcome.features.bundles')}
  • {t('welcome.features.crossPlatform')}
); } interface RuntimeContentProps { onStatusChange: (canProceed: boolean) => void; } function RuntimeContent({ onStatusChange }: RuntimeContentProps) { const { t } = useTranslation('setup'); const gatewayStatus = useGatewayStore((state) => state.status); const startGateway = useGatewayStore((state) => state.start); const [checks, setChecks] = useState({ nodejs: { status: 'checking' as 'checking' | 'success' | 'error', message: '' }, openclaw: { status: 'checking' as 'checking' | 'success' | 'error', message: '' }, gateway: { status: 'checking' as 'checking' | 'success' | 'error', message: '' }, }); const [showLogs, setShowLogs] = useState(false); const [logContent, setLogContent] = useState(''); const [openclawDir, setOpenclawDir] = useState(''); const gatewayTimeoutRef = useRef(null); const runChecks = useCallback(async () => { // Reset checks setChecks({ nodejs: { status: 'checking', message: '' }, openclaw: { status: 'checking', message: '' }, gateway: { status: 'checking', message: '' }, }); // Check Node.js — always available in Electron setChecks((prev) => ({ ...prev, nodejs: { status: 'success', message: t('runtime.status.success') }, })); // Check OpenClaw package status try { const openclawStatus = await window.electron.ipcRenderer.invoke('openclaw:status') as { packageExists: boolean; isBuilt: boolean; dir: string; version?: string; }; setOpenclawDir(openclawStatus.dir); if (!openclawStatus.packageExists) { setChecks((prev) => ({ ...prev, openclaw: { status: 'error', message: `OpenClaw package not found at: ${openclawStatus.dir}` }, })); } else if (!openclawStatus.isBuilt) { setChecks((prev) => ({ ...prev, openclaw: { status: 'error', message: 'OpenClaw package found but dist is missing' }, })); } else { const versionLabel = openclawStatus.version ? ` v${openclawStatus.version}` : ''; setChecks((prev) => ({ ...prev, openclaw: { status: 'success', message: `OpenClaw package ready${versionLabel}` }, })); } } catch (error) { setChecks((prev) => ({ ...prev, openclaw: { status: 'error', message: `Check failed: ${error}` }, })); } // Check Gateway — read directly from store to avoid stale closure // Don't immediately report error; gateway may still be initializing const currentGateway = useGatewayStore.getState().status; if (currentGateway.state === 'running') { setChecks((prev) => ({ ...prev, gateway: { status: 'success', message: `Running on port ${currentGateway.port}` }, })); } else if (currentGateway.state === 'error') { setChecks((prev) => ({ ...prev, gateway: { status: 'error', message: currentGateway.error || t('runtime.status.error') }, })); } else { // Gateway is 'stopped', 'starting', or 'reconnecting' // Keep as 'checking' — the dedicated useEffect will update when status changes setChecks((prev) => ({ ...prev, gateway: { status: 'checking', message: currentGateway.state === 'starting' ? t('runtime.status.checking') : 'Waiting for gateway...' }, })); } }, [t]); useEffect(() => { runChecks(); }, [runChecks]); // Update canProceed when gateway status changes useEffect(() => { const allPassed = checks.nodejs.status === 'success' && checks.openclaw.status === 'success' && (checks.gateway.status === 'success' || gatewayStatus.state === 'running'); onStatusChange(allPassed); }, [checks, gatewayStatus, onStatusChange]); // Update gateway check when gateway status changes useEffect(() => { if (gatewayStatus.state === 'running') { setChecks((prev) => ({ ...prev, gateway: { status: 'success', message: t('runtime.status.gatewayRunning', { port: gatewayStatus.port }) }, })); } else if (gatewayStatus.state === 'error') { setChecks((prev) => ({ ...prev, gateway: { status: 'error', message: gatewayStatus.error || 'Failed to start' }, })); } else if (gatewayStatus.state === 'starting' || gatewayStatus.state === 'reconnecting') { setChecks((prev) => ({ ...prev, gateway: { status: 'checking', message: 'Starting...' }, })); } // 'stopped' state: keep current check status (likely 'checking') to allow startup time }, [gatewayStatus, t]); // Gateway startup timeout — show error only after giving enough time to initialize useEffect(() => { if (gatewayTimeoutRef.current) { clearTimeout(gatewayTimeoutRef.current); gatewayTimeoutRef.current = null; } // If gateway is already in a terminal state, no timeout needed if (gatewayStatus.state === 'running' || gatewayStatus.state === 'error') { return; } // Set timeout for non-terminal states (stopped, starting, reconnecting) gatewayTimeoutRef.current = setTimeout(() => { setChecks((prev) => { if (prev.gateway.status === 'checking') { return { ...prev, gateway: { status: 'error', message: 'Gateway startup timed out' }, }; } return prev; }); }, 600 * 1000); // 600 seconds — enough for gateway to fully initialize return () => { if (gatewayTimeoutRef.current) { clearTimeout(gatewayTimeoutRef.current); gatewayTimeoutRef.current = null; } }; }, [gatewayStatus.state]); const handleStartGateway = async () => { setChecks((prev) => ({ ...prev, gateway: { status: 'checking', message: 'Starting...' }, })); await startGateway(); }; const handleShowLogs = async () => { try { const logs = await window.electron.ipcRenderer.invoke('log:readFile', 100) as string; setLogContent(logs); setShowLogs(true); } catch { setLogContent('(Failed to load logs)'); setShowLogs(true); } }; const handleOpenLogDir = async () => { try { const logDir = await window.electron.ipcRenderer.invoke('log:getDir') as string; if (logDir) { await window.electron.ipcRenderer.invoke('shell:showItemInFolder', logDir); } } catch { // ignore } }; const ERROR_TRUNCATE_LEN = 30; const renderStatus = (status: 'checking' | 'success' | 'error', message: string) => { if (status === 'checking') { return ( {message || 'Checking...'} ); } if (status === 'success') { return ( {message} ); } const isLong = message.length > ERROR_TRUNCATE_LEN; const displayMsg = isLong ? message.slice(0, ERROR_TRUNCATE_LEN) : message; return ( {displayMsg} {isLong && ( ... {message} )} ); }; return (

{t('runtime.title')}

{t('runtime.nodejs')}
{renderStatus(checks.nodejs.status, checks.nodejs.message)}
{t('runtime.openclaw')} {openclawDir && (

{openclawDir}

)}
{renderStatus(checks.openclaw.status, checks.openclaw.message)}
Gateway Service {checks.gateway.status === 'error' && ( )}
{renderStatus(checks.gateway.status, checks.gateway.message)}
{(checks.nodejs.status === 'error' || checks.openclaw.status === 'error') && (

{t('runtime.issue.title')}

{t('runtime.issue.desc')}

)} {/* Log viewer panel */} {showLogs && (

Application Logs

            {logContent || '(No logs available yet)'}
          
)}
); } interface ProviderContentProps { providers: ProviderTypeInfo[]; selectedProvider: string | null; onSelectProvider: (id: string | null) => void; apiKey: string; onApiKeyChange: (key: string) => void; onConfiguredChange: (configured: boolean) => void; } function ProviderContent({ providers, selectedProvider, onSelectProvider, apiKey, onApiKeyChange, onConfiguredChange, }: ProviderContentProps) { const { t } = useTranslation(['setup', 'settings']); const [showKey, setShowKey] = useState(false); const [validating, setValidating] = useState(false); const [keyValid, setKeyValid] = useState(null); const [selectedProviderConfigId, setSelectedProviderConfigId] = useState(null); const [baseUrl, setBaseUrl] = useState(''); const [modelId, setModelId] = useState(''); const [providerMenuOpen, setProviderMenuOpen] = useState(false); const providerMenuRef = useRef(null); const [authMode, setAuthMode] = useState<'oauth' | 'apikey'>('oauth'); // OAuth Flow State const [oauthFlowing, setOauthFlowing] = useState(false); const [oauthData, setOauthData] = useState<{ verificationUri: string; userCode: string; expiresIn: number; } | null>(null); const [oauthError, setOauthError] = useState(null); // Manage OAuth events useEffect(() => { const handleCode = (data: unknown) => { setOauthData(data as { verificationUri: string; userCode: string; expiresIn: number }); setOauthError(null); }; const handleSuccess = async () => { setOauthFlowing(false); setOauthData(null); setKeyValid(true); if (selectedProvider) { try { await window.electron.ipcRenderer.invoke('provider:setDefault', selectedProvider); } catch (error) { console.error('Failed to set default provider:', error); } } onConfiguredChange(true); toast.success(t('provider.valid')); }; const handleError = (data: unknown) => { setOauthError((data as { message: string }).message); setOauthData(null); }; window.electron.ipcRenderer.on('oauth:code', handleCode); window.electron.ipcRenderer.on('oauth:success', handleSuccess); window.electron.ipcRenderer.on('oauth:error', handleError); return () => { // Clean up manually if the API provides removeListener, though `on` in preloads might not return an unsub. // Easiest is to just let it be, or if they have `off`: if (typeof window.electron.ipcRenderer.off === 'function') { window.electron.ipcRenderer.off('oauth:code', handleCode); window.electron.ipcRenderer.off('oauth:success', handleSuccess); window.electron.ipcRenderer.off('oauth:error', handleError); } }; }, [onConfiguredChange, t, selectedProvider]); const handleStartOAuth = async () => { if (!selectedProvider) return; setOauthFlowing(true); setOauthData(null); setOauthError(null); // Default to global region for MiniMax in setup const region = 'global'; try { await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedProvider, region); } catch (e) { setOauthError(String(e)); setOauthFlowing(false); } }; const handleCancelOAuth = async () => { setOauthFlowing(false); setOauthData(null); setOauthError(null); await window.electron.ipcRenderer.invoke('provider:cancelOAuth'); }; // On mount, try to restore previously configured provider useEffect(() => { let cancelled = false; (async () => { try { const list = await window.electron.ipcRenderer.invoke('provider:list') as Array<{ id: string; type: string; hasKey: boolean }>; const defaultId = await window.electron.ipcRenderer.invoke('provider:getDefault') as string | null; const setupProviderTypes = new Set(providers.map((p) => p.id)); const setupCandidates = list.filter((p) => setupProviderTypes.has(p.type)); const preferred = (defaultId && setupCandidates.find((p) => p.id === defaultId)) || setupCandidates.find((p) => p.hasKey) || setupCandidates[0]; if (preferred && !cancelled) { onSelectProvider(preferred.type); setSelectedProviderConfigId(preferred.id); const typeInfo = providers.find((p) => p.id === preferred.type); const requiresKey = typeInfo?.requiresApiKey ?? false; onConfiguredChange(!requiresKey || preferred.hasKey); const storedKey = await window.electron.ipcRenderer.invoke('provider:getApiKey', preferred.id) as string | null; if (storedKey) { onApiKeyChange(storedKey); } } else if (!cancelled) { onConfiguredChange(false); } } catch (error) { if (!cancelled) { console.error('Failed to load provider list:', error); } } })(); return () => { cancelled = true; }; }, [onApiKeyChange, onConfiguredChange, onSelectProvider, providers]); // When provider changes, load stored key + reset base URL useEffect(() => { let cancelled = false; (async () => { if (!selectedProvider) return; try { const list = await window.electron.ipcRenderer.invoke('provider:list') as Array<{ id: string; type: string; hasKey: boolean }>; const defaultId = await window.electron.ipcRenderer.invoke('provider:getDefault') as string | null; const sameType = list.filter((p) => p.type === selectedProvider); const preferredInstance = (defaultId && sameType.find((p) => p.id === defaultId)) || sameType.find((p) => p.hasKey) || sameType[0]; const providerIdForLoad = preferredInstance?.id || selectedProvider; setSelectedProviderConfigId(providerIdForLoad); const savedProvider = await window.electron.ipcRenderer.invoke( 'provider:get', providerIdForLoad ) as { baseUrl?: string; model?: string } | null; const storedKey = await window.electron.ipcRenderer.invoke('provider:getApiKey', providerIdForLoad) as string | null; if (!cancelled) { if (storedKey) { onApiKeyChange(storedKey); } const info = providers.find((p) => p.id === selectedProvider); setBaseUrl(savedProvider?.baseUrl || info?.defaultBaseUrl || ''); setModelId(savedProvider?.model || info?.defaultModelId || ''); } } catch (error) { if (!cancelled) { console.error('Failed to load provider key:', error); } } })(); return () => { cancelled = true; }; }, [onApiKeyChange, selectedProvider, providers]); useEffect(() => { if (!providerMenuOpen) return; const handlePointerDown = (event: MouseEvent) => { if (providerMenuRef.current && !providerMenuRef.current.contains(event.target as Node)) { setProviderMenuOpen(false); } }; const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') { setProviderMenuOpen(false); } }; document.addEventListener('mousedown', handlePointerDown); document.addEventListener('keydown', handleEscape); return () => { document.removeEventListener('mousedown', handlePointerDown); document.removeEventListener('keydown', handleEscape); }; }, [providerMenuOpen]); const selectedProviderData = providers.find((p) => p.id === selectedProvider); const selectedProviderIconUrl = selectedProviderData ? getProviderIconUrl(selectedProviderData.id) : undefined; const showBaseUrlField = selectedProviderData?.showBaseUrl ?? false; const showModelIdField = selectedProviderData?.showModelId ?? false; const requiresKey = selectedProviderData?.requiresApiKey ?? false; const isOAuth = selectedProviderData?.isOAuth ?? false; const supportsApiKey = selectedProviderData?.supportsApiKey ?? false; const useOAuthFlow = isOAuth && (!supportsApiKey || authMode === 'oauth'); const handleValidateAndSave = async () => { if (!selectedProvider) return; setValidating(true); setKeyValid(null); try { // Validate key if the provider requires one and a key was entered if (requiresKey && apiKey) { const result = await window.electron.ipcRenderer.invoke( 'provider:validateKey', selectedProviderConfigId || selectedProvider, apiKey, { baseUrl: baseUrl.trim() || undefined } ) as { valid: boolean; error?: string }; setKeyValid(result.valid); if (!result.valid) { toast.error(result.error || t('provider.invalid')); setValidating(false); return; } } else { setKeyValid(true); } const effectiveModelId = selectedProviderData?.defaultModelId || modelId.trim() || undefined; const providerIdForSave = selectedProvider === 'custom' ? (selectedProviderConfigId?.startsWith('custom-') ? selectedProviderConfigId : `custom-${crypto.randomUUID()}`) : selectedProvider; // Save provider config + API key, then set as default const saveResult = await window.electron.ipcRenderer.invoke( 'provider:save', { id: providerIdForSave, name: selectedProvider === 'custom' ? t('settings:aiProviders.custom') : (selectedProviderData?.name || selectedProvider), type: selectedProvider, baseUrl: baseUrl.trim() || undefined, model: effectiveModelId, enabled: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, apiKey || undefined ) as { success: boolean; error?: string }; if (!saveResult.success) { throw new Error(saveResult.error || 'Failed to save provider config'); } const defaultResult = await window.electron.ipcRenderer.invoke( 'provider:setDefault', providerIdForSave ) as { success: boolean; error?: string }; if (!defaultResult.success) { throw new Error(defaultResult.error || 'Failed to set default provider'); } setSelectedProviderConfigId(providerIdForSave); onConfiguredChange(true); toast.success(t('provider.valid')); } catch (error) { setKeyValid(false); onConfiguredChange(false); toast.error('Configuration failed: ' + String(error)); } finally { setValidating(false); } }; // Can the user submit? const canSubmit = selectedProvider && (requiresKey ? apiKey.length > 0 : true) && (showModelIdField ? modelId.trim().length > 0 : true) && !useOAuthFlow; const handleSelectProvider = (providerId: string) => { onSelectProvider(providerId); setSelectedProviderConfigId(null); onConfiguredChange(false); onApiKeyChange(''); setKeyValid(null); setProviderMenuOpen(false); setAuthMode('oauth'); }; return (
{/* Provider selector — dropdown */}
{providerMenuOpen && (
{providers.map((p) => { const iconUrl = getProviderIconUrl(p.id); const isSelected = selectedProvider === p.id; return ( ); })}
)}
{/* Dynamic config fields based on selected provider */} {selectedProvider && ( {/* Base URL field (for siliconflow, ollama, custom) */} {showBaseUrlField && (
{ setBaseUrl(e.target.value); onConfiguredChange(false); }} autoComplete="off" className="bg-background border-input" />
)} {/* Model ID field (for siliconflow etc.) */} {showModelIdField && (
{ setModelId(e.target.value); onConfiguredChange(false); }} autoComplete="off" className="bg-background border-input" />

{t('provider.modelIdDesc')}

)} {/* Auth mode toggle for providers supporting both */} {isOAuth && supportsApiKey && (
)} {/* API Key field (hidden for ollama) */} {(!isOAuth || (supportsApiKey && authMode === 'apikey')) && requiresKey && (
{ onApiKeyChange(e.target.value); onConfiguredChange(false); setKeyValid(null); }} autoComplete="off" className="pr-10 bg-background border-input" />
)} {/* Device OAuth Trigger */} {useOAuthFlow && (

This provider requires signing in via your browser.

{/* OAuth Active State Modal / Inline View */} {oauthFlowing && (
{/* Background pulse effect */}
{oauthError ? (

Authentication Failed

{oauthError}

) : !oauthData ? (

Requesting secure login code...

) : (

Approve Login

1. Copy the authorization code below.

2. Open the login page in your browser.

3. Paste the code to approve access.

{oauthData.userCode}
Waiting for approval in browser...
)}
)}
)} {/* Validate & Save */} {keyValid !== null && (

{keyValid ? `✓ ${t('provider.valid')}` : `✗ ${t('provider.invalid')}`}

)}

{t('provider.storedLocally')}

)}
); } // ==================== Setup Channel Content ==================== function SetupChannelContent() { const { t } = useTranslation(['setup', 'channels']); const [selectedChannel, setSelectedChannel] = useState(null); const [configValues, setConfigValues] = useState>({}); const [showSecrets, setShowSecrets] = useState>({}); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [validationError, setValidationError] = useState(null); const meta: ChannelMeta | null = selectedChannel ? CHANNEL_META[selectedChannel] : null; const primaryChannels = getPrimaryChannels(); useEffect(() => { let cancelled = false; (async () => { if (!selectedChannel) return; try { const result = await window.electron.ipcRenderer.invoke( 'channel:getFormValues', selectedChannel ) as { success: boolean; values?: Record }; if (cancelled) return; if (result.success && result.values) { setConfigValues(result.values); } else { setConfigValues({}); } } catch { if (!cancelled) { setConfigValues({}); } } })(); return () => { cancelled = true; }; }, [selectedChannel]); const isFormValid = () => { if (!meta) return false; return meta.configFields .filter((f: ChannelConfigField) => f.required) .every((f: ChannelConfigField) => configValues[f.key]?.trim()); }; const handleSave = async () => { if (!selectedChannel || !meta || !isFormValid()) return; setSaving(true); setValidationError(null); try { // Validate credentials first const validation = await window.electron.ipcRenderer.invoke( 'channel:validateCredentials', selectedChannel, configValues ) as { success: boolean; valid?: boolean; errors?: string[]; details?: Record }; if (!validation.valid) { setValidationError((validation.errors || ['Validation failed']).join(', ')); setSaving(false); return; } // Save config await window.electron.ipcRenderer.invoke('channel:saveConfig', selectedChannel, { ...configValues }); const botName = validation.details?.botUsername ? ` (@${validation.details.botUsername})` : ''; toast.success(`${meta.name} configured${botName}`); setSaved(true); } catch (error) { setValidationError(String(error)); } finally { setSaving(false); } }; // Already saved — show success if (saved) { return (

{t('channel.connected', { name: meta?.name || 'Channel' })}

{t('channel.connectedDesc')}

); } // Channel type not selected — show picker if (!selectedChannel) { return (
📡

{t('channel.title')}

{t('channel.subtitle')}

{primaryChannels.map((type) => { const channelMeta = CHANNEL_META[type]; if (channelMeta.connectionType !== 'token') return null; return ( ); })}
); } // Channel selected — show config form return (

{meta?.icon} {t('channel.configure', { name: meta?.name })}

{t(meta?.description || '')}

{/* Instructions */}

{t('channel.howTo')}

{meta?.docsUrl && ( )}
    {meta?.instructions.map((inst, i) => (
  1. {t(inst)}
  2. ))}
{/* Config fields */} {meta?.configFields.map((field: ChannelConfigField) => { const isPassword = field.type === 'password'; return (
setConfigValues((prev) => ({ ...prev, [field.key]: e.target.value }))} autoComplete="off" className="font-mono text-sm bg-background border-input" /> {isPassword && ( )}
{field.description && (

{t(field.description)}

)}
); })} {/* Validation error */} {validationError && (
{validationError}
)} {/* Save button */}
); } // NOTE: SkillsContent component removed - auto-install essential skills // Installation status for each skill type InstallStatus = 'pending' | 'installing' | 'completed' | 'failed'; interface SkillInstallState { id: string; name: string; description: string; status: InstallStatus; } interface InstallingContentProps { skills: DefaultSkill[]; onComplete: (installedSkills: string[]) => void; onSkip: () => void; } function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProps) { const { t } = useTranslation('setup'); const [skillStates, setSkillStates] = useState( skills.map((s) => ({ ...s, status: 'pending' as InstallStatus })) ); const [overallProgress, setOverallProgress] = useState(0); const [errorMessage, setErrorMessage] = useState(null); const installStarted = useRef(false); // Real installation process useEffect(() => { if (installStarted.current) return; installStarted.current = true; const runRealInstall = async () => { try { // Step 1: Initialize all skills to 'installing' state for UI setSkillStates(prev => prev.map(s => ({ ...s, status: 'installing' }))); setOverallProgress(10); // Step 2: Call the backend to install uv and setup Python const result = await window.electron.ipcRenderer.invoke('uv:install-all') as { success: boolean; error?: string }; if (result.success) { setSkillStates(prev => prev.map(s => ({ ...s, status: 'completed' }))); setOverallProgress(100); await new Promise((resolve) => setTimeout(resolve, 800)); onComplete(skills.map(s => s.id)); } else { setSkillStates(prev => prev.map(s => ({ ...s, status: 'failed' }))); setErrorMessage(result.error || 'Unknown error during installation'); toast.error('Environment setup failed'); } } catch (err) { setSkillStates(prev => prev.map(s => ({ ...s, status: 'failed' }))); setErrorMessage(String(err)); toast.error('Installation error'); } }; runRealInstall(); }, [skills, onComplete]); const getStatusIcon = (status: InstallStatus) => { switch (status) { case 'pending': return
; case 'installing': return ; case 'completed': return ; case 'failed': return ; } }; const getStatusText = (skill: SkillInstallState) => { switch (skill.status) { case 'pending': return {t('installing.status.pending')}; case 'installing': return {t('installing.status.installing')}; case 'completed': return {t('installing.status.installed')}; case 'failed': return {t('installing.status.failed')}; } }; return (
⚙️

{t('installing.title')}

{t('installing.subtitle')}

{/* Progress bar */}
{t('installing.progress')} {overallProgress}%
{/* Skill list */}
{skillStates.map((skill) => (
{getStatusIcon(skill.status)}

{skill.name}

{skill.description}

{getStatusText(skill)}
))}
{/* Error Message Display */} {errorMessage && (

{t('installing.error')}

                {errorMessage}
              
)} {!errorMessage && (

{t('installing.wait')}

)}
); } interface CompleteContentProps { selectedProvider: string | null; installedSkills: string[]; } function CompleteContent({ selectedProvider, installedSkills }: CompleteContentProps) { const { t } = useTranslation(['setup', 'settings']); const gatewayStatus = useGatewayStore((state) => state.status); const providerData = providers.find((p) => p.id === selectedProvider); const installedSkillNames = defaultSkills .filter((s) => installedSkills.includes(s.id)) .map((s) => s.name) .join(', '); return (
🎉

{t('complete.title')}

{t('complete.subtitle')}

{t('complete.provider')} {providerData ? {getProviderIconUrl(providerData.id) ? {providerData.name} : providerData.icon} {providerData.id === 'custom' ? t('settings:aiProviders.custom') : providerData.name} : '—'}
{t('complete.components')} {installedSkillNames || `${installedSkills.length} ${t('installing.status.installed')}`}
{t('complete.gateway')} {gatewayStatus.state === 'running' ? `✓ ${t('complete.running')}` : gatewayStatus.state}

{t('complete.footer')}

); } export default Setup;