'use client' import Image from 'next/image' import { createPortal } from 'react-dom' import { useState, useEffect, useCallback } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Loader } from '@/components/ui/loader' import { useMissionControl } from '@/store' import { useNavigateToPanel } from '@/lib/navigation' import { clampWizardStep, getWizardSteps, stepIdAt } from '@/lib/onboarding-flow' import { SecurityScanCard } from '@/components/onboarding/security-scan-card' import { clearOnboardingReplayFromStart, markOnboardingDismissedThisSession, readOnboardingReplayFromStart } from '@/lib/onboarding-session' interface StepInfo { id: string title: string completed: boolean } interface OnboardingState { showOnboarding: boolean currentStep: number steps: StepInfo[] } interface DiagSecurityCheck { name: string pass: boolean detail: string } interface DashboardRegistration { registered: boolean alreadySet: boolean } interface SystemCapabilities { claudeSessions: number agentCount: number gatewayConnected: boolean hasSkills: boolean dashboardRegistration: DashboardRegistration | null } /** Mode-aware Tailwind classes — local=amber, gateway=cyan */ function modeColors(isGateway: boolean) { return isGateway ? { text: 'text-void-cyan', border: 'border-void-cyan/30', bg: 'bg-void-cyan', bgLight: 'bg-void-cyan/5', bgBtn: 'bg-void-cyan/20', hoverBg: 'hover:bg-void-cyan/30', hoverBorder: 'hover:border-void-cyan/30', hoverBgLight: 'hover:bg-void-cyan/10', dot: 'bg-void-cyan', dotDim: 'bg-void-cyan/40' } : { text: 'text-void-amber', border: 'border-void-amber/30', bg: 'bg-void-amber', bgLight: 'bg-void-amber/5', bgBtn: 'bg-void-amber/20', hoverBg: 'hover:bg-void-amber/30', hoverBorder: 'hover:border-void-amber/30', hoverBgLight: 'hover:bg-void-amber/10', dot: 'bg-void-amber', dotDim: 'bg-void-amber/40' } } export function OnboardingWizard() { const { showOnboarding, setShowOnboarding, dashboardMode, gatewayAvailable, interfaceMode, setInterfaceMode } = useMissionControl() const navigateToPanel = useNavigateToPanel() const t = useTranslations('onboarding') const [step, setStep] = useState(0) const [slideDir, setSlideDir] = useState<'left' | 'right'>('left') const [animating, setAnimating] = useState(false) const [state, setState] = useState(null) const [credentialStatus, setCredentialStatus] = useState<{ authOk: boolean; apiKeyOk: boolean } | null>(null) const [closing, setClosing] = useState(false) const [completionMessage, setCompletionMessage] = useState(false) const [capabilities, setCapabilities] = useState({ claudeSessions: 0, agentCount: 0, gatewayConnected: false, hasSkills: false, dashboardRegistration: null, }) const [mounted, setMounted] = useState(false) useEffect(() => { setMounted(true) }, []) useEffect(() => { if (!showOnboarding) return const previousOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' fetch('/api/onboarding') .then(r => r.ok ? r.json() : null) .then(data => { if (data) { setState(data) const shouldReplayFromStart = readOnboardingReplayFromStart() setStep((current) => { const incoming = shouldReplayFromStart ? 0 : (typeof data.currentStep === 'number' ? data.currentStep : current) return clampWizardStep(incoming, data?.steps?.length || 0) }) if (shouldReplayFromStart) { clearOnboardingReplayFromStart() } } }) .catch(() => {}) // Fetch system capabilities in parallel Promise.allSettled([ fetch('/api/status?action=capabilities').then(r => r.ok ? r.json() : null), fetch('/api/agents?limit=1').then(r => r.ok ? r.json() : null), ]).then(([statusResult, agentsResult]) => { const statusData = statusResult.status === 'fulfilled' ? statusResult.value : null const agentsData = agentsResult.status === 'fulfilled' ? agentsResult.value : null setCapabilities({ claudeSessions: statusData?.claudeSessions ?? 0, gatewayConnected: statusData?.gateway ?? false, agentCount: agentsData?.total ?? 0, hasSkills: false, dashboardRegistration: statusData?.dashboardRegistration ?? null, }) }) return () => { document.body.style.overflow = previousOverflow } }, [showOnboarding]) const STEPS = getWizardSteps(capabilities.gatewayConnected) const credentialsStepIndex = STEPS.findIndex((s) => s.id === 'credentials') useEffect(() => { setStep((current) => clampWizardStep(current, STEPS.length)) }, [STEPS.length]) useEffect(() => { if (step !== credentialsStepIndex || credentialStatus) return fetch('/api/diagnostics') .then(r => r.ok ? r.json() : null) .then(data => { if (data?.security?.checks) { const checks = data.security.checks as DiagSecurityCheck[] const authOk = checks.find(c => c.name === 'Auth password secure')?.pass ?? false const apiKeyOk = checks.find(c => c.name === 'API key configured')?.pass ?? false setCredentialStatus({ authOk, apiKeyOk }) } }) .catch(() => {}) }, [step, credentialStatus, credentialsStepIndex]) const completeStep = useCallback(async (stepId: string) => { await fetch('/api/onboarding', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'complete_step', step: stepId }), }).catch(() => {}) }, []) const finish = useCallback(async () => { setCompletionMessage(true) await fetch('/api/onboarding', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'complete' }), }).catch(() => {}) setTimeout(() => { setClosing(true) markOnboardingDismissedThisSession() setTimeout(() => setShowOnboarding(false), 300) }, 1200) }, [setShowOnboarding]) const skip = useCallback(async () => { setClosing(true) await fetch('/api/onboarding', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'skip' }), }).catch(() => {}) markOnboardingDismissedThisSession() setTimeout(() => setShowOnboarding(false), 300) }, [setShowOnboarding]) const goNext = useCallback(() => { const currentId = stepIdAt(step, STEPS) if (currentId) completeStep(currentId) setSlideDir('left') setAnimating(true) setTimeout(() => { setStep((s) => Math.min(s + 1, STEPS.length - 1)) setAnimating(false) }, 150) }, [step, STEPS, completeStep]) const goBack = useCallback(() => { setSlideDir('right') setAnimating(true) setTimeout(() => { setStep((s) => Math.max(s - 1, 0)) setAnimating(false) }, 150) }, []) useEffect(() => { if (!showOnboarding) return const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault() skip() } } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [showOnboarding, skip]) if (!mounted || !showOnboarding || !state) return null const totalSteps = STEPS.length const isGateway = dashboardMode === 'full' || gatewayAvailable return createPortal(
{/* Backdrop */}
{/* Modal */}
{/* Progress bar */}
{/* Step indicator */}
{Array.from({ length: totalSteps }).map((_, i) => (
))}
{STEPS[step]?.title}
{/* Content */}
{completionMessage && (
{t('stationOnline')}

{t('stationReady')}

)} {STEPS[step]?.id === 'welcome' && ( )} {STEPS[step]?.id === 'interface-mode' && ( )} {STEPS[step]?.id === 'gateway-link' && ( )} {STEPS[step]?.id === 'credentials' && ( )}
, document.body ) } function StepWelcome({ isGateway, capabilities, onNext, onSkip }: { isGateway: boolean capabilities: SystemCapabilities onNext: () => void onSkip: () => void }) { const mc = modeColors(isGateway) const t = useTranslations('onboarding.welcome') const tc = useTranslations('common') return ( <>
Mission Control

{t('title')}

{t('description')}

{/* Live status chips */}
0} label={capabilities.claudeSessions > 0 ? t('activeSessionsDetected', { count: capabilities.claudeSessions }) : t('noActiveSessions')} /> 0} label={capabilities.agentCount > 0 ? t('agentsRegistered', { count: capabilities.agentCount }) : t('noAgentsYet')} /> {capabilities.gatewayConnected && capabilities.dashboardRegistration && ( )}
{/* Mode cards — both visible, detected mode highlighted */}

{t('availableModes')}

{/* Local mode card */}
{!isGateway && ( {tc('detected')} )}

{t('localMode')}

  • {t('monitorClaude')}
  • {t('taskTracking')}
  • {t('sessionHistory')}
{isGateway && (

{t('singlePilot')}

)}
{/* Gateway mode card */}
{isGateway && ( {tc('detected')} )}

{t('gatewayMode')}

  • {t('orchestrateAgents')}
  • {t('memorySkills')}
  • {t('webhookIntegrations')}
{!isGateway && (

{t('requiresGateway')}

)}
) } function StatusChip({ ok, label }: { ok: boolean; label: string }) { return (
{label}
) } function StepInterfaceMode({ isGateway, onNext, onBack }: { isGateway: boolean onNext: () => void onBack: () => void }) { const mc = modeColors(isGateway) const t = useTranslations('onboarding.interfaceMode') const tc = useTranslations('common') const { interfaceMode, setInterfaceMode } = useMissionControl() const [selected, setSelected] = useState<'essential' | 'full'>(interfaceMode) const handleSelect = async (mode: 'essential' | 'full') => { setSelected(mode) setInterfaceMode(mode) try { await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings: { 'general.interface_mode': mode } }), }) } catch {} } return ( <>

{t('title')}

{t('description')}

{/* Essential card */} {/* Full card */}
) } function StepGatewayLink({ isGateway, registration, onNext, onBack }: { isGateway: boolean registration: DashboardRegistration | null onNext: () => void onBack: () => void }) { const mc = modeColors(isGateway) const t = useTranslations('onboarding.gatewayLink') const tc = useTranslations('common') const [healthOk, setHealthOk] = useState(null) const [testing, setTesting] = useState(false) const testConnection = async () => { setTesting(true) try { const res = await fetch('/api/gateways/health', { method: 'POST' }) setHealthOk(res.ok) } catch { setHealthOk(false) } finally { setTesting(false) } } const configured = registration?.registered || registration?.alreadySet return ( <>

{t('title')}

{t('description')}

[{configured ? '+' : '~'}]

{t('originRegistered')}

{configured ? t('originAdded') : t('registrationPending')}

[{configured ? '+' : '-'}]

{t('deviceAuthConfigured')}

{configured ? t('deviceAuthDisabled') : t('deviceAuthWillConfigure')}

{healthOk === true && ( {t('gatewayReachable')} )} {healthOk === false && ( {t('gatewayUnreachable')} )}
) } function StepCredentials({ isGateway, status, onFinish, onBack, navigateToPanel, onClose, }: { isGateway: boolean status: { authOk: boolean; apiKeyOk: boolean } | null onFinish: () => void onBack: () => void navigateToPanel: (panel: string) => void onClose: () => void }) { const mc = modeColors(isGateway) const t = useTranslations('onboarding.credentials') const tc = useTranslations('common') const allGood = status?.authOk && status?.apiKeyOk return ( <>

{t('title')}

{t('description')}

{!status ? (
) : (
[{status.authOk ? '+' : 'x'}]

{t('adminPassword')}

{status.authOk ? t('passwordStrong') : t('passwordWeak')}

[{status.apiKeyOk ? '+' : 'x'}]

{t('apiKey')}

{status.apiKeyOk ? t('apiKeyConfigured') : t('apiKeyNotSet')}

{!allGood && ( )}

{t('securityScan')}

{t('securityScanDescription')}

)}
) }