ALwrity Version 0.5.0 (Fastapi + React )

This commit is contained in:
ajaysi
2025-08-06 12:48:02 +05:30
parent f28a919caa
commit 32f97fa6b3
476 changed files with 115544 additions and 28747 deletions

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { Box, Typography, List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
import {
Accessibility,
Keyboard,
Visibility,
Hearing,
TouchApp
} from '@mui/icons-material';
const AccessibilityGuide: React.FC = () => {
return (
<Box sx={{ p: 3, background: 'rgba(0,0,0,0.02)', borderRadius: 2 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Accessibility />
Accessibility Features
</Typography>
<List dense>
<ListItem>
<ListItemIcon>
<Keyboard />
</ListItemIcon>
<ListItemText
primary="Keyboard Navigation"
secondary="Use Tab, Enter, and Arrow keys to navigate"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<Visibility />
</ListItemIcon>
<ListItemText
primary="High Contrast"
secondary="All text meets WCAG contrast requirements"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<Hearing />
</ListItemIcon>
<ListItemText
primary="Screen Reader Support"
secondary="ARIA labels and semantic HTML structure"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<TouchApp />
</ListItemIcon>
<ListItemText
primary="Touch Friendly"
secondary="Large touch targets for mobile devices"
/>
</ListItem>
</List>
</Box>
);
};
export default AccessibilityGuide;

View File

@@ -0,0 +1,741 @@
import React, { useEffect, useState } from 'react';
import {
Box,
TextField,
Typography,
Alert,
Card,
CardContent,
Fade,
Zoom,
Chip,
IconButton,
Collapse,
Divider,
Link,
Container,
Paper,
Grid,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
List,
ListItem,
ListItemIcon,
ListItemText
} from '@mui/material';
import {
Visibility,
VisibilityOff,
CheckCircle,
Error,
Info,
Key,
Security,
HelpOutline,
Warning,
Star,
VerifiedUser,
Lock,
Launch,
Info as InfoIcon
} from '@mui/icons-material';
import { getApiKeys, saveApiKey } from '../../api/onboarding';
import { useOnboardingStyles } from './common/useOnboardingStyles';
import {
validateApiKey,
getKeyStatus,
isFormValid,
debounce,
formatErrorMessage
} from './common/onboardingUtils';
import OnboardingButton from './common/OnboardingButton';
interface ApiKeyStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
const ApiKeyStep: React.FC<ApiKeyStepProps> = ({ onContinue, updateHeaderContent }) => {
const [openaiKey, setOpenaiKey] = useState('');
const [geminiKey, setGeminiKey] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [showOpenaiKey, setShowOpenaiKey] = useState(false);
const [showGeminiKey, setShowGeminiKey] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [savedKeys, setSavedKeys] = useState<Record<string, string>>({});
const [benefitsModalOpen, setBenefitsModalOpen] = useState(false);
const [selectedProvider, setSelectedProvider] = useState<any>(null);
const [keysLoaded, setKeysLoaded] = useState(false);
const styles = useOnboardingStyles();
useEffect(() => {
if (!keysLoaded) {
loadExistingKeys();
}
// Update header content when component mounts
updateHeaderContent({
title: 'Connect Your AI Services',
description: 'Alwrity uses AI to generate high-quality, personalized content for your brand. Connect at least one AI service to enable intelligent content creation, style analysis, and automated writing assistance.'
});
}, [updateHeaderContent, keysLoaded]);
const loadExistingKeys = async () => {
if (keysLoaded) return; // Prevent multiple calls
try {
console.log('ApiKeyStep: Loading API keys...');
const keys = await getApiKeys();
setSavedKeys(keys);
if (keys.openai) setOpenaiKey(keys.openai);
if (keys.gemini) setGeminiKey(keys.gemini);
setKeysLoaded(true);
console.log('ApiKeyStep: API keys loaded successfully');
} catch (error) {
console.error('ApiKeyStep: Error loading API keys:', error);
setKeysLoaded(true); // Set to true even on error to prevent infinite retries
}
};
const handleContinue = async () => {
setLoading(true);
setError(null);
setSuccess(null);
try {
const promises = [];
if (openaiKey.trim()) {
promises.push(saveApiKey('openai', openaiKey.trim()));
}
if (geminiKey.trim()) {
promises.push(saveApiKey('gemini', geminiKey.trim()));
}
await Promise.all(promises);
setSuccess('API keys saved successfully!');
await loadExistingKeys();
// Auto-continue after a short delay
setTimeout(() => {
onContinue();
}, 1500);
} catch (err) {
setError(formatErrorMessage(err));
console.error('Error saving API keys:', err);
} finally {
setLoading(false);
}
};
const aiProviders = [
{
name: 'OpenAI',
description: 'Advanced language model for content generation',
benefits: ['High-quality text generation', 'Creative content creation', 'Natural language processing'],
key: openaiKey,
setKey: setOpenaiKey,
showKey: showOpenaiKey,
setShowKey: setShowOpenaiKey,
placeholder: 'sk-...',
status: getKeyStatus(openaiKey, 'openai'),
link: 'https://platform.openai.com/api-keys',
free: false,
recommended: true
},
{
name: 'Google Gemini',
description: 'Google\'s latest AI model for content creation',
benefits: ['Multimodal capabilities', 'Real-time information', 'Google\'s latest technology'],
key: geminiKey,
setKey: setGeminiKey,
showKey: showGeminiKey,
setShowKey: setShowGeminiKey,
placeholder: 'AIza...',
status: getKeyStatus(geminiKey, 'gemini'),
link: 'https://makersuite.google.com/app/apikey',
free: true,
recommended: true
}
];
const hasAtLeastOneKey = openaiKey.trim() || geminiKey.trim();
const isValid = hasAtLeastOneKey;
const handleBenefitsClick = (provider: any) => {
setSelectedProvider(provider);
setBenefitsModalOpen(true);
};
const handleCloseBenefitsModal = () => {
setBenefitsModalOpen(false);
setSelectedProvider(null);
};
return (
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
{/* AI Providers */}
<Box sx={{ mb: 4 }}>
<Grid container spacing={3}>
{aiProviders.map((provider, index) => (
<Grid item xs={12} md={6} key={provider.name}>
<Zoom in={true} timeout={700 + index * 100}>
<Card
sx={{
border: `1px solid ${
provider.status === 'valid'
? 'rgba(16, 185, 129, 0.2)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.2)'
: 'rgba(0,0,0,0.08)'
}`,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04)',
transform: 'translateY(-1px)',
borderColor: provider.status === 'valid'
? 'rgba(16, 185, 129, 0.4)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.4)'
: 'rgba(0,0,0,0.12)'
},
position: 'relative',
overflow: 'hidden',
background: 'rgba(255, 255, 255, 0.8)',
backdropFilter: 'blur(10px)',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 2,
background: provider.status === 'valid'
? 'linear-gradient(90deg, rgba(16, 185, 129, 0.6) 0%, rgba(5, 150, 105, 0.6) 100%)'
: provider.status === 'invalid'
? 'linear-gradient(90deg, rgba(239, 68, 68, 0.6) 0%, rgba(220, 38, 38, 0.6) 100%)'
: 'linear-gradient(90deg, rgba(107, 114, 128, 0.3) 0%, rgba(75, 85, 99, 0.3) 100%)',
}
}}
>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: '50%',
background: provider.recommended
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}>
<Key sx={{ color: 'white', fontSize: 20 }} />
</Box>
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6" sx={{
fontWeight: 600,
mb: 0.5,
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: '1.125rem'
}}>
{provider.name}
</Typography>
{provider.recommended && (
<Chip
label="Recommended"
color="success"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 20
}}
/>
)}
{provider.free && (
<Chip
label="Free Tier"
color="primary"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 20
}}
/>
)}
</Box>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
{provider.description}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Benefits Button - Inline with Get Help */}
<Button
variant="text"
onClick={() => handleBenefitsClick(provider)}
startIcon={<InfoIcon />}
sx={{
color: 'primary.main',
fontWeight: 600,
fontSize: '0.75rem',
fontFamily: 'Inter, system-ui, sans-serif',
textTransform: 'none',
padding: '2px 6px',
borderRadius: 1,
minWidth: 'auto',
'&:hover': {
background: 'rgba(102, 126, 234, 0.08)',
transform: 'translateY(-1px)'
}
}}
>
Benefits ({provider.benefits.length})
</Button>
{provider.status === 'valid' && (
<Chip
icon={<CheckCircle />}
label="Valid"
color="success"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 24
}}
/>
)}
{provider.status === 'invalid' && (
<Chip
icon={<Error />}
label="Invalid"
color="error"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 24
}}
/>
)}
</Box>
</Box>
{/* Enhanced API Key Input */}
<TextField
fullWidth
type={provider.showKey ? 'text' : 'password'}
value={provider.key}
onChange={(e) => provider.setKey(e.target.value)}
placeholder={provider.placeholder}
variant="outlined"
size="small"
InputProps={{
startAdornment: (
<Lock sx={{ color: 'text.secondary', mr: 1, fontSize: 16 }} />
),
endAdornment: (
<IconButton
onClick={() => provider.setShowKey(!provider.showKey)}
edge="end"
size="small"
sx={{
color: 'text.secondary',
'&:hover': {
color: 'primary.main',
background: 'rgba(102, 126, 234, 0.08)'
}
}}
>
{provider.showKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
border: '1px solid rgba(0,0,0,0.12)',
background: 'rgba(255, 255, 255, 0.8)',
'&:hover': {
borderColor: 'rgba(0,0,0,0.24)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
},
'&.Mui-focused': {
borderColor: provider.status === 'valid'
? 'rgba(16, 185, 129, 0.6)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.6)'
: 'rgba(102, 126, 234, 0.6)',
boxShadow: `0 0 0 2px ${
provider.status === 'valid'
? 'rgba(16, 185, 129, 0.1)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.1)'
: 'rgba(102, 126, 234, 0.1)'
}, 0 2px 8px rgba(0, 0, 0, 0.08)`,
'& .MuiOutlinedInput-notchedOutline': {
border: 'none'
}
},
'& .MuiOutlinedInput-notchedOutline': {
border: 'none'
}
},
'& .MuiInputBase-input': {
padding: '12px 14px',
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 500,
fontSize: '0.875rem'
}
}}
/>
{/* Enhanced Link with Icon */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1.5 }}>
<Link
href={provider.link}
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
fontWeight: 600,
fontSize: '0.9rem',
color: 'primary.main',
textDecoration: 'none',
fontFamily: 'Inter, system-ui, sans-serif',
padding: '4px 8px',
borderRadius: 1,
transition: 'all 0.2s ease',
'&:hover': {
background: 'rgba(102, 126, 234, 0.08)',
textDecoration: 'none',
transform: 'translateY(-1px)'
}
}}
>
Get API Key
<Launch sx={{ fontSize: 16 }} />
</Link>
</Box>
{savedKeys[provider.name.toLowerCase()] && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="caption" color="success.main" sx={{
fontWeight: 500,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
Key already saved and secured
</Typography>
</Box>
)}
</CardContent>
</Card>
</Zoom>
</Grid>
))}
</Grid>
</Box>
{/* Description moved below cards */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h6" color="text.secondary" sx={{
mb: 2,
lineHeight: 1.6,
maxWidth: 800,
mx: 'auto',
fontWeight: 500,
opacity: 0.8,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
Alwrity uses AI to generate high-quality, personalized content for your brand. Connect at least one AI service to enable intelligent content creation, style analysis, and automated writing assistance.
</Typography>
{/* Get Help Link moved to description area */}
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<OnboardingButton
variant="text"
onClick={() => setShowHelp(!showHelp)}
icon={<HelpOutline />}
size="small"
>
{showHelp ? 'Hide Help' : 'Get Help'}
</OnboardingButton>
</Box>
</Box>
{/* Benefits Modal */}
<Dialog
open={benefitsModalOpen}
onClose={handleCloseBenefitsModal}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.1)',
border: '1px solid rgba(0, 0, 0, 0.08)'
}
}}
>
<DialogTitle sx={{
pb: 1,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600
}}>
{selectedProvider?.name} Benefits
</DialogTitle>
<DialogContent sx={{ pt: 0 }}>
<Typography variant="body2" color="text.secondary" sx={{
mb: 2,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
Discover what {selectedProvider?.name} can do for your content creation:
</Typography>
<List sx={{ pt: 0 }}>
{selectedProvider?.benefits.map((benefit: string, index: number) => (
<ListItem key={index} sx={{ px: 0, py: 1 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
background: 'primary.main',
flexShrink: 0
}} />
</ListItemIcon>
<ListItemText
primary={benefit}
sx={{
'& .MuiListItemText-primary': {
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 500,
fontSize: '0.875rem'
}
}}
/>
</ListItem>
))}
</List>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 1 }}>
<Button
onClick={handleCloseBenefitsModal}
variant="contained"
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif'
}}
>
Got it
</Button>
</DialogActions>
</Dialog>
{/* Help Section */}
<Collapse in={showHelp}>
<Zoom in={showHelp} timeout={1600}>
<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,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
}}>
<Typography variant="h6" gutterBottom sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 3,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600
}}>
<HelpOutline color="primary" />
How to Get Your AI API Keys
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 1,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
<Star sx={{ color: 'warning.main', fontSize: 20 }} />
Recommended Providers
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box>
<Typography variant="subtitle2" sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
OpenAI
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
Visit{' '}
<Link href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" sx={{
fontWeight: 600,
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline'
}
}}>
platform.openai.com
</Link>
, sign up, and create an API key in your account settings.
</Typography>
</Box>
<Box>
<Typography variant="subtitle2" sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
Google Gemini
</Typography>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
Visit{' '}
<Link href="https://makersuite.google.com/app/apikey" target="_blank" rel="noopener noreferrer" sx={{
fontWeight: 600,
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline'
}
}}>
makersuite.google.com
</Link>
, create an account, and generate an API key.
</Typography>
</Box>
</Box>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box>
<Typography variant="subtitle1" gutterBottom sx={{
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 1,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
<Info sx={{ color: 'info.main', fontSize: 20 }} />
Why AI Services Matter
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<strong>Content Generation:</strong> Create high-quality, engaging content for your brand.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<strong>Style Analysis:</strong> Analyze your brand's voice and tone for consistency.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<strong>Automated Writing:</strong> Generate blog posts, social media content, and more.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<strong>Personalization:</strong> Tailor content to your specific audience and goals.
</Typography>
</Box>
</Box>
</Grid>
</Grid>
</Paper>
</Zoom>
</Collapse>
{/* Alerts */}
<Box sx={{ mt: 3 }}>
{error && (
<Fade in={true}>
<Alert severity="error" sx={{
mb: 2,
borderRadius: 2,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
{error}
</Alert>
</Fade>
)}
{success && (
<Fade in={true}>
<Alert severity="success" sx={{
mb: 2,
borderRadius: 2,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
{success}
</Alert>
</Fade>
)}
</Box>
{/* Security Notice */}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary" sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 0.5,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<Lock sx={{ fontSize: 14 }} />
Your API keys are encrypted and stored securely on your device
</Typography>
</Box>
</Container>
</Fade>
);
};
export default ApiKeyStep;

View File

@@ -0,0 +1,660 @@
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,752 @@
import React, { useEffect, useState } from 'react';
import {
Box,
Button,
TextField,
Typography,
Alert,
CircularProgress,
Card,
CardContent,
Grid,
Tabs,
Tab,
Chip,
Divider,
FormControlLabel,
Switch,
Accordion,
AccordionSummary,
AccordionDetails,
IconButton,
Tooltip,
Fade,
Zoom,
Paper,
List,
ListItem,
ListItemIcon,
ListItemText,
ListItemSecondaryAction
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
CheckCircle as CheckIcon,
Error as ErrorIcon,
Info as InfoIcon,
Add as AddIcon,
Settings as SettingsIcon,
Link as LinkIcon,
Launch as LaunchIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
// Social Media Icons
Facebook as FacebookIcon,
Twitter as TwitterIcon,
Instagram as InstagramIcon,
LinkedIn as LinkedInIcon,
YouTube as YouTubeIcon,
VideoLibrary as TikTokIcon, // Using VideoLibrary as alternative for TikTok
Pinterest as PinterestIcon,
// Platform Icons
Web as WordPressIcon, // Using Web as alternative for WordPress
Web as WebIcon,
// AI and Analytics Icons
Analytics as AnalyticsIcon,
AutoAwesome as AutoAwesomeIcon,
TrendingUp as TrendingUpIcon,
Schedule as ScheduleIcon,
ContentPaste as ContentPasteIcon,
SmartToy as SmartToyIcon,
// Status Icons
Warning as WarningIcon,
HelpOutline as HelpOutlineIcon,
Verified as VerifiedIcon,
Close as CloseIcon
} from '@mui/icons-material';
interface IntegrationsStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
interface IntegrationConfig {
id: string;
name: string;
description: string;
icon: React.ReactNode;
category: 'social' | 'platform' | 'analytics';
apiKeyField: string;
apiKeyPlaceholder: string;
setupUrl: string;
features: string[];
isConnected: boolean;
apiKey: string;
showApiKey: boolean;
isEnabled: boolean;
status: 'connected' | 'disconnected' | 'error' | 'pending';
}
const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateHeaderContent }) => {
const [activeTab, setActiveTab] = useState(0);
const [integrations, setIntegrations] = useState<IntegrationConfig[]>([
// Social Media Platforms
{
id: 'facebook',
name: 'Facebook',
description: 'Connect your Facebook page for AI-powered content creation and automated posting',
icon: <FacebookIcon />,
category: 'social',
apiKeyField: 'facebook_access_token',
apiKeyPlaceholder: 'EAA...',
setupUrl: 'https://developers.facebook.com/apps/',
features: ['AI Content Generation', 'Automated Posting', 'Trend Analysis', 'Engagement Tracking'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'twitter',
name: 'Twitter/X',
description: 'Connect your Twitter account for AI-powered tweets and trend analysis',
icon: <TwitterIcon />,
category: 'social',
apiKeyField: 'twitter_bearer_token',
apiKeyPlaceholder: 'AAAA...',
setupUrl: 'https://developer.twitter.com/en/portal/dashboard',
features: ['AI Tweet Generation', 'Trend Analysis', 'Automated Posting', 'Hashtag Optimization'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'instagram',
name: 'Instagram',
description: 'Connect your Instagram account for AI-powered content and caption generation',
icon: <InstagramIcon />,
category: 'social',
apiKeyField: 'instagram_access_token',
apiKeyPlaceholder: 'IGQ...',
setupUrl: 'https://developers.facebook.com/apps/',
features: ['AI Caption Generation', 'Hashtag Optimization', 'Content Scheduling', 'Engagement Analytics'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'linkedin',
name: 'LinkedIn',
description: 'Connect your LinkedIn profile for professional content creation and networking',
icon: <LinkedInIcon />,
category: 'social',
apiKeyField: 'linkedin_access_token',
apiKeyPlaceholder: 'AQV...',
setupUrl: 'https://www.linkedin.com/developers/',
features: ['Professional Content', 'Network Analysis', 'Industry Insights', 'Thought Leadership'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'youtube',
name: 'YouTube',
description: 'Connect your YouTube channel for AI-powered video descriptions and SEO optimization',
icon: <YouTubeIcon />,
category: 'social',
apiKeyField: 'youtube_api_key',
apiKeyPlaceholder: 'AIza...',
setupUrl: 'https://console.developers.google.com/',
features: ['Video Description AI', 'SEO Optimization', 'Trend Analysis', 'Content Strategy'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'tiktok',
name: 'TikTok',
description: 'Connect your TikTok account for AI-powered video captions and trend analysis',
icon: <TikTokIcon />,
category: 'social',
apiKeyField: 'tiktok_access_token',
apiKeyPlaceholder: 'TikTok...',
setupUrl: 'https://developers.tiktok.com/',
features: ['Video Caption AI', 'Trend Analysis', 'Hashtag Optimization', 'Viral Content'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'pinterest',
name: 'Pinterest',
description: 'Connect your Pinterest account for AI-powered pin descriptions and board optimization',
icon: <PinterestIcon />,
category: 'social',
apiKeyField: 'pinterest_access_token',
apiKeyPlaceholder: 'Pinterest...',
setupUrl: 'https://developers.pinterest.com/',
features: ['Pin Description AI', 'Board Optimization', 'Visual Content Strategy', 'SEO Enhancement'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
// Website Platforms
{
id: 'wordpress',
name: 'WordPress',
description: 'Connect your WordPress site for AI-powered content management and SEO optimization',
icon: <WordPressIcon />,
category: 'platform',
apiKeyField: 'wordpress_api_key',
apiKeyPlaceholder: 'wp_...',
setupUrl: 'https://wordpress.org/plugins/rest-api/',
features: ['AI Content Creation', 'SEO Optimization', 'Automated Publishing', 'Performance Analytics'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'wix',
name: 'Wix',
description: 'Connect your Wix website for AI-powered content management and optimization',
icon: <WebIcon />,
category: 'platform',
apiKeyField: 'wix_api_key',
apiKeyPlaceholder: 'wix_...',
setupUrl: 'https://developers.wix.com/',
features: ['AI Content Creation', 'SEO Optimization', 'Automated Updates', 'Performance Tracking'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
}
]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
useEffect(() => {
updateHeaderContent({
title: 'Connect Your Platforms',
description: 'Integrate your social media accounts and websites to enable AI-powered content creation, automated posting, and comprehensive analytics across all your platforms.'
});
}, [updateHeaderContent]);
useEffect(() => {
// Prefill integrations on mount
const fetchIntegrations = async () => {
try {
const res = await fetch('/api/onboarding/integrations');
const data = await res.json();
if (data.success && Array.isArray(data.integrations)) {
setIntegrations(prev => prev.map(intg => {
const found = data.integrations.find((i: any) => i.id === intg.id);
if (found) {
return {
...intg,
apiKey: found.apiKey || '',
isConnected: !!found.isConnected,
isEnabled: typeof found.isEnabled === 'boolean' ? found.isEnabled : intg.isEnabled,
status: found.status || intg.status,
};
}
return intg;
}));
}
} catch (err) {
console.error('IntegrationsStep: Error pre-filling integrations', err);
}
};
fetchIntegrations();
}, []);
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue);
};
const handleApiKeyChange = (integrationId: string, value: string) => {
setIntegrations(prev => prev.map(integration =>
integration.id === integrationId
? { ...integration, apiKey: value }
: integration
));
};
const handleToggleApiKeyVisibility = (integrationId: string) => {
setIntegrations(prev => prev.map(integration =>
integration.id === integrationId
? { ...integration, showApiKey: !integration.showApiKey }
: integration
));
};
const handleToggleIntegration = (integrationId: string) => {
setIntegrations(prev => prev.map(integration =>
integration.id === integrationId
? { ...integration, isEnabled: !integration.isEnabled }
: integration
));
};
const handleConnectIntegration = async (integrationId: string) => {
const integration = integrations.find(i => i.id === integrationId);
if (!integration) return;
setLoading(true);
setError(null);
try {
// Simulate API call to connect integration
await new Promise(resolve => setTimeout(resolve, 2000));
setIntegrations(prev => prev.map(i =>
i.id === integrationId
? { ...i, isConnected: true, status: 'connected' }
: i
));
setSuccess(`${integration.name} connected successfully!`);
} catch (err) {
setError(`Failed to connect ${integration.name}. Please check your API key and try again.`);
setIntegrations(prev => prev.map(i =>
i.id === integrationId
? { ...i, status: 'error' }
: i
));
} finally {
setLoading(false);
}
};
const handleContinue = async () => {
const connectedIntegrations = integrations.filter(i => i.isConnected);
if (connectedIntegrations.length === 0) {
setError('Please connect at least one platform to continue.');
return;
}
console.log('IntegrationsStep: handleContinue called');
console.log('IntegrationsStep: Connected integrations:', connectedIntegrations.length);
console.log('IntegrationsStep: Current step should be 5 (IntegrationsStep)');
console.log('IntegrationsStep: Calling onContinue()');
try {
// Add a small delay to see the logs
await new Promise(resolve => setTimeout(resolve, 100));
onContinue();
} catch (error) {
console.error('IntegrationsStep: Error in onContinue:', error);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'connected': return 'success';
case 'error': return 'error';
case 'pending': return 'warning';
default: return 'default';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'connected': return <CheckIcon color="success" />;
case 'error': return <ErrorIcon color="error" />;
case 'pending': return <CircularProgress size={16} />;
default: return <InfoIcon color="action" />;
}
};
const renderIntegrationCard = (integration: IntegrationConfig) => (
<Zoom in timeout={300}>
<Card
sx={{
mb: 2,
border: integration.isConnected ? '2px solid success.main' : '1px solid rgba(0,0,0,0.12)',
background: integration.isConnected ? 'success.50' : 'background.paper',
transition: 'all 0.3s ease'
}}
>
<CardContent sx={{ p: 3 }}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
<Box display="flex" alignItems="center" gap={2}>
<Box sx={{
color: integration.isConnected ? 'success.main' : 'primary.main',
fontSize: 32
}}>
{integration.icon}
</Box>
<Box>
<Typography variant="h6" fontWeight={600}>
{integration.name}
</Typography>
<Typography variant="body2" color="textSecondary">
{integration.description}
</Typography>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={1}>
{getStatusIcon(integration.status)}
<Chip
label={integration.status}
color={getStatusColor(integration.status) as any}
size="small"
/>
</Box>
</Box>
<Grid container spacing={2} mb={2}>
<Grid item xs={12} md={8}>
<TextField
label={`${integration.name} API Key`}
type={integration.showApiKey ? 'text' : 'password'}
value={integration.apiKey}
onChange={(e) => handleApiKeyChange(integration.id, e.target.value)}
placeholder={integration.apiKeyPlaceholder}
fullWidth
size="small"
disabled={integration.isConnected}
InputProps={{
endAdornment: (
<IconButton
onClick={() => handleToggleApiKeyVisibility(integration.id)}
edge="end"
>
{integration.showApiKey ? <VisibilityOffIcon /> : <VisibilityIcon />}
</IconButton>
),
}}
/>
</Grid>
<Grid item xs={12} md={4}>
<Box display="flex" gap={1}>
<Button
variant="outlined"
size="small"
startIcon={<LaunchIcon />}
onClick={() => window.open(integration.setupUrl, '_blank')}
fullWidth
>
Setup Guide
</Button>
{!integration.isConnected && (
<Button
variant="contained"
size="small"
startIcon={<LinkIcon />}
onClick={() => handleConnectIntegration(integration.id)}
disabled={!integration.apiKey || loading}
fullWidth
>
Connect
</Button>
)}
</Box>
</Grid>
</Grid>
<Box mb={2}>
<Typography variant="subtitle2" color="primary" gutterBottom>
Features:
</Typography>
<Box display="flex" flexWrap="wrap" gap={1}>
{integration.features.map((feature, index) => (
<Chip
key={index}
label={feature}
size="small"
variant="outlined"
icon={<AutoAwesomeIcon />}
/>
))}
</Box>
</Box>
<FormControlLabel
control={
<Switch
checked={integration.isEnabled}
onChange={() => handleToggleIntegration(integration.id)}
disabled={!integration.isConnected}
/>
}
label="Enable AI-powered features for this platform"
/>
</CardContent>
</Card>
</Zoom>
);
const renderTabContent = (category: 'social' | 'platform' | 'analytics') => {
const categoryIntegrations = integrations.filter(i => i.category === category);
return (
<Box>
{categoryIntegrations.map(integration => renderIntegrationCard(integration))}
</Box>
);
};
const connectedCount = integrations.filter(i => i.isConnected).length;
const enabledCount = integrations.filter(i => i.isEnabled).length;
return (
<Box sx={{ maxWidth: 1200, mx: 'auto', p: 3 }}>
{/* Header Section */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h4" fontWeight={700} gutterBottom>
Connect Your Platforms
</Typography>
<Typography variant="body1" color="textSecondary" sx={{ mb: 3, maxWidth: 800, mx: 'auto' }}>
Integrate your social media accounts and websites to enable AI-powered content creation,
automated posting, and comprehensive analytics across all your platforms.
</Typography>
{/* Stats Cards */}
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} md={4}>
<Paper elevation={2} sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="primary" fontWeight={700}>
{integrations.length}
</Typography>
<Typography variant="body2" color="textSecondary">
Available Platforms
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Paper elevation={2} sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="success.main" fontWeight={700}>
{connectedCount}
</Typography>
<Typography variant="body2" color="textSecondary">
Connected Platforms
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Paper elevation={2} sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="info.main" fontWeight={700}>
{enabledCount}
</Typography>
<Typography variant="body2" color="textSecondary">
AI Features Enabled
</Typography>
</Paper>
</Grid>
</Grid>
</Box>
{/* Info Alert */}
<Alert severity="info" sx={{ mb: 3 }}>
<Typography variant="body2">
<strong>How it works:</strong> Connect your platforms using their API keys. Once connected,
ALwrity can generate AI-powered content, analyze trends, and automatically post to your platforms.
Your API keys are securely stored and never shared.
</Typography>
</Alert>
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
{success}
</Alert>
)}
{/* Tabs for Different Categories */}
<Paper elevation={2} sx={{ mb: 3 }}>
<Tabs
value={activeTab}
onChange={handleTabChange}
variant="fullWidth"
sx={{
borderBottom: 1,
borderColor: 'divider',
'& .MuiTab-root': {
textTransform: 'none',
fontWeight: 600,
fontSize: '1rem'
}
}}
>
<Tab
label={
<Box display="flex" alignItems="center" gap={1}>
<AutoAwesomeIcon />
Social Media ({integrations.filter(i => i.category === 'social').length})
</Box>
}
/>
<Tab
label={
<Box display="flex" alignItems="center" gap={1}>
<WebIcon />
Website Platforms ({integrations.filter(i => i.category === 'platform').length})
</Box>
}
/>
</Tabs>
</Paper>
{/* Tab Content */}
<Box sx={{ mb: 4 }}>
{activeTab === 0 && renderTabContent('social')}
{activeTab === 1 && renderTabContent('platform')}
</Box>
{/* Features Preview */}
{connectedCount > 0 && (
<Accordion sx={{ mb: 3 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box display="flex" alignItems="center" gap={1}>
<SmartToyIcon color="primary" />
<Typography variant="h6">AI Features Preview</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card sx={{ p: 2 }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<ContentPasteIcon color="primary" />
<Typography variant="h6">Content Creation</Typography>
</Box>
<List dense>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="AI-powered content generation" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Platform-specific optimization" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Hashtag and SEO optimization" />
</ListItem>
</List>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card sx={{ p: 2 }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<ScheduleIcon color="primary" />
<Typography variant="h6">Automation</Typography>
</Box>
<List dense>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Automated posting schedules" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Cross-platform content distribution" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Smart timing optimization" />
</ListItem>
</List>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card sx={{ p: 2 }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<AnalyticsIcon color="primary" />
<Typography variant="h6">Analytics</Typography>
</Box>
<List dense>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Performance tracking" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Trend analysis" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Engagement insights" />
</ListItem>
</List>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card sx={{ p: 2 }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<TrendingUpIcon color="primary" />
<Typography variant="h6">Optimization</Typography>
</Box>
<List dense>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Content performance optimization" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Audience targeting" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="ROI tracking" />
</ListItem>
</List>
</Card>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
)}
{/* Continue Button */}
<Box display="flex" justifyContent="center" mt={4}>
<Button
variant="contained"
size="large"
onClick={handleContinue}
disabled={connectedCount === 0}
startIcon={connectedCount > 0 ? <CheckIcon /> : <WarningIcon />}
sx={{
px: 4,
py: 1.5,
fontSize: '1.1rem',
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
}
}}
>
{connectedCount === 0
? 'Connect at least one platform to continue'
: `Continue with ${connectedCount} connected platform${connectedCount > 1 ? 's' : ''}`
}
</Button>
</Box>
</Box>
);
};
export default IntegrationsStep;

View File

@@ -0,0 +1,362 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
TextField,
Typography,
Alert,
MenuItem,
FormControl,
InputLabel,
Select,
Chip,
OutlinedInput,
FormHelperText,
Switch,
FormControlLabel,
Accordion,
AccordionSummary,
AccordionDetails,
Divider
} from '@mui/material';
import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
import {
validateContentStyle,
configureBrandVoice,
processPersonalizationSettings,
getPersonalizationConfigurationOptions,
generateContentGuidelines,
ContentStyleRequest,
BrandVoiceRequest,
AdvancedSettingsRequest,
PersonalizationSettingsRequest
} from '../../api/componentLogic';
interface PersonalizationStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
const PersonalizationStep: React.FC<PersonalizationStepProps> = ({ onContinue, updateHeaderContent }) => {
// Content Style State
const [writingStyle, setWritingStyle] = useState('Professional');
const [tone, setTone] = useState('Neutral');
const [contentLength, setContentLength] = useState('Standard');
// Brand Voice State
const [personalityTraits, setPersonalityTraits] = useState<string[]>(['Professional']);
const [voiceDescription, setVoiceDescription] = useState('');
const [keywords, setKeywords] = useState('');
// Advanced Settings State
const [seoOptimization, setSeoOptimization] = useState(false);
const [readabilityLevel, setReadabilityLevel] = useState('Standard');
const [contentStructure, setContentStructure] = useState<string[]>(['Introduction', 'Key Points', 'Conclusion']);
// UI State
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [configurationOptions, setConfigurationOptions] = useState<any>(null);
useEffect(() => {
async function loadConfigurationOptions() {
try {
const options = await getPersonalizationConfigurationOptions();
setConfigurationOptions(options.options);
} catch (e) {
console.error('Failed to load configuration options:', e);
}
}
loadConfigurationOptions();
// Update header content when component mounts
updateHeaderContent({
title: 'Customize Your Experience',
description: 'Personalize Alwrity to match your brand voice, content style, and writing preferences. Configure how AI generates content to ensure it aligns with your brand identity and resonates with your audience.'
});
}, [updateHeaderContent]);
const handleContinue = async () => {
setError(null);
setSuccess(null);
setLoading(true);
try {
// Validate content style
const contentStyleRequest: ContentStyleRequest = {
writing_style: writingStyle,
tone: tone,
content_length: contentLength
};
const contentStyleValidation = await validateContentStyle(contentStyleRequest);
if (!contentStyleValidation.valid) {
setError(`Content style validation failed: ${contentStyleValidation.errors.join(', ')}`);
setLoading(false);
return;
}
// Configure brand voice
const brandVoiceRequest: BrandVoiceRequest = {
personality_traits: personalityTraits,
voice_description: voiceDescription,
keywords: keywords
};
const brandVoiceValidation = await configureBrandVoice(brandVoiceRequest);
if (!brandVoiceValidation.valid) {
setError(`Brand voice validation failed: ${brandVoiceValidation.errors.join(', ')}`);
setLoading(false);
return;
}
// Process complete settings
const advancedSettingsRequest: AdvancedSettingsRequest = {
seo_optimization: seoOptimization,
readability_level: readabilityLevel,
content_structure: contentStructure
};
const completeSettingsRequest: PersonalizationSettingsRequest = {
content_style: contentStyleRequest,
brand_voice: brandVoiceRequest,
advanced_settings: advancedSettingsRequest
};
const settingsValidation = await processPersonalizationSettings(completeSettingsRequest);
if (!settingsValidation.valid) {
setError(`Settings validation failed: ${settingsValidation.errors.join(', ')}`);
setLoading(false);
return;
}
// Generate content guidelines
const guidelines = await generateContentGuidelines(settingsValidation.settings);
if (guidelines.success) {
setSuccess('Personalization settings saved successfully! Content guidelines generated.');
// TODO: Store guidelines for later use
onContinue();
} else {
setError('Failed to generate content guidelines.');
}
} catch (e) {
setError('Failed to save personalization settings. Please try again.');
console.error('Personalization error:', e);
} finally {
setLoading(false);
}
};
const handlePersonalityTraitsChange = (event: any) => {
const value = event.target.value;
setPersonalityTraits(typeof value === 'string' ? value.split(',') : value);
};
const handleContentStructureChange = (event: any) => {
const value = event.target.value;
setContentStructure(typeof value === 'string' ? value.split(',') : value);
};
if (!configurationOptions) {
return (
<Box>
<Typography variant="h6" gutterBottom>
Personalize Your Experience
</Typography>
<Alert severity="info">Loading configuration options...</Alert>
</Box>
);
}
return (
<Box>
{/* Enhanced Explanatory Text */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h6" color="text.secondary" sx={{
mb: 3,
lineHeight: 1.6,
maxWidth: 800,
mx: 'auto',
fontWeight: 500,
opacity: 0.8
}}>
Configure your content style, brand voice, and advanced settings to tailor the AI experience to your needs.
This ensures that all generated content aligns with your brand identity and resonates with your target audience.
</Typography>
</Box>
{/* Content Style Section */}
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1" fontWeight="bold">Content Style</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControl fullWidth>
<InputLabel>Writing Style</InputLabel>
<Select
value={writingStyle}
onChange={(e) => setWritingStyle(e.target.value)}
label="Writing Style"
>
{configurationOptions.writing_styles?.map((style: string) => (
<MenuItem key={style} value={style}>{style}</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Tone</InputLabel>
<Select
value={tone}
onChange={(e) => setTone(e.target.value)}
label="Tone"
>
{configurationOptions.tones?.map((toneOption: string) => (
<MenuItem key={toneOption} value={toneOption}>{toneOption}</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Content Length</InputLabel>
<Select
value={contentLength}
onChange={(e) => setContentLength(e.target.value)}
label="Content Length"
>
{configurationOptions.content_lengths?.map((length: string) => (
<MenuItem key={length} value={length}>{length}</MenuItem>
))}
</Select>
</FormControl>
</Box>
</AccordionDetails>
</Accordion>
{/* Brand Voice Section */}
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1" fontWeight="bold">Brand Voice</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControl fullWidth>
<InputLabel>Personality Traits</InputLabel>
<Select
multiple
value={personalityTraits}
onChange={handlePersonalityTraitsChange}
input={<OutlinedInput label="Personality Traits" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
>
{configurationOptions.personality_traits?.map((trait: string) => (
<MenuItem key={trait} value={trait}>{trait}</MenuItem>
))}
</Select>
<FormHelperText>Select traits that best describe your brand</FormHelperText>
</FormControl>
<TextField
label="Brand Voice Description"
value={voiceDescription}
onChange={(e) => setVoiceDescription(e.target.value)}
fullWidth
multiline
rows={3}
helperText="Describe how your brand should sound in content (optional)"
/>
<TextField
label="Brand Keywords"
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
fullWidth
helperText="Enter key terms that should be used in your content (optional)"
/>
</Box>
</AccordionDetails>
</Accordion>
{/* Advanced Settings Section */}
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1" fontWeight="bold">Advanced Settings</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControlLabel
control={
<Switch
checked={seoOptimization}
onChange={(e) => setSeoOptimization(e.target.checked)}
/>
}
label="Enable SEO Optimization"
/>
<FormControl fullWidth>
<InputLabel>Readability Level</InputLabel>
<Select
value={readabilityLevel}
onChange={(e) => setReadabilityLevel(e.target.value)}
label="Readability Level"
>
{configurationOptions.readability_levels?.map((level: string) => (
<MenuItem key={level} value={level}>{level}</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Content Structure</InputLabel>
<Select
multiple
value={contentStructure}
onChange={handleContentStructureChange}
input={<OutlinedInput label="Content Structure" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
>
{configurationOptions.content_structures?.map((structure: string) => (
<MenuItem key={structure} value={structure}>{structure}</MenuItem>
))}
</Select>
<FormHelperText>Select required content sections</FormHelperText>
</FormControl>
</Box>
</AccordionDetails>
</Accordion>
<Divider sx={{ my: 2 }} />
{error && <Alert severity="error" sx={{ mt: 2 }}>{error}</Alert>}
{success && <Alert severity="success" sx={{ mt: 2 }}>{success}</Alert>}
<Button
variant="contained"
color="primary"
onClick={handleContinue}
sx={{ mt: 2 }}
disabled={loading}
>
{loading ? 'Saving Settings...' : 'Continue'}
</Button>
</Box>
);
};
export default PersonalizationStep;

View File

@@ -0,0 +1,914 @@
import React, { useEffect, useState } from 'react';
import {
Box,
TextField,
Typography,
Alert,
Card,
CardContent,
Fade,
Zoom,
Chip,
IconButton,
Collapse,
Divider,
Link,
Container,
Paper,
Grid,
FormControl,
InputLabel,
Select,
MenuItem,
OutlinedInput,
FormHelperText,
Switch,
FormControlLabel,
Button,
CircularProgress,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogActions
} from '@mui/material';
import {
Visibility,
VisibilityOff,
CheckCircle,
Error as ErrorIcon,
Info,
Search,
HelpOutline,
Warning,
Star,
VerifiedUser,
Lock,
Science,
TrendingUp,
Security,
AutoAwesome,
School,
Link as LinkIcon,
Launch,
Close
} from '@mui/icons-material';
import { getApiKeys, saveApiKey } from '../../api/onboarding';
import { configureResearchPreferences } from '../../api/componentLogic';
import { useOnboardingStyles } from './common/useOnboardingStyles';
import {
validateApiKey,
getKeyStatus,
isFormValid,
debounce,
formatErrorMessage
} from './common/onboardingUtils';
import OnboardingButton from './common/OnboardingButton';
import OnboardingCard from './common/OnboardingCard';
interface ResearchStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
const ResearchStep: React.FC<ResearchStepProps> = ({ onContinue, updateHeaderContent }) => {
console.log('ResearchStep: Component rendered');
// API Keys State
const [tavilyKey, setTavilyKey] = useState('');
const [serperKey, setSerperKey] = useState('');
const [exaKey, setExaKey] = useState('');
const [firecrawlKey, setFirecrawlKey] = useState('');
// User Information State
const [fullName, setFullName] = useState('');
const [email, setEmail] = useState('');
const [company, setCompany] = useState('');
const [role, setRole] = useState('Content Creator');
// Research Preferences State
const [researchDepth, setResearchDepth] = useState('Comprehensive');
const [contentTypes, setContentTypes] = useState<string[]>(['Blog Posts', 'Social Media', 'Articles']);
const [autoResearch, setAutoResearch] = useState(true);
const [factualContent, setFactualContent] = useState(true);
// UI State
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [showTavilyKey, setShowTavilyKey] = useState(false);
const [showSerperKey, setShowSerperKey] = useState(false);
const [showExaKey, setShowExaKey] = useState(false);
const [showFirecrawlKey, setShowFirecrawlKey] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [savedKeys, setSavedKeys] = useState<Record<string, string>>({});
const [benefitsDialog, setBenefitsDialog] = useState<{ open: boolean; provider: any }>({ open: false, provider: null });
const [keysLoaded, setKeysLoaded] = useState(false);
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
const styles = useOnboardingStyles();
useEffect(() => {
console.log('ResearchStep: useEffect triggered', { keysLoaded });
if (!keysLoaded) {
console.log('ResearchStep: Calling debouncedLoadKeys');
debouncedLoadKeys();
} else {
console.log('ResearchStep: Keys already loaded, skipping debouncedLoadKeys');
}
loadWebsiteDefaults();
}, [keysLoaded]); // Removed updateHeaderContent from dependencies
useEffect(() => {
updateHeaderContent({
title: "Configure AI Research",
description: "Set up research APIs and preferences for intelligent content generation"
});
}, [updateHeaderContent]);
useEffect(() => {
// Prefill research preferences on mount
const fetchPreferences = async () => {
if (preferencesLoaded) {
console.log('ResearchStep: Preferences already loaded, skipping API call');
return;
}
try {
console.log('ResearchStep: Loading research preferences...');
const res = await import('../../api/componentLogic');
const { getResearchPreferences } = res;
const data = await getResearchPreferences();
if (data && data.preferences) {
if (data.preferences.research_depth) setResearchDepth(data.preferences.research_depth);
if (data.preferences.content_types) setContentTypes(data.preferences.content_types);
if (typeof data.preferences.auto_research === 'boolean') setAutoResearch(data.preferences.auto_research);
if (typeof data.preferences.factual_content === 'boolean') setFactualContent(data.preferences.factual_content);
}
setPreferencesLoaded(true);
console.log('ResearchStep: Research preferences loaded successfully');
} catch (err) {
console.error('ResearchStep: Error pre-filling research preferences', err);
setPreferencesLoaded(true); // Set to true even on error to prevent infinite retries
}
};
fetchPreferences();
}, []); // Empty dependency array to run only once on mount
const loadExistingKeys = async () => {
if (keysLoaded) {
console.log('ResearchStep: Keys already loaded, skipping API call');
return; // Prevent multiple calls
}
console.log('ResearchStep: Starting to load API keys...');
try {
const keys = await getApiKeys();
console.log('ResearchStep: API keys loaded successfully:', Object.keys(keys));
setSavedKeys(keys);
if (keys.tavily) setTavilyKey(keys.tavily);
if (keys.serperapi) setSerperKey(keys.serperapi);
if (keys.exa) setExaKey(keys.exa);
if (keys.firecrawl) setFirecrawlKey(keys.firecrawl);
setKeysLoaded(true); // Set keysLoaded to true after keys are loaded
console.log('ResearchStep: Keys loaded and state updated');
} catch (error: any) {
console.error('ResearchStep: Error loading API keys:', error);
// Don't show error for rate limiting - it will retry automatically
if (error.response?.status !== 429) {
setError(`Failed to load API keys: ${error.message || 'Unknown error'}`);
}
setKeysLoaded(true); // Set to true even on error to prevent infinite retries
console.log('ResearchStep: Set keysLoaded to true after error');
}
};
// Debounced version to prevent rapid calls
const debouncedLoadKeys = debounce(() => {
console.log('ResearchStep: debouncedLoadKeys called');
loadExistingKeys();
}, 1000);
const loadWebsiteDefaults = async () => {
try {
// TODO: Load website analysis data and populate intelligent defaults
// This would be based on the website URL from step 2
// For now, we'll use sensible defaults
setCompany('Your Company');
setRole('Content Creator');
setResearchDepth('Comprehensive');
setContentTypes(['Blog Posts', 'Social Media', 'Articles']);
} catch (error) {
console.error('Error loading website defaults:', error);
}
};
const handleSave = async () => {
setLoading(true);
setError(null);
setSuccess(null);
try {
const promises = [];
// Save API keys
if (tavilyKey.trim()) {
promises.push(saveApiKey('tavily', tavilyKey.trim()));
}
if (serperKey.trim()) {
promises.push(saveApiKey('serperapi', serperKey.trim()));
}
if (exaKey.trim()) {
promises.push(saveApiKey('exa', exaKey.trim()));
}
if (firecrawlKey.trim()) {
promises.push(saveApiKey('firecrawl', firecrawlKey.trim()));
}
// Save research preferences to database
const researchPreferences = {
research_depth: researchDepth,
content_types: contentTypes,
auto_research: autoResearch,
factual_content: factualContent
};
const preferencesResponse = await configureResearchPreferences(researchPreferences);
if (!preferencesResponse.valid) {
const errorMessage = preferencesResponse.errors?.join(', ') || 'Unknown error';
const error = `Failed to save research preferences: ${errorMessage}`;
throw error;
}
await Promise.all(promises);
setSuccess('Research configuration and preferences saved successfully!');
// Auto-continue after a short delay
setTimeout(() => {
onContinue();
}, 1500);
} catch (err) {
setError(formatErrorMessage(err));
console.error('Error saving research configuration:', err);
} finally {
setLoading(false);
}
};
const researchProviders = [
{
name: 'Tavily AI',
description: 'Intelligent web research and content analysis',
benefits: ['Factual content generation', 'Real-time information', 'Comprehensive research'],
key: tavilyKey,
setKey: setTavilyKey,
showKey: showTavilyKey,
setShowKey: setShowTavilyKey,
placeholder: 'tvly-...',
status: getKeyStatus(tavilyKey, 'tavily'),
link: 'https://tavily.com/',
free: true,
recommended: true
},
{
name: 'Exa',
description: 'Advanced web search and content discovery',
benefits: ['High-quality search results', 'Content verification', 'Source credibility'],
key: exaKey,
setKey: setExaKey,
showKey: showExaKey,
setShowKey: setShowExaKey,
placeholder: 'exa-...',
status: getKeyStatus(exaKey, 'exa'),
link: 'https://exa.ai/',
free: true,
recommended: true
},
{
name: 'Serper API',
description: 'Google search results and web data',
benefits: ['Google search integration', 'Real-time data', 'Comprehensive coverage'],
key: serperKey,
setKey: setSerperKey,
showKey: showSerperKey,
setShowKey: setShowSerperKey,
placeholder: 'serper-...',
status: getKeyStatus(serperKey, 'serperapi'),
link: 'https://serper.dev/',
free: true,
recommended: false
},
{
name: 'Firecrawl',
description: 'Web content extraction and processing',
benefits: ['Content extraction', 'Data processing', 'Structured information'],
key: firecrawlKey,
setKey: setFirecrawlKey,
showKey: showFirecrawlKey,
setShowKey: setShowFirecrawlKey,
placeholder: 'firecrawl-...',
status: getKeyStatus(firecrawlKey, 'firecrawl'),
link: 'https://firecrawl.dev/',
free: true,
recommended: false
}
];
const hasAtLeastOneKey = tavilyKey.trim() || exaKey.trim() || serperKey.trim() || firecrawlKey.trim();
const isValid = fullName.trim() && email.trim() && company.trim();
return (
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
{/* Importance Notice */}
<Paper elevation={0} sx={{
p: 3,
mb: 4,
textAlign: 'left',
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid rgba(245, 158, 11, 0.2)',
borderRadius: 2
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2 }}>
<AutoAwesome sx={{ color: 'warning.main', fontSize: 24 }} />
<Typography variant="h6" color="warning.dark" sx={{ fontWeight: 600 }}>
Why Research APIs Matter
</Typography>
</Box>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Factual Content
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Generate content based on real, verified information instead of AI hallucinations.
</Typography>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<TrendingUp sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Real-time Data
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Access current information, trends, and latest developments in your industry.
</Typography>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Security sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Source Verification
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Verify facts and cite reliable sources to build trust with your audience.
</Typography>
</Grid>
</Grid>
</Paper>
{/* Research Providers */}
<Box sx={{ mb: 4 }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<Search sx={{ color: 'primary.main' }} />
Research API Providers
</Typography>
<Grid container spacing={3}>
{researchProviders.map((provider, index) => (
<Grid item xs={12} md={6} key={provider.name}>
<Zoom in={true} timeout={700 + index * 100}>
<Card
sx={{
background: provider.status === 'valid'
? 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)'
: provider.status === 'invalid'
? 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)'
: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: `2px solid ${
provider.status === 'valid'
? '#10b981'
: provider.status === 'invalid'
? '#ef4444'
: 'rgba(0,0,0,0.08)'
}`,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: provider.status === 'valid'
? '0 8px 25px rgba(16, 185, 129, 0.25), 0 0 0 1px rgba(16, 185, 129, 0.1)'
: provider.status === 'invalid'
? '0 8px 25px rgba(239, 68, 68, 0.25), 0 0 0 1px rgba(239, 68, 68, 0.1)'
: '0 8px 25px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)',
transform: 'translateY(-2px)'
},
position: 'relative',
overflow: 'hidden',
borderRadius: 3,
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 3,
background: provider.status === 'valid'
? 'linear-gradient(90deg, #10b981 0%, #059669 100%)'
: provider.status === 'invalid'
? 'linear-gradient(90deg, #ef4444 0%, #dc2626 100%)'
: 'linear-gradient(90deg, #6b7280 0%, #4b5563 100%)',
},
'&::after': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: provider.status === 'valid'
? 'radial-gradient(circle at top right, rgba(16, 185, 129, 0.1) 0%, transparent 70%)'
: provider.status === 'invalid'
? 'radial-gradient(circle at top right, rgba(239, 68, 68, 0.1) 0%, transparent 70%)'
: 'radial-gradient(circle at top right, rgba(107, 114, 128, 0.1) 0%, transparent 70%)',
pointerEvents: 'none'
}
}}
>
<CardContent sx={{ p: 2.5, position: 'relative', zIndex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{
width: 36,
height: 36,
borderRadius: '50%',
background: provider.recommended
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
}}>
<Search sx={{ color: 'white', fontSize: 18 }} />
</Box>
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.25 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 0 }}>
{provider.name}
</Typography>
{provider.recommended && (
<Chip
label="Recommended"
color="success"
size="small"
sx={{ fontWeight: 600, height: 20 }}
/>
)}
{provider.free && (
<Chip
label="Free Tier"
color="primary"
size="small"
sx={{ fontWeight: 600, height: 20 }}
/>
)}
</Box>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.875rem' }}>
{provider.description}
</Typography>
</Box>
</Box>
{provider.status === 'valid' && (
<Chip
icon={<CheckCircle />}
label="Valid"
color="success"
size="small"
sx={{ fontWeight: 600, height: 24 }}
/>
)}
{provider.status === 'invalid' && (
<Chip
icon={<ErrorIcon />}
label="Invalid"
color="error"
size="small"
sx={{ fontWeight: 600, height: 24 }}
/>
)}
</Box>
<Box sx={{ mb: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Benefits:
</Typography>
<Tooltip title="View all benefits">
<IconButton
size="small"
onClick={() => setBenefitsDialog({ open: true, provider })}
sx={{
color: 'primary.main',
'&:hover': {
background: 'rgba(59, 130, 246, 0.1)'
}
}}
>
<HelpOutline sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
</Box>
</Box>
<TextField
fullWidth
type={provider.showKey ? 'text' : 'password'}
value={provider.key}
onChange={(e) => provider.setKey(e.target.value)}
placeholder={provider.placeholder}
variant="outlined"
size="small"
InputProps={{
startAdornment: (
<Lock sx={{ color: 'text.secondary', mr: 1, fontSize: 16 }} />
),
endAdornment: (
<IconButton
onClick={() => provider.setShowKey(!provider.showKey)}
edge="end"
size="small"
>
{provider.showKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
background: 'rgba(255, 255, 255, 0.9)',
backdropFilter: 'blur(10px)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.8)',
border: '1px solid rgba(0, 0, 0, 0.08)',
transition: 'all 0.2s ease-in-out',
'&:hover': {
background: 'rgba(255, 255, 255, 0.95)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.9)',
border: '1px solid rgba(0, 0, 0, 0.12)'
},
'&.Mui-focused': {
background: 'rgba(255, 255, 255, 0.98)',
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1), 0 4px 12px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.95)',
border: '1px solid rgba(59, 130, 246, 0.3)'
}
}
}}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<LinkIcon sx={{ color: 'text.secondary', fontSize: 14 }} />
<Link
href={provider.link}
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
fontWeight: 600,
fontSize: '0.875rem'
}}
>
Get API Key
<Launch sx={{ fontSize: 14 }} />
</Link>
</Box>
{savedKeys[provider.name.toLowerCase()] && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="caption" color="success.main" sx={{ fontWeight: 500 }}>
Key already saved and secured
</Typography>
</Box>
)}
</CardContent>
</Card>
</Zoom>
</Grid>
))}
</Grid>
</Box>
{/* Research Preferences */}
<Zoom in={true} timeout={1400}>
<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
}}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<School sx={{ color: 'success.main' }} />
Research Preferences
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<FormControl fullWidth>
<InputLabel>Research Depth</InputLabel>
<Select
value={researchDepth}
onChange={(e) => setResearchDepth(e.target.value)}
label="Research Depth"
size="medium"
>
<MenuItem value="Basic">Basic - Quick overview</MenuItem>
<MenuItem value="Standard">Standard - Balanced depth</MenuItem>
<MenuItem value="Comprehensive">Comprehensive - Detailed analysis</MenuItem>
<MenuItem value="Expert">Expert - In-depth research</MenuItem>
</Select>
<FormHelperText>Choose how detailed you want the AI research to be</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}>
<FormControl fullWidth>
<InputLabel>Content Types</InputLabel>
<Select
multiple
value={contentTypes}
onChange={(e) => setContentTypes(typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value)}
input={<OutlinedInput label="Content Types" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} size="small" />
))}
</Box>
)}
size="medium"
>
<MenuItem value="Blog Posts">Blog Posts</MenuItem>
<MenuItem value="Social Media">Social Media</MenuItem>
<MenuItem value="Articles">Articles</MenuItem>
<MenuItem value="Email Newsletters">Email Newsletters</MenuItem>
<MenuItem value="Product Descriptions">Product Descriptions</MenuItem>
<MenuItem value="Landing Pages">Landing Pages</MenuItem>
</Select>
<FormHelperText>Choose what types of content you want to research</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={12}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControlLabel
control={
<Switch
checked={autoResearch}
onChange={(e) => setAutoResearch(e.target.checked)}
color="primary"
/>
}
label="Enable Automated Research"
/>
<Typography variant="body2" color="text.secondary" sx={{ ml: 4 }}>
Automatically start research when content topics are added
</Typography>
<FormControlLabel
control={
<Switch
checked={factualContent}
onChange={(e) => setFactualContent(e.target.checked)}
color="primary"
/>
}
label="Prioritize Factual Content"
/>
<Typography variant="body2" color="text.secondary" sx={{ ml: 4 }}>
Focus on generating content based on verified facts and sources
</Typography>
</Box>
</Grid>
</Grid>
</Paper>
</Zoom>
{/* Help Section */}
<Collapse in={showHelp}>
<Zoom in={showHelp} timeout={1600}>
<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="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<HelpOutline color="primary" />
How to Get Your Research API Keys
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<Star sx={{ color: 'warning.main', fontSize: 20 }} />
Recommended Providers
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Tavily AI
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
Visit{' '}
<Link href="https://tavily.com/" target="_blank" rel="noopener noreferrer" sx={{ fontWeight: 600 }}>
tavily.com
</Link>
, sign up for free, and get your API key from the dashboard.
</Typography>
</Box>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Exa
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
Visit{' '}
<Link href="https://exa.ai/" target="_blank" rel="noopener noreferrer" sx={{ fontWeight: 600 }}>
exa.ai
</Link>
, create an account, and access your API key in the settings.
</Typography>
</Box>
</Box>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<Info sx={{ color: 'info.main', fontSize: 20 }} />
Why These APIs Matter
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Typography variant="body2" color="text.secondary">
<strong>Factual Content:</strong> Generate content based on real, verified information instead of AI hallucinations.
</Typography>
<Typography variant="body2" color="text.secondary">
<strong>Real-time Data:</strong> Access current information, trends, and latest developments in your industry.
</Typography>
<Typography variant="body2" color="text.secondary">
<strong>Source Verification:</strong> Verify facts and cite reliable sources to build trust with your audience.
</Typography>
<Typography variant="body2" color="text.secondary">
<strong>Free Tiers:</strong> Most providers offer generous free tiers to get you started.
</Typography>
</Box>
</Box>
</Grid>
</Grid>
</Paper>
</Zoom>
</Collapse>
{/* Alerts */}
<Box sx={{ mt: 3 }}>
{error && (
<Fade in={true}>
<Alert severity="error" sx={{ mb: 2, borderRadius: 2 }}>
{error}
</Alert>
</Fade>
)}
{success && (
<Fade in={true}>
<Alert severity="success" sx={{ mb: 2, borderRadius: 2 }}>
{success}
</Alert>
</Fade>
)}
</Box>
{/* Action Buttons */}
<Box sx={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'center', mt: 4 }}>
<OnboardingButton
variant="text"
onClick={() => setShowHelp(!showHelp)}
icon={<HelpOutline />}
>
{showHelp ? 'Hide Help' : 'Get Help'}
</OnboardingButton>
</Box>
{/* Security Notice */}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5 }}>
<Lock sx={{ fontSize: 14 }} />
Your API keys are encrypted and stored securely on your device
</Typography>
</Box>
{/* Benefits Dialog */}
<Dialog
open={benefitsDialog.open}
onClose={() => setBenefitsDialog({ open: false, provider: null })}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.1)'
}
}}
>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white',
borderRadius: '12px 12px 0 0'
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Search sx={{ fontSize: 24 }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
{benefitsDialog.provider?.name} Benefits
</Typography>
</Box>
<IconButton
onClick={() => setBenefitsDialog({ open: false, provider: null })}
sx={{ color: 'white' }}
>
<Close />
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 3 }}>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
{benefitsDialog.provider?.description}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{benefitsDialog.provider?.benefits.map((benefit: string, index: number) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{
width: 32,
height: 32,
borderRadius: '50%',
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<CheckCircle sx={{ color: 'white', fontSize: 18 }} />
</Box>
<Typography variant="body1" sx={{ fontWeight: 500 }}>
{benefit}
</Typography>
</Box>
))}
</Box>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
<Button
variant="outlined"
onClick={() => setBenefitsDialog({ open: false, provider: null })}
sx={{ borderRadius: 2 }}
>
Close
</Button>
<Button
variant="contained"
onClick={() => {
if (benefitsDialog.provider?.link) {
window.open(benefitsDialog.provider.link, '_blank');
}
}}
sx={{
borderRadius: 2,
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
'&:hover': {
background: 'linear-gradient(135deg, #059669 0%, #047857 100%)'
}
}}
>
Get API Key
</Button>
</DialogActions>
</Dialog>
</Container>
</Fade>
);
};
export default ResearchStep;

