Added onboarding progress tracking & landing page

This commit is contained in:
ajaysi
2025-10-02 13:20:15 +05:30
parent e57d2577f8
commit 510b79bbf8
135 changed files with 25917 additions and 5768 deletions

View File

@@ -1,696 +1,112 @@
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 { Lock } from '@mui/icons-material';
import OnboardingButton from './common/OnboardingButton';
import {
HelpSection,
BenefitsModal,
useApiKeyStep
} from './ApiKeyStep/utils';
import ApiKeyCarousel from './ApiKeyStep/utils/ApiKeyCarousel';
import ApiKeySidebar from './ApiKeyStep/utils/ApiKeySidebar';
interface ApiKeyStepProps {
onContinue: () => void;
onContinue: (stepData?: any) => 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 [currentProvider, setCurrentProvider] = useState(0);
const [focusedProvider, setFocusedProvider] = useState<any>(null);
const styles = useOnboardingStyles();
const {
loading,
error,
success,
showHelp,
savedKeys,
benefitsModalOpen,
selectedProvider,
providers,
isValid,
setShowHelp,
handleContinue,
handleBenefitsClick,
handleCloseBenefitsModal,
} = useApiKeyStep(onContinue);
const handleProviderFocus = (provider: any) => {
setFocusedProvider(provider);
};
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.'
description: 'Configure your AI providers to unlock intelligent content creation, research capabilities, and enhanced user 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
// Set initial focused provider
if (providers.length > 0) {
setFocusedProvider(providers[currentProvider] ?? providers[0]);
}
};
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);
};
}, [updateHeaderContent, providers, currentProvider]);
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>
<form onSubmit={(e) => { e.preventDefault(); handleContinue(); }}>
{/* Main Content Layout */}
<Grid container spacing={4} sx={{ mb: 4 }}>
{/* Carousel Section */}
<Grid item xs={12} lg={8}>
<ApiKeyCarousel
providers={providers}
currentProvider={currentProvider}
setCurrentProvider={setCurrentProvider}
onProviderFocus={handleProviderFocus}
/>
</Grid>
{/* Sidebar Section */}
<Grid item xs={12} lg={4}>
<ApiKeySidebar
currentProvider={focusedProvider}
allProviders={providers}
currentStep={currentProvider + 1}
totalSteps={providers.length}
/>
</Grid>
))}
</Grid>
</Box>
{/* Description moved below cards */}
{/* Get Help Section */}
<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"
sx={{ mb: 2 }}
>
{showHelp ? 'Hide Help' : 'Get Help'}
{showHelp ? 'Hide Setup Help' : 'Need Setup Help?'}
</OnboardingButton>
</Box>
</Box>
{/* Benefits Modal */}
<Dialog
<BenefitsModal
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>
selectedProvider={selectedProvider}
/>
{/* 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>
<HelpSection showHelp={showHelp} />
{/* Alerts */}
<Box sx={{ mt: 3 }}>
@@ -719,20 +135,68 @@ const ApiKeyStep: React.FC<ApiKeyStepProps> = ({ onContinue, updateHeaderContent
)}
</Box>
{/* Continue Button */}
<Box sx={{ mt: 6, display: 'flex', justifyContent: 'center' }}>
<OnboardingButton
variant="primary"
type="submit"
loading={loading}
disabled={!isValid || loading}
size="large"
sx={{
px: 6,
py: 2.5,
fontSize: '1.1rem',
fontWeight: 700,
borderRadius: 4,
background: isValid
? 'linear-gradient(135deg, #10B981 0%, #059669 100%)'
: 'linear-gradient(135deg, #94A3B8 0%, #64748B 100%)',
boxShadow: isValid
? '0 12px 32px rgba(16, 185, 129, 0.3), 0 6px 12px rgba(16, 185, 129, 0.2)'
: '0 8px 16px rgba(148, 163, 184, 0.2)',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
transform: isValid ? 'translateY(-3px) scale(1.02)' : 'none',
boxShadow: isValid
? '0 16px 40px rgba(16, 185, 129, 0.4), 0 8px 16px rgba(16, 185, 129, 0.3)'
: '0 8px 16px rgba(148, 163, 184, 0.2)',
},
'&:disabled': {
'&:hover': {
transform: 'none',
}
}
}}
>
{isValid ? 'Continue to Website Analysis' : 'Complete All Required API Keys'}
</OnboardingButton>
</Box>
{/* Security Notice */}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary" sx={{
<Box sx={{
mt: 4,
textAlign: 'center',
p: 3,
borderRadius: 3,
background: 'linear-gradient(135deg, #F8FAFC 0%, #F1F5F9 100%)',
border: '1px solid #E2E8F0',
}}>
<Typography variant="body2" sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 0.5,
gap: 1,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
fontWeight: 500,
color: '#475569',
fontSize: '0.95rem',
}}>
<Lock sx={{ fontSize: 14 }} />
<Lock sx={{ fontSize: 18, color: '#10B981' }} />
Your API keys are encrypted and stored securely on your device
</Typography>
</Box>
</form>
</Container>
</Fade>
);

View File

@@ -0,0 +1,519 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Card,
CardContent,
TextField,
IconButton,
Button,
Typography,
Stepper,
Step,
StepLabel,
StepConnector,
Fade,
LinearProgress,
} from '@mui/material';
import {
Visibility,
VisibilityOff,
Lock,
Launch,
CheckCircle,
NavigateNext,
NavigateBefore,
Key,
ContentPasteRounded,
} from '@mui/icons-material';
import { styled } from '@mui/material/styles';
interface ApiKeyCarouselProps {
providers: Array<{
name: string;
description: string;
key: string;
setKey: (key: string) => void;
showKey: boolean;
setShowKey: (show: boolean) => void;
placeholder: string;
status: 'valid' | 'invalid' | 'empty';
link: string;
free: boolean;
recommended: boolean;
benefits: string[];
}>;
currentProvider: number;
setCurrentProvider: (index: number) => void;
onProviderFocus: (provider: any) => void;
}
const CustomStepConnector = styled(StepConnector)(({ theme }) => ({
'&.MuiStepConnector-alternativeLabel': {
top: 10,
left: 'calc(-50% + 16px)',
right: 'calc(50% + 16px)',
},
'& .MuiStepConnector-line': {
height: 3,
border: 0,
background: 'linear-gradient(90deg, #E2E8F0 0%, #CBD5E1 100%)',
borderRadius: 2,
},
'&.MuiStepConnector-active .MuiStepConnector-line': {
background: 'linear-gradient(90deg, #3B82F6 0%, #1D4ED8 100%)',
},
'&.MuiStepConnector-completed .MuiStepConnector-line': {
background: 'linear-gradient(90deg, #10B981 0%, #059669 100%)',
},
}));
const ApiKeyCarousel: React.FC<ApiKeyCarouselProps> = ({
providers,
currentProvider,
setCurrentProvider,
onProviderFocus,
}) => {
const [autoProgress, setAutoProgress] = useState(false);
const provider = providers[currentProvider];
const getAccentColor = (name: string) => {
const n = name.toLowerCase();
if (n === 'gemini') return '#3B82F6';
if (n === 'exa') return '#10B981';
return '#8B5CF6';
};
useEffect(() => {
// Auto-advance to next provider when current one is completed
if (provider.status === 'valid' && currentProvider < providers.length - 1) {
const timer = setTimeout(() => {
setCurrentProvider(currentProvider + 1);
onProviderFocus(providers[currentProvider + 1]);
}, 1500);
return () => clearTimeout(timer);
}
}, [provider.status, currentProvider, providers, setCurrentProvider, onProviderFocus]);
useEffect(() => {
// Focus on current provider for sidebar
onProviderFocus(provider);
}, [currentProvider, provider, onProviderFocus]);
const handleNext = () => {
if (currentProvider < providers.length - 1) {
const next = currentProvider + 1;
setCurrentProvider(next);
// proactively sync sidebar
onProviderFocus(providers[next]);
}
};
const handlePrevious = () => {
if (currentProvider > 0) {
const prev = currentProvider - 1;
setCurrentProvider(prev);
// proactively sync sidebar
onProviderFocus(providers[prev]);
}
};
const getStepIcon = (index: number) => {
const stepProvider = providers[index];
if (stepProvider.status === 'valid') {
return <CheckCircle sx={{ color: 'success.main' }} />;
}
return <Key sx={{ color: stepProvider === provider ? 'primary.main' : 'text.disabled' }} />;
};
return (
<Box sx={{ width: '100%', maxWidth: 600, mx: 'auto' }}>
{/* Progress Stepper - Hidden as requested */}
{/* <Box sx={{
mb: 4,
p: 3,
borderRadius: 3,
background: 'linear-gradient(135deg, #F8FAFC 0%, #F1F5F9 100%)',
border: '1px solid rgba(226, 232, 240, 0.8)',
backdropFilter: 'blur(10px)',
}}>
<Stepper
activeStep={currentProvider}
alternativeLabel
connector={<CustomStepConnector />}
>
{providers.map((prov, index) => (
<Step key={prov.name} completed={prov.status === 'valid'}>
<StepLabel
icon={getStepIcon(index)}
onClick={() => setCurrentProvider(index)}
sx={{
cursor: 'pointer',
'& .MuiStepLabel-label': {
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600,
fontSize: '0.875rem',
color: prov.status === 'valid' ? '#059669' :
index === currentProvider ? '#1D4ED8' : '#64748B',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
},
'& .MuiStepLabel-iconContainer': {
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
transform: 'scale(1.1)',
}
}
}}
>
{prov.name}
</StepLabel>
</Step>
))}
</Stepper>
</Box> */}
{/* Current Provider Card */}
<Fade in={true} key={currentProvider} timeout={300}>
<Card
sx={{
border: '1px solid #E2E8F0',
borderRadius: 4,
background: 'linear-gradient(135deg, #FFFFFF 0%, #F8FAFC 100%)',
backdropFilter: 'blur(20px)',
boxShadow: '0 16px 32px rgba(2, 6, 23, 0.08), 0 6px 12px rgba(2, 6, 23, 0.06)',
position: 'relative',
overflow: 'visible',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: '0 20px 40px rgba(2, 6, 23, 0.10), 0 8px 16px rgba(2, 6, 23, 0.06)'
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 4,
background: 'radial-gradient(1200px 300px at 50% -100px, rgba(59,130,246,0.08), rgba(255,255,255,0) 60%)',
pointerEvents: 'none',
}
}}
>
{/* Progress indicator for valid status */}
{provider.status === 'valid' && (
<LinearProgress
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 4,
borderRadius: '12px 12px 0 0',
backgroundColor: 'success.light',
'& .MuiLinearProgress-bar': {
backgroundColor: 'success.main',
},
}}
variant="determinate"
value={100}
/>
)}
<CardContent sx={{ p: 4 }}>
{/* Provider Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<Box
sx={{
width: 56,
height: 56,
borderRadius: '50%',
background: provider.recommended
? 'linear-gradient(135deg, #10B981 0%, #059669 100%)'
: provider.name.toLowerCase() === 'gemini'
? 'linear-gradient(135deg, #4285F4 0%, #1557B0 100%)'
: provider.name.toLowerCase() === 'exa'
? 'linear-gradient(135deg, #10B981 0%, #047857 100%)'
: 'linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.08)',
border: '3px solid rgba(255, 255, 255, 0.2)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
transform: 'translateY(-2px) scale(1.05)',
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.15), 0 6px 12px rgba(0, 0, 0, 0.1)',
}
}}
>
<Key sx={{ color: 'white', fontSize: 28 }} />
</Box>
<Box sx={{ flex: 1 }}>
<Typography
variant="h4"
sx={{
fontWeight: 700,
fontFamily: 'Inter, system-ui, sans-serif',
background: 'linear-gradient(135deg, #1E293B 0%, #475569 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
mb: 1,
fontSize: '1.75rem',
}}
>
{provider.name}
</Typography>
<Typography
variant="body1"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 500,
color: '#64748B',
fontSize: '1.1rem',
lineHeight: 1.5,
}}
>
{provider.description}
</Typography>
</Box>
{provider.status === 'valid' && (
<CheckCircle sx={{ color: 'success.main', fontSize: 32 }} />
)}
</Box>
{/* API Key Input */}
<Box sx={{ mb: 3 }}>
<TextField
fullWidth
type={provider.showKey ? 'text' : 'password'}
value={provider.key}
onChange={(e) => provider.setKey(e.target.value)}
placeholder={provider.placeholder}
variant="outlined"
name={`api-key-${provider.name.toLowerCase()}`}
autoComplete="off"
autoFocus
InputProps={{
startAdornment: <Lock sx={{ color: '#64748B', mr: 2, fontSize: 22 }} />,
endAdornment: (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<IconButton
aria-label="Paste API key from clipboard"
onClick={async () => {
try {
const text = await navigator.clipboard.readText();
if (text) provider.setKey(text.trim());
} catch (e) {
// no-op
}
}}
edge="end"
sx={{
color: '#64748B',
'&:hover': {
color: getAccentColor(provider.name),
background: 'rgba(148, 163, 184, 0.15)',
},
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}}
title="Paste"
>
<ContentPasteRounded />
</IconButton>
<IconButton
aria-label={provider.showKey ? 'Hide API key' : 'Show API key'}
onClick={() => provider.setShowKey(!provider.showKey)}
edge="end"
sx={{
color: '#64748B',
'&:hover': {
color: getAccentColor(provider.name),
background: 'rgba(148, 163, 184, 0.15)',
transform: 'scale(1.05)',
},
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}}
title={provider.showKey ? 'Hide' : 'Show'}
>
{provider.showKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
</Box>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
fontSize: '1.1rem',
background: 'linear-gradient(135deg, #FFFFFF 0%, #F8FAFC 100%)',
border: '2px solid #E2E8F0',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'& .MuiOutlinedInput-notchedOutline': {
border: 'none',
},
'&:hover': {
borderColor: '#CBD5E1',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.08), 0 4px 8px rgba(0, 0, 0, 0.04)',
transform: 'translateY(-1px)',
},
'&.Mui-focused': {
borderColor: getAccentColor(provider.name),
boxShadow: `0 0 0 4px ${getAccentColor(provider.name)}22, 0 8px 24px rgba(0, 0, 0, 0.12)`,
transform: 'translateY(-2px)',
},
},
'& .MuiInputBase-input': {
padding: '18px 24px',
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 500,
color: '#1E293B',
'&::placeholder': {
color: '#94A3B8',
opacity: 1,
}
},
}}
/>
</Box>
{/* Get API Key Button */}
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 4 }}>
<Button
href={provider.link}
target="_blank"
rel="noopener noreferrer"
variant="contained"
startIcon={<Launch />}
sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
textTransform: 'none',
px: 4,
py: 2,
borderRadius: 3,
fontSize: '1rem',
background: provider.name.toLowerCase() === 'gemini'
? 'linear-gradient(135deg, #4285F4 0%, #1557B0 100%)'
: provider.name.toLowerCase() === 'exa'
? 'linear-gradient(135deg, #10B981 0%, #047857 100%)'
: 'linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%)',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.15), 0 4px 8px rgba(0, 0, 0, 0.1)',
border: 'none',
color: 'white',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
transform: 'translateY(-2px) scale(1.02)',
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.2), 0 6px 12px rgba(0, 0, 0, 0.15)',
background: provider.name.toLowerCase() === 'gemini'
? 'linear-gradient(135deg, #1557B0 0%, #0D47A1 100%)'
: provider.name.toLowerCase() === 'exa'
? 'linear-gradient(135deg, #047857 0%, #065F46 100%)'
: 'linear-gradient(135deg, #7C3AED 0%, #6D28D9 100%)',
},
}}
>
Get {provider.name} API Key
</Button>
</Box>
{/* Navigation */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<IconButton
onClick={handlePrevious}
disabled={currentProvider === 0}
sx={{
width: 48,
height: 48,
background: 'linear-gradient(135deg, #FFFFFF 0%, #F8FAFC 100%)',
border: '2px solid #E2E8F0',
color: '#64748B',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
background: 'linear-gradient(135deg, #F8FAFC 0%, #F1F5F9 100%)',
borderColor: '#CBD5E1',
color: '#475569',
transform: 'translateY(-2px) scale(1.05)',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.08)',
},
'&:disabled': {
opacity: 0.4,
transform: 'none',
'&:hover': {
transform: 'none',
}
},
}}
>
<NavigateBefore sx={{ fontSize: 24 }} />
</IconButton>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
px: 3,
py: 1.5,
borderRadius: 3,
background: 'linear-gradient(135deg, #F8FAFC 0%, #FFFFFF 100%)',
border: `2px solid ${getAccentColor(provider.name)}22`,
}}>
<Typography
variant="body1"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600,
color: '#334155',
fontSize: '1rem',
}}
>
{currentProvider + 1}
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
color: '#64748B',
}}
>
of {providers.length}
</Typography>
</Box>
<IconButton
onClick={handleNext}
disabled={currentProvider === providers.length - 1}
sx={{
width: 48,
height: 48,
background: 'linear-gradient(135deg, #FFFFFF 0%, #F8FAFC 100%)',
border: '2px solid #E2E8F0',
color: '#64748B',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
background: 'linear-gradient(135deg, #F8FAFC 0%, #F1F5F9 100%)',
borderColor: '#CBD5E1',
color: '#475569',
transform: 'translateY(-2px) scale(1.05)',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.08)',
},
'&:disabled': {
opacity: 0.4,
transform: 'none',
'&:hover': {
transform: 'none',
}
},
}}
>
<NavigateNext sx={{ fontSize: 24 }} />
</IconButton>
</Box>
</CardContent>
</Card>
</Fade>
</Box>
);
};
export default ApiKeyCarousel;

