import React, { useState, useEffect, useRef } from 'react'; import { motion } from 'framer-motion'; import { X, Key, Eye, EyeOff, Loader2, ChevronDown, Search, CreditCard, CheckCircle } from 'lucide-react'; import { useBodyScrollLock } from '../hooks/useBodyScrollLock'; import { useAuthenticatedFetch } from '../hooks/useAuthenticatedFetch'; import type { AIProvider, UserPreferences } from '../types'; interface SubscriptionInfo { hasAccess: boolean; accessType: 'group' | 'subscription' | 'none'; generationCount: number | null; generationLimit: number | null; generationsRemaining: number | null; } interface OpenAIModel { id: string; owned_by: string; } interface ApiKeyModalProps { isOpen: boolean; onClose: () => void; preferences: UserPreferences; onSave: (prefs: Partial) => Promise; saving: boolean; hasAIAccess: boolean; subscription?: SubscriptionInfo | null; hasEarlyAccess?: boolean; } export const ApiKeyModal: React.FC = ({ isOpen, onClose, preferences, onSave, saving, hasAIAccess, subscription, hasEarlyAccess, }) => { useBodyScrollLock(isOpen); const { authFetch } = useAuthenticatedFetch(); const [localProvider, setLocalProvider] = useState(preferences.aiProvider || 'gemini'); const [localGeminiKey, setLocalGeminiKey] = useState(preferences.geminiApiKey || ''); const [localGeminiModel, setLocalGeminiModel] = useState(preferences.geminiModel || ''); const [localOpenRouterKey, setLocalOpenRouterKey] = useState(preferences.openRouterApiKey || ''); const [localOpenRouterModel, setLocalOpenRouterModel] = useState(preferences.openRouterModel || ''); const [localOpenAIKey, setLocalOpenAIKey] = useState(preferences.openAIApiKey || ''); const [localOpenAIModel, setLocalOpenAIModel] = useState(preferences.openAIModel || ''); const [showGeminiKey, setShowGeminiKey] = useState(false); const [showOpenRouterKey, setShowOpenRouterKey] = useState(false); const [showOpenAIKey, setShowOpenAIKey] = useState(false); const [openAIModels, setOpenAIModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); const [modelSearchQuery, setModelSearchQuery] = useState(''); const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false); const modelDropdownRef = useRef(null); const [portalLoading, setPortalLoading] = useState(false); const [portalError, setPortalError] = useState(null); const [showPortalBanner, setShowPortalBanner] = useState(false); useEffect(() => { if (isOpen) { setLocalProvider(preferences.aiProvider || 'gemini'); setLocalGeminiKey(preferences.geminiApiKey || ''); setLocalGeminiModel(preferences.geminiModel || ''); setLocalOpenRouterKey(preferences.openRouterApiKey || ''); setLocalOpenRouterModel(preferences.openRouterModel || ''); setLocalOpenAIKey(preferences.openAIApiKey || ''); setLocalOpenAIModel(preferences.openAIModel || ''); setModelSearchQuery(''); setIsModelDropdownOpen(false); const params = new URLSearchParams(window.location.search); if (params.get('portal') === 'return') { setShowPortalBanner(true); } } }, [isOpen, preferences]); const dismissPortalBanner = () => { setShowPortalBanner(false); const url = new URL(window.location.href); url.searchParams.delete('portal'); window.history.replaceState({}, '', url.toString()); }; useEffect(() => { const fetchOpenAIModels = async () => { if (!localOpenAIKey || localOpenAIKey.length < 10) { setOpenAIModels([]); return; } setLoadingModels(true); try { const response = await fetch('https://api.openai.com/v1/models', { headers: { 'Authorization': `Bearer ${localOpenAIKey}`, }, }); if (response.ok) { const data = await response.json(); const models = (data.data as (OpenAIModel & { created: number })[]) .filter(m => !m.id.includes('realtime') && !m.id.includes('audio') && !m.id.includes('tts') && !m.id.includes('whisper') && !m.id.includes('dall-e') && !m.id.includes('embedding')) .sort((a, b) => b.created - a.created); setOpenAIModels(models); } else { setOpenAIModels([]); } } catch { setOpenAIModels([]); } finally { setLoadingModels(false); } }; const debounceTimer = setTimeout(fetchOpenAIModels, 500); return () => clearTimeout(debounceTimer); }, [localOpenAIKey]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) { setIsModelDropdownOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const filteredModels = openAIModels.filter(model => model.id.toLowerCase().includes(modelSearchQuery.toLowerCase()) ); if (!isOpen) return null; const handleSave = async () => { await onSave({ aiProvider: localProvider, geminiApiKey: localGeminiKey || undefined, geminiModel: localGeminiModel || undefined, openRouterApiKey: localOpenRouterKey || undefined, openRouterModel: localOpenRouterModel || undefined, openAIApiKey: localOpenAIKey || undefined, openAIModel: localOpenAIModel || undefined, }); onClose(); }; const handleManageSubscription = async () => { setPortalLoading(true); setPortalError(null); try { const response = await authFetch('/api/payments/portal', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ returnUrl: `${window.location.origin}?portal=return`, }), }); if (!response.ok) { throw new Error('Failed to create customer portal session'); } const data = await response.json(); if (data.url) { window.location.href = data.url; } else { throw new Error('No portal URL returned'); } } catch (err) { console.error('Portal error:', err); setPortalError('Failed to open subscription management. Please try again.'); setPortalLoading(false); } }; return ( e.stopPropagation()} >

AI Settings

Configure your AI provider

{showPortalBanner && (

Subscription Updated

Your subscription settings have been updated successfully.

)} {subscription?.accessType === 'subscription' && (
Subscription Active
{subscription.generationsRemaining !== null && ( {subscription.generationsRemaining} generations left )}

Manage your billing, payment methods, and invoices.

{portalError && (

{portalError}

)}
)}

Choose the provider you want to use for quiz generation.

{localProvider === 'gemini' && ( <>

Get your key from{' '} aistudio.google.com/apikey {hasAIAccess ? ' or leave empty to use the system key.' : '.'}

setLocalGeminiKey(e.target.value)} placeholder="AIza..." className="w-full px-4 py-3 pr-12 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800" />

Default: gemini-3-flash-preview

setLocalGeminiModel(e.target.value)} placeholder="gemini-3-flash-preview" className="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800" />
)} {localProvider === 'openai' && ( <>

Get your key from{' '} platform.openai.com/api-keys

setLocalOpenAIKey(e.target.value)} placeholder="sk-..." className="w-full px-4 py-3 pr-12 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800" />

{localOpenAIKey ? 'Select a model from your account' : 'Enter API key to load models'}

localOpenAIKey && setIsModelDropdownOpen(!isModelDropdownOpen)} > {localOpenAIModel || 'gpt-5-mini'} {loadingModels ? ( ) : ( )}
{isModelDropdownOpen && openAIModels.length > 0 && (
setModelSearchQuery(e.target.value)} placeholder="Search models..." className="w-full pl-9 pr-3 py-2 text-sm rounded-lg border border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800" autoFocus />
{filteredModels.length > 0 ? ( filteredModels.map((model) => ( )) ) : (
No models found
)}
)}
)} {localProvider === 'openrouter' && ( <>

Get your key from{' '} openrouter.ai/keys

setLocalOpenRouterKey(e.target.value)} placeholder="sk-or-..." className="w-full px-4 py-3 pr-12 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800" />

Default: google/gemini-3-flash-preview

setLocalOpenRouterModel(e.target.value)} placeholder="google/gemini-3-flash-preview" className="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800" />
)}
); };