627 lines
22 KiB
TypeScript
627 lines
22 KiB
TypeScript
import React, { useEffect, useState, useCallback } from 'react';
|
|
import {
|
|
Box,
|
|
Stepper,
|
|
Step,
|
|
StepLabel,
|
|
Button,
|
|
Typography,
|
|
Paper,
|
|
LinearProgress,
|
|
Fade,
|
|
Slide,
|
|
useTheme,
|
|
useMediaQuery,
|
|
IconButton,
|
|
Tooltip,
|
|
Container
|
|
} from '@mui/material';
|
|
import {
|
|
ArrowBack,
|
|
ArrowForward,
|
|
CheckCircle,
|
|
HelpOutline,
|
|
Close
|
|
} from '@mui/icons-material';
|
|
import UserBadge from '../shared/UserBadge';
|
|
import { startOnboarding, getCurrentStep, setCurrentStep, getProgress } from '../../api/onboarding';
|
|
import { apiClient } from '../../api/client';
|
|
import ApiKeyStep from './ApiKeyStep';
|
|
import WebsiteStep from './WebsiteStep';
|
|
import CompetitorAnalysisStep from './CompetitorAnalysisStep';
|
|
import PersonalizationStep from './PersonalizationStep';
|
|
import IntegrationsStep from './IntegrationsStep';
|
|
import FinalStep from './FinalStep';
|
|
|
|
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<WizardProps> = ({ 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<any>(null);
|
|
const [stepHeaderContent, setStepHeaderContent] = useState<StepHeaderContent>({
|
|
title: steps[0].label,
|
|
description: steps[0].description
|
|
});
|
|
|
|
const theme = useTheme();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
|
|
useEffect(() => {
|
|
console.log('Wizard: Component mounted');
|
|
const init = async () => {
|
|
try {
|
|
setLoading(true);
|
|
console.log('Wizard: Starting initialization...');
|
|
|
|
// 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 { user, onboarding, session } = data;
|
|
|
|
// Set state from cached data - NO API CALLS NEEDED!
|
|
setActiveStep(onboarding.current_step - 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
|
|
});
|
|
|
|
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 { user, onboarding, session } = response.data;
|
|
|
|
// Cache for future use
|
|
sessionStorage.setItem('onboarding_init', JSON.stringify(response.data));
|
|
|
|
// Set state from API response
|
|
setActiveStep(onboarding.current_step - 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
|
|
});
|
|
} catch (error) {
|
|
console.error('Error initializing onboarding:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
init();
|
|
}, []);
|
|
|
|
const handleNext = async (rawStepData?: any) => {
|
|
if (rawStepData && typeof rawStepData === 'object') {
|
|
if (typeof rawStepData.preventDefault === 'function') {
|
|
rawStepData.preventDefault();
|
|
}
|
|
if (typeof rawStepData.stopPropagation === 'function') {
|
|
rawStepData.stopPropagation();
|
|
}
|
|
}
|
|
|
|
const currentStepData = rawStepData && typeof rawStepData === 'object' && 'nativeEvent' in rawStepData
|
|
? undefined
|
|
: rawStepData;
|
|
|
|
// 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);
|
|
|
|
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);
|
|
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');
|
|
}
|
|
};
|
|
|
|
const handleBack = 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);
|
|
};
|
|
|
|
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(content);
|
|
}, []);
|
|
|
|
const handleComplete = async () => {
|
|
console.log('Wizard: handleComplete called - completing onboarding');
|
|
try {
|
|
// Call onComplete to notify parent component
|
|
onComplete?.();
|
|
} catch (error) {
|
|
console.error('Error completing onboarding:', error);
|
|
}
|
|
};
|
|
|
|
const renderStepContent = (step: number) => {
|
|
console.log('Wizard: renderStepContent called with step:', step, 'stepData:', stepData);
|
|
|
|
const stepComponents = [
|
|
<ApiKeyStep key="api-keys" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
|
|
<WebsiteStep key="website" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
|
|
<CompetitorAnalysisStep
|
|
key="research"
|
|
onContinue={handleNext}
|
|
onBack={handleBack}
|
|
userUrl={stepData?.website || ''}
|
|
industryContext={stepData?.industryContext}
|
|
/>,
|
|
<PersonalizationStep key="personalization" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
|
|
<IntegrationsStep key="integrations" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
|
|
<FinalStep key="final" onContinue={handleComplete} updateHeaderContent={updateHeaderContent} />
|
|
];
|
|
|
|
return (
|
|
<Slide direction={direction} in={true} mountOnEnter unmountOnExit>
|
|
<Box sx={{ minHeight: '500px', display: 'flex', flexDirection: 'column' }}>
|
|
{stepComponents[step]}
|
|
</Box>
|
|
</Slide>
|
|
);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
justifyContent="center"
|
|
alignItems="center"
|
|
minHeight="100vh"
|
|
sx={{
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
}}
|
|
>
|
|
<Fade in={true}>
|
|
<Paper
|
|
elevation={24}
|
|
sx={{
|
|
p: 4,
|
|
borderRadius: 3,
|
|
background: 'rgba(255, 255, 255, 0.98)',
|
|
backdropFilter: 'blur(20px)',
|
|
border: '1px solid rgba(255, 255, 255, 0.3)',
|
|
maxWidth: 400,
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<Typography variant="h5" align="center" gutterBottom sx={{ fontWeight: 600 }}>
|
|
Setting up your workspace...
|
|
</Typography>
|
|
<LinearProgress
|
|
sx={{
|
|
mt: 3,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
backgroundColor: 'rgba(0,0,0,0.08)',
|
|
'& .MuiLinearProgress-bar': {
|
|
borderRadius: 4,
|
|
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
|
|
}
|
|
}}
|
|
/>
|
|
</Paper>
|
|
</Fade>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
minHeight: '100vh',
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
p: { xs: 2, md: 4 },
|
|
position: 'relative',
|
|
'&::before': {
|
|
content: '""',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
background: 'radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%)',
|
|
pointerEvents: 'none',
|
|
}
|
|
}}
|
|
>
|
|
<Paper
|
|
elevation={24}
|
|
sx={{
|
|
maxWidth: { xs: '100%', md: '1200px' },
|
|
width: '100%',
|
|
borderRadius: 4,
|
|
overflow: 'hidden',
|
|
background: 'rgba(255, 255, 255, 0.98)',
|
|
backdropFilter: 'blur(20px)',
|
|
border: '1px solid rgba(255, 255, 255, 0.3)',
|
|
position: 'relative',
|
|
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
|
}}
|
|
>
|
|
{/* Header with Stepper */}
|
|
<Box
|
|
sx={{
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
color: 'white',
|
|
p: { xs: 3, md: 4 },
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
'&::before': {
|
|
content: '""',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
background: 'radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%)',
|
|
pointerEvents: 'none',
|
|
}
|
|
}}
|
|
>
|
|
{/* Progress Message */}
|
|
{showProgressMessage && (
|
|
<Fade in={showProgressMessage}>
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
background: 'rgba(16, 185, 129, 0.9)',
|
|
color: 'white',
|
|
p: 2,
|
|
textAlign: 'center',
|
|
zIndex: 10,
|
|
backdropFilter: 'blur(10px)',
|
|
borderBottom: '1px solid rgba(255, 255, 255, 0.2)'
|
|
}}
|
|
>
|
|
<Typography variant="body1" sx={{ fontWeight: 600 }}>
|
|
{progressMessage}
|
|
</Typography>
|
|
</Box>
|
|
</Fade>
|
|
)}
|
|
|
|
{/* Top Row - Title and Actions */}
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3, position: 'relative', zIndex: 1 }}>
|
|
<Box sx={{ flex: 1 }}>
|
|
<UserBadge colorMode="dark" />
|
|
</Box>
|
|
<Box sx={{ flex: 2, textAlign: 'center' }}>
|
|
<Typography variant="h4" sx={{ fontWeight: 700, letterSpacing: '-0.025em' }}>
|
|
{stepHeaderContent.title}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ display: 'flex', gap: 1, flex: 1, justifyContent: 'flex-end' }}>
|
|
<Tooltip title="Get Help" arrow>
|
|
<IconButton
|
|
onClick={() => setShowHelp(!showHelp)}
|
|
sx={{
|
|
color: 'white',
|
|
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
|
backdropFilter: 'blur(10px)',
|
|
'&:hover': {
|
|
bgcolor: 'rgba(255, 255, 255, 0.2)',
|
|
}
|
|
}}
|
|
>
|
|
<HelpOutline />
|
|
</IconButton>
|
|
</Tooltip>
|
|
<Tooltip title="Skip for now" arrow>
|
|
<IconButton
|
|
sx={{
|
|
color: 'white',
|
|
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
|
backdropFilter: 'blur(10px)',
|
|
'&:hover': {
|
|
bgcolor: 'rgba(255, 255, 255, 0.2)',
|
|
}
|
|
}}
|
|
>
|
|
<Close />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Progress Bar */}
|
|
<Box sx={{ mb: 3, position: 'relative', zIndex: 1 }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
|
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 500 }}>
|
|
Setup Progress
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 600 }}>
|
|
{Math.round(progress)}% Complete
|
|
</Typography>
|
|
</Box>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={progress}
|
|
sx={{
|
|
height: 8,
|
|
borderRadius: 4,
|
|
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
'& .MuiLinearProgress-bar': {
|
|
borderRadius: 4,
|
|
background: 'linear-gradient(90deg, #fff 0%, #f8fafc 100%)',
|
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Stepper in Header */}
|
|
<Box sx={{ position: 'relative', zIndex: 1 }}>
|
|
<Stepper
|
|
activeStep={activeStep}
|
|
alternativeLabel={!isMobile}
|
|
sx={{
|
|
'& .MuiStepLabel-root': {
|
|
cursor: 'pointer',
|
|
},
|
|
'& .MuiStepLabel-label': {
|
|
fontSize: '0.875rem',
|
|
fontWeight: 600,
|
|
color: 'white',
|
|
},
|
|
'& .MuiStepLabel-labelContainer': {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
},
|
|
'& .MuiStepLabel-label.Mui-completed': {
|
|
color: 'rgba(255, 255, 255, 0.9)',
|
|
},
|
|
'& .MuiStepLabel-label.Mui-active': {
|
|
color: 'white',
|
|
},
|
|
'& .MuiStepLabel-label.Mui-disabled': {
|
|
color: 'rgba(255, 255, 255, 0.6)',
|
|
},
|
|
}}
|
|
>
|
|
{steps.map((step, index) => (
|
|
<Step key={step.label}>
|
|
<StepLabel
|
|
onClick={() => handleStepClick(index)}
|
|
sx={{
|
|
cursor: index <= activeStep ? 'pointer' : 'default',
|
|
'& .MuiStepLabel-iconContainer': {
|
|
background: index <= activeStep
|
|
? 'rgba(255, 255, 255, 0.2)'
|
|
: 'rgba(255, 255, 255, 0.1)',
|
|
borderRadius: '50%',
|
|
width: 40,
|
|
height: 40,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: index <= activeStep ? 'white' : 'rgba(255, 255, 255, 0.6)',
|
|
fontSize: '1.2rem',
|
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
boxShadow: index <= activeStep
|
|
? '0 4px 12px rgba(255, 255, 255, 0.2)'
|
|
: 'none',
|
|
'&:hover': {
|
|
transform: index <= activeStep ? 'scale(1.05)' : 'none',
|
|
boxShadow: index <= activeStep
|
|
? '0 6px 16px rgba(255, 255, 255, 0.3)'
|
|
: 'none',
|
|
}
|
|
},
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
|
|
<Typography variant="h6" sx={{ mb: 0.5 }}>
|
|
{step.icon}
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'center' }}>
|
|
{step.label}
|
|
</Typography>
|
|
</Box>
|
|
</StepLabel>
|
|
</Step>
|
|
))}
|
|
</Stepper>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Content */}
|
|
<Box sx={{ p: { xs: 2, md: 3 }, pt: 2 }}>
|
|
<Fade in={true} timeout={400}>
|
|
<Box>
|
|
{renderStepContent(activeStep)}
|
|
</Box>
|
|
</Fade>
|
|
</Box>
|
|
|
|
{/* Navigation */}
|
|
<Box
|
|
sx={{
|
|
p: { xs: 2, md: 3 },
|
|
pt: 2,
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
borderTop: '1px solid rgba(0,0,0,0.08)',
|
|
background: 'rgba(0,0,0,0.02)',
|
|
}}
|
|
>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={handleBack}
|
|
disabled={activeStep === 0}
|
|
startIcon={<ArrowBack />}
|
|
sx={{
|
|
borderRadius: 2,
|
|
textTransform: 'none',
|
|
fontWeight: 600,
|
|
borderColor: 'rgba(0,0,0,0.2)',
|
|
color: 'text.primary',
|
|
'&:hover': {
|
|
borderColor: 'rgba(0,0,0,0.4)',
|
|
background: 'rgba(0,0,0,0.04)',
|
|
},
|
|
'&:disabled': {
|
|
borderColor: 'rgba(0,0,0,0.1)',
|
|
color: 'rgba(0,0,0,0.3)',
|
|
}
|
|
}}
|
|
>
|
|
Back
|
|
</Button>
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Typography variant="body2" sx={{ opacity: 0.7, fontWeight: 500 }}>
|
|
Step {activeStep + 1} of {steps.length}
|
|
</Typography>
|
|
{activeStep === steps.length - 1 && (
|
|
<CheckCircle sx={{ color: 'success.main', fontSize: 20 }} />
|
|
)}
|
|
</Box>
|
|
|
|
<Button
|
|
variant="contained"
|
|
onClick={handleNext}
|
|
disabled={activeStep === steps.length - 1}
|
|
endIcon={activeStep === steps.length - 1 ? <CheckCircle /> : <ArrowForward />}
|
|
sx={{
|
|
borderRadius: 2,
|
|
textTransform: 'none',
|
|
fontWeight: 600,
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
|
|
'&:hover': {
|
|
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
|
|
transform: 'translateY(-1px)',
|
|
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
|
|
},
|
|
'&:disabled': {
|
|
background: 'rgba(0,0,0,0.1)',
|
|
color: 'rgba(0,0,0,0.4)',
|
|
boxShadow: 'none',
|
|
transform: 'none',
|
|
}
|
|
}}
|
|
>
|
|
{activeStep === steps.length - 1 ? 'Complete Setup' : 'Continue'}
|
|
</Button>
|
|
</Box>
|
|
</Paper>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default Wizard;
|