View File

@@ -0,0 +1,516 @@
import React from 'react';
import {
Box,
Typography,
Card,
CardContent,
List,
ListItem,
ListItemIcon,
ListItemText,
Chip,
Divider,
Alert,
} from '@mui/material';
import {
CheckCircle,
Star,
Security,
Speed,
TrendingUp,
Insights,
Search,
Assistant,
Key,
MoneyOff,
Recommend,
} from '@mui/icons-material';
interface Provider {
name: string;
description: string;
benefits: string[];
status: 'valid' | 'invalid' | 'empty';
free: boolean;
recommended: boolean;
}
interface ApiKeySidebarProps {
currentProvider: Provider | null;
allProviders: Provider[];
currentStep: number;
totalSteps: number;
}
const ApiKeySidebar: React.FC<ApiKeySidebarProps> = ({ currentProvider, allProviders, currentStep, totalSteps }) => {
// Shared dark card styling to keep sidebar visuals consistent
const darkCardSx = {
borderRadius: 4,
background: 'linear-gradient(135deg, #1F2937 0%, #111827 100%)',
border: '1px solid rgba(148, 163, 184, 0.12)',
boxShadow: '0 24px 48px rgba(0, 0, 0, 0.35), 0 8px 16px rgba(0, 0, 0, 0.25)'
} as const;
// Get API key status summary for all providers
const getApiKeyStatusSummary = () => {
const validCount = allProviders.filter(p => p.status === 'valid').length;
const invalidCount = allProviders.filter(p => p.status === 'invalid').length;
const emptyCount = allProviders.filter(p => p.status === 'empty').length;
return {
valid: validCount,
invalid: invalidCount,
empty: emptyCount,
total: allProviders.length
};
};
const statusSummary = getApiKeyStatusSummary();
const getProviderIcon = (name: string) => {
switch (name.toLowerCase()) {
case 'gemini':
return <Star sx={{ color: '#4285F4' }} />;
case 'exa':
return <Search sx={{ color: '#10b981' }} />;
case 'copilotkit':
return <Assistant sx={{ color: '#8B5CF6' }} />;
default:
return <Key />;
}
};
const getProviderDetails = (name: string) => {
switch (name.toLowerCase()) {
case 'gemini':
return {
fullName: 'Google Gemini AI',
purpose: 'Advanced Content Generation',
keyFeatures: [
'Multi-modal AI understanding',
'Long context processing',
'High-quality content creation',
'Code generation capabilities',
'Multiple language support'
],
useCases: [
'Blog post generation',
'Social media content',
'Email templates',
'Product descriptions',
'SEO-optimized articles'
],
pricing: 'Free tier: 15 requests/min, 1M tokens/min',
setupTime: '2 minutes'
};
case 'exa':
return {
fullName: 'Exa AI Search',
purpose: 'Intelligent Web Research',
keyFeatures: [
'Semantic web search',
'Real-time data retrieval',
'Content summarization',
'Source verification',
'Trend analysis'
],
useCases: [
'Market research',
'Fact-checking content',
'Competitor analysis',
'Industry insights',
'News monitoring'
],
pricing: 'Free tier: 1,000 searches/month',
setupTime: '1 minute'
};
case 'copilotkit':
return {
fullName: 'CopilotKit Assistant',
purpose: 'Enhanced User Experience',
keyFeatures: [
'In-app AI assistance',
'Context-aware responses',
'Workflow automation',
'Real-time suggestions',
'User interaction tracking'
],
useCases: [
'Writing assistance',
'Content optimization',
'User guidance',
'Process automation',
'Quality assurance'
],
pricing: 'Free tier: 10,000 requests/month',
setupTime: '3 minutes'
};
default:
return null;
}
};
const getProviderHelp = (name: string) => {
switch (name.toLowerCase()) {
case 'gemini':
return {
docUrl: 'https://ai.google.dev/',
tips: [
'Use unrestricted key for development; restrict by HTTP referrer for production.',
'Enable Generative Language API in your Google Cloud project.',
'If you see 429 errors, lower temperature or increase quota.'
],
accent: '#3B82F6'
};
case 'exa':
return {
docUrl: 'https://docs.exa.ai/',
tips: [
'Use semantic search for long-form topics; include site filters when needed.',
'Keep result size small (top_k 5-10) for fastest responses.',
'Rotate key if you encounter 401 — keys expire when regenerated.'
],
accent: '#10B981'
};
case 'copilotkit':
return {
docUrl: 'https://docs.copilotkit.ai/',
tips: [
'Public key starts with ck_pub_ — never paste secret keys in the browser.',
'Enable domain allowlist in CopilotKit console for production.',
'Check usage dashboard to monitor token consumption.'
],
accent: '#8B5CF6'
};
default:
return { docUrl: '#', tips: [], accent: '#3B82F6' };
}
};
if (!currentProvider) {
return (
<Card sx={{ height: 'fit-content', borderRadius: 3 }}>
<CardContent sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
API Configuration Overview
</Typography>
<Typography variant="body2" color="text.secondary">
Configure your AI services to unlock ALwrity's full potential.
</Typography>
</CardContent>
</Card>
);
}
const details = getProviderDetails(currentProvider.name);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, height: 'fit-content' }}>
{/* Dynamic Carousel Progress */}
<Card sx={{ ...darkCardSx }}>
<CardContent sx={{ p: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1 }}>
<Typography
variant="h6"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 700,
color: '#E2E8F0',
fontSize: '1.25rem',
}}
>
{currentProvider ? currentProvider.name : 'API Key Setup'}
</Typography>
{/* API Key Status Summary */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
{statusSummary.valid > 0 && (
<Chip
label={`${statusSummary.valid} Valid`}
size="small"
sx={{
background: 'linear-gradient(135deg, #10B981 0%, #059669 100%)',
color: 'white',
fontWeight: 500,
fontSize: '0.75rem',
height: 20,
}}
/>
)}
{statusSummary.invalid > 0 && (
<Chip
label={`${statusSummary.invalid} Invalid`}
size="small"
sx={{
background: 'linear-gradient(135deg, #EF4444 0%, #DC2626 100%)',
color: 'white',
fontWeight: 500,
fontSize: '0.75rem',
height: 20,
}}
/>
)}
{statusSummary.empty > 0 && (
<Chip
label={`${statusSummary.empty} Pending`}
size="small"
sx={{
background: 'linear-gradient(135deg, #6B7280 0%, #4B5563 100%)',
color: 'white',
fontWeight: 500,
fontSize: '0.75rem',
height: 20,
}}
/>
)}
</Box>
</Box>
<Chip
label={`${currentStep} of ${totalSteps}`}
sx={{
background: currentStep === totalSteps
? 'linear-gradient(135deg, #10B981 0%, #059669 100%)'
: 'linear-gradient(135deg, #3B82F6 0%, #1D4ED8 100%)',
color: 'white',
fontWeight: 600,
fontSize: '0.875rem',
'& .MuiChip-label': {
px: 1.5,
}
}}
size="small"
/>
</Box>
{/* Compact Status - Removed detailed provider list for space efficiency */}
</CardContent>
</Card>
{/* Current Provider Details (specific to selected provider) */}
<Card sx={{ ...darkCardSx }}>
<CardContent sx={{ p: 4 }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
{getProviderIcon(currentProvider.name)}
<Box sx={{ flex: 1 }}>
<Typography
variant="h6"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 700,
mb: 0.5,
color: '#E2E8F0',
fontSize: '1.25rem',
}}
>
{details?.fullName || currentProvider.name}
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
color: '#CBD5E1',
fontWeight: 500,
fontSize: '0.95rem',
}}
>
{details?.purpose || currentProvider.description}
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{currentProvider.recommended && (
<Chip
icon={<Recommend />}
label="Recommended"
sx={{
background: 'linear-gradient(135deg, #10B981 0%, #059669 100%)',
color: 'white',
fontWeight: 600,
fontSize: '0.75rem',
'& .MuiChip-icon': {
color: 'white',
}
}}
size="small"
/>
)}
{currentProvider.free && (
<Chip
icon={<MoneyOff />}
label="Free Tier"
sx={{
background: 'linear-gradient(135deg, #3B82F6 0%, #1D4ED8 100%)',
color: 'white',
fontWeight: 600,
fontSize: '0.75rem',
'& .MuiChip-icon': {
color: 'white',
}
}}
size="small"
/>
)}
</Box>
</Box>
{details && (
<>
{/* Key Features */}
<Box sx={{ mb: 3 }}>
<Typography
variant="subtitle2"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600,
mb: 1.5,
color: '#E2E8F0',
}}
>
Key Features
</Typography>
<List dense sx={{ pt: 0 }}>
{details.keyFeatures.slice(0, 4).map((feature, index) => (
<ListItem key={index} sx={{ pl: 0, py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<CheckCircle sx={{ fontSize: 16, color: '#10B981' }} />
</ListItemIcon>
<ListItemText
primary={feature}
primaryTypographyProps={{
variant: 'body2',
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: '0.875rem',
color: '#CBD5E1'
}}
/>
</ListItem>
))}
</List>
</Box>
<Divider sx={{ my: 2, borderColor: 'rgba(148,163,184,0.16)' }} />
{/* Use Cases */}
<Box sx={{ mb: 3 }}>
<Typography
variant="subtitle2"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600,
mb: 1.5,
color: '#E2E8F0',
}}
>
Perfect For
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{details.useCases.slice(0, 3).map((useCase, index) => (
<Chip
key={index}
label={useCase}
size="small"
sx={{
background: 'rgba(148, 163, 184, 0.08)',
border: '1px solid rgba(148, 163, 184, 0.18)',
color: '#E2E8F0',
fontWeight: 500,
fontSize: '0.75rem'
}}
/>
))}
</Box>
</Box>
{/* Quick Info */}
<Box sx={{
borderRadius: 2,
p: 2,
background: 'rgba(30, 41, 59, 0.6)',
border: '1px solid rgba(148,163,184,0.16)'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="caption" sx={{ color: '#CBD5E1' }}>
Pricing
</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#E2E8F0' }}>
{details.pricing}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="caption" sx={{ color: '#CBD5E1' }}>
Setup Time
</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#E2E8F0' }}>
{details.setupTime}
</Typography>
</Box>
</Box>
{/* Quick Setup Help (provider-specific) */}
<Box sx={{ mt: 2, p: 2.5, borderRadius: 2, background: 'rgba(17,24,39,0.6)', border: '1px solid rgba(148,163,184,0.16)' }}>
<Typography variant="subtitle2" sx={{ fontFamily: 'Inter, system-ui, sans-serif', fontWeight: 700, mb: 1.25, color: '#E2E8F0' }}>
Quick Setup
</Typography>
<List dense sx={{ pt: 0 }}>
{getProviderHelp(currentProvider.name).tips.map((tip, i) => (
<ListItem key={i} sx={{ pl: 0, py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 28 }}>
<Insights sx={{ fontSize: 16, color: getProviderHelp(currentProvider.name).accent }} />
</ListItemIcon>
<ListItemText
primary={tip}
primaryTypographyProps={{ variant: 'body2', fontFamily: 'Inter, system-ui, sans-serif', fontSize: '0.85rem', color: '#CBD5E1' }}
/>
</ListItem>
))}
</List>
</Box>
</>
)}
</CardContent>
</Card>
{/* Benefits */}
{currentProvider.benefits.length > 0 && (
<Card sx={{ ...darkCardSx }}>
<CardContent sx={{ p: 4 }}>
<Typography
variant="h6"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 700,
mb: 2,
color: '#E2E8F0',
}}
>
Why This Matters
</Typography>
<List dense sx={{ pt: 0 }}>
{currentProvider.benefits.map((benefit, index) => (
<ListItem key={index} sx={{ pl: 0, py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<TrendingUp sx={{ fontSize: 16, color: getProviderHelp(currentProvider.name).accent }} />
</ListItemIcon>
<ListItemText
primary={benefit}
primaryTypographyProps={{
variant: 'body2',
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: '0.875rem',
color: '#CBD5E1'
}}
/>
</ListItem>
))}
</List>
</CardContent>
</Card>
)}
</Box>
);
};
export default ApiKeySidebar;