View File

@@ -0,0 +1,280 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
TextField,
Typography,
Alert,
CircularProgress,
Card,
CardContent,
CardActions,
Grid,
Chip,
Divider
} from '@mui/material';
import { getApiKeys } from '../../api/onboarding';
import {
processResearchTopic,
processResearchResults,
validateResearchRequest,
getResearchProvidersInfo,
generateResearchReport,
ResearchTopicRequest
} from '../../api/componentLogic';
const ResearchTestStep: React.FC<{ onContinue: () => void }> = ({ onContinue }) => {
const [topic, setTopic] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [researchResults, setResearchResults] = useState<any>(null);
const [providersInfo, setProvidersInfo] = useState<any>(null);
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
useEffect(() => {
async function loadData() {
try {
// Load API keys
const keys = await getApiKeys();
setApiKeys(keys);
// Load providers info
const providers = await getResearchProvidersInfo();
setProvidersInfo(providers.providers_info);
} catch (e) {
console.error('Failed to load research data:', e);
}
}
loadData();
}, []);
const handleResearch = async () => {
if (!topic.trim()) {
setError('Please enter a research topic.');
return;
}
setLoading(true);
setError(null);
setSuccess(null);
setResearchResults(null);
try {
// Validate research request
const validation = await validateResearchRequest(topic, apiKeys);
if (!validation.valid) {
setError(`Research validation failed: ${validation.errors.join(', ')}`);
if (validation.warnings.length > 0) {
console.warn('Research warnings:', validation.warnings);
}
setLoading(false);
return;
}
// Process research topic
const request: ResearchTopicRequest = {
topic: topic.trim(),
api_keys: apiKeys
};
const results = await processResearchTopic(request);
if (!results.success) {
setError(`Research failed: ${results.error}`);
setLoading(false);
return;
}
// Process research results
const processedResults = await processResearchResults(results);
if (processedResults.success) {
setResearchResults(processedResults.processed_results);
setSuccess('Research completed successfully!');
} else {
setError('Failed to process research results.');
}
} catch (e) {
setError('Research failed. Please try again.');
console.error('Research error:', e);
} finally {
setLoading(false);
}
};
const handleGenerateReport = async () => {
if (!researchResults) {
setError('No research results available to generate report.');
return;
}
setLoading(true);
try {
const report = await generateResearchReport({ processed_results: researchResults });
if (report.success) {
setSuccess('Research report generated successfully!');
console.log('Generated report:', report.report);
} else {
setError('Failed to generate research report.');
}
} catch (e) {
setError('Failed to generate research report.');
console.error('Report generation error:', e);
} finally {
setLoading(false);
}
};
const availableProviders = providersInfo ? Object.keys(providersInfo.providers).filter(
provider => apiKeys[providersInfo.providers[provider].api_key_name]
) : [];
return (
<Box>
<Typography variant="h6" gutterBottom>
Test Research Functionality
</Typography>
<Typography variant="body2" color="textSecondary" gutterBottom>
Test the AI research capabilities with your configured settings and API keys.
</Typography>
{/* Research Input */}
<Card sx={{ mb: 2 }}>
<CardContent>
<Typography variant="subtitle1" gutterBottom>
Research Topic
</Typography>
<TextField
label="Enter a topic to research"
value={topic}
onChange={(e) => setTopic(e.target.value)}
fullWidth
multiline
rows={2}
placeholder="e.g., 'Latest trends in artificial intelligence'"
disabled={loading}
/>
{availableProviders.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="caption" color="textSecondary">
Available providers: {availableProviders.map(provider => (
<Chip key={provider} label={provider} size="small" sx={{ mr: 0.5 }} />
))}
</Typography>
</Box>
)}
</CardContent>
<CardActions>
<Button
variant="contained"
onClick={handleResearch}
disabled={loading || !topic.trim()}
>
{loading ? 'Researching...' : 'Start Research'}
</Button>
</CardActions>
</Card>
{/* Research Results */}
{researchResults && (
<Card sx={{ mb: 2 }}>
<CardContent>
<Typography variant="subtitle1" gutterBottom>
Research Results
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="body2" color="textSecondary">
<strong>Topic:</strong> {researchResults.topic}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body2" color="textSecondary">
<strong>Summary:</strong>
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
{researchResults.summary}
</Typography>
</Grid>
{researchResults.key_insights && researchResults.key_insights.length > 0 && (
<Grid item xs={12} md={6}>
<Typography variant="body2" color="textSecondary">
<strong>Key Insights:</strong>
</Typography>
<Box sx={{ mt: 1 }}>
{researchResults.key_insights.map((insight: string, index: number) => (
<Chip
key={index}
label={insight}
size="small"
sx={{ mr: 0.5, mb: 0.5 }}
/>
))}
</Box>
</Grid>
)}
{researchResults.trends && researchResults.trends.length > 0 && (
<Grid item xs={12} md={6}>
<Typography variant="body2" color="textSecondary">
<strong>Trends:</strong>
</Typography>
<Box sx={{ mt: 1 }}>
{researchResults.trends.map((trend: string, index: number) => (
<Chip
key={index}
label={trend}
size="small"
variant="outlined"
sx={{ mr: 0.5, mb: 0.5 }}
/>
))}
</Box>
</Grid>
)}
{researchResults.metadata && (
<Grid item xs={12}>
<Divider sx={{ my: 1 }} />
<Typography variant="caption" color="textSecondary">
<strong>Research Details:</strong>
Confidence: {Math.round((researchResults.metadata.confidence_score || 0) * 100)}% |
Depth: {researchResults.metadata.research_depth} |
Providers: {researchResults.metadata.providers_used?.join(', ')}
</Typography>
</Grid>
)}
</Grid>
</CardContent>
<CardActions>
<Button
variant="outlined"
onClick={handleGenerateReport}
disabled={loading}
>
Generate Report
</Button>
</CardActions>
</Card>
)}
{error && <Alert severity="error" sx={{ mt: 2 }}>{error}</Alert>}
{success && <Alert severity="success" sx={{ mt: 2 }}>{success}</Alert>}
<Button
variant="contained"
color="primary"
onClick={onContinue}
sx={{ mt: 2 }}
>
Continue to Next Step
</Button>
</Box>
);
};
export default ResearchTestStep;

