/** * 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 { Check, ChevronLeft, ChevronRight, Loader2, AlertCircle, RefreshCw, CheckCircle2, } from 'lucide-react'; import { TitleBar } from '@/components/layout/TitleBar'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { hostApiFetch } from '@/lib/host-api'; import { cn } from '@/lib/utils'; import { useSettingsStore } from '@/stores/settings'; import { useTranslation } from 'react-i18next'; import type { TFunction } from 'i18next'; import { SUPPORTED_LANGUAGES } from '@/i18n'; import logoSvg from '@/assets/logo.svg'; import { getAuthUserDisplayName } from '@/lib/auth-user-display'; import { useAuthStore } from '@/stores/auth'; import { toast } from 'sonner'; interface SetupStep { id: string; title: string; description: string; } const STEP = { WELCOME: 0, RUNTIME: 1, INSTALLING: 2, COMPLETE: 3, } as const; const getSteps = (t: TFunction): SetupStep[] => [ { id: 'welcome', title: t('steps.welcome.title'), description: t('steps.welcome.description'), }, { id: 'runtime', title: t('steps.runtime.title'), description: t('steps.runtime.description'), }, { id: 'installing', title: t('steps.installing.title'), description: t('steps.installing.description'), }, { id: 'complete', title: t('steps.complete.title'), description: t('steps.complete.description'), }, ]; // Default skills to auto-install (no additional API keys required) interface DefaultSkill { id: string; name: string; description: string; } const getDefaultSkills = (t: TFunction): DefaultSkill[] => [ { id: 'coding-runtime', name: t('defaultSkills.codingRuntime.name'), description: t('defaultSkills.codingRuntime.description') }, { id: 'python-env', name: t('defaultSkills.python-env.name'), description: t('defaultSkills.python-env.description') }, { id: 'code-assist', name: t('defaultSkills.code-assist.name'), description: t('defaultSkills.code-assist.description') }, { id: 'file-tools', name: t('defaultSkills.file-tools.name'), description: t('defaultSkills.file-tools.description') }, { id: 'terminal', name: t('defaultSkills.terminal.name'), description: t('defaultSkills.terminal.description') }, ]; const SETUP_MESSAGE_INTERVAL_MS = 1600; const RUNTIME_CHECK_STUCK_MS = 8000; type RuntimeProgressStatus = 'checking' | 'success' | 'error'; type InstallPhase = 'installing' | 'failed'; function getTranslatedMessages(t: TFunction, key: string, fallback: string[]) { const value = t(key, { returnObjects: true }) as unknown; if (Array.isArray(value)) { const messages = value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0); if (messages.length > 0) return messages; } return fallback; } function useRotatingSetupMessage(messages: string[], active = true) { const [index, setIndex] = useState(0); useEffect(() => { if (!active || messages.length <= 1) return undefined; const timer = window.setInterval(() => { setIndex((current) => (current + 1) % messages.length); }, SETUP_MESSAGE_INTERVAL_MS); return () => window.clearInterval(timer); }, [active, messages]); return messages[index % Math.max(messages.length, 1)] ?? messages[0] ?? ''; } export function Setup() { const { t } = useTranslation('setup'); const navigate = useNavigate(); const [currentStep, setCurrentStep] = useState(STEP.WELCOME); // Setup state // Installation state for the Installing step const [installedSkills, setInstalledSkills] = useState([]); // Runtime check status const [runtimeChecksPassed, setRuntimeChecksPassed] = useState(false); const steps = getSteps(t); 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.INSTALLING: return false; // Cannot manually proceed, auto-proceeds when done case STEP.COMPLETE: return true; default: return true; } }, [safeStepIndex, 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.INSTALLING && ( )} {safeStepIndex === STEP.COMPLETE && ( )}
{/* Navigation - hidden during installation step */} {safeStepIndex !== STEP.INSTALLING && (
{!isFirstStep && ( )}
{!isLastStep && safeStepIndex !== STEP.RUNTIME && ( )}
)}
); } // ==================== Step Content Components ==================== function WelcomeContent() { const { t } = useTranslation(['setup', 'settings']); const { language, setLanguage } = useSettingsStore(); const authUser = useAuthStore((state) => state.user); const userDisplayName = getAuthUserDisplayName(authUser); return (
Makelore