View File

@@ -0,0 +1,123 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
List,
ListItem,
ListItemIcon,
ListItemText,
Box,
Typography,
} from '@mui/material';
export interface Provider {
name: string;
description: string;
benefits: string[];
key: string;
setKey: (key: string) => void;
showKey: boolean;
setShowKey: (show: boolean) => void;
placeholder: string;
status: 'valid' | 'invalid' | 'empty';
link: string;
free: boolean;
recommended: boolean;
}
interface BenefitsModalProps {
open: boolean;
onClose: () => void;
selectedProvider: Provider | null;
}
const BenefitsModal: React.FC<BenefitsModalProps> = ({
open,
onClose,
selectedProvider,
}) => {
return (
<Dialog
open={open}
onClose={onClose}
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={onClose}
variant="contained"
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
Got it
</Button>
</DialogActions>
</Dialog>
);
};
export default BenefitsModal;

View File

@@ -0,0 +1,250 @@
import React from 'react';
import {
Box,
Typography,
Paper,
Grid,
Link,
Collapse,
} from '@mui/material';
import {
HelpOutline,
Star,
Info,
} from '@mui/icons-material';
interface HelpSectionProps {
showHelp: boolean;
}
const HelpSection: React.FC<HelpSectionProps> = ({ showHelp }) => {
return (
<Collapse in={showHelp}>
<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 }} />
Required Providers
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box>
<Typography
variant="subtitle2"
sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
Google Gemini
</Typography>
<Typography
variant="body2"
color="text.secondary"
gutterBottom
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>
<Typography
variant="subtitle2"
sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
Exa AI
</Typography>
<Typography
variant="body2"
color="text.secondary"
gutterBottom
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
Visit{' '}
<Link
href="https://dashboard.exa.ai/login"
target="_blank"
rel="noopener noreferrer"
sx={{
fontWeight: 600,
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
}}
>
dashboard.exa.ai
</Link>
, sign up for a free account, and create an API key.
</Typography>
</Box>
<Box>
<Typography
variant="subtitle2"
sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
CopilotKit
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
Visit{' '}
<Link
href="https://copilotkit.ai"
target="_blank"
rel="noopener noreferrer"
sx={{
fontWeight: 600,
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
}}
>
copilotkit.ai
</Link>
, sign up, and generate a public API key (starts with ck_pub_).
</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 These 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>Gemini:</strong> Powers AI content generation and intelligent writing assistance.
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
<strong>Exa AI:</strong> Enables advanced web research and real-time information gathering.
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
<strong>CopilotKit:</strong> Provides in-app AI assistant for enhanced user experience.
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
<strong>All Required:</strong> These three services work together to provide complete AI functionality.
</Typography>
</Box>
</Box>
</Grid>
</Grid>
</Paper>
</Collapse>
);
};
export default HelpSection;

View File

@@ -0,0 +1,332 @@
import React from 'react';
import {
Box,
TextField,
Typography,
Chip,
IconButton,
Button,
Card,
CardContent,
Tooltip,
} from '@mui/material';
import {
Visibility,
VisibilityOff,
CheckCircle,
Error,
Key,
Lock,
Launch,
Info as InfoIcon,
Recommend,
MoneyOff,
} from '@mui/icons-material';
export interface Provider {
name: string;
description: string;
benefits: string[];
key: string;
setKey: (key: string) => void;
showKey: boolean;
setShowKey: (show: boolean) => void;
placeholder: string;
status: 'valid' | 'invalid' | 'empty';
link: string;
free: boolean;
recommended: boolean;
}
interface ProviderCardProps {
provider: Provider;
savedKeys: Record<string, string>;
onBenefitsClick: (provider: Provider) => void;
}
const ProviderCard: React.FC<ProviderCardProps> = ({
provider,
savedKeys,
onBenefitsClick,
}) => {
return (
<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 sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography
variant="h6"
sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: '1.125rem',
color: 'text.primary',
}}
>
{provider.name}
</Typography>
{provider.recommended && (
<Tooltip title="Recommended by ALwrity" arrow>
<Recommend
sx={{
color: 'success.main',
fontSize: 18,
cursor: 'help',
}}
/>
</Tooltip>
)}
{provider.free && (
<Tooltip title="Free tier available" arrow>
<MoneyOff
sx={{
color: 'primary.main',
fontSize: 18,
cursor: 'help',
}}
/>
</Tooltip>
)}
</Box>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
lineHeight: 1.4,
}}
>
{provider.description}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
variant="text"
onClick={() => onBenefitsClick(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>
<TextField
fullWidth
type={provider.showKey ? 'text' : 'password'}
value={provider.key}
onChange={(e) => provider.setKey(e.target.value)}
placeholder={provider.placeholder}
variant="outlined"
size="small"
name={`api-key-${provider.name.toLowerCase()}`}
autoComplete="off"
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',
},
}}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1.5 }}>
<Button
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 }} />
</Button>
</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>
);
};
export default ProviderCard;

View File

@@ -0,0 +1,7 @@
export { default as ProviderCard } from './ProviderCard';
export { default as HelpSection } from './HelpSection';
export { default as BenefitsModal } from './BenefitsModal';
export { useApiKeyStep } from './useApiKeyStep';
export { default as ApiKeyCarousel } from './ApiKeyCarousel';
export { default as ApiKeySidebar } from './ApiKeySidebar';
export type { Provider } from './ProviderCard';

View File

@@ -0,0 +1,271 @@
import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '@clerk/clerk-react';
import { getApiKeysForOnboarding, getStep1ApiKeysFromProgress, saveApiKey } from '../../../../api/onboarding';
import { getKeyStatus, formatErrorMessage } from '../../common/onboardingUtils';
import { Provider } from './ProviderCard';
export const useApiKeyStep = (onContinue: (stepData?: any) => void) => {
const { getToken } = useAuth();
const [geminiKey, setGeminiKey] = useState('');
const [exaKey, setExaKey] = useState('');
const [copilotkitKey, setCopilotkitKey] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [showGeminiKey, setShowGeminiKey] = useState(false);
const [showExaKey, setShowExaKey] = useState(false);
const [showCopilotkitKey, setShowCopilotkitKey] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [savedKeys, setSavedKeys] = useState<Record<string, string>>({});
const [benefitsModalOpen, setBenefitsModalOpen] = useState(false);
const [selectedProvider, setSelectedProvider] = useState<Provider | null>(null);
const [keysLoaded, setKeysLoaded] = useState(false);
const loadExistingKeys = useCallback(async () => {
try {
console.log('ApiKeyStep: Loading API keys...');
// 1) Try .env/unmasked endpoint
const envKeys = await getApiKeysForOnboarding();
// 2) If missing, fallback to saved progress payload
const progressKeys = await getStep1ApiKeysFromProgress();
const merged = {
gemini: envKeys.gemini ?? progressKeys.gemini ?? '',
exa: envKeys.exa ?? progressKeys.exa ?? '',
copilotkit: envKeys.copilotkit ?? progressKeys.copilotkit ?? '',
} as Record<string, string>;
setSavedKeys(merged);
if (merged.gemini) setGeminiKey(merged.gemini);
if (merged.exa) setExaKey(merged.exa);
if (merged.copilotkit) setCopilotkitKey(merged.copilotkit);
setKeysLoaded(true);
console.log('ApiKeyStep: API keys loaded successfully', merged);
} 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);
// Validate that all required API keys are provided
console.log('ApiKeyStep: Validating API keys - Gemini:', !!geminiKey.trim(), 'Exa:', !!exaKey.trim(), 'CopilotKit:', !!copilotkitKey.trim());
if (!geminiKey.trim() || !exaKey.trim() || !copilotkitKey.trim()) {
const missingKeys = [];
if (!geminiKey.trim()) missingKeys.push('Gemini');
if (!exaKey.trim()) missingKeys.push('Exa');
if (!copilotkitKey.trim()) missingKeys.push('CopilotKit');
setError(`Please provide all required API keys. Missing: ${missingKeys.join(', ')}`);
setLoading(false);
return;
}
// Validate API key formats
if (!geminiKey.trim().startsWith('AIza')) {
setError('Gemini API key must start with "AIza"');
setLoading(false);
return;
}
// Exa API keys are UUIDs (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
const exaUuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!exaUuidRegex.test(exaKey.trim())) {
setError('Exa API key must be a valid UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)');
setLoading(false);
return;
}
if (!copilotkitKey.trim().startsWith('ck_pub_')) {
setError('CopilotKit API key must start with "ck_pub_"');
setLoading(false);
return;
}
try {
// First, save all API keys individually
const promises = [];
if (geminiKey.trim()) {
promises.push(saveApiKey('gemini', geminiKey.trim()));
}
if (exaKey.trim()) {
promises.push(saveApiKey('exa', exaKey.trim()));
}
if (copilotkitKey.trim()) {
promises.push(saveApiKey('copilotkit', copilotkitKey.trim()));
// Store CopilotKit key in localStorage for frontend use
localStorage.setItem('copilotkit_api_key', copilotkitKey.trim());
console.log('ApiKeyStep: CopilotKit key saved to localStorage for frontend CopilotKit provider');
}
try {
await Promise.all(promises);
} catch (saveError: any) {
console.error('Error saving API keys:', saveError);
setError('Failed to save API keys. Please try again.');
setLoading(false);
return;
}
// Trigger CopilotKit reinitialization
if (copilotkitKey.trim()) {
window.dispatchEvent(new CustomEvent('copilotkit-key-updated', {
detail: { apiKey: copilotkitKey.trim() }
}));
}
// Then complete the step with the API keys data
const stepData = {
api_keys: {
gemini: geminiKey.trim(),
exa: exaKey.trim(),
copilotkit: copilotkitKey.trim()
}
};
// Complete step 1 with the API keys data
console.log('ApiKeyStep: Attempting to complete step 1 with data:', stepData);
let response;
try {
response = await fetch('/api/onboarding/step/1/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await getToken()}`
},
body: JSON.stringify({ data: stepData })
});
console.log('ApiKeyStep: Step completion response status:', response.status);
} catch (fetchError: any) {
console.error('Network error completing step:', fetchError);
setError('Network error. Please check your connection and try again.');
setLoading(false);
return;
}
if (!response.ok) {
let errorMessage = 'Failed to complete step';
try {
const errorData = await response.json();
console.log('ApiKeyStep: Error response data:', errorData);
errorMessage = errorData.detail || errorMessage;
} catch (parseError) {
console.error('Error parsing error response:', parseError);
errorMessage = `Server error (${response.status}). Please try again.`;
}
console.log('ApiKeyStep: Setting error message:', errorMessage);
setError(errorMessage);
setLoading(false);
return; // Don't continue if step completion fails
}
setSuccess('API keys saved successfully!');
await loadExistingKeys();
// Auto-continue after a short delay with step data
setTimeout(() => {
onContinue(stepData);
}, 1500);
} catch (err) {
setError(formatErrorMessage(err));
console.error('Error saving API keys:', err);
} finally {
setLoading(false);
}
};
const providers: Provider[] = [
{
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,
},
{
name: 'Exa AI',
description: 'Advanced web search and research capabilities',
benefits: ['Real-time web search', 'Content discovery', 'Research automation'],
key: exaKey,
setKey: setExaKey,
showKey: showExaKey,
setShowKey: setShowExaKey,
placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
status: getKeyStatus(exaKey, 'exa'),
link: 'https://dashboard.exa.ai/login',
free: true,
recommended: true,
},
{
name: 'CopilotKit',
description: 'In-app AI assistant for enhanced user experience',
benefits: ['Interactive AI chat', 'Context-aware assistance', 'Seamless integration'],
key: copilotkitKey,
setKey: setCopilotkitKey,
showKey: showCopilotkitKey,
setShowKey: setShowCopilotkitKey,
placeholder: 'ck_pub_...',
status: getKeyStatus(copilotkitKey, 'copilotkit'),
link: 'https://copilotkit.ai',
free: true,
recommended: true,
},
];
// All three keys are required
const isValid = geminiKey.trim() && exaKey.trim() && copilotkitKey.trim();
const handleBenefitsClick = (provider: Provider) => {
setSelectedProvider(provider);
setBenefitsModalOpen(true);
};
const handleCloseBenefitsModal = () => {
setBenefitsModalOpen(false);
setSelectedProvider(null);
};
useEffect(() => {
loadExistingKeys();
}, [loadExistingKeys]);
return {
// State
geminiKey,
exaKey,
copilotkitKey,
loading,
error,
success,
showGeminiKey,
showExaKey,
showCopilotkitKey,
showHelp,
savedKeys,
benefitsModalOpen,
selectedProvider,
keysLoaded,
providers,
isValid,
// Actions
setShowHelp,
handleContinue,
handleBenefitsClick,
handleCloseBenefitsModal,
loadExistingKeys,
};
};

View File

