import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { Box, Paper, Fade, Slide, useTheme, useMediaQuery } from '@mui/material'; import { getCurrentStep, setCurrentStep } from '../../api/onboarding'; import { apiClient } from '../../api/client'; import ApiKeyStep from './ApiKeyStep'; import WebsiteStep from './WebsiteStep'; import CompetitorAnalysisStep from './CompetitorAnalysisStep'; import PersonaStep from './PersonaStep'; import IntegrationsStep from './IntegrationsStep'; import FinalStep from './FinalStep'; import { WizardHeader } from './common/WizardHeader'; import { WizardNavigation } from './common/WizardNavigation'; import { WizardLoadingState } from './common/WizardLoadingState'; const steps = [ { label: 'API Keys', description: 'Connect your AI services', icon: '🔑' }, { label: 'Website', description: 'Set up your website', icon: '🌐' }, { label: 'Research', description: 'Discover competitors', icon: '🔍' }, { label: 'Personalization', description: 'Customize your experience', icon: '⚙️' }, { label: 'Integrations', description: 'Connect additional services', icon: '🔗' }, { label: 'Finish', description: 'Complete setup', icon: '✅' } ]; interface WizardProps { onComplete?: () => void; } interface StepHeaderContent { title: string; description: string; } const Wizard: React.FC = ({ onComplete }) => { const [activeStep, setActiveStep] = useState(0); const [loading, setLoading] = useState(true); const [progress, setProgressState] = useState(0); const [direction, setDirection] = useState<'left' | 'right'>('right'); const [showHelp, setShowHelp] = useState(false); const [showProgressMessage, setShowProgressMessage] = useState(false); const [progressMessage, setProgressMessage] = useState(''); // sessionId removed - backend uses Clerk user ID from auth token const [stepData, setStepData] = useState(null); const [competitorDataCollector, setCompetitorDataCollector] = useState<(() => any) | null>(null); const [isCurrentStepValid, setIsCurrentStepValid] = useState(false); const [stepValidationStates, setStepValidationStates] = useState>({}); const [stepHeaderContent, setStepHeaderContent] = useState({ title: steps[0].label, description: steps[0].description }); // Step validation function const isStepDataValid = useCallback((step: number, data: any): boolean => { console.log(`Wizard: Validating step ${step} with data:`, data); switch (step) { case 0: // API Keys const hasApiKeys = data && data.api_keys && Object.keys(data.api_keys).length > 0; console.log(`Wizard: Step 0 (API Keys) validation:`, !!hasApiKeys); return !!hasApiKeys; case 1: // Website Analysis const hasWebsite = data && (data.website || data.website_url); console.log(`Wizard: Step 1 (Website) validation:`, !!hasWebsite); return !!hasWebsite; case 2: // Competitor Analysis const hasCompetitorData = data && (data.competitors || data.researchSummary || data.sitemapAnalysis); console.log(`Wizard: Step 2 (Competitor Analysis) validation:`, { hasCompetitorData: !!hasCompetitorData, hasCompetitors: !!(data && data.competitors), hasResearchSummary: !!(data && data.researchSummary), hasSitemapAnalysis: !!(data && data.sitemapAnalysis), dataKeys: data ? Object.keys(data) : 'no data' }); return !!hasCompetitorData; case 3: // Persona Generation const hasValidPersonaData = data && data.corePersona && data.platformPersonas && Object.keys(data.platformPersonas).length > 0 && data.qualityMetrics; console.log(`Wizard: Step 3 (Persona Generation) validation:`, { hasValidPersonaData: !!hasValidPersonaData, hasCorePersona: !!(data && data.corePersona), hasPlatformPersonas: !!(data && data.platformPersonas), platformPersonasCount: data && data.platformPersonas ? Object.keys(data.platformPersonas).length : 0, hasQualityMetrics: !!(data && data.qualityMetrics), dataKeys: data ? Object.keys(data) : 'no data' }); return !!hasValidPersonaData; case 4: // Integrations console.log(`Wizard: Step 4 (Integrations) validation: always true (optional)`); return true; // Integrations step is optional case 5: // Final Step console.log(`Wizard: Step 5 (Final) validation: always true`); return true; // Final step is always valid default: console.log(`Wizard: Unknown step ${step} validation: false`); return false; } }, []); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); // Use refs to avoid dependency cycles const stepDataRef = useRef(stepData); const competitorDataCollectorRef = useRef(competitorDataCollector); // Keep refs in sync with state useEffect(() => { stepDataRef.current = stepData; console.log('Wizard: stepData changed:', stepData); }, [stepData]); useEffect(() => { competitorDataCollectorRef.current = competitorDataCollector; console.log('Wizard: competitorDataCollector changed:', competitorDataCollector); }, [competitorDataCollector]); // Validate current step data useEffect(() => { console.log(`Wizard: Validation effect triggered - activeStep: ${activeStep}, stepData:`, stepData); console.log(`Wizard: stepData type:`, typeof stepData, 'keys:', stepData ? Object.keys(stepData) : 'no data'); console.log(`Wizard: stepValidationStates:`, stepValidationStates); // For step 0 (API Keys), step 1 (Website), and step 3 (Persona), use the step validation state if available if ((activeStep === 0 || activeStep === 1 || activeStep === 3) && stepValidationStates[activeStep] !== undefined) { console.log(`Wizard: Using step validation state for step ${activeStep}:`, stepValidationStates[activeStep]); setIsCurrentStepValid(stepValidationStates[activeStep]); return; } // For other steps, use the existing validation logic // For CompetitorAnalysisStep, also check the competitorDataCollector data let dataToValidate = stepData; if (activeStep === 2 && competitorDataCollector) { console.log(`Wizard: Using competitorDataCollector data for validation:`, competitorDataCollector); dataToValidate = competitorDataCollector; } const isValid = isStepDataValid(activeStep, dataToValidate); console.log(`Wizard: Validation result for step ${activeStep}:`, isValid); console.log(`Wizard: Setting isCurrentStepValid to:`, isValid); setIsCurrentStepValid(isValid); }, [activeStep, stepData, isStepDataValid, competitorDataCollector, stepValidationStates]); // Debug: log all state changes useEffect(() => { console.log('Wizard: Render triggered - activeStep:', activeStep, 'direction:', direction); }, [activeStep, direction]); // Debug: log Continue button state useEffect(() => { console.log(`Wizard: isCurrentStepValid changed to: ${isCurrentStepValid} (Continue button ${isCurrentStepValid ? 'ENABLED' : 'DISABLED'})`); }, [isCurrentStepValid]); // Handle validation changes from individual steps const handleStepValidationChange = useCallback((step: number, isValid: boolean) => { console.log(`Wizard: handleStepValidationChange called - step: ${step}, isValid: ${isValid}`); setStepValidationStates(prev => { // Only update if the value actually changed if (prev[step] === isValid) { console.log(`Wizard: Validation state unchanged for step ${step}, skipping update`); return prev; // Return same reference to prevent re-render } const newState = { ...prev, [step]: isValid }; console.log(`Wizard: Updated stepValidationStates:`, newState); return newState; }); }, []); // Memoized callback specifically for ApiKeyStep to prevent infinite loops const handleApiKeyValidationChange = useCallback((isValid: boolean) => { handleStepValidationChange(0, isValid); }, [handleStepValidationChange]); // Memoize the onDataReady callback to prevent infinite loops const handleCompetitorDataReady = useCallback((dataCollector: (() => any) | undefined) => { console.log('Wizard: onDataReady called with:', dataCollector); console.log('Wizard: dataCollector type:', typeof dataCollector); if (typeof dataCollector === 'function') { setCompetitorDataCollector(dataCollector); } else { console.error('Wizard: dataCollector is not a function:', dataCollector); } }, []); useEffect(() => { console.log('Wizard: Component mounted'); const init = async () => { try { setLoading(true); console.log('Wizard: Starting initialization...'); // Fast local restore: try localStorage active step first (non-authoritative) const cachedActiveStep = localStorage.getItem('onboarding_active_step'); if (cachedActiveStep !== null) { const stepIdx = Math.max(0, Math.min(steps.length - 1, parseInt(cachedActiveStep, 10))); if (!Number.isNaN(stepIdx)) { console.log('Wizard: Provisional activeStep from localStorage:', stepIdx); setActiveStep(stepIdx); } } // Check if we already have init data from App (cached in sessionStorage) const cachedInit = sessionStorage.getItem('onboarding_init'); if (cachedInit) { console.log('Wizard: Using cached init data from batch endpoint'); const data = JSON.parse(cachedInit); // Extract data from batch response const { onboarding, session } = data; // Load step data, especially research data from step 3 and persona data from step 4 if (onboarding.steps && Array.isArray(onboarding.steps)) { // Load research preferences from step 3 const step3Data = onboarding.steps.find((step: any) => step.step_number === 3); if (step3Data && step3Data.data) { console.log('Wizard: Loading research data from step 3:', Object.keys(step3Data.data)); setStepData((prevData: any) => ({ ...prevData, ...step3Data.data })); } // Load persona data from step 4 const step4Data = onboarding.steps.find((step: any) => step.step_number === 4); if (step4Data && step4Data.data) { console.log('Wizard: Loading persona data from step 4:', Object.keys(step4Data.data)); setStepData((prevData: any) => ({ ...prevData, ...step4Data.data })); } } // Set state from cached data - NO API CALLS NEEDED! let computedStep = Math.max(1, Math.min(steps.length, onboarding.current_step)); // If localStorage has a higher step index, prefer it for UX continuity const lsStep = localStorage.getItem('onboarding_active_step'); if (lsStep !== null) { const lsIdx = Math.max(0, Math.min(steps.length - 1, parseInt(lsStep, 10))); if (!Number.isNaN(lsIdx)) { computedStep = Math.max(computedStep, lsIdx + 1); } } setActiveStep(computedStep - 1); setProgressState(onboarding.completion_percentage); // Note: Session managed by Clerk auth, no need to track separately console.log('Wizard: Initialized from cache:', { step: onboarding.current_step, progress: onboarding.completion_percentage, userId: session.session_id, // Clerk user ID from backend hasPersonaData: !!stepData }); setLoading(false); return; // ← Skip redundant API calls! } // Fallback: If no cached data (shouldn't happen), make batch call console.log('Wizard: No cached data, making batch init call'); const response = await apiClient.get('/api/onboarding/init'); const { onboarding, session } = response.data; // Load step data, especially research data from step 3 and persona data from step 4 if (onboarding.steps && Array.isArray(onboarding.steps)) { // Load research preferences from step 3 const step3Data = onboarding.steps.find((step: any) => step.step_number === 3); if (step3Data && step3Data.data) { console.log('Wizard: Loading research data from step 3 API call:', Object.keys(step3Data.data)); setStepData((prevData: any) => ({ ...prevData, ...step3Data.data })); } // Load persona data from step 4 const step4Data = onboarding.steps.find((step: any) => step.step_number === 4); if (step4Data && step4Data.data) { console.log('Wizard: Loading persona data from step 4 API call:', Object.keys(step4Data.data)); setStepData((prevData: any) => ({ ...prevData, ...step4Data.data })); } } // Cache for future use sessionStorage.setItem('onboarding_init', JSON.stringify(response.data)); // Set state from API response let computedStep = Math.max(1, Math.min(steps.length, onboarding.current_step)); const lsStep = localStorage.getItem('onboarding_active_step'); if (lsStep !== null) { const lsIdx = Math.max(0, Math.min(steps.length - 1, parseInt(lsStep, 10))); if (!Number.isNaN(lsIdx)) { computedStep = Math.max(computedStep, lsIdx + 1); } } setActiveStep(computedStep - 1); setProgressState(onboarding.completion_percentage); // Note: Session managed by Clerk auth, no need to track separately console.log('Wizard: Initialized from API:', { step: onboarding.current_step, progress: onboarding.completion_percentage, userId: session.session_id, // Clerk user ID from backend hasPersonaData: !!stepData }); } catch (error) { console.error('Error initializing onboarding:', error); } finally { setLoading(false); } }; init(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Run only once on mount - stepData is used for logging only const handleNext = useCallback(async (rawStepData?: any) => { console.log('Wizard: handleNext called'); console.log('Wizard: Current step:', activeStep); console.log('Wizard: Step data:', stepDataRef.current); console.log('Wizard: competitorDataCollector:', competitorDataCollectorRef.current); console.log('Wizard: competitorDataCollector type:', typeof competitorDataCollectorRef.current); if (rawStepData && typeof rawStepData === 'object') { if (typeof rawStepData.preventDefault === 'function') { rawStepData.preventDefault(); } if (typeof rawStepData.stopPropagation === 'function') { rawStepData.stopPropagation(); } } let currentStepData = rawStepData && typeof rawStepData === 'object' && 'nativeEvent' in rawStepData ? undefined : rawStepData; // Special handling for CompetitorAnalysisStep (step 2) if (activeStep === 2) { console.log('Wizard: Handling CompetitorAnalysisStep data...'); // If we have data from onContinue, use it if (currentStepData) { console.log('Wizard: Using data from CompetitorAnalysisStep onContinue:', currentStepData); } else { // Fallback: try to get data from collector const collector = competitorDataCollectorRef.current; if (collector && typeof collector === 'function') { console.log('Wizard: Collecting data from CompetitorAnalysisStep collector...'); currentStepData = collector(); } else if (collector && typeof collector === 'object') { console.warn('Wizard: competitorDataCollector is an object; using it directly as step data'); currentStepData = collector; } else { console.warn('Wizard: competitorDataCollector not available; using empty data'); // Fallback: create minimal data structure to prevent errors const currentData = stepDataRef.current; currentStepData = { competitors: [], researchSummary: null, sitemapAnalysis: null, userUrl: currentData?.website || '', industryContext: currentData?.industryContext, analysisTimestamp: new Date().toISOString() }; } } } // Merge research data with existing step data for CompetitorAnalysisStep if (activeStep === 2 && currentStepData) { console.log('Wizard: Merging CompetitorAnalysisStep data with existing step data...'); // Merge research data with existing step data const currentData = stepDataRef.current || {}; const researchData = currentStepData || {}; // Ensure we have research data if (researchData.competitors || researchData.researchSummary || researchData.sitemapAnalysis) { currentStepData = { ...currentData, // Preserve existing data (website, etc.) ...researchData, // Add/update research data // Ensure all required research fields are present competitors: researchData.competitors || currentData.competitors, researchSummary: researchData.researchSummary || currentData.researchSummary, sitemapAnalysis: researchData.sitemapAnalysis || currentData.sitemapAnalysis, // Mark this as the research step stepType: 'research', completedAt: new Date().toISOString() }; console.log('Wizard: Merged research data:', currentStepData); } else { console.warn('Wizard: No research data provided, using existing step data'); currentStepData = currentData; } } // Special handling for PersonaStep (step 3) if (activeStep === 3) { console.log('Wizard: Handling PersonaStep data...'); // If we have data from onContinue, use it if (currentStepData && currentStepData.corePersona && currentStepData.qualityMetrics) { console.log('Wizard: Using persona data from PersonaStep onContinue:', currentStepData); // Data is already in currentStepData, no need to modify it } else { // Check if we have valid persona data in stepData const currentData = stepDataRef.current || {}; const hasValidPersonaData = currentData.corePersona && currentData.platformPersonas && Object.keys(currentData.platformPersonas).length > 0 && currentData.qualityMetrics; console.log('Wizard: Persona data validation:', { hasCorePersona: !!currentData.corePersona, hasPlatformPersonas: !!currentData.platformPersonas, platformPersonasCount: currentData.platformPersonas ? Object.keys(currentData.platformPersonas).length : 0, hasQualityMetrics: !!currentData.qualityMetrics, hasValidPersonaData }); if (hasValidPersonaData) { console.log('Wizard: Using existing valid persona data from stepData'); currentStepData = currentData; } else { console.warn('Wizard: No valid persona data available for PersonaStep - cannot complete step'); // Don't try to complete the step if we don't have valid persona data console.log('Wizard: Aborting step completion - missing valid persona data'); setLoading(false); setShowProgressMessage(false); setProgressMessage(''); return; } } } // Store step data in state if (currentStepData) { setStepData(currentStepData); } console.log('Wizard: handleNext called with stepData:', currentStepData); console.log('Wizard: Current activeStep:', activeStep); console.log('Wizard: Steps length:', steps.length); setDirection('right'); const nextStep = activeStep + 1; console.log('Wizard: Next step will be:', nextStep); // Show progress message const newProgress = ((nextStep + 1) / steps.length) * 100; setProgressMessage(`Your data is saved, moving to the next step. Progress is ${Math.round(newProgress)}%`); setShowProgressMessage(true); // Hide message after 3 seconds setTimeout(() => { setShowProgressMessage(false); }, 3000); // Complete the current step (activeStep + 1 because steps are 1-indexed) const currentStepNumber = activeStep + 1; const stepWasCompleted = currentStepData && typeof currentStepData === 'object' && ( currentStepData.website || currentStepData.businessData || currentStepData.competitors || currentStepData.researchSummary || currentStepData.sitemapAnalysis || currentStepData.corePersona || currentStepData.platformPersonas || currentStepData.qualityMetrics ); console.log('Wizard: Step completion check:', { currentStepNumber, hasData: !!currentStepData, dataKeys: currentStepData ? Object.keys(currentStepData) : [], stepWasCompleted, website: !!currentStepData?.website, businessData: !!currentStepData?.businessData, competitors: !!currentStepData?.competitors, researchSummary: !!currentStepData?.researchSummary, sitemapAnalysis: !!currentStepData?.sitemapAnalysis, corePersona: !!currentStepData?.corePersona, platformPersonas: !!currentStepData?.platformPersonas, qualityMetrics: !!currentStepData?.qualityMetrics }); if (!stepWasCompleted) { console.warn('Wizard: No serialized step data supplied; skipping backend completion for step', currentStepNumber); } else { console.log('Wizard: Completing current step:', currentStepNumber, 'with data:', currentStepData); try { await setCurrentStep(currentStepNumber, currentStepData); } catch (error) { console.error('Wizard: Failed to complete step with backend. Aborting progression.', error); setShowProgressMessage(false); setProgressMessage(''); setLoading(false); return; } console.log('Wizard: Checking backend step after completion...'); const stepResponse = await getCurrentStep(); console.log('Wizard: Backend says current step should be:', stepResponse.step); } setActiveStep(nextStep); try { localStorage.setItem('onboarding_active_step', String(nextStep)); } catch (_e) {} console.log('Wizard: Setting activeStep to:', nextStep); // Update progress setProgressState(newProgress); // If this is the final step, call onComplete if (nextStep === steps.length - 1) { console.log('Wizard: This is the final step, calling onComplete'); onComplete?.(); } else { console.log('Wizard: Not the final step, continuing to next step'); } }, [activeStep, onComplete]); const handleBack = useCallback(async () => { setDirection('left'); const prevStep = activeStep - 1; setActiveStep(prevStep); // Do not complete a step when navigating back; just update UI state // Backend step progression should only occur on forward completion with valid data // Update progress const newProgress = ((prevStep + 1) / steps.length) * 100; setProgressState(newProgress); }, [activeStep]); const handleStepClick = (stepIndex: number) => { if (stepIndex <= activeStep) { setDirection(stepIndex > activeStep ? 'right' : 'left'); setActiveStep(stepIndex); // Do not complete a step on arbitrary step navigation; only adjust UI } }; const updateHeaderContent = useCallback((content: StepHeaderContent) => { setStepHeaderContent(prev => { if (prev.title === content.title && prev.description === content.description) { return prev; } return content; }); }, []); const handleComplete = useCallback(async () => { console.log('Wizard: handleComplete called - completing onboarding'); try { // Call onComplete to notify parent component onComplete?.(); } catch (error) { console.error('Error completing onboarding:', error); } }, [onComplete]); // Memoize data objects passed as props to avoid recreating them each render const personaOnboardingData = useMemo(() => ({ websiteAnalysis: stepData?.analysis, competitorResearch: stepData?.competitors, sitemapAnalysis: stepData?.sitemapAnalysis, businessData: stepData?.businessData }), [stepData?.analysis, stepData?.competitors, stepData?.sitemapAnalysis, stepData?.businessData]); const personaStepData = useMemo(() => ({ corePersona: stepData?.corePersona, platformPersonas: stepData?.platformPersonas, qualityMetrics: stepData?.qualityMetrics, selectedPlatforms: stepData?.selectedPlatforms }), [stepData?.corePersona, stepData?.platformPersonas, stepData?.qualityMetrics, stepData?.selectedPlatforms]); const renderStepContent = (step: number) => { const stepComponents = [ , handleStepValidationChange(1, isValid)} />, , handleStepValidationChange(3, isValid)} onboardingData={personaOnboardingData} stepData={personaStepData} />, , ]; return ( {stepComponents[step]} ); }; // Show loading state if loading if (loading) { return ; } return ( {/* Header with Stepper */} setShowHelp(!showHelp)} /> {/* Content */} {renderStepContent(activeStep)} {/* Navigation */} ); }; export default Wizard;