{userDisplayName ? `${userDisplayName},欢迎回来!` : 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 stuckTimeoutRef = useRef(null); const [runtimeStatus, setRuntimeStatus] = useState('checking'); const [runtimeProgress, setRuntimeProgress] = useState(12); const runtimeMessages = useMemo( () => getTranslatedMessages(t, 'runtime.progress.messages', [ 'Preparing the local workspace...', 'Checking bundled tools...', 'Warming up the coding assistant...', ]), [t], ); const rotatingMessage = useRotatingSetupMessage(runtimeMessages, runtimeStatus === 'checking'); const clearStuckTimeout = useCallback(() => { if (stuckTimeoutRef.current !== null) { window.clearTimeout(stuckTimeoutRef.current); stuckTimeoutRef.current = null; } }, []); const markSuccess = useCallback(() => { clearStuckTimeout(); setRuntimeStatus('success'); setRuntimeProgress(100); onStatusChange(true); }, [clearStuckTimeout, onStatusChange]); const markFailure = useCallback(() => { clearStuckTimeout(); setRuntimeStatus('error'); setRuntimeProgress(100); onStatusChange(false); }, [clearStuckTimeout, onStatusChange]); const startStuckTimeout = useCallback(() => { clearStuckTimeout(); stuckTimeoutRef.current = window.setTimeout(() => { setRuntimeStatus((current) => (current === 'checking' ? 'error' : current)); setRuntimeProgress(100); onStatusChange(false); }, RUNTIME_CHECK_STUCK_MS); }, [clearStuckTimeout, onStatusChange]); const runChecks = useCallback(async () => { onStatusChange(false); setRuntimeStatus('checking'); setRuntimeProgress(18); startStuckTimeout(); try { setRuntimeProgress(48); const current = await hostApiFetch<{ runtime?: string }>('/api/app/runtime-info'); if (current.runtime === 'pi') { markSuccess(); return; } markFailure(); } catch { markFailure(); } }, [markFailure, markSuccess, onStatusChange, startStuckTimeout]); useEffect(() => { const timer = window.setTimeout(() => { void runChecks(); }, 0); return () => { window.clearTimeout(timer); clearStuckTimeout(); }; }, [clearStuckTimeout, runChecks]); const statusMessage = runtimeStatus === 'success' ? t('runtime.progress.done') : runtimeStatus === 'error' ? t('runtime.progress.stuck') : rotatingMessage; return (
{runtimeStatus === 'checking' && } {runtimeStatus === 'success' && } {runtimeStatus === 'error' && }

{t('runtime.title')}

{statusMessage}

{t('runtime.progress.label')} {runtimeProgress}%
{runtimeStatus === 'error' && (
)}
); } interface InstallingContentProps { skills: DefaultSkill[]; onComplete: (installedSkills: string[]) => void; } function InstallingContent({ skills, onComplete }: InstallingContentProps) { const { t } = useTranslation('setup'); const [installPhase, setInstallPhase] = useState('installing'); const [overallProgress, setOverallProgress] = useState(0); const [installAttempt, setInstallAttempt] = useState(0); const installMessages = useMemo( () => getTranslatedMessages(t, 'installing.progressMessages', [ 'Preparing your first project workspace...', 'Connecting the built-in coding tools...', 'Finishing the starter setup...', ]), [t], ); const rotatingMessage = useRotatingSetupMessage(installMessages, installPhase === 'installing'); useEffect(() => { let cancelled = false; const delay = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms)); const runInstall = async () => { try { setInstallPhase('installing'); setOverallProgress(14); await delay(350); if (cancelled) return; setOverallProgress(46); await delay(450); if (cancelled) return; setOverallProgress(78); await delay(450); if (cancelled) return; setOverallProgress(100); await delay(450); if (cancelled) return; onComplete(skills.map(s => s.id)); } catch { if (cancelled) return; setInstallPhase('failed'); setOverallProgress(100); toast.error(t('installing.error')); } }; void runInstall(); return () => { cancelled = true; }; }, [installAttempt, onComplete, skills, t]); return (
{installPhase === 'failed' ? ( ) : ( )}

{t('installing.title')}

{installPhase === 'failed' ? t('installing.stuck') : rotatingMessage}

{t('installing.progress')} {overallProgress}%
{installPhase === 'installing' ? (

{t('installing.wait')}

) : (
)}
); } interface CompleteContentProps { installedSkills: string[]; } function CompleteContent({ installedSkills }: CompleteContentProps) { const { t } = useTranslation(['setup', 'settings']); const installedSkillNames = getDefaultSkills(t) .filter((s: DefaultSkill) => installedSkills.includes(s.id)) .map((s: DefaultSkill) => s.name) .join(', '); return (
🎉

{t('complete.title')}

{t('complete.subtitle')}

{t('complete.components')} {installedSkillNames || `${installedSkills.length} ${t('installing.status.installed')}`}
{t('complete.runtime')} {t('complete.running')}

{t('complete.footer')}

); } export default Setup;