View File

@@ -0,0 +1,330 @@
import React, { useState } from 'react';
import {
Box,
Typography,
TextField,
Button,
Alert,
CircularProgress,
Card,
CardContent,
Grid,
Chip,
Accordion,
AccordionSummary,
AccordionDetails,
Divider,
IconButton,
Tooltip
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
ContentCopy as CopyIcon,
CheckCircle as CheckIcon,
Error as ErrorIcon,
Info as InfoIcon
} from '@mui/icons-material';
import { useOnboardingStyles } from './common/useOnboardingStyles';
interface StyleDetectionStepProps {
onContinue: () => void;
}
interface StyleAnalysis {
writing_style?: {
tone: string;
voice: string;
complexity: string;
engagement_level: string;
};
content_characteristics?: {
sentence_structure: string;
vocabulary_level: string;
paragraph_organization: string;
content_flow: string;
};
target_audience?: {
demographics: string[];
expertise_level: string;
industry_focus: string;
geographic_focus: string;
};
recommended_settings?: {
writing_tone: string;
target_audience: string;
content_type: string;
creativity_level: string;
geographic_location: string;
};
}
const StyleDetectionStep: React.FC<StyleDetectionStepProps> = ({ onContinue }) => {
const classes = useOnboardingStyles();
const [url, setUrl] = useState('');
const [textSample, setTextSample] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [analysis, setAnalysis] = useState<StyleAnalysis | null>(null);
const [activeTab, setActiveTab] = useState<'url' | 'text'>('url');
const handleAnalyze = async () => {
setError(null);
setSuccess(null);
setLoading(true);
try {
// Validate and fix URL format if using URL tab
let requestUrl = url;
if (activeTab === 'url') {
const fixedUrl = fixUrlFormat(url);
if (!fixedUrl) {
setError('Please enter a valid website URL (starting with http:// or https://)');
setLoading(false);
return;
}
requestUrl = fixedUrl;
}
const requestData = {
url: activeTab === 'url' ? requestUrl : undefined,
text_sample: activeTab === 'text' ? textSample : undefined,
include_patterns: true,
include_guidelines: true
};
const response = await fetch('/api/onboarding/style-detection/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
});
const result = await response.json();
if (result.success) {
setAnalysis(result.style_analysis);
setSuccess('Style analysis completed successfully!');
} else {
setError(result.error || 'Analysis failed');
}
} catch (err) {
setError('Failed to analyze content. Please try again.');
} finally {
setLoading(false);
}
};
const fixUrlFormat = (url: string): string | null => {
if (!url) return null;
// Remove leading/trailing whitespace
let fixedUrl = url.trim();
// Check if URL already has a protocol but is missing slashes
if (fixedUrl.startsWith('https:/') && !fixedUrl.startsWith('https://')) {
fixedUrl = fixedUrl.replace('https:/', 'https://');
} else if (fixedUrl.startsWith('http:/') && !fixedUrl.startsWith('http://')) {
fixedUrl = fixedUrl.replace('http:/', 'http://');
}
// Add protocol if missing
if (!fixedUrl.startsWith('http://') && !fixedUrl.startsWith('https://')) {
fixedUrl = 'https://' + fixedUrl;
}
// Fix missing slash after protocol
if (fixedUrl.includes('://') && !fixedUrl.split('://')[1].startsWith('/')) {
fixedUrl = fixedUrl.replace('://', ':///');
}
// Ensure only two slashes after protocol
if (fixedUrl.includes(':///')) {
fixedUrl = fixedUrl.replace(':///', '://');
}
// Basic URL validation
try {
new URL(fixedUrl);
return fixedUrl;
} catch {
return null;
}
};
const handleContinue = () => {
if (analysis) {
onContinue();
} else {
setError('Please complete style analysis before continuing');
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const renderAnalysisSection = (title: string, data: any, icon: React.ReactNode) => (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box display="flex" alignItems="center" gap={1}>
{icon}
<Typography variant="h6">{title}</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
{Object.entries(data).map(([key, value]) => (
<Grid item xs={12} sm={6} key={key}>
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</Typography>
<Typography variant="body2">
{Array.isArray(value) ? value.join(', ') : String(value)}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</AccordionDetails>
</Accordion>
);
return (
<Box sx={classes.container}>
<Typography variant="h4" gutterBottom sx={classes.headerTitle}>
🎨 Style Detection
</Typography>
<Typography variant="body1" color="textSecondary" gutterBottom>
Analyze your writing style to get personalized content generation recommendations.
</Typography>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Content Source
</Typography>
<Box mb={3}>
<Button
variant={activeTab === 'url' ? 'contained' : 'outlined'}
onClick={() => setActiveTab('url')}
sx={{ mr: 2 }}
>
Website URL
</Button>
<Button
variant={activeTab === 'text' ? 'contained' : 'outlined'}
onClick={() => setActiveTab('text')}
>
Text Sample
</Button>
</Box>
{activeTab === 'url' ? (
<TextField
fullWidth
label="Website URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://yourwebsite.com"
helperText="Enter your website URL to analyze your content style"
margin="normal"
/>
) : (
<TextField
fullWidth
multiline
rows={6}
label="Text Sample"
value={textSample}
onChange={(e) => setTextSample(e.target.value)}
placeholder="Paste your content samples here..."
helperText="Provide 2-3 samples of your best content (min 50 characters)"
margin="normal"
/>
)}
<Box mt={3}>
<Button
variant="contained"
onClick={handleAnalyze}
disabled={loading || (!url && !textSample)}
startIcon={loading ? <CircularProgress size={20} /> : null}
fullWidth
>
{loading ? 'Analyzing...' : 'Analyze Style'}
</Button>
</Box>
</CardContent>
</Card>
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mt: 2 }}>
{success}
</Alert>
)}
{analysis && (
<Card sx={{ mt: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Style Analysis Results
</Typography>
{analysis.writing_style && renderAnalysisSection(
'Writing Style',
analysis.writing_style,
<InfoIcon color="primary" />
)}
{analysis.content_characteristics && renderAnalysisSection(
'Content Characteristics',
analysis.content_characteristics,
<InfoIcon color="secondary" />
)}
{analysis.target_audience && renderAnalysisSection(
'Target Audience',
analysis.target_audience,
<InfoIcon color="success" />
)}
{analysis.recommended_settings && renderAnalysisSection(
'Recommended Settings',
analysis.recommended_settings,
<CheckIcon color="primary" />
)}
</CardContent>
</Card>
)}
<Box mt={3} display="flex" justifyContent="space-between">
<Button variant="outlined" disabled>
Previous
</Button>
<Button
variant="contained"
onClick={handleContinue}
disabled={!analysis}
endIcon={<CheckIcon />}
>
Continue
</Button>
</Box>
</Box>
);
};
export default StyleDetectionStep;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,557 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
Box,
Stepper,
Step,
StepLabel,
Button,
Typography,
Paper,
LinearProgress,
Fade,
Slide,
useTheme,
useMediaQuery,
IconButton,
Tooltip,
Container
} from '@mui/material';
import {
ArrowBack,
ArrowForward,
CheckCircle,
HelpOutline,
Close
} from '@mui/icons-material';
import { startOnboarding, getCurrentStep, setCurrentStep, getProgress } from '../../api/onboarding';
import ApiKeyStep from './ApiKeyStep';
import WebsiteStep from './WebsiteStep';
import ResearchStep from './ResearchStep';
import PersonalizationStep from './PersonalizationStep';
import IntegrationsStep from './IntegrationsStep';
import FinalStep from './FinalStep';
const steps = [
{ label: 'API Keys', description: 'Connect your AI services', icon: '🔑' },
{ label: 'Website', description: 'Set up your website', icon: '🌐' },
{ label: 'Research', description: 'Configure research tools', icon: '🔍' },
{ label: 'Personalization', description: 'Customize your experience', icon: '⚙️' },
{ label: 'Integrations', description: 'Connect additional services', icon: '🔗' },
{ label: 'Finish', description: 'Complete setup', icon: '✅' }
];
interface WizardProps {
onComplete?: () => void;
}
interface StepHeaderContent {
title: string;
description: string;
}
const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(true);
const [progress, setProgressState] = useState(0);
const [direction, setDirection] = useState<'left' | 'right'>('right');
const [showHelp, setShowHelp] = useState(false);
const [showProgressMessage, setShowProgressMessage] = useState(false);
const [progressMessage, setProgressMessage] = useState('');
const [stepHeaderContent, setStepHeaderContent] = useState<StepHeaderContent>({
title: steps[0].label,
description: steps[0].description
});
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
useEffect(() => {
console.log('Wizard: Component mounted');
const init = async () => {
try {
setLoading(true);
console.log('Wizard: Starting initialization...');
// Check if there's existing progress first
const stepResponse = await getCurrentStep();
console.log('Wizard: Backend returned step:', stepResponse.step);
// Only start onboarding if we're at step 1 (no progress)
if (stepResponse.step === 1) {
console.log('Wizard: No existing progress, starting new onboarding');
await startOnboarding();
} else {
console.log('Wizard: Existing progress found, continuing from step:', stepResponse.step);
}
// Get the current step and progress
const finalStepResponse = await getCurrentStep();
const progressResponse = await getProgress();
console.log('Wizard: Final step:', finalStepResponse.step);
console.log('Wizard: Backend returned progress:', progressResponse.progress);
console.log('Wizard: Setting activeStep to:', finalStepResponse.step - 1);
setActiveStep(finalStepResponse.step - 1);
setProgressState(progressResponse.progress);
console.log('Wizard: Initialization complete');
} catch (error) {
console.error('Error initializing onboarding:', error);
} finally {
setLoading(false);
}
};
init();
}, []);
const handleNext = async () => {
console.log('Wizard: handleNext called');
console.log('Wizard: Current activeStep:', activeStep);
console.log('Wizard: Steps length:', steps.length);
setDirection('right');
const nextStep = activeStep + 1;
console.log('Wizard: Next step will be:', nextStep);
// Show progress message
const newProgress = ((nextStep + 1) / steps.length) * 100;
setProgressMessage(`Your data is saved, moving to the next step. Progress is ${Math.round(newProgress)}%`);
setShowProgressMessage(true);
// Hide message after 3 seconds
setTimeout(() => {
setShowProgressMessage(false);
}, 3000);
// Complete the current step (activeStep + 1 because steps are 1-indexed)
const currentStepNumber = activeStep + 1;
console.log('Wizard: Completing current step:', currentStepNumber);
await setCurrentStep(currentStepNumber);
// Check what step the backend thinks we should be on after completion
console.log('Wizard: Checking backend step after completion...');
const stepResponse = await getCurrentStep();
console.log('Wizard: Backend says current step should be:', stepResponse.step);
setActiveStep(nextStep);
console.log('Wizard: Setting activeStep to:', nextStep);
// Update progress
setProgressState(newProgress);
// If this is the final step, call onComplete
if (nextStep === steps.length - 1) {
console.log('Wizard: This is the final step, calling onComplete');
onComplete?.();
} else {
console.log('Wizard: Not the final step, continuing to next step');
}
};
const handleBack = async () => {
setDirection('left');
const prevStep = activeStep - 1;
setActiveStep(prevStep);
await setCurrentStep(prevStep + 1);
// Update progress
const newProgress = ((prevStep + 1) / steps.length) * 100;
setProgressState(newProgress);
};
const handleStepClick = (stepIndex: number) => {
if (stepIndex <= activeStep) {
setDirection(stepIndex > activeStep ? 'right' : 'left');
setActiveStep(stepIndex);
setCurrentStep(stepIndex + 1);
}
};
const updateHeaderContent = useCallback((content: StepHeaderContent) => {
setStepHeaderContent(content);
}, []);
const handleComplete = async () => {
console.log('Wizard: handleComplete called - completing onboarding');
try {
// Call onComplete to notify parent component
onComplete?.();
} catch (error) {
console.error('Error completing onboarding:', error);
}
};
const renderStepContent = (step: number) => {
const stepComponents = [
<ApiKeyStep key="api-keys" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<WebsiteStep key="website" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<ResearchStep key="research" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<PersonalizationStep key="personalization" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<IntegrationsStep key="integrations" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<FinalStep key="final" onContinue={handleComplete} updateHeaderContent={updateHeaderContent} />
];
return (
<Slide direction={direction} in={true} mountOnEnter unmountOnExit>
<Box sx={{ minHeight: '500px', display: 'flex', flexDirection: 'column' }}>
{stepComponents[step]}
</Box>
</Slide>
);
};
if (loading) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="100vh"
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}
>
<Fade in={true}>
<Paper
elevation={24}
sx={{
p: 4,
borderRadius: 3,
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
maxWidth: 400,
width: '100%',
}}
>
<Typography variant="h5" align="center" gutterBottom sx={{ fontWeight: 600 }}>
Setting up your workspace...
</Typography>
<LinearProgress
sx={{
mt: 3,
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(0,0,0,0.08)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
}
}}
/>
</Paper>
</Fade>
</Box>
);
}
return (
<Box
sx={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: { xs: 2, md: 4 },
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%)',
pointerEvents: 'none',
}
}}
>
<Paper
elevation={24}
sx={{
maxWidth: { xs: '100%', md: '1200px' },
width: '100%',
borderRadius: 4,
overflow: 'hidden',
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
position: 'relative',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
}}
>
{/* Header with Stepper */}
<Box
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
p: { xs: 3, md: 4 },
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%)',
pointerEvents: 'none',
}
}}
>
{/* Progress Message */}
{showProgressMessage && (
<Fade in={showProgressMessage}>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
background: 'rgba(16, 185, 129, 0.9)',
color: 'white',
p: 2,
textAlign: 'center',
zIndex: 10,
backdropFilter: 'blur(10px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.2)'
}}
>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{progressMessage}
</Typography>
</Box>
</Fade>
)}
{/* Top Row - Title and Actions */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3, position: 'relative', zIndex: 1 }}>
<Box sx={{ flex: 1 }} />
<Box sx={{ flex: 2, textAlign: 'center' }}>
<Typography variant="h4" sx={{ fontWeight: 700, letterSpacing: '-0.025em' }}>
{stepHeaderContent.title}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1, flex: 1, justifyContent: 'flex-end' }}>
<Tooltip title="Get Help" arrow>
<IconButton
onClick={() => setShowHelp(!showHelp)}
sx={{
color: 'white',
bgcolor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)',
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.2)',
}
}}
>
<HelpOutline />
</IconButton>
</Tooltip>
<Tooltip title="Skip for now" arrow>
<IconButton
sx={{
color: 'white',
bgcolor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)',
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.2)',
}
}}
>
<Close />
</IconButton>
</Tooltip>
</Box>
</Box>
{/* Progress Bar */}
<Box sx={{ mb: 3, position: 'relative', zIndex: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 500 }}>
Setup Progress
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 600 }}>
{Math.round(progress)}% Complete
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.2)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #fff 0%, #f8fafc 100%)',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}
}}
/>
</Box>
{/* Stepper in Header */}
<Box sx={{ position: 'relative', zIndex: 1 }}>
<Stepper
activeStep={activeStep}
alternativeLabel={!isMobile}
sx={{
'& .MuiStepLabel-root': {
cursor: 'pointer',
},
'& .MuiStepLabel-label': {
fontSize: '0.875rem',
fontWeight: 600,
color: 'white',
},
'& .MuiStepLabel-labelContainer': {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
'& .MuiStepLabel-label.Mui-completed': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiStepLabel-label.Mui-active': {
color: 'white',
},
'& .MuiStepLabel-label.Mui-disabled': {
color: 'rgba(255, 255, 255, 0.6)',
},
}}
>
{steps.map((step, index) => (
<Step key={step.label}>
<StepLabel
onClick={() => handleStepClick(index)}
sx={{
cursor: index <= activeStep ? 'pointer' : 'default',
'& .MuiStepLabel-iconContainer': {
background: index <= activeStep
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(255, 255, 255, 0.1)',
borderRadius: '50%',
width: 40,
height: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: index <= activeStep ? 'white' : 'rgba(255, 255, 255, 0.6)',
fontSize: '1.2rem',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: index <= activeStep
? '0 4px 12px rgba(255, 255, 255, 0.2)'
: 'none',
'&:hover': {
transform: index <= activeStep ? 'scale(1.05)' : 'none',
boxShadow: index <= activeStep
? '0 6px 16px rgba(255, 255, 255, 0.3)'
: 'none',
}
},
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
<Typography variant="h6" sx={{ mb: 0.5 }}>
{step.icon}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'center' }}>
{step.label}
</Typography>
</Box>
</StepLabel>
</Step>
))}
</Stepper>
</Box>
</Box>
{/* Content */}
<Box sx={{ p: { xs: 2, md: 3 }, pt: 2 }}>
<Fade in={true} timeout={400}>
<Box>
{renderStepContent(activeStep)}
</Box>
</Fade>
</Box>
{/* Navigation */}
<Box
sx={{
p: { xs: 2, md: 3 },
pt: 2,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderTop: '1px solid rgba(0,0,0,0.08)',
background: 'rgba(0,0,0,0.02)',
}}
>
<Button
variant="outlined"
onClick={handleBack}
disabled={activeStep === 0}
startIcon={<ArrowBack />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
borderColor: 'rgba(0,0,0,0.2)',
color: 'text.primary',
'&:hover': {
borderColor: 'rgba(0,0,0,0.4)',
background: 'rgba(0,0,0,0.04)',
},
'&:disabled': {
borderColor: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.3)',
}
}}
>
Back
</Button>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" sx={{ opacity: 0.7, fontWeight: 500 }}>
Step {activeStep + 1} of {steps.length}
</Typography>
{activeStep === steps.length - 1 && (
<CheckCircle sx={{ color: 'success.main', fontSize: 20 }} />
)}
</Box>
<Button
variant="contained"
onClick={handleNext}
disabled={activeStep === steps.length - 1}
endIcon={activeStep === steps.length - 1 ? <CheckCircle /> : <ArrowForward />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
}
}}
>
{activeStep === steps.length - 1 ? 'Complete Setup' : 'Continue'}
</Button>
</Box>
</Paper>
</Box>
);
};
export default Wizard;

