ALwrity Version 0.5.0 (Fastapi + React )
This commit is contained in:
@@ -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;
|
||||
741
frontend/src/components/OnboardingWizard/ApiKeyStep.tsx
Normal file
741
frontend/src/components/OnboardingWizard/ApiKeyStep.tsx
Normal 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;
|
||||
660
frontend/src/components/OnboardingWizard/FinalStep.tsx
Normal file
660
frontend/src/components/OnboardingWizard/FinalStep.tsx
Normal 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;
|
||||
752
frontend/src/components/OnboardingWizard/IntegrationsStep.tsx
Normal file
752
frontend/src/components/OnboardingWizard/IntegrationsStep.tsx
Normal 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;
|
||||
362
frontend/src/components/OnboardingWizard/PersonalizationStep.tsx
Normal file
362
frontend/src/components/OnboardingWizard/PersonalizationStep.tsx
Normal 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;
|
||||
914
frontend/src/components/OnboardingWizard/ResearchStep.tsx
Normal file
914
frontend/src/components/OnboardingWizard/ResearchStep.tsx
Normal 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;
|
||||
280
frontend/src/components/OnboardingWizard/ResearchTestStep.tsx
Normal file
280
frontend/src/components/OnboardingWizard/ResearchTestStep.tsx
Normal 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;
|
||||
330
frontend/src/components/OnboardingWizard/StyleDetectionStep.tsx
Normal file
330
frontend/src/components/OnboardingWizard/StyleDetectionStep.tsx
Normal 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;
|
||||
1363
frontend/src/components/OnboardingWizard/WebsiteStep.tsx
Normal file
1363
frontend/src/components/OnboardingWizard/WebsiteStep.tsx
Normal file
File diff suppressed because it is too large
Load Diff
557
frontend/src/components/OnboardingWizard/Wizard.tsx
Normal file
557
frontend/src/components/OnboardingWizard/Wizard.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user