Files
ALwrity/frontend/src/components/OnboardingWizard/Wizard.tsx

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;