Added onboarding progress tracking & landing page

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

View File

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

View File

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

View File

@@ -0,0 +1,123 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
List,
ListItem,
ListItemIcon,
ListItemText,
Box,
Typography,
} from '@mui/material';
export interface Provider {
name: string;
description: string;
benefits: string[];
key: string;
setKey: (key: string) => void;
showKey: boolean;
setShowKey: (show: boolean) => void;
placeholder: string;
status: 'valid' | 'invalid' | 'empty';
link: string;
free: boolean;
recommended: boolean;
}
interface BenefitsModalProps {
open: boolean;
onClose: () => void;
selectedProvider: Provider | null;
}
const BenefitsModal: React.FC<BenefitsModalProps> = ({
open,
onClose,
selectedProvider,
}) => {
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.1)',
border: '1px solid rgba(0, 0, 0, 0.08)',
},
}}
>
<DialogTitle
sx={{
pb: 1,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600,
}}
>
{selectedProvider?.name} Benefits
</DialogTitle>
<DialogContent sx={{ pt: 0 }}>
<Typography
variant="body2"
color="text.secondary"
sx={{
mb: 2,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
Discover what {selectedProvider?.name} can do for your content creation:
</Typography>
<List sx={{ pt: 0 }}>
{selectedProvider?.benefits.map((benefit: string, index: number) => (
<ListItem key={index} sx={{ px: 0, py: 1 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
background: 'primary.main',
flexShrink: 0,
}}
/>
</ListItemIcon>
<ListItemText
primary={benefit}
sx={{
'& .MuiListItemText-primary': {
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 500,
fontSize: '0.875rem',
},
}}
/>
</ListItem>
))}
</List>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 1 }}>
<Button
onClick={onClose}
variant="contained"
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
Got it
</Button>
</DialogActions>
</Dialog>
);
};
export default BenefitsModal;

View File

@@ -0,0 +1,250 @@
import React from 'react';
import {
Box,
Typography,
Paper,
Grid,
Link,
Collapse,
} from '@mui/material';
import {
HelpOutline,
Star,
Info,
} from '@mui/icons-material';
interface HelpSectionProps {
showHelp: boolean;
}
const HelpSection: React.FC<HelpSectionProps> = ({ showHelp }) => {
return (
<Collapse in={showHelp}>
<Paper
elevation={0}
sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid rgba(59, 130, 246, 0.2)',
borderRadius: 3,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
}}
>
<Typography
variant="h6"
gutterBottom
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 3,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600,
}}
>
<HelpOutline color="primary" />
How to Get Your AI API Keys
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Box sx={{ mb: 3 }}>
<Typography
variant="subtitle1"
gutterBottom
sx={{
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 1,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
<Star sx={{ color: 'warning.main', fontSize: 20 }} />
Required Providers
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box>
<Typography
variant="subtitle2"
sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
Google Gemini
</Typography>
<Typography
variant="body2"
color="text.secondary"
gutterBottom
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
Visit{' '}
<Link
href="https://makersuite.google.com/app/apikey"
target="_blank"
rel="noopener noreferrer"
sx={{
fontWeight: 600,
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
}}
>
makersuite.google.com
</Link>
, create an account, and generate an API key.
</Typography>
</Box>
<Box>
<Typography
variant="subtitle2"
sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
Exa AI
</Typography>
<Typography
variant="body2"
color="text.secondary"
gutterBottom
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
Visit{' '}
<Link
href="https://dashboard.exa.ai/login"
target="_blank"
rel="noopener noreferrer"
sx={{
fontWeight: 600,
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
}}
>
dashboard.exa.ai
</Link>
, sign up for a free account, and create an API key.
</Typography>
</Box>
<Box>
<Typography
variant="subtitle2"
sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
CopilotKit
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
Visit{' '}
<Link
href="https://copilotkit.ai"
target="_blank"
rel="noopener noreferrer"
sx={{
fontWeight: 600,
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
}}
>
copilotkit.ai
</Link>
, sign up, and generate a public API key (starts with ck_pub_).
</Typography>
</Box>
</Box>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box>
<Typography
variant="subtitle1"
gutterBottom
sx={{
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 1,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
<Info sx={{ color: 'info.main', fontSize: 20 }} />
Why These Services Matter
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
<strong>Gemini:</strong> Powers AI content generation and intelligent writing assistance.
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
<strong>Exa AI:</strong> Enables advanced web research and real-time information gathering.
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
<strong>CopilotKit:</strong> Provides in-app AI assistant for enhanced user experience.
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
}}
>
<strong>All Required:</strong> These three services work together to provide complete AI functionality.
</Typography>
</Box>
</Box>
</Grid>
</Grid>
</Paper>
</Collapse>
);
};
export default HelpSection;

View File