@@ -6,7 +6,7 @@ import { onboardingCache } from '../../services/onboardingCache';
interface BusinessDescriptionStepProps {
onBack: () => void;
onContinue: () => void;
onContinue: (businessData?: BusinessInfo) => void;
}
const BusinessDescriptionStep: React.FC<BusinessDescriptionStepProps> = ({ onBack, onContinue }) => {
@@ -56,7 +56,7 @@ const BusinessDescriptionStep: React.FC<BusinessDescriptionStepProps> = ({ onBac
console.log('✅ Business info saved to cache.');
setTimeout(() => {
onContinue();
onContinue(response);
}, 1500); // Give user time to see success message
} catch (err) {
console.error('❌ Error saving business info:', err);
@@ -101,7 +101,7 @@ const BusinessDescriptionStep: React.FC<BusinessDescriptionStepProps> = ({ onBac
onChange={handleChange}
fullWidth
margin="normal"
helperText={`${formData.industry.length}/100 characters`}
helperText={`${(formData.industry || '').length}/100 characters`}
inputProps={{ maxLength: 100 }}
disabled={loading}
/>

View File

@@ -0,0 +1,455 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box,
Typography,
Paper,
CircularProgress,
Alert,
Button,
Grid,
Card,
CardContent,
CardActions,
Chip,
Avatar,
LinearProgress,
Dialog,
DialogTitle,
DialogContent
} from '@mui/material';
import {
Business as BusinessIcon,
Assessment as AssessmentIcon,
OpenInNew as OpenInNewIcon,
Refresh as RefreshIcon,
Share as ShareIcon,
Facebook as FacebookIcon,
Instagram as InstagramIcon,
LinkedIn as LinkedInIcon,
YouTube as YouTubeIcon,
Twitter as TwitterIcon
} from '@mui/icons-material';
import { aiApiClient } from '../../api/client'; // Use aiApiClient for long-running operations
import { useOnboardingStyles } from './common/useOnboardingStyles';
interface Competitor {
url: string;
domain: string;
title: string;
summary: string;
relevance_score: number;
highlights?: string[];
competitive_insights: {
business_model: string;
target_audience: string;
};
content_insights: {
content_focus: string;
content_quality: string;
};
}
interface ResearchSummary {
total_competitors: number;
market_insights: string;
key_findings: string[];
}
interface CompetitorAnalysisStepProps {
onContinue: (researchData?: any) => void;
onBack: () => void;
// sessionId removed - backend uses authenticated user from Clerk token
userUrl: string;
industryContext?: string;
}
const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
onContinue,
onBack,
userUrl,
industryContext
}) => {
const classes = useOnboardingStyles();
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [analysisProgress, setAnalysisProgress] = useState(0);
const [analysisStep, setAnalysisStep] = useState('');
const [competitors, setCompetitors] = useState<Competitor[]>([]);
const [socialMediaAccounts, setSocialMediaAccounts] = useState<any>({});
const [socialMediaCitations, setSocialMediaCitations] = useState<any[]>([]);
const [researchSummary, setResearchSummary] = useState<ResearchSummary | null>(null);
const [error, setError] = useState<string | null>(null);
const [showProgressModal, setShowProgressModal] = useState(false);
const [showHighlightsModal, setShowHighlightsModal] = useState(false);
const [selectedCompetitorHighlights, setSelectedCompetitorHighlights] = useState<string[]>([]);
const [selectedCompetitorTitle, setSelectedCompetitorTitle] = useState<string>('');
const startCompetitorDiscovery = useCallback(async () => {
setIsAnalyzing(true);
setShowProgressModal(true);
setError(null);
setAnalysisProgress(0);
setAnalysisStep('Initializing competitor discovery...');
try {
setAnalysisStep('Validating session...');
setAnalysisProgress(20);
await new Promise(resolve => setTimeout(resolve, 500));
setAnalysisStep('Discovering competitors using AI...');
setAnalysisProgress(40);
await new Promise(resolve => setTimeout(resolve, 1000));
setAnalysisStep('Analyzing competitor content and strategy...');
setAnalysisProgress(60);
await new Promise(resolve => setTimeout(resolve, 1500));
setAnalysisStep('Generating competitive insights...');
setAnalysisProgress(80);
await new Promise(resolve => setTimeout(resolve, 1000));
// Get website URL from props or localStorage
const finalUserUrl = userUrl || localStorage.getItem('website_url') || '';
// Get website analysis data from localStorage or step data
const websiteAnalysisData = localStorage.getItem('website_analysis_data')
? JSON.parse(localStorage.getItem('website_analysis_data')!)
: null;
console.log('CompetitorAnalysisStep: Final URL to use:', finalUserUrl);
console.log('CompetitorAnalysisStep: Making request with data:', {
user_url: finalUserUrl,
industry_context: industryContext,
num_results: 25,
website_analysis_data: websiteAnalysisData
});
const response = await aiApiClient.post('/api/onboarding/step3/discover-competitors', {
// session_id removed - backend gets user from auth token
user_url: finalUserUrl,
industry_context: industryContext,
num_results: 25,
website_analysis_data: websiteAnalysisData
});
const result = response.data;
if (result.success) {
setAnalysisStep('Finalizing analysis...');
setAnalysisProgress(100);
await new Promise(resolve => setTimeout(resolve, 500));
setCompetitors(result.competitors || []);
setSocialMediaAccounts(result.social_media_accounts || {});
setSocialMediaCitations(result.social_media_citations || []);
setResearchSummary(result.research_summary || null);
setShowProgressModal(false);
setIsAnalyzing(false);
} else {
throw new Error(result.error || 'Competitor discovery failed');
}
} catch (err) {
console.error('Competitor discovery error:', err);
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
setIsAnalyzing(false);
setShowProgressModal(false);
}
}, [userUrl, industryContext]); // sessionId removed from dependencies
useEffect(() => {
startCompetitorDiscovery();
}, [startCompetitorDiscovery]);
const handleContinue = () => {
const researchData = {
competitors,
researchSummary,
userUrl,
industryContext,
analysisTimestamp: new Date().toISOString()
};
onContinue(researchData);
};
const handleShowHighlights = (competitor: Competitor) => {
setSelectedCompetitorHighlights(competitor.highlights || []);
setSelectedCompetitorTitle(competitor.title);
setShowHighlightsModal(true);
};
return (
<Box sx={classes.container}>
<Box sx={classes.header}>
<Typography variant="h4" sx={{ fontWeight: 600, mb: 2 }}>
Research Your Competition
</Typography>
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 400 }}>
Discover your competitors and analyze their strategies to gain competitive advantage
</Typography>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
<Button
startIcon={<RefreshIcon />}
onClick={startCompetitorDiscovery}
sx={{ ml: 2 }}
>
Retry
</Button>
</Alert>
)}
{!isAnalyzing && !error && (competitors.length > 0 || researchSummary) && (
<Box>
{researchSummary && (
<Paper sx={{ p: 3, mb: 4, backgroundColor: 'primary.50', border: '1px solid', borderColor: 'primary.200' }}>
<Typography variant="h6" gutterBottom fontWeight={600} color="primary">
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Research Summary
</Typography>
<Grid container spacing={3} mt={1}>
<Grid item xs={12} md={3}>
<Typography variant="h4" color="primary" fontWeight={700}>
{researchSummary.total_competitors}
</Typography>
<Typography variant="body2" color="text.secondary">
Competitors Found
</Typography>
</Grid>
<Grid item xs={12} md={9}>
<Typography variant="body1" color="text.secondary">
{researchSummary.market_insights}
</Typography>
</Grid>
</Grid>
</Paper>
)}
{/* Social Media Accounts Section */}
{Object.keys(socialMediaAccounts).length > 0 && (
<>
<Typography variant="h6" gutterBottom fontWeight={600} mb={3}>
<ShareIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Social Media Presence
</Typography>
<Grid container spacing={2} mb={4}>
{Object.entries(socialMediaAccounts).map(([platform, url]) => {
if (!url) return null;
const platformIcons: { [key: string]: React.ReactNode } = {
facebook: <FacebookIcon />,
instagram: <InstagramIcon />,
linkedin: <LinkedInIcon />,
youtube: <YouTubeIcon />,
twitter: <TwitterIcon />,
tiktok: <ShareIcon /> // Fallback icon for TikTok
};
return (
<Grid item xs={12} sm={6} md={4} key={platform}>
<Card sx={{ height: '100%' }}>
<CardContent>
<Box display="flex" alignItems="center" gap={2}>
<Avatar sx={{ width: 40, height: 40, bgcolor: 'primary.main' }}>
{platformIcons[platform] || <ShareIcon />}
</Avatar>
<Box flex={1}>
<Typography variant="h6" fontWeight={600} textTransform="capitalize">
{platform}
</Typography>
<Button
variant="text"
size="small"
href={url as string}
target="_blank"
rel="noopener noreferrer"
sx={{ p: 0, minWidth: 'auto', textTransform: 'none' }}
>
View Profile
</Button>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
);
})}
</Grid>
</>
)}
<Typography variant="h6" gutterBottom fontWeight={600} mb={3}>
<BusinessIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Discovered Competitors ({competitors.length})
</Typography>
<Grid container spacing={3}>
{competitors.map((competitor, index) => (
<Grid item xs={12} md={6} lg={4} key={index}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent sx={{ flexGrow: 1 }}>
<Box display="flex" alignItems="flex-start" gap={2} mb={2}>
<Avatar sx={{ width: 40, height: 40 }}>
<BusinessIcon />
</Avatar>
<Box flex={1}>
<Typography variant="h6" fontWeight={600} gutterBottom>
{competitor.title}
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
{competitor.domain}
</Typography>
<Chip
label={`${Math.round(competitor.relevance_score * 100)}% Match`}
color="primary"
size="small"
/>
</Box>
</Box>
<Typography variant="body2" color="text.secondary" mb={2}>
{competitor.summary.length > 150
? `${competitor.summary.substring(0, 150)}...`
: competitor.summary
}
</Typography>
</CardContent>
<CardActions sx={{ p: 2, pt: 0 }}>
<Button
size="small"
startIcon={<OpenInNewIcon />}
onClick={() => window.open(competitor.url, '_blank')}
>
Visit Website
</Button>
{competitor.highlights && competitor.highlights.length > 0 && (
<Button
size="small"
variant="outlined"
onClick={() => handleShowHighlights(competitor)}
>
Highlights
</Button>
)}
</CardActions>
</Card>
</Grid>
))}
</Grid>
<Box display="flex" justifyContent="center" mt={4}>
<Button
variant="contained"
size="large"
onClick={handleContinue}
sx={{
px: 4,
py: 1.5,
fontSize: '1.1rem',
fontWeight: 600,
borderRadius: 2
}}
>
Continue to Next Step
</Button>
</Box>
</Box>
)}
<Dialog
open={showProgressModal}
onClose={() => {}}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
p: 3
}
}}
>
<DialogTitle sx={{ textAlign: 'center', pb: 2 }}>
<Box display="flex" alignItems="center" justifyContent="center" gap={2}>
<CircularProgress size={32} color="primary" />
<Typography variant="h6" fontWeight={600}>
Analyzing Your Competition
</Typography>
</Box>
</DialogTitle>
<DialogContent sx={{ textAlign: 'center', pt: 2 }}>
<Typography variant="body1" color="text.secondary" mb={3}>
We're discovering your competitors and analyzing their strategies using AI...
</Typography>
<Box mb={3}>
<LinearProgress
variant="determinate"
value={analysisProgress}
sx={{
height: 8,
borderRadius: 4,
mb: 2
}}
/>
<Typography variant="body2" color="text.secondary">
{analysisProgress}% Complete
</Typography>
</Box>
<Typography variant="body2" color="primary" fontWeight={500}>
{analysisStep}
</Typography>
</DialogContent>
</Dialog>
{/* Highlights Modal */}
<Dialog
open={showHighlightsModal}
onClose={() => setShowHighlightsModal(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>
<Typography variant="h6" fontWeight={600}>
Key Highlights - {selectedCompetitorTitle}
</Typography>
</DialogTitle>
<DialogContent>
{selectedCompetitorHighlights.length > 0 ? (
<Box>
{selectedCompetitorHighlights.map((highlight, index) => (
<Box
key={index}
sx={{
p: 2,
mb: 2,
border: '1px solid',
borderColor: 'divider',
borderRadius: 1,
backgroundColor: 'background.paper'
}}
>
<Typography variant="body2" color="text.secondary">
{highlight}
</Typography>
</Box>
))}
</Box>
) : (
<Typography variant="body2" color="text.secondary">
No highlights available for this competitor.
</Typography>
)}
</DialogContent>
</Dialog>
</Box>
);
};
export default CompetitorAnalysisStep;

View File

