Added onboarding progress tracking & landing page
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user