View File

@@ -0,0 +1,165 @@
import React from 'react';
import { Button, Box, CircularProgress } from '@mui/material';
import { ReactNode } from 'react';
interface OnboardingButtonProps {
variant?: 'primary' | 'secondary' | 'text';
loading?: boolean;
children: ReactNode;
icon?: ReactNode;
iconPosition?: 'start' | 'end';
onClick?: () => void;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
fullWidth?: boolean;
size?: 'small' | 'medium' | 'large';
[key: string]: any;
}
const OnboardingButton: React.FC<OnboardingButtonProps> = ({
variant = 'primary',
loading = false,
children,
icon,
iconPosition = 'start',
onClick,
disabled,
type = 'button',
fullWidth = false,
size = 'medium',
...props
}) => {
const baseStyles = {
borderRadius: 2,
textTransform: 'none' as const,
fontWeight: 600,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative' as const,
overflow: 'hidden' as const,
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
opacity: 0,
transition: 'opacity 0.3s ease',
},
'&:hover::before': {
opacity: 1,
},
};
const getStyles = () => {
const sizeStyles = {
small: { px: 2, py: 1, fontSize: '0.875rem' },
medium: { px: 3, py: 1.5, fontSize: '1rem' },
large: { px: 4, py: 2, fontSize: '1.125rem' },
};
switch (variant) {
case 'primary':
return {
...baseStyles,
...sizeStyles[size],
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
color: 'white',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:active': {
transform: 'translateY(0px)',
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
},
};
case 'secondary':
return {
...baseStyles,
...sizeStyles[size],
borderColor: 'rgba(0,0,0,0.2)',
color: 'text.primary',
background: 'transparent',
'&:hover': {
borderColor: 'rgba(0,0,0,0.4)',
background: 'rgba(0,0,0,0.04)',
transform: 'translateY(-1px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
},
'&:active': {
transform: 'translateY(0px)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
},
'&:disabled': {
borderColor: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.3)',
background: 'transparent',
transform: 'none',
boxShadow: 'none',
}
};
case 'text':
return {
...baseStyles,
...sizeStyles[size],
color: 'primary.main',
background: 'transparent',
'&:hover': {
background: 'rgba(102, 126, 234, 0.08)',
transform: 'translateY(-1px)',
},
'&:active': {
transform: 'translateY(0px)',
},
'&:disabled': {
color: 'rgba(0,0,0,0.3)',
background: 'transparent',
transform: 'none',
}
};
default:
return baseStyles;
}
};
const buttonVariant = variant === 'primary' ? 'contained' : variant === 'secondary' ? 'outlined' : 'text';
return (
<Button
variant={buttonVariant}
onClick={onClick}
disabled={loading || disabled}
type={type}
fullWidth={fullWidth}
startIcon={iconPosition === 'start' && icon && !loading ? icon : undefined}
endIcon={iconPosition === 'end' && icon && !loading ? icon : undefined}
sx={getStyles()}
{...props}
>
{loading ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress
size={size === 'small' ? 16 : size === 'large' ? 24 : 20}
color="inherit"
thickness={4}
/>
{children}
</Box>
) : (
children
)}
</Button>
);
};
export default OnboardingButton;