@@ -1,914 +0,0 @@
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;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
/**
* AnalysisProgressDisplay Component
* Displays the progress tracking for website analysis
*/
import React from 'react';
import {
Box,
Typography,
Card,
CardContent,
LinearProgress,
Stepper,
Step,
StepLabel
} from '@mui/material';
import {
Analytics as AnalyticsIcon
} from '@mui/icons-material';
interface AnalysisProgress {
step: number;
message: string;
completed: boolean;
}
interface AnalysisProgressDisplayProps {
loading: boolean;
progress: AnalysisProgress[];
}
const AnalysisProgressDisplay: React.FC<AnalysisProgressDisplayProps> = ({
loading,
progress
}) => {
const getProgressPercentage = () => {
const completedSteps = progress.filter(p => p.completed).length;
return (completedSteps / progress.length) * 100;
};
if (!loading) {
return null;
}
return (
<Card sx={{ mb: 3, p: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Analysis Progress
</Typography>
<LinearProgress
variant="determinate"
value={getProgressPercentage()}
sx={{ mb: 2 }}
/>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
{Math.round(getProgressPercentage())}% Complete
</Typography>
<Stepper orientation="vertical" activeStep={progress.filter(p => p.completed).length}>
{progress.map((step) => (
<Step key={step.step} completed={step.completed}>
<StepLabel>
<Typography variant="body2">
{step.message}
</Typography>
</StepLabel>
</Step>
))}
</Stepper>
</Card>
);
};
export default AnalysisProgressDisplay;

View File

@@ -0,0 +1,759 @@
/**
* AnalysisResultsDisplay Component
* Displays the comprehensive website analysis results
*/
import React from 'react';
import {
Box,
Typography,
Card,
CardContent,
Grid,
Divider,
Checkbox,
FormControlLabel,
Alert,
Paper,
Tooltip
} from '@mui/material';
import {
AutoAwesome as AutoAwesomeIcon,
Verified as VerifiedIcon,
Psychology as PsychologyIcon,
Analytics as AnalyticsIcon,
TrendingUp as TrendingUpIcon,
Language as LanguageIcon,
Palette as PaletteIcon,
Speed as SpeedIcon,
Group as GroupIcon,
Business as BusinessIcon,
Lightbulb as LightbulbIcon,
Warning as WarningIcon
} from '@mui/icons-material';
// Import rendering utilities
import {
renderKeyInsight,
renderProUpgradeAlert,
renderBrandAnalysisSection,
renderContentStrategyInsightsSection,
renderAIGenerationTipsSection,
renderBestPracticesSection,
renderAvoidElementsSection,
renderStylePatternsSection
} from '../utils/renderUtils';
// Import extracted components
import { EnhancedGuidelinesSection, KeyInsightsGrid } from './index';
import { useOnboardingStyles } from '../../common/useOnboardingStyles';
interface StyleAnalysis {
writing_style?: {
tone: string;
voice: string;
complexity: string;
engagement_level: string;
brand_personality?: string;
formality_level?: string;
emotional_appeal?: string;
};
content_characteristics?: {
sentence_structure: string;
vocabulary_level: string;
paragraph_organization: string;
content_flow: string;
readability_score?: string;
content_density?: string;
visual_elements_usage?: string;
};
target_audience?: {
demographics: string[];
expertise_level: string;
industry_focus: string;
geographic_focus: string;
psychographic_profile?: string;
pain_points?: string[];
motivations?: string[];
};
content_type?: {
primary_type: string;
secondary_types: string[];
purpose: string;
call_to_action: string;
conversion_focus?: string;
educational_value?: string;
};
brand_analysis?: {
brand_voice: string;
brand_values: string[];
brand_positioning: string;
competitive_differentiation: string;
trust_signals: string[];
authority_indicators: string[];
};
content_strategy_insights?: {
strengths: string[];
weaknesses: string[];
opportunities: string[];
threats: string[];
recommended_improvements: string[];
content_gaps: string[];
};
recommended_settings?: {
writing_tone: string;
target_audience: string;
content_type: string;
creativity_level: string;
geographic_location: string;
industry_context?: string;
brand_alignment?: string;
};
guidelines?: {
tone_recommendations: string[];
structure_guidelines: string[];
vocabulary_suggestions: string[];
engagement_tips: string[];
audience_considerations: string[];
brand_alignment?: string[];
seo_optimization?: string[];
conversion_optimization?: string[];
};
best_practices?: string[];
avoid_elements?: string[];
content_strategy?: string;
ai_generation_tips?: string[];
competitive_advantages?: string[];
content_calendar_suggestions?: string[];
style_patterns?: {
sentence_length: string;
vocabulary_patterns: string[];
rhetorical_devices: string[];
paragraph_structure: string;
transition_phrases: string[];
};
patterns?: {
sentence_length: string;
vocabulary_patterns: string[];
rhetorical_devices: string[];
paragraph_structure: string;
transition_phrases: string[];
};
style_consistency?: string;
unique_elements?: string[];
}
interface AnalysisResultsDisplayProps {
analysis: StyleAnalysis;
domainName: string;
useAnalysisForGenAI: boolean;
onUseAnalysisChange: (use: boolean) => void;
}
const AnalysisResultsDisplay: React.FC<AnalysisResultsDisplayProps> = ({
analysis,
domainName,
useAnalysisForGenAI,
onUseAnalysisChange
}) => {
const styles = useOnboardingStyles();
return (
<Box sx={styles.analysisContainer}>
{/* Pro Upgrade Alert */}
{renderProUpgradeAlert()}
{/* Main Analysis Results */}
<Card sx={styles.analysisHeaderCard}>
<CardContent sx={styles.analysisCardContent}>
<Box sx={styles.analysisHeader}>
<VerifiedIcon sx={styles.analysisHeaderIcon} />
<Box>
<Typography variant="h4" sx={styles.analysisHeaderTitle} gutterBottom>
{domainName} Style Analysis
</Typography>
<Typography variant="body1" sx={styles.analysisHeaderSubtitle}>
Comprehensive content analysis and personalized recommendations
</Typography>
</Box>
</Box>
{/* Key Insights Grid */}
<KeyInsightsGrid
writing_style={analysis.writing_style}
target_audience={analysis.target_audience}
content_type={analysis.content_type}
/>
{/* Content Characteristics Section */}
{analysis.content_characteristics && (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<AnalyticsIcon color="info" />
Content Characteristics
</Typography>
<Grid container spacing={2}>
{analysis.content_characteristics.vocabulary_level && (
<Grid item xs={12} md={6}>
<Tooltip title="The complexity and sophistication of words used in the content. Higher levels use more advanced vocabulary while accessible levels use simpler, everyday words." arrow>
<Box>
{renderKeyInsight(
'Vocabulary Level',
analysis.content_characteristics.vocabulary_level,
<LanguageIcon />,
'info'
)}
</Box>
</Tooltip>
</Grid>
)}
{analysis.content_characteristics.readability_score && (
<Grid item xs={12} md={6}>
<Tooltip title="How easy it is for readers to understand the content. Higher scores mean the content is easier to read and comprehend." arrow>
<Box>
{renderKeyInsight(
'Readability Score',
analysis.content_characteristics.readability_score,
<SpeedIcon />,
'success'
)}
</Box>
</Tooltip>
</Grid>
)}
{analysis.content_characteristics.content_density && (
<Grid item xs={12} md={6}>
<Tooltip title="How much information is packed into each section. Moderate density balances information with readability." arrow>
<Box>
{renderKeyInsight(
'Content Density',
analysis.content_characteristics.content_density,
<PaletteIcon />,
'warning'
)}
</Box>
</Tooltip>
</Grid>
)}
{analysis.content_characteristics.sentence_structure && (
<Grid item xs={12} md={6}>
<Tooltip title="The variety and complexity of sentence patterns used. Varied structures keep readers engaged." arrow>
<Box>
{renderKeyInsight(
'Sentence Structure',
analysis.content_characteristics.sentence_structure,
<AnalyticsIcon />,
'secondary'
)}
</Box>
</Tooltip>
</Grid>
)}
{analysis.content_characteristics.paragraph_organization && (
<Grid item xs={12} md={6}>
<Tooltip title="How paragraphs are structured and organized. Clear organization helps readers follow the content easily." arrow>
<Box>
{renderKeyInsight(
'Paragraph Organization',
analysis.content_characteristics.paragraph_organization,
<AnalyticsIcon />,
'primary'
)}
</Box>
</Tooltip>
</Grid>
)}
{analysis.content_characteristics.content_flow && (
<Grid item xs={12} md={6}>
<Tooltip title="How smoothly the content moves from one idea to the next. Good flow keeps readers engaged throughout." arrow>
<Box>
{renderKeyInsight(
'Content Flow',
analysis.content_characteristics.content_flow,
<TrendingUpIcon />,
'success'
)}
</Box>
</Tooltip>
</Grid>
)}
{analysis.content_characteristics.visual_elements_usage && (
<Grid item xs={12} md={6}>
<Tooltip title="How often images, charts, and other visual elements are used to support the text content." arrow>
<Box>
{renderKeyInsight(
'Visual Elements Usage',
analysis.content_characteristics.visual_elements_usage,
<PaletteIcon />,
'warning'
)}
</Box>
</Tooltip>
</Grid>
)}
</Grid>
</Box>
)}
{/* Detailed Target Audience Section */}
{analysis.target_audience && (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<GroupIcon color="info" />
Target Audience Analysis
</Typography>
<Grid container spacing={2}>
{analysis.target_audience.demographics && analysis.target_audience.demographics.length > 0 && (
<Grid item xs={12} md={6}>
{renderKeyInsight(
'Demographics',
analysis.target_audience.demographics,
<GroupIcon />,
'info'
)}
</Grid>
)}
{analysis.target_audience.industry_focus && (
<Grid item xs={12} md={6}>
{renderKeyInsight(
'Industry Focus',
analysis.target_audience.industry_focus,
<BusinessIcon />,
'primary'
)}
</Grid>
)}
{analysis.target_audience.geographic_focus && (
<Grid item xs={12} md={6}>
{renderKeyInsight(
'Geographic Focus',
analysis.target_audience.geographic_focus,
<AnalyticsIcon />,
'secondary'
)}
</Grid>
)}
{analysis.target_audience.psychographic_profile && (
<Grid item xs={12} md={6}>
<Paper elevation={2} sx={styles.analysisAccentPaperSuccess}>
<Box display="flex" alignItems="center" gap={2}>
<Box sx={styles.analysisAccentIconSuccess}>
<PsychologyIcon />
</Box>
<Box flex={1}>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
Psychographic Profile
</Typography>
<Box component="ul" sx={styles.analysisList}>
{Array.isArray(analysis.target_audience.psychographic_profile)
? analysis.target_audience.psychographic_profile.map((item: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={styles.analysisListItem}>
{item}
</Typography>
))
: (
<Typography component="li" variant="body2" sx={styles.analysisListItem}>
{analysis.target_audience.psychographic_profile}
</Typography>
)}
</Box>
</Box>
</Box>
</Paper>
</Grid>
)}
{analysis.target_audience.pain_points && analysis.target_audience.pain_points.length > 0 && (
<Grid item xs={12} md={6}>
<Paper elevation={2} sx={styles.analysisAccentPaperError}>
<Box display="flex" alignItems="center" gap={2}>
<Box sx={styles.analysisAccentIconError}>
<WarningIcon />
</Box>
<Box flex={1}>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
Pain Points
</Typography>
<Box component="ul" sx={styles.analysisList}>
{analysis.target_audience.pain_points.map((painPoint: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={styles.analysisListItem}>
{painPoint}
</Typography>
))}
</Box>
</Box>
</Box>
</Paper>
</Grid>
)}
{analysis.target_audience.motivations && analysis.target_audience.motivations.length > 0 && (
<Grid item xs={12} md={6}>
<Paper elevation={2} sx={styles.analysisAccentPaperSuccess}>
<Box display="flex" alignItems="center" gap={2}>
<Box sx={styles.analysisAccentIconSuccess}>
<TrendingUpIcon />
</Box>
<Box flex={1}>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
Motivations
</Typography>
<Box component="ul" sx={styles.analysisList}>
{analysis.target_audience.motivations.map((motivation: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={styles.analysisListItem}>
{motivation}
</Typography>
))}
</Box>
</Box>
</Box>
</Paper>
</Grid>
)}
</Grid>
</Box>
)}
{/* Content Type Details Section */}
{analysis.content_type && (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<BusinessIcon color="primary" />
Content Type Analysis
</Typography>
<Grid container spacing={2}>
{analysis.content_type.purpose && (
<Grid item xs={12} md={6}>
{renderKeyInsight(
'Content Purpose',
analysis.content_type.purpose,
<AutoAwesomeIcon />,
'primary'
)}
</Grid>
)}
{analysis.content_type.call_to_action && (
<Grid item xs={12} md={6}>
{renderKeyInsight(
'Call to Action Style',
analysis.content_type.call_to_action,
<TrendingUpIcon />,
'success'
)}
</Grid>
)}
{analysis.content_type.conversion_focus && (
<Grid item xs={12} md={6}>
{renderKeyInsight(
'Conversion Focus',
analysis.content_type.conversion_focus,
<AnalyticsIcon />,
'info'
)}
</Grid>
)}
{analysis.content_type.educational_value && (
<Grid item xs={12} md={6}>
{renderKeyInsight(
'Educational Value',
analysis.content_type.educational_value,
<LightbulbIcon />,
'warning'
)}
</Grid>
)}
{analysis.content_type.secondary_types && analysis.content_type.secondary_types.length > 0 && (
<Grid item xs={12}>
<Paper elevation={2} sx={styles.analysisAccentPaperSuccess}>
<Box display="flex" alignItems="center" gap={2}>
<Box sx={styles.analysisAccentIconSuccess}>
<BusinessIcon />
</Box>
<Box flex={1}>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
Secondary Content Types
</Typography>
<Box component="ul" sx={styles.analysisList}>
{analysis.content_type.secondary_types.map((type: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={styles.analysisListItem}>
{type}
</Typography>
))}
</Box>
</Box>
</Box>
</Paper>
</Grid>
)}
</Grid>
</Box>
)}
<Divider sx={styles.analysisDivider} />
{/* Content Strategy */}
{analysis.content_strategy && (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<AutoAwesomeIcon color="primary" />
Content Strategy
</Typography>
<Paper elevation={3} sx={styles.analysisGradientPaperPrimary}>
<Typography variant="body1" sx={styles.analysisParagraph}>
{analysis.content_strategy}
</Typography>
</Paper>
</Box>
)}
{/* Recommended Settings for AI Generation */}
{analysis.recommended_settings && (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<AutoAwesomeIcon color="primary" />
Recommended AI Generation Settings
</Typography>
<Paper elevation={3} sx={styles.analysisGradientPaperPrimary}>
<Grid container spacing={2}>
{analysis.recommended_settings.writing_tone && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={styles.analysisSubheader}>
Writing Tone:
</Typography>
<Typography variant="body1" sx={styles.analysisValue}>
{analysis.recommended_settings.writing_tone}
</Typography>
</Grid>
)}
{analysis.recommended_settings.target_audience && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={styles.analysisSubheader}>
Target Audience:
</Typography>
<Typography variant="body1" sx={styles.analysisValue}>
{analysis.recommended_settings.target_audience}
</Typography>
</Grid>
)}
{analysis.recommended_settings.content_type && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={styles.analysisSubheader}>
Content Type:
</Typography>
<Typography variant="body1" sx={styles.analysisValue}>
{analysis.recommended_settings.content_type}
</Typography>
</Grid>
)}
{analysis.recommended_settings.creativity_level && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={styles.analysisSubheader}>
Creativity Level:
</Typography>
<Typography variant="body1" sx={styles.analysisValue}>
{analysis.recommended_settings.creativity_level}
</Typography>
</Grid>
)}
{analysis.recommended_settings.industry_context && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={styles.analysisSubheader}>
Industry Context:
</Typography>
<Typography variant="body1" sx={styles.analysisValue}>
{analysis.recommended_settings.industry_context}
</Typography>
</Grid>
)}
{analysis.recommended_settings.geographic_location && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={styles.analysisSubheader}>
Geographic Focus:
</Typography>
<Typography variant="body1" sx={styles.analysisValue}>
{analysis.recommended_settings.geographic_location}
</Typography>
</Grid>
)}
{analysis.recommended_settings.brand_alignment && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={styles.analysisSubheader}>
Brand Alignment:
</Typography>
<Typography variant="body1" sx={styles.analysisValue}>
{analysis.recommended_settings.brand_alignment}
</Typography>
</Grid>
)}
</Grid>
</Paper>
</Box>
)}
{/* Brand Analysis */}
{analysis.brand_analysis && renderBrandAnalysisSection(analysis.brand_analysis)}
{/* Content Strategy Insights */}
{analysis.content_strategy_insights && renderContentStrategyInsightsSection(analysis.content_strategy_insights)}
{/* AI Generation Tips */}
{analysis.ai_generation_tips && renderAIGenerationTipsSection(analysis.ai_generation_tips)}
{/* Style Patterns Section */}
{(analysis.style_patterns || analysis.patterns) && (
<Box sx={styles.analysisSection}>
{renderStylePatternsSection(analysis.style_patterns || analysis.patterns)}
</Box>
)}
{/* Style Consistency Section */}
{analysis.style_consistency && (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<AnalyticsIcon color="info" />
Style Consistency
</Typography>
<Paper elevation={3} sx={styles.analysisGradientPaperWarning}>
<Typography variant="body1" sx={styles.analysisParagraph}>
{analysis.style_consistency}
</Typography>
</Paper>
</Box>
)}
{/* Unique Elements Section */}
{analysis.unique_elements && analysis.unique_elements.length > 0 && (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<AutoAwesomeIcon color="primary" />
Unique Style Elements
</Typography>
<Paper elevation={3} sx={styles.analysisGradientPaperAccent}>
<Box component="ul" sx={styles.analysisList}>
{analysis.unique_elements.map((element: string, index: number) => (
<Typography component="li" variant="body1" key={index} sx={styles.analysisListItem}>
{element}
</Typography>
))}
</Box>
</Paper>
</Box>
)}
{/* Enhanced Guidelines Section */}
{analysis.guidelines && (
<EnhancedGuidelinesSection
guidelines={analysis.guidelines}
domainName={domainName}
/>
)}
{/* Best Practices & Avoid Elements */}
<Grid container spacing={2} sx={styles.analysisSection}>
{analysis.best_practices && (
<Grid item xs={12} md={6}>
{renderBestPracticesSection(analysis.best_practices)}
</Grid>
)}
{analysis.avoid_elements && (
<Grid item xs={12} md={6}>
{renderAvoidElementsSection(analysis.avoid_elements)}
</Grid>
)}
</Grid>
{/* Competitive Advantages */}
{analysis.competitive_advantages && analysis.competitive_advantages.length > 0 && (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<TrendingUpIcon color="success" />
Competitive Advantages
</Typography>
<Paper elevation={3} sx={styles.analysisGradientPaperSuccess}>
<Box component="ul" sx={styles.analysisList}>
{analysis.competitive_advantages.map((advantage: string, index: number) => (
<Typography component="li" variant="body1" key={index} sx={styles.analysisListItem}>
{advantage}
</Typography>
))}
</Box>
</Paper>
</Box>
)}
{/* Content Calendar Suggestions */}
{analysis.content_calendar_suggestions && analysis.content_calendar_suggestions.length > 0 && (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<AnalyticsIcon color="primary" />
Content Calendar Suggestions
</Typography>
<Paper elevation={3} sx={styles.analysisGradientPaperInfo}>
<Box component="ul" sx={styles.analysisList}>
{analysis.content_calendar_suggestions.map((suggestion: string, index: number) => (
<Typography component="li" variant="body1" key={index} sx={styles.analysisListItem}>
{suggestion}
</Typography>
))}
</Box>
</Paper>
</Box>
)}
{/* GenAI Integration Checkbox */}
<Box sx={styles.analysisCheckboxContainer}>
<FormControlLabel
control={
<Checkbox
checked={useAnalysisForGenAI}
onChange={(e) => onUseAnalysisChange(e.target.checked)}
color="primary"
size="large"
/>
}
label={
<Box>
<Typography variant="h6" sx={styles.analysisSubheader} gutterBottom>
Use Analysis for AI Content Generation
</Typography>
<Typography variant="body2" color="textSecondary">
Apply this style analysis to personalize AI-generated content, ensuring it matches {domainName}'s voice and tone.
</Typography>
</Box>
}
/>
</Box>
{/* Success Message */}
<Alert severity="success" sx={styles.analysisSuccessAlert}>
<Typography variant="body1" sx={styles.analysisAlertText}>
Analysis complete! Your content style has been analyzed and personalized recommendations are ready.
</Typography>
</Alert>
</CardContent>
</Card>
</Box>
);
};
export default AnalysisResultsDisplay;

