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