View File

@@ -0,0 +1,185 @@
import React from 'react';
import {
Card,
CardContent,
Box,
Typography,
Chip,
Zoom,
useTheme,
Paper
} from '@mui/material';
import { ReactNode } from 'react';
interface OnboardingCardProps {
title: string;
icon: ReactNode;
children: ReactNode;
status?: 'valid' | 'invalid' | 'empty';
statusLabel?: string;
elevation?: number;
delay?: number;
saved?: boolean;
variant?: 'default' | 'info' | 'warning' | 'success';
}
const OnboardingCard: React.FC<OnboardingCardProps> = ({
title,
icon,
children,
status,
statusLabel,
elevation = 2,
delay = 0,
saved = false,
variant = 'default'
}) => {
const theme = useTheme();
const getStatusColor = () => {
switch (status) {
case 'valid':
return '#10b981';
case 'invalid':
return '#ef4444';
default:
return 'transparent';
}
};
const getStatusChip = () => {
if (!status || status === 'empty') return null;
return (
<Chip
icon={status === 'valid' ? <Box component="span"></Box> : <Box component="span"></Box>}
label={statusLabel || (status === 'valid' ? 'Valid' : 'Invalid')}
color={status === 'valid' ? 'success' : 'error'}
size="small"
sx={{ fontWeight: 600, borderRadius: 1 }}
/>
);
};
const getVariantStyles = () => {
switch (variant) {
case 'info':
return {
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid rgba(59, 130, 246, 0.2)',
};
case 'warning':
return {
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid rgba(245, 158, 11, 0.2)',
};
case 'success':
return {
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid rgba(16, 185, 129, 0.2)',
};
default:
return {
background: 'white',
border: `2px solid ${getStatusColor()}`,
};
}
};
return (
<Zoom in={true} timeout={700 + delay}>
<Card
elevation={elevation}
sx={{
...getVariantStyles(),
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden',
'&:hover': {
elevation: 4,
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
},
'&::before': variant === 'default' ? {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 3,
background: status === 'valid'
? 'linear-gradient(90deg, #10b981 0%, #059669 100%)'
: status === 'invalid'
? 'linear-gradient(90deg, #ef4444 0%, #dc2626 100%)'
: 'linear-gradient(90deg, #6b7280 0%, #4b5563 100%)',
} : {},
}}
>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: '50%',
background: variant === 'default'
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: variant === 'info'
? 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'
: variant === 'warning'
? 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}>
{React.cloneElement(icon as React.ReactElement, {
sx: { color: 'white', fontSize: 20 }
})}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 0.5 }}>
{title}
</Typography>
{variant !== 'default' && (
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.4 }}>
{children}
</Typography>
)}
</Box>
</Box>
{getStatusChip()}
</Box>
{variant === 'default' && children}
{saved && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1.5 }}>
<Box component="span" sx={{
width: 16,
height: 16,
borderRadius: '50%',
background: '#10b981',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
color: 'white',
fontWeight: 'bold'
}}>
</Box>
<Typography variant="caption" color="success.main" sx={{ fontWeight: 500 }}>
Already saved and secured
</Typography>
</Box>
)}
</CardContent>
</Card>
</Zoom>
);
};
export default OnboardingCard;