View File

@@ -0,0 +1,149 @@
/**
* Enhanced Guidelines Section Component
* Displays comprehensive content guidelines for the analyzed website
*/
import React from 'react';
import {
Box,
Typography,
Grid
} from '@mui/material';
import {
Psychology as PsychologyIcon,
Analytics as AnalyticsIcon,
TrendingUp as TrendingUpIcon,
Language as LanguageIcon,
Web as WebIcon,
Business as BusinessIcon,
Group as GroupIcon,
Lightbulb as LightbulbIcon
} from '@mui/icons-material';
// Import rendering utilities
import { renderGuidelinesCard } from '../utils/renderUtils';
import { useOnboardingStyles } from '../../common/useOnboardingStyles';
interface Guidelines {
tone_recommendations?: string[];
structure_guidelines?: string[];
vocabulary_suggestions?: string[];
engagement_tips?: string[];
audience_considerations?: string[];
brand_alignment?: string[];
seo_optimization?: string[];
conversion_optimization?: string[];
}
interface EnhancedGuidelinesSectionProps {
guidelines: Guidelines;
domainName: string;
}
const EnhancedGuidelinesSection: React.FC<EnhancedGuidelinesSectionProps> = ({
guidelines,
domainName
}) => {
const styles = useOnboardingStyles();
return (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>
<LightbulbIcon color="primary" />
Enhanced Content Guidelines for {domainName}
</Typography>
<Grid container spacing={3}>
{guidelines.tone_recommendations && (
<Grid item xs={12} md={6}>
{renderGuidelinesCard(
'Tone Recommendations',
guidelines.tone_recommendations,
<PsychologyIcon />,
'primary'
)}
</Grid>
)}
{guidelines.structure_guidelines && (
<Grid item xs={12} md={6}>
{renderGuidelinesCard(
'Structure Guidelines',
guidelines.structure_guidelines,
<AnalyticsIcon />,
'secondary'
)}
</Grid>
)}
{guidelines.engagement_tips && (
<Grid item xs={12} md={6}>
{renderGuidelinesCard(
'Engagement Tips',
guidelines.engagement_tips,
<TrendingUpIcon />,
'success'
)}
</Grid>
)}
{guidelines.vocabulary_suggestions && (
<Grid item xs={12} md={6}>
{renderGuidelinesCard(
'Vocabulary Suggestions',
guidelines.vocabulary_suggestions,
<LanguageIcon />,
'info'
)}
</Grid>
)}
{guidelines.brand_alignment && (
<Grid item xs={12} md={6}>
{renderGuidelinesCard(
'Brand Alignment',
guidelines.brand_alignment,
<BusinessIcon />,
'warning'
)}
</Grid>
)}
{guidelines.seo_optimization && (
<Grid item xs={12} md={6}>
{renderGuidelinesCard(
'SEO Optimization',
guidelines.seo_optimization,
<WebIcon />,
'primary'
)}
</Grid>
)}
{guidelines.conversion_optimization && (
<Grid item xs={12} md={6}>
{renderGuidelinesCard(
'Conversion Optimization',
guidelines.conversion_optimization,
<TrendingUpIcon />,
'success'
)}
</Grid>
)}
{guidelines.audience_considerations && (
<Grid item xs={12} md={6}>
{renderGuidelinesCard(
'Audience Considerations',
guidelines.audience_considerations,
<GroupIcon />,
'info'
)}
</Grid>
)}
</Grid>
</Box>
);
};
export default EnhancedGuidelinesSection;

View File

@@ -0,0 +1,211 @@
/**
* Key Insights Grid Component
* Displays the main key insights in a grid layout
*/
import React from 'react';
import {
Grid,
Tooltip,
Box
} from '@mui/material';
import {
Palette as PaletteIcon,
Speed as SpeedIcon,
Language as LanguageIcon,
TrendingUp as TrendingUpIcon,
Business as BusinessIcon,
Psychology as PsychologyIcon,
Group as GroupIcon,
Explore as ExploreIcon
} from '@mui/icons-material';
// Import rendering utilities
import { renderKeyInsight } from '../utils/renderUtils';
interface WritingStyle {
tone?: string;
voice?: string;
complexity?: string;
engagement_level?: string;
brand_personality?: string;
formality_level?: string;
emotional_appeal?: string;
}
interface TargetAudience {
expertise_level?: string;
geographic_focus?: string;
}
interface ContentType {
primary_type?: string;
}
interface KeyInsightsGridProps {
writing_style?: WritingStyle;
target_audience?: TargetAudience;
content_type?: ContentType;
}
const KeyInsightsGrid: React.FC<KeyInsightsGridProps> = ({
writing_style,
target_audience,
content_type
}) => {
return (
<Grid container spacing={2} sx={{ mb: 2.5 }}>
{writing_style?.tone && (
<Grid item xs={12} md={6}>
<Tooltip title="The emotional quality and attitude of the writing - how it makes readers feel and the mood it creates." arrow>
<Box>
{renderKeyInsight(
'Writing Tone',
writing_style.tone,
<PaletteIcon />,
'primary'
)}
</Box>
</Tooltip>
</Grid>
)}
{writing_style?.complexity && (
<Grid item xs={12} md={6}>
<Tooltip title="How sophisticated or simple the content is. Moderate complexity balances depth with accessibility." arrow>
<Box>
{renderKeyInsight(
'Content Complexity',
writing_style.complexity,
<SpeedIcon />,
'secondary'
)}
</Box>
</Tooltip>
</Grid>
)}
{writing_style?.voice && (
<Grid item xs={12} md={6}>
<Tooltip title="The unique personality and style of the writing - what makes it distinctive and recognizable." arrow>
<Box>
{renderKeyInsight(
'Writing Voice',
writing_style.voice,
<LanguageIcon />,
'info'
)}
</Box>
</Tooltip>
</Grid>
)}
{writing_style?.engagement_level && (
<Grid item xs={12} md={6}>
<Tooltip title="How well the content captures and maintains reader attention throughout the piece." arrow>
<Box>
{renderKeyInsight(
'Engagement Level',
writing_style.engagement_level,
<TrendingUpIcon />,
'success'
)}
</Box>
</Tooltip>
</Grid>
)}
{writing_style?.brand_personality && (
<Grid item xs={12} md={6}>
<Tooltip title="The human characteristics and traits associated with the brand, like friendly, professional, or innovative." arrow>
<Box>
{renderKeyInsight(
'Brand Personality',
writing_style.brand_personality,
<BusinessIcon />,
'warning'
)}
</Box>
</Tooltip>
</Grid>
)}
{writing_style?.formality_level && (
<Grid item xs={12} md={6}>
<Tooltip title="How formal or casual the writing style is. Semi-formal strikes a balance between professional and approachable." arrow>
<Box>
{renderKeyInsight(
'Formality Level',
writing_style.formality_level,
<PsychologyIcon />,
'primary'
)}
</Box>
</Tooltip>
</Grid>
)}
{writing_style?.emotional_appeal && (
<Grid item xs={12} md={6}>
<Tooltip title="How the content connects with readers' emotions - what feelings it aims to evoke." arrow>
<Box>
{renderKeyInsight(
'Emotional Appeal',
writing_style.emotional_appeal,
<PaletteIcon />,
'secondary'
)}
</Box>
</Tooltip>
</Grid>
)}
{target_audience?.expertise_level && (
<Grid item xs={12} md={6}>
<Tooltip title="The skill level and experience of the intended readers - from beginners to experts in the subject matter." arrow>
<Box>
{renderKeyInsight(
'Target Audience',
target_audience.expertise_level,
<GroupIcon />,
'info'
)}
</Box>
</Tooltip>
</Grid>
)}
{target_audience?.geographic_focus && target_audience.geographic_focus.trim() !== '' && (
<Grid item xs={12} md={6}>
<Tooltip title="The geographical regions or areas the content is primarily intended for - local, national, or global reach." arrow>
<Box>
{renderKeyInsight(
'Geographic Focus',
target_audience.geographic_focus,
<ExploreIcon />,
'secondary'
)}
</Box>
</Tooltip>
</Grid>
)}
{content_type?.primary_type && (
<Grid item xs={12} md={6}>
<Tooltip title="The main category or format of content being created - blog posts, tutorials, product descriptions, etc." arrow>
<Box>
{renderKeyInsight(
'Content Type',
content_type.primary_type,
<BusinessIcon />,
'warning'
)}
</Box>
</Tooltip>
</Grid>
)}
</Grid>
);
};
export default KeyInsightsGrid;

View File

@@ -0,0 +1,9 @@
/**
* Website Step Components Index
* Exports all components for the WebsiteStep
*/
export { default as AnalysisResultsDisplay } from './AnalysisResultsDisplay';
export { default as AnalysisProgressDisplay } from './AnalysisProgressDisplay';
export { default as EnhancedGuidelinesSection } from './EnhancedGuidelinesSection';
export { default as KeyInsightsGrid } from './KeyInsightsGrid';

View File

@@ -0,0 +1,29 @@
/**
* Website Step Utils Index
* Exports all utility functions for the WebsiteStep component
*/
// Website utilities
export {
fixUrlFormat,
extractDomainName,
checkExistingAnalysis,
loadExistingAnalysis,
performAnalysis,
fetchLastAnalysis
} from './websiteUtils';
// Rendering utilities
export {
renderKeyInsight,
renderGuidelinesCard,
renderProUpgradeAlert,
renderBrandAnalysisSection,
renderContentStrategyInsightsSection,
renderAIGenerationTipsSection,
renderBestPracticesSection,
renderAvoidElementsSection,
renderAnalysisSection,
renderGuidelinesSection,
renderStylePatternsSection
} from './renderUtils';

View File

