ALwrity onboarding final step

This commit is contained in:
ajaysi
2025-10-10 23:19:28 +05:30
parent e3daebec16
commit b1ebe1034e
38 changed files with 4867 additions and 770 deletions

View File

@@ -21,9 +21,10 @@ import ApiKeySidebar from './ApiKeyStep/utils/ApiKeySidebar';
interface ApiKeyStepProps {
onContinue: (stepData?: any) => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
onValidationChange?: (isValid: boolean) => void;
}
const ApiKeyStep: React.FC<ApiKeyStepProps> = ({ onContinue, updateHeaderContent }) => {
const ApiKeyStep: React.FC<ApiKeyStepProps> = ({ onContinue, updateHeaderContent, onValidationChange }) => {
const [focusedProvider, setFocusedProvider] = useState<any>(null);
const {
@@ -63,11 +64,20 @@ const ApiKeyStep: React.FC<ApiKeyStepProps> = ({ onContinue, updateHeaderContent
}
}, [updateHeaderContent, providers, currentProviderIndex]);
// Notify parent of validation changes
useEffect(() => {
console.log('ApiKeyStep: isValid changed to:', isValid);
console.log('ApiKeyStep: onValidationChange exists:', !!onValidationChange);
if (onValidationChange) {
console.log('ApiKeyStep: Calling onValidationChange with:', isValid);
onValidationChange(isValid);
}
}, [isValid, onValidationChange]);
return (
<>
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
<form onSubmit={(e) => { e.preventDefault(); handleContinue(); }}>
{/* Main Content Layout */}
<Grid container spacing={4} sx={{ mb: 4 }}>
{/* Carousel Section */}
@@ -140,43 +150,6 @@ const ApiKeyStep: React.FC<ApiKeyStepProps> = ({ onContinue, updateHeaderContent
)}
</Box>
{/* Continue Button */}
<Box sx={{ mt: 6, display: 'flex', justifyContent: 'center' }}>
<OnboardingButton
variant="primary"
type="submit"
loading={loading}
disabled={!isValid || loading}
size="large"
sx={{
px: 6,
py: 2.5,
fontSize: '1.1rem',
fontWeight: 700,
borderRadius: 4,
background: isValid
? 'linear-gradient(135deg, #10B981 0%, #059669 100%)'
: 'linear-gradient(135deg, #94A3B8 0%, #64748B 100%)',
boxShadow: isValid
? '0 12px 32px rgba(16, 185, 129, 0.3), 0 6px 12px rgba(16, 185, 129, 0.2)'
: '0 8px 16px rgba(148, 163, 184, 0.2)',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
transform: isValid ? 'translateY(-3px) scale(1.02)' : 'none',
boxShadow: isValid
? '0 16px 40px rgba(16, 185, 129, 0.4), 0 8px 16px rgba(16, 185, 129, 0.3)'
: '0 8px 16px rgba(148, 163, 184, 0.2)',
},
'&:disabled': {
'&:hover': {
transform: 'none',
}
}
}}
>
{isValid ? 'Continue' : 'Complete All Required API Keys'}
</OnboardingButton>
</Box>
{/* Security Notice */}
<Box sx={{
@@ -201,7 +174,6 @@ const ApiKeyStep: React.FC<ApiKeyStepProps> = ({ onContinue, updateHeaderContent
Your API keys are encrypted and stored securely on your device
</Typography>
</Box>
</form>
</Container>
</Fade>

View File

@@ -103,6 +103,19 @@ export const useApiKeyStep = (onContinue: (stepData?: any) => void) => {
// Store CopilotKit key in localStorage for frontend use
localStorage.setItem('copilotkit_api_key', copilotkitKey.trim());
console.log('ApiKeyStep: CopilotKit key saved to localStorage for frontend CopilotKit provider');
// Also save to frontend .env file (for development)
try {
await apiClient.post('/api/frontend-env/update', {
key: 'REACT_APP_COPILOTKIT_API_KEY',
value: copilotkitKey.trim(),
description: 'CopilotKit API key for AI assistant functionality'
});
console.log('ApiKeyStep: CopilotKit key saved to frontend .env file');
} catch (envError) {
console.warn('ApiKeyStep: Failed to save CopilotKit key to frontend .env file:', envError);
// Don't fail the entire process if .env update fails
}
}
try {
@@ -215,7 +228,49 @@ export const useApiKeyStep = (onContinue: (stepData?: any) => void) => {
];
// All three keys are required
const isValid = geminiKey.trim() && exaKey.trim() && copilotkitKey.trim();
const isValid = !!(geminiKey.trim() && exaKey.trim() && copilotkitKey.trim());
// Debug logging
useEffect(() => {
console.log('useApiKeyStep: Validation check:', {
gemini: geminiKey.trim(),
exa: exaKey.trim(),
copilotkit: copilotkitKey.trim(),
isValid
});
}, [geminiKey, exaKey, copilotkitKey, isValid]);
// When keys change and all are valid, auto-save them
useEffect(() => {
if (isValid && (geminiKey || exaKey || copilotkitKey)) {
console.log('useApiKeyStep: All keys valid, auto-saving...');
// Save keys immediately when all are provided
const saveKeys = async () => {
try {
const promises = [];
if (geminiKey.trim()) {
promises.push(saveApiKey('gemini', geminiKey.trim()));
}
if (exaKey.trim()) {
promises.push(saveApiKey('exa', exaKey.trim()));
}
if (copilotkitKey.trim()) {
promises.push(saveApiKey('copilotkit', copilotkitKey.trim()));
// Store CopilotKit key in localStorage for frontend use
localStorage.setItem('copilotkit_api_key', copilotkitKey.trim());
}
await Promise.all(promises);
console.log('useApiKeyStep: All API keys auto-saved successfully (backend handles .env files)');
} catch (error) {
console.error('useApiKeyStep: Auto-save failed:', error);
}
};
saveKeys();
}
}, [geminiKey, exaKey, copilotkitKey, isValid]);
// Auto-scroll to next provider when current one is valid
useEffect(() => {

View File

@@ -1,660 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
Typography,
Alert,
Paper,
Container,
Fade,
Zoom,
Grid,
Chip,
List,
ListItem,
ListItemIcon,
ListItemText,
Accordion,
AccordionSummary,
AccordionDetails,
Card,
CardContent,
IconButton,
Tooltip,
CircularProgress
} from '@mui/material';
import {
CheckCircle,
Rocket,
Star,
TrendingUp,
Security,
ExpandMore,
Visibility,
VisibilityOff,
Lock,
LockOpen,
Settings,
Web,
Psychology,
Business,
ContentCopy
} from '@mui/icons-material';
import OnboardingButton from './common/OnboardingButton';
import { getApiKeys, completeOnboarding, getOnboardingSummary, getWebsiteAnalysisData, getResearchPreferencesData, setCurrentStep } from '../../api/onboarding';
interface FinalStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
interface OnboardingData {
apiKeys: Record<string, string>;
websiteUrl?: string;
researchPreferences?: any;
personalizationSettings?: any;
integrations?: any;
styleAnalysis?: any;
}
interface Capability {
id: string;
title: string;
description: string;
icon: React.ReactElement;
unlocked: boolean;
required?: string[];
}
const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }) => {
const [loading, setLoading] = useState(false);
const [dataLoading, setDataLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [onboardingData, setOnboardingData] = useState<OnboardingData>({
apiKeys: {}
});
const [showApiKeys, setShowApiKeys] = useState(false);
const [expandedSection, setExpandedSection] = useState<string | null>('summary');
useEffect(() => {
updateHeaderContent({
title: 'Review & Launch Alwrity 🚀',
description: 'Review your configuration and confirm all settings before launching your AI-powered content creation workspace.'
});
loadOnboardingData();
}, [updateHeaderContent]);
const loadOnboardingData = async () => {
setDataLoading(true);
try {
// Load comprehensive onboarding summary
const summary = await getOnboardingSummary();
// Load individual data sources for detailed information
const websiteAnalysis = await getWebsiteAnalysisData();
const researchPreferences = await getResearchPreferencesData();
setOnboardingData({
apiKeys: summary.api_keys || {},
websiteUrl: websiteAnalysis?.website_url || summary.website_url,
researchPreferences: researchPreferences || summary.research_preferences,
personalizationSettings: summary.personalization_settings,
integrations: summary.integrations || {},
styleAnalysis: websiteAnalysis?.style_analysis || summary.style_analysis
});
} catch (error) {
console.error('Error loading onboarding data:', error);
// Fallback to just API keys if other endpoints fail
try {
const apiKeys = await getApiKeys();
setOnboardingData({
apiKeys,
websiteUrl: undefined,
researchPreferences: undefined,
personalizationSettings: undefined,
integrations: undefined,
styleAnalysis: undefined
});
} catch (fallbackError) {
console.error('Error loading API keys as fallback:', fallbackError);
}
} finally {
setDataLoading(false);
}
};
const handleLaunch = async () => {
setLoading(true);
setError(null);
try {
console.log('FinalStep: Starting onboarding completion...');
// First, complete step 6 (Final Step) to mark it as completed
console.log('FinalStep: Completing step 6...');
await setCurrentStep(6);
console.log('FinalStep: Step 6 completed successfully');
// Then complete the entire onboarding process
console.log('FinalStep: Completing onboarding...');
await completeOnboarding();
console.log('FinalStep: Onboarding completed successfully');
// Navigate directly to dashboard without calling onContinue
// This bypasses the wizard flow and goes straight to the dashboard
console.log('FinalStep: Navigating to dashboard...');
window.location.href = '/dashboard';
} catch (e: any) {
console.error('FinalStep: Error completing onboarding:', e);
// Provide more specific error messages
let errorMessage = 'Failed to complete onboarding. Please try again.';
if (e.response?.data?.detail) {
errorMessage = e.response.data.detail;
} else if (e.message) {
errorMessage = e.message;
}
setError(errorMessage);
}
setLoading(false);
};
const capabilities: Capability[] = [
{
id: 'ai-content',
title: 'AI Content Generation',
description: 'Generate high-quality, personalized content using advanced AI models',
icon: <ContentCopy />,
unlocked: Object.keys(onboardingData.apiKeys).length > 0,
required: ['API Keys']
},
{
id: 'style-analysis',
title: 'Style Analysis',
description: 'Analyze and match your brand\'s writing style and tone',
icon: <Psychology />,
unlocked: !!onboardingData.websiteUrl,
required: ['Website URL']
},
{
id: 'research-tools',
title: 'AI Research Tools',
description: 'Automated research and fact-checking capabilities',
icon: <TrendingUp />,
unlocked: !!onboardingData.researchPreferences,
required: ['Research Configuration']
},
{
id: 'personalization',
title: 'Content Personalization',
description: 'Tailored content based on your brand voice and preferences',
icon: <Settings />,
unlocked: !!onboardingData.personalizationSettings,
required: ['Personalization Settings']
},
{
id: 'integrations',
title: 'Third-party Integrations',
description: 'Connect with external tools and platforms',
icon: <Business />,
unlocked: !!onboardingData.integrations,
required: ['Integration Setup']
}
];
const getConfiguredProviders = () => {
return Object.keys(onboardingData.apiKeys).map(provider => ({
name: provider.charAt(0).toUpperCase() + provider.slice(1),
configured: true
}));
};
const getMissingRequirements = () => {
const missing = [];
if (Object.keys(onboardingData.apiKeys).length === 0) {
missing.push('At least one AI provider API key');
}
if (!onboardingData.websiteUrl) {
missing.push('Website URL for style analysis');
}
return missing;
};
const unlockedCapabilities = capabilities.filter(cap => cap.unlocked);
const missingRequirements = getMissingRequirements();
return (
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
{/* Loading State */}
{dataLoading && (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 8 }}>
<Box sx={{ textAlign: 'center' }}>
<CircularProgress size={60} sx={{ mb: 2 }} />
<Typography variant="h6" sx={{ mb: 1 }}>
Loading your configuration...
</Typography>
<Typography variant="body2" color="text.secondary">
Retrieving your onboarding data and settings
</Typography>
</Box>
</Box>
)}
{/* Content - Only show when data is loaded */}
{!dataLoading && (
<React.Fragment>
{/* Summary Section */}
<Zoom in={true} timeout={800}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid rgba(16, 185, 129, 0.2)',
borderRadius: 3
}}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 32 }} />
<Typography variant="h4" color="success.main" sx={{ fontWeight: 600 }}>
Setup Summary
</Typography>
</Box>
<Chip
label={`${unlockedCapabilities.length}/${capabilities.length} Capabilities Unlocked`}
color="success"
variant="filled"
icon={<LockOpen />}
/>
</Box>
<Grid container spacing={3}>
{/* Configured Providers */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ background: 'rgba(255, 255, 255, 0.7)', borderRadius: 2 }}>
<CardContent>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<Security sx={{ color: 'primary.main' }} />
AI Providers
</Typography>
<List dense>
{getConfiguredProviders().map((provider, index) => (
<ListItem key={index} sx={{ px: 0 }}>
<ListItemIcon sx={{ minWidth: 36 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 20 }} />
</ListItemIcon>
<ListItemText
primary={provider.name}
secondary="API key configured"
/>
</ListItem>
))}
</List>
</CardContent>
</Card>
</Grid>
{/* Quick Stats */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ background: 'rgba(255, 255, 255, 0.7)', borderRadius: 2 }}>
<CardContent>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<TrendingUp sx={{ color: 'primary.main' }} />
Quick Stats
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2">AI Providers:</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{Object.keys(onboardingData.apiKeys).length} configured
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2">Capabilities:</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{unlockedCapabilities.length} unlocked
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2">Missing:</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: missingRequirements.length > 0 ? 'warning.main' : 'success.main' }}>
{missingRequirements.length} requirements
</Typography>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Paper>
</Zoom>
{/* Detailed Configuration Review */}
<Zoom in={true} timeout={1000}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid rgba(59, 130, 246, 0.2)',
borderRadius: 3
}}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<Settings sx={{ color: 'primary.main' }} />
Configuration Details
</Typography>
<Grid container spacing={3}>
{/* API Keys Section */}
<Grid item xs={12} md={6}>
<Accordion
expanded={expandedSection === 'api-keys'}
onChange={() => setExpandedSection(expandedSection === 'api-keys' ? null : 'api-keys')}
sx={{ background: 'rgba(255, 255, 255, 0.8)', borderRadius: 2 }}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Security sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
API Keys ({Object.keys(onboardingData.apiKeys).length} configured)
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{Object.entries(onboardingData.apiKeys).map(([provider, key]) => (
<Box key={provider} sx={{
p: 2,
border: '1px solid rgba(0,0,0,0.1)',
borderRadius: 1,
background: 'rgba(255,255,255,0.5)'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, textTransform: 'capitalize' }}>
{provider}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Tooltip title={showApiKeys ? 'Hide key' : 'Show key'}>
<IconButton
size="small"
onClick={() => setShowApiKeys(!showApiKeys)}
>
{showApiKeys ? <VisibilityOff /> : <Visibility />}
</IconButton>
</Tooltip>
</Box>
</Box>
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
{showApiKeys ? key : '••••••••••••••••••••••••••••••••'}
</Typography>
</Box>
))}
</Box>
</AccordionDetails>
</Accordion>
</Grid>
{/* Website Configuration */}
<Grid item xs={12} md={6}>
<Accordion
expanded={expandedSection === 'website'}
onChange={() => setExpandedSection(expandedSection === 'website' ? null : 'website')}
sx={{ background: 'rgba(255, 255, 255, 0.8)', borderRadius: 2 }}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Web sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Website Analysis
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
{onboardingData.websiteUrl ? (
<Box>
<Typography variant="body2" sx={{ mb: 2 }}>
<strong>URL:</strong> {onboardingData.websiteUrl}
</Typography>
{onboardingData.styleAnalysis && (
<Typography variant="body2" color="success.main">
Style analysis completed
</Typography>
)}
</Box>
) : (
<Typography variant="body2" color="warning.main">
No website URL configured
</Typography>
)}
</AccordionDetails>
</Accordion>
</Grid>
{/* Research Preferences */}
<Grid item xs={12} md={6}>
<Accordion
expanded={expandedSection === 'research'}
onChange={() => setExpandedSection(expandedSection === 'research' ? null : 'research')}
sx={{ background: 'rgba(255, 255, 255, 0.8)', borderRadius: 2 }}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<TrendingUp sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Research Configuration
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
{onboardingData.researchPreferences ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2">
<strong>Depth:</strong> {onboardingData.researchPreferences.research_depth}
</Typography>
<Typography variant="body2">
<strong>Content Types:</strong> {onboardingData.researchPreferences.content_types?.join(', ')}
</Typography>
<Typography variant="body2">
<strong>Auto Research:</strong> {onboardingData.researchPreferences.auto_research ? 'Enabled' : 'Disabled'}
</Typography>
</Box>
) : (
<Typography variant="body2" color="warning.main">
Research preferences not configured
</Typography>
)}
</AccordionDetails>
</Accordion>
</Grid>
{/* Personalization Settings */}
<Grid item xs={12} md={6}>
<Accordion
expanded={expandedSection === 'personalization'}
onChange={() => setExpandedSection(expandedSection === 'personalization' ? null : 'personalization')}
sx={{ background: 'rgba(255, 255, 255, 0.8)', borderRadius: 2 }}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Psychology sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Personalization
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
{onboardingData.personalizationSettings ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2">
<strong>Style:</strong> {onboardingData.personalizationSettings.writing_style}
</Typography>
<Typography variant="body2">
<strong>Tone:</strong> {onboardingData.personalizationSettings.tone}
</Typography>
<Typography variant="body2">
<strong>Brand Voice:</strong> {onboardingData.personalizationSettings.brand_voice}
</Typography>
</Box>
) : (
<Typography variant="body2" color="warning.main">
Personalization not configured
</Typography>
)}
</AccordionDetails>
</Accordion>
</Grid>
</Grid>
</Paper>
</Zoom>
{/* Capabilities Overview */}
<Zoom in={true} timeout={1200}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid rgba(245, 158, 11, 0.2)',
borderRadius: 3
}}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<Star sx={{ color: 'warning.main' }} />
Capabilities Overview
</Typography>
<Grid container spacing={2}>
{capabilities.map((capability) => (
<Grid item xs={12} sm={6} md={4} key={capability.id}>
<Card elevation={0} sx={{
background: capability.unlocked ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.05)',
border: `1px solid ${capability.unlocked ? 'rgba(16, 185, 129, 0.3)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: 2,
opacity: capability.unlocked ? 1 : 0.6
}}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: '50%',
background: capability.unlocked
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #6b7280 0%, #4b5563 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
{React.cloneElement(capability.icon, {
sx: { color: 'white', fontSize: 20 }
})}
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
{capability.title}
{capability.unlocked ? (
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
) : (
<Lock sx={{ color: 'text.secondary', fontSize: 16 }} />
)}
</Typography>
</Box>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{capability.description}
</Typography>
{!capability.unlocked && capability.required && (
<Box>
<Typography variant="caption" color="text.secondary">
Requires: {capability.required.join(', ')}
</Typography>
</Box>
)}
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Paper>
</Zoom>
{/* Missing Requirements Warning */}
{missingRequirements.length > 0 && (
<Zoom in={true} timeout={1400}>
<Alert
severity="warning"
sx={{ mb: 4, borderRadius: 2 }}
action={
<Button color="inherit" size="small">
Configure Now
</Button>
}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Missing Requirements
</Typography>
<Typography variant="body2">
The following items are recommended for optimal experience: {missingRequirements.join(', ')}
</Typography>
</Alert>
</Zoom>
)}
{/* Alerts */}
<Box sx={{ mt: 3 }}>
{error && (
<Fade in={true}>
<Alert
severity="error"
sx={{ mb: 2, borderRadius: 2 }}
action={
<Button
color="inherit"
size="small"
onClick={() => setError(null)}
>
Dismiss
</Button>
}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Setup Incomplete
</Typography>
<Typography variant="body2">
{error}
</Typography>
</Alert>
</Fade>
)}
</Box>
{/* Action Button */}
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
<OnboardingButton
variant="primary"
onClick={handleLaunch}
loading={loading}
size="large"
icon={<Rocket />}
disabled={Object.keys(onboardingData.apiKeys).length === 0}
>
Launch Alwrity & Complete Setup
</OnboardingButton>
</Box>
{/* Help Text */}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
This will complete your onboarding and launch Alwrity with your configured settings.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
<Star sx={{ fontSize: 16 }} />
Ready to create amazing content with AI-powered assistance
</Typography>
</Box>
</React.Fragment>
)}
</Container>
</Fade>
);
};
export default FinalStep;

View File

@@ -0,0 +1,284 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
Typography,
Alert,
Container,
Fade,
Zoom,
CircularProgress
} from '@mui/material';
import {
Rocket,
Star,
CheckCircle
} from '@mui/icons-material';
import OnboardingButton from '../common/OnboardingButton';
import { getApiKeys, completeOnboarding, getOnboardingSummary, getWebsiteAnalysisData, getResearchPreferencesData, setCurrentStep } from '../../../api/onboarding';
import { SetupSummary, CapabilitiesOverview } from './components';
import { FinalStepProps, OnboardingData, Capability } from './types';
const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }) => {
const [loading, setLoading] = useState(false);
const [dataLoading, setDataLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [onboardingData, setOnboardingData] = useState<OnboardingData>({
apiKeys: {}
});
const [expandedSection, setExpandedSection] = useState<string | null>('summary');
useEffect(() => {
updateHeaderContent({
title: 'Review & Launch Alwrity 🚀',
description: 'Review your configuration and confirm all settings before launching your AI-powered content creation workspace.'
});
loadOnboardingData();
}, [updateHeaderContent]);
const loadOnboardingData = async () => {
setDataLoading(true);
try {
// Load comprehensive onboarding summary
const summary = await getOnboardingSummary();
// Load individual data sources for detailed information
const websiteAnalysis = await getWebsiteAnalysisData();
const researchPreferences = await getResearchPreferencesData();
setOnboardingData({
apiKeys: summary.api_keys || {},
websiteUrl: websiteAnalysis?.website_url || summary.website_url,
researchPreferences: researchPreferences || summary.research_preferences,
personalizationSettings: summary.personalization_settings,
integrations: summary.integrations || {},
styleAnalysis: websiteAnalysis?.style_analysis || summary.style_analysis
});
} catch (error) {
console.error('Error loading onboarding data:', error);
// Fallback to just API keys if other endpoints fail
try {
const apiKeys = await getApiKeys();
setOnboardingData({
apiKeys,
websiteUrl: undefined,
researchPreferences: undefined,
personalizationSettings: undefined,
integrations: undefined,
styleAnalysis: undefined
});
} catch (fallbackError) {
console.error('Error loading API keys as fallback:', fallbackError);
}
} finally {
setDataLoading(false);
}
};
const handleLaunch = async () => {
setLoading(true);
setError(null);
try {
console.log('FinalStep: Starting onboarding completion...');
// First, complete step 6 (Final Step) to mark it as completed
console.log('FinalStep: Completing step 6...');
await setCurrentStep(6);
console.log('FinalStep: Step 6 completed successfully');
// Then complete the entire onboarding process
console.log('FinalStep: Completing onboarding...');
await completeOnboarding();
console.log('FinalStep: Onboarding completed successfully');
// Navigate directly to dashboard without calling onContinue
// This bypasses the wizard flow and goes straight to the dashboard
console.log('FinalStep: Navigating to dashboard...');
window.location.href = '/dashboard';
} catch (e: any) {
console.error('FinalStep: Error completing onboarding:', e);
// Provide more specific error messages
let errorMessage = 'Failed to complete onboarding. Please try again.';
if (e.response?.data?.detail) {
errorMessage = e.response.data.detail;
} else if (e.message) {
errorMessage = e.message;
}
setError(errorMessage);
}
setLoading(false);
};
const capabilities: Capability[] = [
{
id: 'ai-content',
title: 'AI Content Generation',
description: 'Generate high-quality, personalized content using advanced AI models',
icon: <CheckCircle />,
unlocked: Object.keys(onboardingData.apiKeys).length > 0,
required: ['API Keys']
},
{
id: 'style-analysis',
title: 'Style Analysis',
description: 'Analyze and match your brand\'s writing style and tone',
icon: <CheckCircle />,
unlocked: !!onboardingData.websiteUrl,
required: ['Website URL']
},
{
id: 'research-tools',
title: 'AI Research Tools',
description: 'Automated research and fact-checking capabilities',
icon: <CheckCircle />,
unlocked: !!onboardingData.researchPreferences,
required: ['Research Configuration']
},
{
id: 'personalization',
title: 'Content Personalization',
description: 'Tailored content based on your brand voice and preferences',
icon: <CheckCircle />,
unlocked: !!onboardingData.personalizationSettings,
required: ['Personalization Settings']
},
{
id: 'integrations',
title: 'Third-party Integrations',
description: 'Connect with external tools and platforms',
icon: <CheckCircle />,
unlocked: !!onboardingData.integrations,
required: ['Integration Setup']
}
];
const getMissingRequirements = () => {
const missing = [];
if (Object.keys(onboardingData.apiKeys).length === 0) {
missing.push('At least one AI provider API key');
}
if (!onboardingData.websiteUrl) {
missing.push('Website URL for style analysis');
}
return missing;
};
const missingRequirements = getMissingRequirements();
return (
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
{/* Loading State */}
{dataLoading && (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 8 }}>
<Box sx={{ textAlign: 'center' }}>
<CircularProgress size={60} sx={{ mb: 2 }} />
<Typography variant="h6" sx={{ mb: 1 }}>
Loading your configuration...
</Typography>
<Typography variant="body2" color="text.secondary">
Retrieving your onboarding data and settings
</Typography>
</Box>
</Box>
)}
{/* Content - Only show when data is loaded */}
{!dataLoading && (
<React.Fragment>
{/* Setup Summary */}
<SetupSummary
onboardingData={onboardingData}
capabilities={capabilities}
expandedSection={expandedSection}
setExpandedSection={setExpandedSection}
/>
{/* Capabilities Overview */}
<CapabilitiesOverview capabilities={capabilities} />
{/* Missing Requirements Warning */}
{missingRequirements.length > 0 && (
<Zoom in={true} timeout={1400}>
<Alert
severity="warning"
sx={{ mb: 4, borderRadius: 2 }}
action={
<Button color="inherit" size="small">
Configure Now
</Button>
}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Missing Requirements
</Typography>
<Typography variant="body2">
The following items are recommended for optimal experience: {missingRequirements.join(', ')}
</Typography>
</Alert>
</Zoom>
)}
{/* Alerts */}
<Box sx={{ mt: 3 }}>
{error && (
<Fade in={true}>
<Alert
severity="error"
sx={{ mb: 2, borderRadius: 2 }}
action={
<Button
color="inherit"
size="small"
onClick={() => setError(null)}
>
Dismiss
</Button>
}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Setup Incomplete
</Typography>
<Typography variant="body2">
{error}
</Typography>
</Alert>
</Fade>
)}
</Box>
{/* Action Button */}
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
<OnboardingButton
variant="primary"
onClick={handleLaunch}
loading={loading}
size="large"
icon={<Rocket />}
disabled={Object.keys(onboardingData.apiKeys).length === 0}
>
Launch Alwrity & Complete Setup
</OnboardingButton>
</Box>
{/* Help Text */}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
This will complete your onboarding and launch Alwrity with your configured settings.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
<Star sx={{ fontSize: 16 }} />
Ready to create amazing content with AI-powered assistance
</Typography>
</Box>
</React.Fragment>
)}
</Container>
</Fade>
);
};
export default FinalStep;

View File

@@ -0,0 +1,96 @@
# FinalStep Component Structure
This folder contains the refactored FinalStep component for the Onboarding Wizard, organized into smaller, reusable components.
## File Structure
```
FinalStep/
├── FinalStep.tsx # Main component container
├── components/
│ ├── SetupSummary.tsx # Combined setup summary and configuration details
│ └── CapabilitiesOverview.tsx # Capabilities overview section
├── types.ts # Shared TypeScript interfaces
├── index.ts # Export barrel file
└── README.md # This documentation
```
## Components
### FinalStep.tsx
- **Purpose**: Main container component for the final onboarding step
- **Responsibilities**:
- Data loading and state management
- Launch button handling
- Error handling
- Orchestrating child components
### SetupSummary.tsx
- **Purpose**: Combined setup summary and configuration details
- **Features**:
- AI Providers list with checkmarks
- Quick Stats overview
- Compact configuration cards (API Keys, Website Analysis, Research Config, Personalization)
- Expandable details for each configuration section
- Clickable cards with hover effects
### CapabilitiesOverview.tsx
- **Purpose**: Display unlocked capabilities and requirements
- **Features**:
- Visual capability cards with icons
- Locked/unlocked states
- Requirement information for locked capabilities
## Types
### OnboardingData
```typescript
interface OnboardingData {
apiKeys: Record<string, string>;
websiteUrl?: string;
researchPreferences?: any;
personalizationSettings?: any;
integrations?: any;
styleAnalysis?: any;
}
```
### Capability
```typescript
interface Capability {
id: string;
title: string;
description: string;
icon: React.ReactElement;
unlocked: boolean;
required?: string[];
}
```
## Usage
```tsx
import FinalStep from './FinalStep';
<FinalStep
onContinue={handleContinue}
updateHeaderContent={updateHeaderContent}
/>
```
## Design Features
1. **Combined Layout**: Setup Summary and Configuration Details are now in one cohesive section
2. **Compact Cards**: 4-column grid for configuration items (matches design requirements)
3. **Interactive Elements**: Clickable cards with expandable details
4. **Responsive Design**: Works on mobile and desktop
5. **Consistent Styling**: Maintains the green gradient theme with proper spacing
## Recent Changes
- ✅ Combined Configuration Details into Setup Summary
- ✅ Created compact, clickable configuration cards
- ✅ Maintained all existing functionality
- ✅ Improved readability and space efficiency
- ✅ Organized code into smaller, reusable components
- ✅ Added proper TypeScript interfaces

View File

@@ -0,0 +1,94 @@
import React from 'react';
import {
Box,
Paper,
Zoom,
Grid,
Typography,
Card,
CardContent
} from '@mui/material';
import {
Star,
CheckCircle,
Lock
} from '@mui/icons-material';
import { Capability } from '../types';
interface CapabilitiesOverviewProps {
capabilities: Capability[];
}
export const CapabilitiesOverview: React.FC<CapabilitiesOverviewProps> = ({ capabilities }) => {
return (
<Zoom in={true} timeout={1200}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid rgba(245, 158, 11, 0.2)',
borderRadius: 3
}}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1, color: '#000000 !important' }}>
<Star sx={{ color: 'warning.main' }} />
Capabilities Overview
</Typography>
<Grid container spacing={2}>
{capabilities.map((capability) => (
<Grid item xs={12} sm={6} md={4} key={capability.id}>
<Card elevation={0} sx={{
background: capability.unlocked ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.05)',
border: `1px solid ${capability.unlocked ? 'rgba(16, 185, 129, 0.3)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: 2,
opacity: capability.unlocked ? 1 : 0.6
}}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: '50%',
background: capability.unlocked
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #6b7280 0%, #4b5563 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
{React.cloneElement(capability.icon, {
sx: { color: 'white', fontSize: 20 }
})}
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1, color: '#000000 !important' }}>
{capability.title}
{capability.unlocked ? (
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
) : (
<Lock sx={{ color: '#666666 !important', fontSize: 16 }} />
)}
</Typography>
</Box>
</Box>
<Typography variant="body2" sx={{ mb: 2, color: '#000000 !important' }}>
{capability.description}
</Typography>
{!capability.unlocked && capability.required && (
<Box>
<Typography variant="caption" sx={{ color: '#000000 !important' }}>
Requires: {capability.required.join(', ')}
</Typography>
</Box>
)}
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Paper>
</Zoom>
);
};
export default CapabilitiesOverview;