View File

@@ -0,0 +1,124 @@
import React from 'react';
import {
Box,
Typography,
Fade,
Zoom,
useTheme,
Container
} from '@mui/material';
import { ReactNode } from 'react';
interface OnboardingStepLayoutProps {
icon: ReactNode;
title: string;
subtitle: string;
children: ReactNode;
maxWidth?: number | string;
showIcon?: boolean;
centered?: boolean;
}
const OnboardingStepLayout: React.FC<OnboardingStepLayoutProps> = ({
icon,
title,
subtitle,
children,
maxWidth = 800,
showIcon = true,
centered = true
}) => {
const theme = useTheme();
return (
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
{/* Header */}
<Box sx={{
textAlign: centered ? 'center' : 'left',
mb: 4,
maxWidth: maxWidth,
mx: centered ? 'auto' : 0
}}>
<Zoom in={true} timeout={600}>
<Box>
{showIcon && (
<Box sx={{
mb: 3,
display: 'flex',
justifyContent: centered ? 'center' : 'flex-start',
position: 'relative'
}}>
<Box sx={{
width: 80,
height: 80,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 25px rgba(102, 126, 234, 0.3)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: -2,
left: -2,
right: -2,
bottom: -2,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
opacity: 0.3,
zIndex: -1,
}
}}>
{React.cloneElement(icon as React.ReactElement, {
sx: { fontSize: 36, color: 'white' }
})}
</Box>
</Box>
)}
<Typography
variant="h4"
gutterBottom
sx={{
fontWeight: 700,
mb: 2,
letterSpacing: '-0.025em',
color: 'text.primary'
}}
>
{title}
</Typography>
<Typography
variant="body1"
color="text.secondary"
sx={{
lineHeight: 1.6,
maxWidth: 600,
mx: centered ? 'auto' : 0,
fontSize: '1.1rem'
}}
>
{subtitle}
</Typography>
</Box>
</Zoom>
</Box>
{/* Content */}
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: 3,
maxWidth: maxWidth,
mx: centered ? 'auto' : 0
}}>
{children}
</Box>
</Container>
</Fade>
);
};
export default OnboardingStepLayout;