@@ -0,0 +1,507 @@
/**
* Website Step Rendering Utility Functions
* Extracted rendering components for website analysis display
*/
import React from 'react';
import {
Box,
Typography,
Paper,
Card,
CardContent,
Grid,
Accordion,
AccordionSummary,
AccordionDetails,
Alert,
Button,
Slide,
Zoom,
Divider
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
CheckCircle as CheckIcon,
Info as InfoIcon,
Psychology as PsychologyIcon,
TrendingUp as TrendingUpIcon,
Analytics as AnalyticsIcon,
Business as BusinessIcon,
AutoAwesome as AutoAwesomeIcon,
Star as StarIcon,
Warning as WarningIcon,
Language as LanguageIcon,
Web as WebIcon,
Palette as PaletteIcon,
Speed as SpeedIcon,
Group as GroupIcon
} from '@mui/icons-material';
/**
* Renders a key insight card with icon and value
*/
export const renderKeyInsight = (
title: string,
value: string | string[],
icon: React.ReactNode,
color: string = 'primary'
) => (
<Paper
elevation={3}
sx={{
p: 2,
mb: 1.5,
borderRadius: 2,
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.12) 100%)',
border: `1px solid rgba(255, 255, 255, 0.15)`,
borderLeft: `4px solid`,
borderLeftColor: `${color}.main`,
backdropFilter: 'blur(10px)',
transition: 'all 0.2s ease-in-out',
'&:hover': {
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.16) 100%)',
border: `1px solid rgba(255, 255, 255, 0.25)`,
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.3)'
}
}}
>
<Box display="flex" alignItems="center" gap={2}>
<Box sx={{ color: `${color}.main`, fontSize: '1.2rem' }}>
{icon}
</Box>
<Box flex={1}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom sx={{ fontWeight: 600, fontSize: '0.85rem' }}>
{title}
</Typography>
<Typography variant="body1" fontWeight={600} color="text.primary" sx={{ fontSize: '0.95rem' }}>
{Array.isArray(value) ? value.join(', ') : value}
</Typography>
</Box>
</Box>
</Paper>
);
/**
* Renders a guidelines card with title, items, and icon
*/
export const renderGuidelinesCard = (
title: string,
items: string[],
icon: React.ReactNode,
color: string = 'primary'
) => (
<Zoom in timeout={600}>
<Card sx={{ mb: 2, border: `1px solid ${color}.light` }}>
<CardContent>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<Box sx={{ color: `${color}.main` }}>
{icon}
</Box>
<Typography variant="h6" fontWeight={600}>
{title}
</Typography>
</Box>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{items.map((item, index) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1, lineHeight: 1.6 }}>
{item}
</Typography>
))}
</Box>
</CardContent>
</Card>
</Zoom>
);
/**
* Renders the pro upgrade alert
*/
export const renderProUpgradeAlert = () => (
<Slide direction="up" in timeout={1000}>
<Alert
severity="info"
sx={{
mb: 3,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
'& .MuiAlert-icon': { color: 'white' }
}}
action={
<Button color="inherit" size="small" variant="outlined" sx={{ color: 'white', borderColor: 'white' }}>
Learn More
</Button>
}
>
<Typography variant="subtitle2" gutterBottom>
<StarIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Limited Analysis Scope
</Typography>
<Typography variant="body2">
This analysis is based on your homepage only. <strong>ALwrity Pro</strong> can index your entire website and social media content for comprehensive personalized content generation.
</Typography>
</Alert>
</Slide>
);
/**
* Renders the brand analysis section
*/
export const renderBrandAnalysisSection = (brandAnalysis: any) => (
<Zoom in timeout={700}>
<Card sx={{ mb: 2, border: '2px solid info.light', background: 'info.50' }}>
<CardContent>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<BusinessIcon color="info" />
<Typography variant="h6" fontWeight={600} color="info.main">
Brand Analysis
</Typography>
</Box>
<Grid container spacing={2}>
{brandAnalysis.brand_voice && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" color="primary" gutterBottom>
Brand Voice:
</Typography>
<Typography variant="body2" sx={{ mb: 2 }}>
{brandAnalysis.brand_voice}
</Typography>
</Grid>
)}
{brandAnalysis.brand_positioning && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" color="primary" gutterBottom>
Brand Positioning:
</Typography>
<Typography variant="body2" sx={{ mb: 2 }}>
{brandAnalysis.brand_positioning}
</Typography>
</Grid>
)}
{brandAnalysis.brand_values && brandAnalysis.brand_values.length > 0 && (
<Grid item xs={12}>
<Typography variant="subtitle2" color="primary" gutterBottom>
Brand Values:
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{brandAnalysis.brand_values.map((value: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1 }}>
{value}
</Typography>
))}
</Box>
</Grid>
)}
</Grid>
</CardContent>
</Card>
</Zoom>
);
/**
* Renders the content strategy insights section
*/
export const renderContentStrategyInsightsSection = (insights: any) => (
<Zoom in timeout={800}>
<Card sx={{ mb: 2, border: '2px solid secondary.light', background: 'secondary.50' }}>
<CardContent>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<AnalyticsIcon color="secondary" />
<Typography variant="h6" fontWeight={600} color="secondary.main">
Content Strategy Insights
</Typography>
</Box>
<Grid container spacing={3}>
{insights.strengths && insights.strengths.length > 0 && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" color="success.main" gutterBottom>
Strengths:
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{insights.strengths.map((strength: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1 }}>
{strength}
</Typography>
))}
</Box>
</Grid>
)}
{insights.opportunities && insights.opportunities.length > 0 && (
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" color="info.main" gutterBottom>
🎯 Opportunities:
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{insights.opportunities.map((opportunity: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1 }}>
{opportunity}
</Typography>
))}
</Box>
</Grid>
)}
{insights.recommended_improvements && insights.recommended_improvements.length > 0 && (
<Grid item xs={12}>
<Typography variant="subtitle2" color="primary" gutterBottom>
🔧 Recommended Improvements:
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{insights.recommended_improvements.map((improvement: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1 }}>
{improvement}
</Typography>
))}
</Box>
</Grid>
)}
</Grid>
</CardContent>
</Card>
</Zoom>
);
/**
* Renders the AI generation tips section
*/
export const renderAIGenerationTipsSection = (tips: string[]) => (
<Zoom in timeout={900}>
<Card sx={{ mb: 2, border: '2px solid primary.light', background: 'primary.50' }}>
<CardContent>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<AutoAwesomeIcon color="primary" />
<Typography variant="h6" fontWeight={600} color="primary.main">
AI Content Generation Tips
</Typography>
</Box>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{tips.map((tip: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1, lineHeight: 1.6 }}>
{tip}
</Typography>
))}
</Box>
</CardContent>
</Card>
</Zoom>
);
/**
* Renders a best practices section card
*/
export const renderBestPracticesSection = (bestPractices: string[]) => (
<Zoom in timeout={800}>
<Card sx={{ border: '2px solid success.light', background: 'success.50' }}>
<CardContent>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<CheckIcon color="success" />
<Typography variant="h6" fontWeight={600} color="success.main">
Best Practices
</Typography>
</Box>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{bestPractices.map((practice, index) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1, lineHeight: 1.6 }}>
{practice}
</Typography>
))}
</Box>
</CardContent>
</Card>
</Zoom>
);
/**
* Renders an avoid elements section card
*/
export const renderAvoidElementsSection = (avoidElements: string[]) => (
<Zoom in timeout={1000}>
<Card sx={{ border: '2px solid warning.light', background: 'warning.50' }}>
<CardContent>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<WarningIcon color="warning" />
<Typography variant="h6" fontWeight={600} color="warning.main">
Elements to Avoid
</Typography>
</Box>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{avoidElements.map((element, index) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1, lineHeight: 1.6 }}>
{element}
</Typography>
))}
</Box>
</CardContent>
</Card>
</Zoom>
);
/**
* Renders a generic analysis section accordion
*/
export const renderAnalysisSection = (
title: string,
data: any,
icon: React.ReactNode,
description?: string
) => (
<Accordion key={title} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box display="flex" alignItems="center" gap={1}>
{icon}
<Typography variant="h6">{title}</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
{description && (
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
{description}
</Typography>
)}
<Grid container spacing={2}>
{Object.entries(data).map(([key, value]) => (
<Grid item xs={12} md={6} key={key}>
<Typography variant="subtitle2" color="primary" gutterBottom>
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:
</Typography>
<Typography variant="body2">
{Array.isArray(value) ? value.join(', ') : String(value)}
</Typography>
</Grid>
))}
</Grid>
</AccordionDetails>
</Accordion>
);
/**
* Renders the guidelines section accordion
*/
export const renderGuidelinesSection = (guidelines: any) => (
<Accordion sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box display="flex" alignItems="center" gap={1}>
<PsychologyIcon color="primary" />
<Typography variant="h6">Content Guidelines</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
Personalized recommendations for improving your content creation based on your writing style analysis.
</Typography>
{guidelines.tone_recommendations && (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" color="primary" gutterBottom>
Tone Recommendations
</Typography>
<Box component="ul" sx={{ pl: 2 }}>
{guidelines.tone_recommendations.map((rec: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1 }}>
{rec}
</Typography>
))}
</Box>
</Box>
)}
{guidelines.structure_guidelines && (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" color="primary" gutterBottom>
Structure Guidelines
</Typography>
<Box component="ul" sx={{ pl: 2 }}>
{guidelines.structure_guidelines.map((guideline: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1 }}>
{guideline}
</Typography>
))}
</Box>
</Box>
)}
{guidelines.vocabulary_suggestions && (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" color="primary" gutterBottom>
Vocabulary Suggestions
</Typography>
<Box component="ul" sx={{ pl: 2 }}>
{guidelines.vocabulary_suggestions.map((suggestion: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1 }}>
{suggestion}
</Typography>
))}
</Box>
</Box>
)}
{guidelines.engagement_tips && (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" color="primary" gutterBottom>
Engagement Tips
</Typography>
<Box component="ul" sx={{ pl: 2 }}>
{guidelines.engagement_tips.map((tip: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1 }}>
{tip}
</Typography>
))}
</Box>
</Box>
)}
{guidelines.audience_considerations && (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" color="primary" gutterBottom>
Audience Considerations
</Typography>
<Box component="ul" sx={{ pl: 2 }}>
{guidelines.audience_considerations.map((consideration: string, index: number) => (
<Typography component="li" variant="body2" key={index} sx={{ mb: 1 }}>
{consideration}
</Typography>
))}
</Box>
</Box>
)}
</AccordionDetails>
</Accordion>
);
/**
* Renders the style patterns section accordion
*/
export const renderStylePatternsSection = (patterns: any) => (
<Accordion sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box display="flex" alignItems="center" gap={1}>
<AnalyticsIcon color="secondary" />
<Typography variant="h6">Style Patterns</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
Recurring patterns and characteristics identified in your writing style.
</Typography>
<Grid container spacing={2}>
{Object.entries(patterns).map(([key, value]) => (
<Grid item xs={12} md={6} key={key}>
<Typography variant="subtitle2" color="primary" gutterBottom>
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:
</Typography>
<Typography variant="body2">
{Array.isArray(value) ? value.join(', ') : String(value)}
</Typography>
</Grid>
))}
</Grid>
</AccordionDetails>
</Accordion>
);

View File