View File

@@ -0,0 +1,343 @@
import React, { useState } from 'react';
import {
Box,
Paper,
Zoom,
Grid,
Typography,
Card,
CardContent,
Chip,
Tooltip,
IconButton
} from '@mui/material';
import {
CheckCircle,
Security,
TrendingUp,
Settings,
Web,
Psychology,
LockOpen,
Visibility,
VisibilityOff
} from '@mui/icons-material';
import { OnboardingData, Capability } from '../types';
interface SetupSummaryProps {
onboardingData: OnboardingData;
capabilities: Capability[];
expandedSection: string | null;
setExpandedSection: (section: string | null) => void;
}
export const SetupSummary: React.FC<SetupSummaryProps> = ({
onboardingData,
capabilities,
expandedSection,
setExpandedSection
}) => {
const [showApiKeys, setShowApiKeys] = useState(false);
const unlockedCapabilities = capabilities.filter(cap => cap.unlocked);
return (
<Zoom in={true} timeout={800}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid rgba(16, 185, 129, 0.2)',
borderRadius: 3
}}>
{/* Header with Stats Chips */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 4, flexWrap: 'wrap', gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 32 }} />
<Typography variant="h4" color="success.main" sx={{ fontWeight: 600 }}>
Setup Summary
</Typography>
</Box>
{/* Stats Chips */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
<Chip
label={`${Object.keys(onboardingData.apiKeys).length} AI Providers`}
color="primary"
variant="filled"
size="small"
icon={<Security />}
/>
<Chip
label={`${unlockedCapabilities.length}/${capabilities.length} Capabilities`}
color="success"
variant="filled"
size="small"
icon={<LockOpen />}
/>
<Chip
label="1 Missing"
color="warning"
variant="filled"
size="small"
/>
</Box>
</Box>
{/* Main Content Grid - Compact Single Card */}
<Grid container spacing={3}>
{/* Configuration Details Card */}
<Grid item xs={12}>
<Card elevation={0} sx={{ background: 'rgba(255, 255, 255, 0.9)', borderRadius: 2 }}>
<CardContent sx={{ p: 3 }}>
{/* Configuration Details Header - Updated for readability */}
<Typography variant="h6" sx={{
fontWeight: 600,
mb: 3,
display: 'flex',
alignItems: 'center',
gap: 1,
color: '#000000 !important'
}}>
<Settings sx={{ color: 'primary.main' }} />
Configuration Details
</Typography>
<Grid container spacing={2}>
{/* API Keys */}
<Grid item xs={6} sm={3}>
<Box
sx={{
p: 2,
border: '1px solid rgba(0,0,0,0.1)',
borderRadius: 1,
background: 'rgba(255,255,255,0.5)',
cursor: 'pointer',
'&:hover': { background: 'rgba(255,255,255,0.7)' }
}}
onClick={() => setExpandedSection(expandedSection === 'api-keys' ? null : 'api-keys')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Security sx={{ color: 'primary.main', fontSize: 18 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#000000' }}>
API Keys
</Typography>
</Box>
<Typography variant="body2" sx={{ color: '#000000' }}>
{Object.keys(onboardingData.apiKeys).length} configured
</Typography>
</Box>
</Grid>
{/* Website Analysis */}
<Grid item xs={6} sm={3}>
<Box
sx={{
p: 2,
border: '1px solid rgba(0,0,0,0.1)',
borderRadius: 1,
background: 'rgba(255,255,255,0.5)',
cursor: 'pointer',
'&:hover': { background: 'rgba(255,255,255,0.7)' }
}}
onClick={() => setExpandedSection(expandedSection === 'website' ? null : 'website')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Web sx={{ color: 'primary.main', fontSize: 18 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#000000' }}>
Website Analysis
</Typography>
</Box>
<Typography variant="body2" sx={{ color: '#000000' }}>
{onboardingData.websiteUrl ? 'Configured' : 'Not set'}
</Typography>
</Box>
</Grid>
{/* Research Configuration */}
<Grid item xs={6} sm={3}>
<Box
sx={{
p: 2,
border: '1px solid rgba(0,0,0,0.1)',
borderRadius: 1,
background: 'rgba(255,255,255,0.5)',
cursor: 'pointer',
'&:hover': { background: 'rgba(255,255,255,0.7)' }
}}
onClick={() => setExpandedSection(expandedSection === 'research' ? null : 'research')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<TrendingUp sx={{ color: 'primary.main', fontSize: 18 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#000000' }}>
Research Config
</Typography>
</Box>
<Typography variant="body2" sx={{ color: '#000000' }}>
{onboardingData.researchPreferences ? 'Configured' : 'Not set'}
</Typography>
</Box>
</Grid>
{/* Personalization */}
<Grid item xs={6} sm={3}>
<Box
sx={{
p: 2,
border: '1px solid rgba(0,0,0,0.1)',
borderRadius: 1,
background: 'rgba(255,255,255,0.5)',
cursor: 'pointer',
'&:hover': { background: 'rgba(255,255,255,0.7)' }
}}
onClick={() => setExpandedSection(expandedSection === 'personalization' ? null : 'personalization')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Psychology sx={{ color: 'primary.main', fontSize: 18 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#000000' }}>
Personalization
</Typography>
</Box>
<Typography variant="body2" sx={{ color: '#000000' }}>
{onboardingData.personalizationSettings ? 'Configured' : 'Not set'}
</Typography>
</Box>
</Grid>
</Grid>
{/* Expandable Details */}
{(expandedSection === 'api-keys' || expandedSection === 'website' || expandedSection === 'research' || expandedSection === 'personalization') && (
<Box sx={{ mt: 3 }}>
<Paper elevation={0} sx={{
background: 'rgba(255, 255, 255, 0.9)',
borderRadius: 2,
p: 3
}}>
{/* API Keys Details */}
{expandedSection === 'api-keys' && (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1, color: '#000000' }}>
<Security sx={{ color: 'primary.main' }} />
API Keys ({Object.keys(onboardingData.apiKeys).length} configured)
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{Object.entries(onboardingData.apiKeys).map(([provider, key]) => (
<Box key={provider} sx={{
p: 2,
border: '1px solid rgba(0,0,0,0.1)',
borderRadius: 1,
background: 'rgba(255,255,255,0.5)'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, textTransform: 'capitalize' }}>
{provider}
</Typography>
<Tooltip title={showApiKeys ? 'Hide key' : 'Show key'}>
<IconButton
size="small"
onClick={() => setShowApiKeys(!showApiKeys)}
>
{showApiKeys ? <VisibilityOff /> : <Visibility />}
</IconButton>
</Tooltip>
</Box>
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
{showApiKeys ? key : '••••••••••••••••••••••••••••••••'}
</Typography>
</Box>
))}
</Box>
</Box>
)}
{/* Website Analysis Details */}
{expandedSection === 'website' && (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1, color: '#000000' }}>
<Web sx={{ color: 'primary.main' }} />
Website Analysis
</Typography>
{onboardingData.websiteUrl ? (
<Box>
<Typography variant="body2" sx={{ mb: 2 }}>
<strong>URL:</strong> {onboardingData.websiteUrl}
</Typography>
{onboardingData.styleAnalysis && (
<Typography variant="body2" color="success.main">
Style analysis completed
</Typography>
)}
</Box>
) : (
<Typography variant="body2" color="warning.main">
No website URL configured
</Typography>
)}
</Box>
)}
{/* Research Configuration Details */}
{expandedSection === 'research' && (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1, color: '#000000' }}>
<TrendingUp sx={{ color: 'primary.main' }} />
Research Configuration
</Typography>
{onboardingData.researchPreferences ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2">
<strong>Depth:</strong> {onboardingData.researchPreferences.research_depth}
</Typography>
<Typography variant="body2">
<strong>Content Types:</strong> {onboardingData.researchPreferences.content_types?.join(', ')}
</Typography>
<Typography variant="body2">
<strong>Auto Research:</strong> {onboardingData.researchPreferences.auto_research ? 'Enabled' : 'Disabled'}
</Typography>
</Box>
) : (
<Typography variant="body2" color="warning.main">
Research preferences not configured
</Typography>
)}
</Box>
)}
{/* Personalization Details */}
{expandedSection === 'personalization' && (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1, color: '#000000' }}>
<Psychology sx={{ color: 'primary.main' }} />
Personalization
</Typography>
{onboardingData.personalizationSettings ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2">
<strong>Style:</strong> {onboardingData.personalizationSettings.writing_style}
</Typography>
<Typography variant="body2">
<strong>Tone:</strong> {onboardingData.personalizationSettings.tone}
</Typography>
<Typography variant="body2">
<strong>Brand Voice:</strong> {onboardingData.personalizationSettings.brand_voice}
</Typography>
</Box>
) : (
<Typography variant="body2" color="warning.main">
Personalization not configured
</Typography>
)}
</Box>
)}
</Paper>
</Box>
)}
</CardContent>
</Card>
</Grid>
</Grid>
</Paper>
</Zoom>
);
};
export default SetupSummary;

View File

@@ -0,0 +1,3 @@
export { default as SetupSummary } from './SetupSummary';
export { default as CapabilitiesOverview } from './CapabilitiesOverview';

View File

@@ -0,0 +1,3 @@
export { default } from './FinalStep';
export { default as SetupSummary } from './components/SetupSummary';
export { default as CapabilitiesOverview } from './components/CapabilitiesOverview';

View File

@@ -0,0 +1,22 @@
export interface OnboardingData {
apiKeys: Record<string, string>;
websiteUrl?: string;
researchPreferences?: any;
personalizationSettings?: any;
integrations?: any;
styleAnalysis?: any;
}
export interface Capability {
id: string;
title: string;
description: string;
icon: React.ReactElement;
unlocked: boolean;
required?: string[];
}
export interface FinalStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}