View File

@@ -0,0 +1,104 @@
// Validation utilities
export const validateApiKey = (key: string, provider: string): boolean | null => {
if (!key.trim()) return null;
const patterns = {
openai: /^sk-[a-zA-Z0-9]{32,}$/,
gemini: /^AIza[a-zA-Z0-9_-]{35}$/,
anthropic: /^sk-ant-[a-zA-Z0-9]{32,}$/,
mistral: /^[a-zA-Z0-9]{32,}$/,
};
const pattern = patterns[provider as keyof typeof patterns];
return pattern ? pattern.test(key) : true;
};
export const getKeyStatus = (key: string, provider: string): 'valid' | 'invalid' | 'empty' => {
if (!key.trim()) return 'empty';
const isValid = validateApiKey(key, provider);
return isValid ? 'valid' : 'invalid';
};
// Animation utilities
export const getAnimationDelay = (index: number, baseDelay: number = 100): number => {
return baseDelay * index;
};
export const getSlideDirection = (currentStep: number, targetStep: number): 'left' | 'right' => {
return targetStep > currentStep ? 'right' : 'left';
};
// Progress utilities
export const calculateProgress = (currentStep: number, totalSteps: number): number => {
return ((currentStep + 1) / totalSteps) * 100;
};
// Form utilities
export const isFormValid = (values: Record<string, string>): boolean => {
return Object.values(values).some(value => value.trim() !== '');
};
// Status utilities
export const getStatusColor = (status: 'valid' | 'invalid' | 'empty'): string => {
switch (status) {
case 'valid':
return '#4caf50';
case 'invalid':
return '#f44336';
default:
return 'transparent';
}
};
export const getStatusLabel = (status: 'valid' | 'invalid' | 'empty'): string => {
switch (status) {
case 'valid':
return 'Valid';
case 'invalid':
return 'Invalid';
default:
return '';
}
};
// Auto-save utilities
export const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number
): ((...args: Parameters<T>) => void) => {
let timeoutId: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
};
// Error handling utilities
export const formatErrorMessage = (error: any): string => {
if (typeof error === 'string') return error;
if (error?.message) return error.message;
return 'An unexpected error occurred. Please try again.';
};
// URL validation
export const isValidUrl = (url: string): boolean => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
// Text validation
export const validateRequired = (value: string): boolean => {
return value.trim().length > 0;
};
export const validateMinLength = (value: string, minLength: number): boolean => {
return value.trim().length >= minLength;
};
export const validateMaxLength = (value: string, maxLength: number): boolean => {
return value.trim().length <= maxLength;
};