@@ -0,0 +1,273 @@
/**
* Website Step Utility Functions
* Extracted utility functions for website analysis and URL handling
*/
import { apiClient } from '../../../../api/client';
/**
* Fixes URL format by adding protocol if missing and ensuring proper format
* @param url - The URL string to fix
* @returns Fixed URL string or null if invalid
*/
export 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;
}
};
/**
* Extracts domain name from URL for personalization
* @param url - The URL to extract domain from
* @returns Formatted domain name or fallback text
*/
export const extractDomainName = (url: string): string => {
try {
const domain = new URL(url).hostname.replace('www.', '');
return domain.charAt(0).toUpperCase() + domain.slice(1);
} catch {
return 'Your Website';
}
};
/**
* Checks for existing analysis for a given URL
* @param url - The URL to check for existing analysis
* @returns Promise<boolean> - Whether existing analysis was found
*/
export const checkExistingAnalysis = async (url: string): Promise<{
exists: boolean;
analysis?: any;
error?: string;
}> => {
try {
console.log('WebsiteStep: Checking existing analysis for URL:', url);
const response = await apiClient.get(`/api/onboarding/style-detection/check-existing/${encodeURIComponent(url)}`);
const result = response.data;
if (result.exists) {
console.log('WebsiteStep: Existing analysis found:', result);
return {
exists: true,
analysis: result
};
} else {
console.log('WebsiteStep: No existing analysis found');
return {
exists: false
};
}
} catch (error) {
console.error('WebsiteStep: Error checking existing analysis:', error);
return {
exists: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
};
/**
* Loads existing analysis by ID
* @param analysisId - The ID of the analysis to load
* @param website - The website URL for domain extraction
* @returns Promise<boolean> - Whether loading was successful
*/
export const loadExistingAnalysis = async (analysisId: number, website: string): Promise<{
success: boolean;
analysis?: any;
domainName?: string;
error?: string;
}> => {
try {
const response = await apiClient.get(`/api/onboarding/style-detection/analysis/${analysisId}`);
const result = response.data;
if (result.success && result.analysis) {
// Extract domain name for personalization
const extractedDomain = extractDomainName(website);
// Combine all analysis data into a comprehensive object
const comprehensiveAnalysis = {
...result.analysis.style_analysis,
guidelines: result.analysis.style_guidelines,
best_practices: result.analysis.style_guidelines?.best_practices,
avoid_elements: result.analysis.style_guidelines?.avoid_elements,
content_strategy: result.analysis.style_guidelines?.content_strategy,
style_patterns: result.analysis.style_patterns,
style_consistency: result.analysis.style_patterns?.style_consistency,
unique_elements: result.analysis.style_patterns?.unique_elements
};
return {
success: true,
analysis: comprehensiveAnalysis,
domainName: extractedDomain
};
}
return {
success: false,
error: 'Analysis not found'
};
} catch (error) {
console.error('Error loading existing analysis:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
};
/**
* Performs new website analysis
* @param fixedUrl - The fixed URL to analyze
* @param updateProgress - Callback function to update progress
* @returns Promise<object> - Analysis result
*/
export const performAnalysis = async (
fixedUrl: string,
updateProgress: (step: number, message: string) => void
): Promise<{
success: boolean;
analysis?: any;
domainName?: string;
warning?: string;
error?: string;
}> => {
try {
// Simulate progress updates
updateProgress(1, 'Website URL validated');
const requestData = {
url: fixedUrl,
include_patterns: true,
include_guidelines: true
};
updateProgress(2, 'Starting content crawl...');
const response = await apiClient.post('/api/onboarding/style-detection/complete', requestData);
updateProgress(3, 'Content extracted successfully');
updateProgress(4, 'Style analysis in progress...');
updateProgress(5, 'Content characteristics analyzed');
updateProgress(6, 'Target audience identified');
updateProgress(7, 'Recommendations generated');
const result = response.data;
if (result.success) {
// Extract domain name for personalization
const extractedDomain = extractDomainName(fixedUrl);
// Combine all analysis data into a comprehensive object
const comprehensiveAnalysis = {
...result.style_analysis,
guidelines: result.style_guidelines,
best_practices: result.style_guidelines?.best_practices,
avoid_elements: result.style_guidelines?.avoid_elements,
content_strategy: result.style_guidelines?.content_strategy,
style_patterns: result.style_patterns,
style_consistency: result.style_patterns?.style_consistency,
unique_elements: result.style_patterns?.unique_elements
};
return {
success: true,
analysis: comprehensiveAnalysis,
domainName: extractedDomain,
warning: result.warning
};
} else {
// Handle specific error cases
let errorMessage = result.error || 'Analysis failed';
if (errorMessage.includes('API key') || errorMessage.includes('configure')) {
errorMessage = 'API keys not configured. Please complete step 1 of onboarding to configure your AI provider API keys.';
} else if (errorMessage.includes('library not available')) {
errorMessage = 'AI provider library not available. Please ensure your AI provider is properly configured in step 1.';
} else if (errorMessage.includes('crawl') || errorMessage.includes('website')) {
errorMessage = 'Unable to access the website. Please check the URL and ensure the website is publicly accessible.';
}
return {
success: false,
error: errorMessage
};
}
} catch (error) {
console.error('Analysis error:', error);
return {
success: false,
error: 'Failed to analyze website. Please check your internet connection and try again.'
};
}
};
/**
* Fetches the last analysis from session for pre-filling
* @returns Promise<object> - Last analysis data
*/
export const fetchLastAnalysis = async (): Promise<{
success: boolean;
website?: string;
analysis?: any;
error?: string;
}> => {
try {
// Fixed: Added /onboarding prefix to match backend router
const res = await apiClient.get('/api/onboarding/style-detection/session-analyses');
const data = res.data;
if (data.success && Array.isArray(data.analyses) && data.analyses.length > 0) {
// Pick the most recent analysis (assuming sorted by date desc, else sort here)
const last = data.analyses[0];
if (last && last.website_url) {
return {
success: true,
website: last.website_url,
analysis: last.style_analysis
};
}
}
return {
success: false,
error: 'No previous analysis found'
};
} catch (err) {
console.error('WebsiteStep: Error pre-filling from last analysis', err);
return {
success: false,
error: err instanceof Error ? err.message : 'Unknown error'
};
}
};

View File

@@ -23,10 +23,12 @@ import {
HelpOutline,
Close
} from '@mui/icons-material';
import UserBadge from '../shared/UserBadge';
import { startOnboarding, getCurrentStep, setCurrentStep, getProgress } from '../../api/onboarding';
import { apiClient } from '../../api/client';
import ApiKeyStep from './ApiKeyStep';
import WebsiteStep from './WebsiteStep';
import ResearchStep from './ResearchStep';
import CompetitorAnalysisStep from './CompetitorAnalysisStep';
import PersonalizationStep from './PersonalizationStep';
import IntegrationsStep from './IntegrationsStep';
import FinalStep from './FinalStep';
@@ -34,7 +36,7 @@ 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: 'Research', description: 'Discover competitors', icon: '🔍' },
{ label: 'Personalization', description: 'Customize your experience', icon: '⚙️' },
{ label: 'Integrations', description: 'Connect additional services', icon: '🔗' },
{ label: 'Finish', description: 'Complete setup', icon: '✅' }
@@ -57,6 +59,8 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
const [showHelp, setShowHelp] = useState(false);
const [showProgressMessage, setShowProgressMessage] = useState(false);
const [progressMessage, setProgressMessage] = useState('');
// sessionId removed - backend uses Clerk user ID from auth token
const [stepData, setStepData] = useState<any>(null);
const [stepHeaderContent, setStepHeaderContent] = useState<StepHeaderContent>({
title: steps[0].label,
description: steps[0].description
@@ -72,27 +76,49 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
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);
// Check if we already have init data from App (cached in sessionStorage)
const cachedInit = sessionStorage.getItem('onboarding_init');
// 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);
if (cachedInit) {
console.log('Wizard: Using cached init data from batch endpoint');
const data = JSON.parse(cachedInit);
// Extract data from batch response
const { user, onboarding, session } = data;
// Set state from cached data - NO API CALLS NEEDED!
setActiveStep(onboarding.current_step - 1);
setProgressState(onboarding.completion_percentage);
// Note: Session managed by Clerk auth, no need to track separately
console.log('Wizard: Initialized from cache:', {
step: onboarding.current_step,
progress: onboarding.completion_percentage,
userId: session.session_id // Clerk user ID from backend
});
setLoading(false);
return; // ← Skip redundant API calls!
}
// 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');
// Fallback: If no cached data (shouldn't happen), make batch call
console.log('Wizard: No cached data, making batch init call');
const response = await apiClient.get('/api/onboarding/init');
const { user, onboarding, session } = response.data;
// Cache for future use
sessionStorage.setItem('onboarding_init', JSON.stringify(response.data));
// Set state from API response
setActiveStep(onboarding.current_step - 1);
setProgressState(onboarding.completion_percentage);
// Note: Session managed by Clerk auth, no need to track separately
console.log('Wizard: Initialized from API:', {
step: onboarding.current_step,
progress: onboarding.completion_percentage,
userId: session.session_id // Clerk user ID from backend
});
} catch (error) {
console.error('Error initializing onboarding:', error);
} finally {
@@ -102,8 +128,26 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
init();
}, []);
const handleNext = async () => {
console.log('Wizard: handleNext called');
const handleNext = async (rawStepData?: any) => {
if (rawStepData && typeof rawStepData === 'object') {
if (typeof rawStepData.preventDefault === 'function') {
rawStepData.preventDefault();
}
if (typeof rawStepData.stopPropagation === 'function') {
rawStepData.stopPropagation();
}
}
const currentStepData = rawStepData && typeof rawStepData === 'object' && 'nativeEvent' in rawStepData
? undefined
: rawStepData;
// Store step data in state
if (currentStepData) {
setStepData(currentStepData);
}
console.log('Wizard: handleNext called with stepData:', currentStepData);
console.log('Wizard: Current activeStep:', activeStep);
console.log('Wizard: Steps length:', steps.length);
@@ -124,13 +168,28 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
// 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);
const stepWasCompleted = currentStepData && typeof currentStepData === 'object' && (currentStepData.website || currentStepData.businessData);
if (!stepWasCompleted) {
console.warn('Wizard: No serialized step data supplied; skipping backend completion for step', currentStepNumber);
} else {
console.log('Wizard: Completing current step:', currentStepNumber, 'with data:', currentStepData);
try {
await setCurrentStep(currentStepNumber, currentStepData);
} catch (error) {
console.error('Wizard: Failed to complete step with backend. Aborting progression.', error);
setShowProgressMessage(false);
setProgressMessage('');
setLoading(false);
return;
}
console.log('Wizard: Checking backend step after completion...');
const stepResponse = await getCurrentStep();
console.log('Wizard: Backend says current step should be:', stepResponse.step);
}
setActiveStep(nextStep);
console.log('Wizard: Setting activeStep to:', nextStep);
@@ -151,7 +210,8 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
setDirection('left');
const prevStep = activeStep - 1;
setActiveStep(prevStep);
await setCurrentStep(prevStep + 1);
// Do not complete a step when navigating back; just update UI state
// Backend step progression should only occur on forward completion with valid data
// Update progress
const newProgress = ((prevStep + 1) / steps.length) * 100;
@@ -162,7 +222,7 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
if (stepIndex <= activeStep) {
setDirection(stepIndex > activeStep ? 'right' : 'left');
setActiveStep(stepIndex);
setCurrentStep(stepIndex + 1);
// Do not complete a step on arbitrary step navigation; only adjust UI
}
};
@@ -181,10 +241,18 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
};
const renderStepContent = (step: number) => {
console.log('Wizard: renderStepContent called with step:', step, 'stepData:', stepData);
const stepComponents = [
<ApiKeyStep key="api-keys" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<WebsiteStep key="website" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<ResearchStep key="research" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<CompetitorAnalysisStep
key="research"
onContinue={handleNext}
onBack={handleBack}
userUrl={stepData?.website || ''}
industryContext={stepData?.industryContext}
/>,
<PersonalizationStep key="personalization" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<IntegrationsStep key="integrations" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<FinalStep key="final" onContinue={handleComplete} updateHeaderContent={updateHeaderContent} />
@@ -327,7 +395,9 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
{/* 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: 1 }}>
<UserBadge colorMode="dark" />
</Box>
<Box sx={{ flex: 2, textAlign: 'center' }}>
<Typography variant="h4" sx={{ fontWeight: 700, letterSpacing: '-0.025em' }}>
{stepHeaderContent.title}

View File

@@ -1,4 +1,4 @@
import { useTheme } from '@mui/material';
import { useTheme, alpha } from '@mui/material/styles';
export const useOnboardingStyles = () => {
const theme = useTheme();
@@ -236,6 +236,230 @@ export const useOnboardingStyles = () => {
buttonSpacing: {
gap: 2,
},
// Analysis step styles
analysisContainer: {
display: 'flex',
flexDirection: 'column',
gap: 2,
width: '100%',
},
analysisHeaderCard: {
mb: 2,
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.08) 100%)',
borderRadius: 2,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
border: `1px solid rgba(255, 255, 255, 0.1)`,
backdropFilter: 'blur(20px)',
overflow: 'hidden',
},
analysisCardContent: {
p: { xs: 2, md: 3 },
},
analysisHeader: {
display: 'flex',
alignItems: 'center',
gap: 1.5,
mb: 2,
},
analysisHeaderIcon: {
fontSize: 28,
color: theme.palette.success.main,
},
analysisHeaderTitle: {
fontWeight: 700,
letterSpacing: '-0.025em',
color: theme.palette.text.primary,
fontSize: '1.5rem',
},
analysisHeaderSubtitle: {
color: theme.palette.text.secondary,
fontSize: '0.95rem',
lineHeight: 1.5,
mt: 0.5,
},
analysisSection: {
mb: 2.5,
},
analysisSectionHeader: {
display: 'flex',
alignItems: 'center',
gap: 1,
fontWeight: 600,
color: theme.palette.text.primary,
fontSize: '1.1rem',
mb: 1.5,
},
analysisSubheader: {
fontWeight: 600,
mb: 0.5,
color: theme.palette.text.secondary,
fontSize: '0.9rem',
},
analysisDivider: {
my: 2,
opacity: 0.6,
},
analysisParagraph: {
lineHeight: 1.6,
fontSize: '0.95rem',
color: theme.palette.text.primary,
},
analysisGradientPaperPrimary: {
p: { xs: 2, md: 2.5 },
borderRadius: 2,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
boxShadow: '0 12px 28px rgba(118, 75, 162, 0.4)',
border: '1px solid rgba(118, 75, 162, 0.4)',
backdropFilter: 'blur(10px)',
},
analysisGradientPaperWarning: {
p: { xs: 2, md: 2.5 },
borderRadius: 2,
background: 'linear-gradient(135deg, #ff9800 0%, #ff5722 100%)',
color: 'white',
boxShadow: '0 12px 28px rgba(255, 87, 34, 0.4)',
border: '1px solid rgba(255, 152, 0, 0.4)',
backdropFilter: 'blur(10px)',
},
analysisGradientPaperSuccess: {
p: { xs: 2, md: 2.5 },
borderRadius: 2,
background: 'linear-gradient(135deg, #4caf50 0%, #43a047 100%)',
color: 'white',
boxShadow: '0 12px 28px rgba(76, 175, 80, 0.4)',
border: '1px solid rgba(67, 160, 71, 0.4)',
backdropFilter: 'blur(10px)',
},
analysisGradientPaperInfo: {
p: { xs: 2, md: 2.5 },
borderRadius: 2,
background: 'linear-gradient(135deg, #2196f3 0%, #21cbf3 100%)',
color: 'white',
boxShadow: '0 12px 28px rgba(33, 150, 243, 0.4)',
border: '1px solid rgba(33, 203, 243, 0.4)',
backdropFilter: 'blur(10px)',
},
analysisGradientPaperAccent: {
p: { xs: 2, md: 2.5 },
borderRadius: 2,
background: 'linear-gradient(135deg, #9c27b0 0%, #673ab7 100%)',
color: 'white',
boxShadow: '0 12px 28px rgba(156, 39, 176, 0.4)',
border: '1px solid rgba(103, 58, 183, 0.4)',
backdropFilter: 'blur(10px)',
},
analysisAccentPaperError: {
p: { xs: 2, md: 2.5 },
mb: 2,
borderRadius: 2,
borderLeft: `4px solid ${theme.palette.error.main}`,
background: 'linear-gradient(135deg, rgba(244, 67, 54, 0.15) 0%, rgba(244, 67, 54, 0.08) 100%)',
border: `1px solid rgba(244, 67, 54, 0.2)`,
backdropFilter: 'blur(10px)',
},
analysisAccentPaperSuccess: {
p: { xs: 2, md: 2.5 },
mb: 2,
borderRadius: 2,
borderLeft: `4px solid ${theme.palette.success.main}`,
background: 'linear-gradient(135deg, rgba(76, 175, 80, 0.15) 0%, rgba(76, 175, 80, 0.08) 100%)',
border: `1px solid rgba(76, 175, 80, 0.2)`,
backdropFilter: 'blur(10px)',
},
analysisAccentPaperInfo: {
p: { xs: 2, md: 2.5 },
mb: 2,
borderRadius: 2,
borderLeft: `4px solid ${theme.palette.info.main}`,
background: 'linear-gradient(135deg, rgba(33, 150, 243, 0.15) 0%, rgba(33, 150, 243, 0.08) 100%)',
border: `1px solid rgba(33, 150, 243, 0.2)`,
backdropFilter: 'blur(10px)',
},
analysisAccentIconError: {
color: theme.palette.error.main,
},
analysisAccentIconSuccess: {
color: theme.palette.success.main,
},
analysisAccentIconInfo: {
color: theme.palette.info.main,
},
analysisList: {
pl: 2,
m: 0,
listStyle: 'disc',
'& li': {
marginBottom: 1,
},
},
analysisListItem: {
lineHeight: 1.6,
},
analysisLabel: {
fontWeight: 600,
opacity: 0.85,
},
analysisValue: {
fontWeight: 500,
},
analysisInfoBadge: {
display: 'inline-flex',
alignItems: 'center',
gap: 1,
px: 1.5,
py: 0.5,
borderRadius: 999,
background: alpha(theme.palette.primary.light, 0.15),
color: theme.palette.primary.main,
fontSize: '0.875rem',
fontWeight: 600,
},
analysisCheckboxContainer: {
p: { xs: 2.5, md: 3 },
background: alpha(theme.palette.primary.light, 0.2),
borderRadius: 2,
border: `2px solid ${alpha(theme.palette.primary.main, 0.28)}`,
mb: 3,
},
analysisSuccessAlert: {
borderRadius: 2,
mb: 0,
},
analysisAlertText: {
fontWeight: 500,
},
};
return styles;