ALwrity onboarding final step
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
284
frontend/src/components/OnboardingWizard/FinalStep/FinalStep.tsx
Normal file
284
frontend/src/components/OnboardingWizard/FinalStep/FinalStep.tsx
Normal 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;
|
||||
96
frontend/src/components/OnboardingWizard/FinalStep/README.md
Normal file
96
frontend/src/components/OnboardingWizard/FinalStep/README.md
Normal 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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as SetupSummary } from './SetupSummary';
|
||||
export { default as CapabilitiesOverview } from './CapabilitiesOverview';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default } from './FinalStep';
|
||||
export { default as SetupSummary } from './components/SetupSummary';
|
||||
export { default as CapabilitiesOverview } from './components/CapabilitiesOverview';
|
||||
22
frontend/src/components/OnboardingWizard/FinalStep/types.ts
Normal file
22
frontend/src/components/OnboardingWizard/FinalStep/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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:', {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>,
|
||||
|
||||
Reference in New Issue
Block a user