View File

@@ -0,0 +1,242 @@
import { useTheme } from '@mui/material';
export const useOnboardingStyles = () => {
const theme = useTheme();
const styles = {
// Layout styles
container: {
maxWidth: 800,
mx: 'auto',
},
// Header styles
header: {
textAlign: 'center',
mb: 4,
},
headerIcon: {
fontSize: 64,
color: 'primary.main',
mb: 2,
},
headerIconContainer: {
width: 80,
height: 80,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 25px rgba(102, 126, 234, 0.3)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: -2,
left: -2,
right: -2,
bottom: -2,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
opacity: 0.3,
zIndex: -1,
}
},
headerTitle: {
fontWeight: 700,
letterSpacing: '-0.025em',
},
headerSubtitle: {
color: 'text.secondary',
lineHeight: 1.6,
maxWidth: 600,
mx: 'auto',
},
// Card styles
card: {
elevation: 2,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
borderRadius: 3,
'&:hover': {
elevation: 4,
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
},
},
cardContent: {
p: 3,
},
cardHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
},
cardTitle: {
display: 'flex',
alignItems: 'center',
gap: 1.5,
},
cardIconContainer: {
width: 40,
height: 40,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
// Button styles
primaryButton: {
borderRadius: 2,
textTransform: 'none' as const,
fontWeight: 600,
px: 4,
py: 1.5,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
},
},
secondaryButton: {
borderRadius: 2,
textTransform: 'none' as const,
fontWeight: 600,
borderColor: 'rgba(0,0,0,0.2)',
color: 'text.primary',
'&:hover': {
borderColor: 'rgba(0,0,0,0.4)',
background: 'rgba(0,0,0,0.04)',
},
'&:disabled': {
borderColor: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.3)',
}
},
textButton: {
textTransform: 'none' as const,
fontWeight: 600,
},
// Form styles
textField: {
'& .MuiOutlinedInput-root': {
borderRadius: 2,
},
'& .MuiInputBase-input': {
padding: '12px 16px',
},
},
// Alert styles
alert: {
borderRadius: 2,
'& .MuiAlert-icon': {
fontSize: 20,
},
},
// Paper styles
infoPaper: {
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid rgba(59, 130, 246, 0.2)',
borderRadius: 2,
},
warningPaper: {
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid rgba(245, 158, 11, 0.2)',
borderRadius: 2,
},
successPaper: {
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid rgba(16, 185, 129, 0.2)',
borderRadius: 2,
},
// Progress styles
progressBar: {
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(0,0,0,0.08)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
}
},
// Chip styles
chip: {
fontWeight: 600,
borderRadius: 1,
},
// Divider styles
divider: {
my: 2,
opacity: 0.6,
},
// Link styles
link: {
fontWeight: 600,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
},
// Animation styles
fadeIn: {
animation: 'fadeIn 0.5s ease-in-out',
},
slideUp: {
animation: 'slideUp 0.3s ease-out',
},
// Responsive styles
responsiveContainer: {
maxWidth: { xs: '100%', md: 800 },
mx: 'auto',
px: { xs: 2, md: 3 },
},
// Spacing utilities
sectionSpacing: {
mb: 4,
},
cardSpacing: {
gap: 3,
},
buttonSpacing: {
gap: 2,
},
};
return styles;
};