View File

@@ -21,6 +21,7 @@ import { ComingSoonSection } from './PersonaStep/ComingSoonSection';
interface PersonaStepProps {
onContinue: (personaData: PersonaData) => void;
updateHeaderContent: (content: StepHeaderContent) => void;
onValidationChange?: (isValid: boolean) => void;
onboardingData?: {
websiteAnalysis?: any;
competitorResearch?: any;
@@ -61,6 +62,7 @@ interface QualityMetrics {
const PersonaStep: React.FC<PersonaStepProps> = ({
onContinue,
updateHeaderContent,
onValidationChange,
onboardingData = {},
stepData
}) => {
@@ -325,6 +327,23 @@ const PersonaStep: React.FC<PersonaStepProps> = ({
}
}, [corePersona, platformPersonas, qualityMetrics, selectedPlatforms, onContinue]);
// Validation effect - notify wizard when persona data is ready
useEffect(() => {
const isValid = !!(corePersona && platformPersonas && Object.keys(platformPersonas).length > 0 && qualityMetrics);
console.log('PersonaStep: Validation check:', {
corePersona: !!corePersona,
platformPersonas: !!platformPersonas,
platformPersonasCount: platformPersonas ? Object.keys(platformPersonas).length : 0,
qualityMetrics: !!qualityMetrics,
isValid
});
if (onValidationChange) {
console.log('PersonaStep: Calling onValidationChange with:', isValid);
onValidationChange(isValid);
}
}, [corePersona, platformPersonas, qualityMetrics, onValidationChange]);
// Auto-call onContinue when persona data is ready
useEffect(() => {
console.log('PersonaStep: Checking persona data readiness:', {

View File

@@ -36,6 +36,7 @@ import {
interface WebsiteStepProps {
onContinue: (stepData?: any) => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
onValidationChange?: (isValid: boolean) => void;
}
interface StyleAnalysis {
@@ -148,7 +149,7 @@ interface ExistingAnalysis {
// MAIN COMPONENT
// =============================================================================
const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderContent }) => {
const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderContent, onValidationChange }) => {
const [website, setWebsite] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@@ -178,6 +179,16 @@ const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderConte
});
}, [updateHeaderContent]);
// Notify parent when validation state changes
useEffect(() => {
const isValid = !!(website.trim() && analysis);
console.log('WebsiteStep: Validation check:', { website: website.trim(), analysis: !!analysis, isValid });
if (onValidationChange) {
console.log('WebsiteStep: Calling onValidationChange with:', isValid);
onValidationChange(isValid);
}
}, [website, analysis, onValidationChange]);
useEffect(() => {
// Prefill from last session analysis on mount
const loadLastAnalysis = async () => {
@@ -517,31 +528,6 @@ const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderConte
useAnalysisForGenAI={useAnalysisForGenAI}
onUseAnalysisChange={setUseAnalysisForGenAI}
/>
{/* Continue Button */}
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
<Button
variant="contained"
size="large"
onClick={handleContinue}
disabled={loading}
sx={{
px: 4,
py: 1.5,
fontSize: '1.1rem',
fontWeight: 600,
borderRadius: 2,
boxShadow: '0 4px 14px rgba(25, 118, 210, 0.4)',
'&:hover': {
boxShadow: '0 6px 20px rgba(25, 118, 210, 0.6)',
transform: 'translateY(-2px)'
},
transition: 'all 0.2s ease-in-out'
}}
>
Continue to Next Step
</Button>
</Box>
</Box>
)}

View File

@@ -101,18 +101,18 @@ const TargetAudienceAnalysisSection: React.FC<TargetAudienceAnalysisSectionProps
<PsychologyIcon />
</Box>
<Box flex={1}>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
<Typography variant="subtitle2" sx={{ color: '#1a202c !important', fontWeight: 600 }} gutterBottom>
Psychographic Profile
</Typography>
<Box component="ul" sx={styles.analysisList}>
{Array.isArray(targetAudience.psychographic_profile)
? targetAudience.psychographic_profile.map((item: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={styles.analysisListItem}>
<Typography component="li" variant="body2" key={index} sx={{ ...styles.analysisListItem, color: '#1a202c !important' }}>
{item}
</Typography>
))
: (
<Typography component="li" variant="body2" sx={styles.analysisListItem}>
<Typography component="li" variant="body2" sx={{ ...styles.analysisListItem, color: '#1a202c !important' }}>
{targetAudience.psychographic_profile}
</Typography>
)}
@@ -131,12 +131,12 @@ const TargetAudienceAnalysisSection: React.FC<TargetAudienceAnalysisSectionProps
<WarningIcon />
</Box>
<Box flex={1}>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
<Typography variant="subtitle2" sx={{ color: '#1a202c !important', fontWeight: 600 }} gutterBottom>
Pain Points
</Typography>
<Box component="ul" sx={styles.analysisList}>
{targetAudience.pain_points.map((painPoint: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={styles.analysisListItem}>
<Typography component="li" variant="body2" key={index} sx={{ ...styles.analysisListItem, color: '#1a202c !important' }}>
{painPoint}
</Typography>
))}
@@ -155,12 +155,12 @@ const TargetAudienceAnalysisSection: React.FC<TargetAudienceAnalysisSectionProps
<TrendingUpIcon />
</Box>
<Box flex={1}>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
<Typography variant="subtitle2" sx={{ color: '#1a202c !important', fontWeight: 600 }} gutterBottom>
Motivations
</Typography>
<Box component="ul" sx={styles.analysisList}>
{targetAudience.motivations.map((motivation: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={styles.analysisListItem}>
<Typography component="li" variant="body2" key={index} sx={{ ...styles.analysisListItem, color: '#1a202c !important' }}>
{motivation}
</Typography>
))}

View File

@@ -66,7 +66,7 @@ const KeyInsightCard: React.FC<KeyInsightProps> = ({
mb: 0,
borderRadius: 2.5,
// Force high-contrast base color so nested text never inherits a light color
color: isDark ? '#ffffff' : '#1a202c',
color: isDark ? '#ffffff !important' : '#1a202c !important',
background: isDark
? `linear-gradient(135deg, ${alpha(paletteColor.main, 0.08)} 0%, ${alpha(paletteColor.main, 0.04)} 100%)`
: `linear-gradient(135deg, ${alpha(paletteColor.main, 0.06)} 0%, ${alpha(paletteColor.light, 0.08)} 100%)`,
@@ -76,6 +76,10 @@ const KeyInsightCard: React.FC<KeyInsightProps> = ({
: alpha(paletteColor.main, 0.15),
borderLeftWidth: '5px',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
// Ensure all child elements inherit proper text color
'& *': {
color: 'inherit !important'
},
'&:hover': {
background: isDark
? `linear-gradient(135deg, ${alpha(paletteColor.main, 0.12)} 0%, ${alpha(paletteColor.main, 0.08)} 100%)`
@@ -114,10 +118,13 @@ const KeyInsightCard: React.FC<KeyInsightProps> = ({
fontSize: '0.78rem',
letterSpacing: '0.6px',
textTransform: 'uppercase',
color: isDark ? '#ffffff !important' : '#0f172a !important',
color: isDark ? '#ffffff !important' : '#1a202c !important',
textShadow: isDark ? 'none' : '0 1px 0 rgba(255,255,255,0.6)',
mb: 0.5,
display: 'block'
display: 'block',
// Force high contrast for readability
WebkitTextFillColor: isDark ? '#ffffff' : '#1a202c',
WebkitTextStroke: '0px transparent'
}}
>
{title}
@@ -127,8 +134,11 @@ const KeyInsightCard: React.FC<KeyInsightProps> = ({
sx={{
fontWeight: 700,
fontSize: '1.1rem',
color: isDark ? '#ffffff !important' : '#0b1220 !important',
lineHeight: 1.35
color: isDark ? '#ffffff !important' : '#1a202c !important',
lineHeight: 1.35,
// Force high contrast for readability
WebkitTextFillColor: isDark ? '#ffffff' : '#1a202c',
WebkitTextStroke: '0px transparent'
}}
>
{Array.isArray(value) ? value.join(', ') : value}

View File

@@ -49,6 +49,7 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
const [stepData, setStepData] = useState<any>(null);
const [competitorDataCollector, setCompetitorDataCollector] = useState<(() => any) | null>(null);
const [isCurrentStepValid, setIsCurrentStepValid] = useState<boolean>(false);
const [stepValidationStates, setStepValidationStates] = useState<Record<number, boolean>>({});
const [stepHeaderContent, setStepHeaderContent] = useState<StepHeaderContent>({
title: steps[0].label,
description: steps[0].description
@@ -61,18 +62,24 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
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;
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;
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, 'Data keys:', data ? Object.keys(data) : 'no data');
return hasCompetitorData;
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 &&
@@ -81,14 +88,14 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
Object.keys(data.platformPersonas).length > 0 &&
data.qualityMetrics;
console.log(`Wizard: Step 3 (Persona Generation) validation:`, {
hasValidPersonaData,
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;
return !!hasValidPersonaData;
case 4: // Integrations
console.log(`Wizard: Step 4 (Integrations) validation: always true (optional)`);
@@ -126,7 +133,16 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
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) {
@@ -138,12 +154,37 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
console.log(`Wizard: Validation result for step ${activeStep}:`, isValid);
console.log(`Wizard: Setting isCurrentStepValid to:`, isValid);
setIsCurrentStepValid(isValid);
}, [activeStep, stepData, isStepDataValid, competitorDataCollector]);
}, [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) => {
@@ -516,8 +557,8 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
const renderStepContent = (step: number) => {
const stepComponents = [
<ApiKeyStep key="api-keys" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<WebsiteStep key="website" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<ApiKeyStep key="api-keys" onContinue={handleNext} updateHeaderContent={updateHeaderContent} onValidationChange={handleApiKeyValidationChange} />,
<WebsiteStep key="website" onContinue={handleNext} updateHeaderContent={updateHeaderContent} onValidationChange={(isValid) => handleStepValidationChange(1, isValid)} />,
<CompetitorAnalysisStep
key="research"
onContinue={handleNext}
@@ -530,6 +571,7 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
key="personalization"
onContinue={handleNext}
updateHeaderContent={updateHeaderContent}
onValidationChange={(isValid) => handleStepValidationChange(3, isValid)}
onboardingData={personaOnboardingData}
stepData={personaStepData}
/>,