@@ -0,0 +1,332 @@
import React from 'react';
import {
Box,
TextField,
Typography,
Chip,
IconButton,
Button,
Card,
CardContent,
Tooltip,
} from '@mui/material';
import {
Visibility,
VisibilityOff,
CheckCircle,
Error,
Key,
Lock,
Launch,
Info as InfoIcon,
Recommend,
MoneyOff,
} from '@mui/icons-material';
export interface Provider {
name: string;
description: string;
benefits: string[];
key: string;
setKey: (key: string) => void;
showKey: boolean;
setShowKey: (show: boolean) => void;
placeholder: string;
status: 'valid' | 'invalid' | 'empty';
link: string;
free: boolean;
recommended: boolean;
}
interface ProviderCardProps {
provider: Provider;
savedKeys: Record<string, string>;
onBenefitsClick: (provider: Provider) => void;
}
const ProviderCard: React.FC<ProviderCardProps> = ({
provider,
savedKeys,
onBenefitsClick,
}) => {
return (
<Card
sx={{
border: `1px solid ${
provider.status === 'valid'
? 'rgba(16, 185, 129, 0.2)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.2)'
: 'rgba(0,0,0,0.08)'
}`,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04)',
transform: 'translateY(-1px)',
borderColor:
provider.status === 'valid'
? 'rgba(16, 185, 129, 0.4)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.4)'
: 'rgba(0,0,0,0.12)',
},
position: 'relative',
overflow: 'hidden',
background: 'rgba(255, 255, 255, 0.8)',
backdropFilter: 'blur(10px)',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 2,
background:
provider.status === 'valid'
? 'linear-gradient(90deg, rgba(16, 185, 129, 0.6) 0%, rgba(5, 150, 105, 0.6) 100%)'
: provider.status === 'invalid'
? 'linear-gradient(90deg, rgba(239, 68, 68, 0.6) 0%, rgba(220, 38, 38, 0.6) 100%)'
: 'linear-gradient(90deg, rgba(107, 114, 128, 0.3) 0%, rgba(75, 85, 99, 0.3) 100%)',
},
}}
>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box
sx={{
width: 40,
height: 40,
borderRadius: '50%',
background: provider.recommended
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
>
<Key sx={{ color: 'white', fontSize: 20 }} />
</Box>
<Box sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography
variant="h6"
sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: '1.125rem',
color: 'text.primary',
}}
>
{provider.name}
</Typography>
{provider.recommended && (
<Tooltip title="Recommended by ALwrity" arrow>
<Recommend
sx={{
color: 'success.main',
fontSize: 18,
cursor: 'help',
}}
/>
</Tooltip>
)}
{provider.free && (
<Tooltip title="Free tier available" arrow>
<MoneyOff
sx={{
color: 'primary.main',
fontSize: 18,
cursor: 'help',
}}
/>
</Tooltip>
)}
</Box>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400,
lineHeight: 1.4,
}}
>
{provider.description}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
variant="text"
onClick={() => onBenefitsClick(provider)}
startIcon={<InfoIcon />}
sx={{
color: 'primary.main',
fontWeight: 600,
fontSize: '0.75rem',
fontFamily: 'Inter, system-ui, sans-serif',
textTransform: 'none',
padding: '2px 6px',
borderRadius: 1,
minWidth: 'auto',
'&:hover': {
background: 'rgba(102, 126, 234, 0.08)',
transform: 'translateY(-1px)',
},
}}
>
Benefits ({provider.benefits.length})
</Button>
{provider.status === 'valid' && (
<Chip
icon={<CheckCircle />}
label="Valid"
color="success"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 24,
}}
/>
)}
{provider.status === 'invalid' && (
<Chip
icon={<Error />}
label="Invalid"
color="error"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 24,
}}
/>
)}
</Box>
</Box>
<TextField
fullWidth
type={provider.showKey ? 'text' : 'password'}
value={provider.key}
onChange={(e) => provider.setKey(e.target.value)}
placeholder={provider.placeholder}
variant="outlined"
size="small"
name={`api-key-${provider.name.toLowerCase()}`}
autoComplete="off"
InputProps={{
startAdornment: <Lock sx={{ color: 'text.secondary', mr: 1, fontSize: 16 }} />,
endAdornment: (
<IconButton
onClick={() => provider.setShowKey(!provider.showKey)}
edge="end"
size="small"
sx={{
color: 'text.secondary',
'&:hover': {
color: 'primary.main',
background: 'rgba(102, 126, 234, 0.08)',
},
}}
>
{provider.showKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
border: '1px solid rgba(0,0,0,0.12)',
background: 'rgba(255, 255, 255, 0.8)',
'&:hover': {
borderColor: 'rgba(0,0,0,0.24)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
},
'&.Mui-focused': {
borderColor:
provider.status === 'valid'
? 'rgba(16, 185, 129, 0.6)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.6)'
: 'rgba(102, 126, 234, 0.6)',
boxShadow: `0 0 0 2px ${
provider.status === 'valid'
? 'rgba(16, 185, 129, 0.1)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.1)'
: 'rgba(102, 126, 234, 0.1)'
}, 0 2px 8px rgba(0, 0, 0, 0.08)`,
'& .MuiOutlinedInput-notchedOutline': {
border: 'none',
},
},
'& .MuiOutlinedInput-notchedOutline': {
border: 'none',
},
},
'& .MuiInputBase-input': {
padding: '12px 14px',
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 500,
fontSize: '0.875rem',
},
}}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1.5 }}>
<Button
href={provider.link}
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
fontWeight: 600,
fontSize: '0.9rem',
color: 'primary.main',
textDecoration: 'none',
fontFamily: 'Inter, system-ui, sans-serif',
padding: '4px 8px',
borderRadius: 1,
transition: 'all 0.2s ease',
'&:hover': {
background: 'rgba(102, 126, 234, 0.08)',
textDecoration: 'none',
transform: 'translateY(-1px)',
},
}}
>
Get API Key
<Launch sx={{ fontSize: 16 }} />
</Button>
</Box>
{savedKeys[provider.name.toLowerCase()] && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
<Typography
variant="caption"
color="success.main"
sx={{
fontWeight: 500,
fontFamily: 'Inter, system-ui, sans-serif',
}}
>
Key already saved and secured
</Typography>
</Box>
)}
</CardContent>
</Card>
);
};
export default ProviderCard;

View File

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

View File

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