import { useEffect, useMemo, useState } from 'react' import { KeyRound, X, Zap } from 'lucide-react' import { ApiKeyHelperRow, LtxApiKeyInput } from './LtxApiKeyInput' export type ApiKeyType = 'ltx' | 'fal' export interface ApiGatewaySection { keyType: ApiKeyType title: string description: string required: boolean isConfigured: boolean inputLabel: string placeholder?: string onSave: (apiKey: string) => Promise | void onGetKey?: () => void getKeyLabel?: string } export interface ApiGatewayModalProps { isOpen: boolean onClose: () => void title: string description: string sections: ApiGatewaySection[] blocking?: boolean } const KEY_TYPE_META: Record = { ltx: { icon: Zap, iconClass: 'text-blue-400', chipClass: 'bg-amber-500/10 text-amber-300', }, fal: { icon: KeyRound, iconClass: 'text-cyan-400', chipClass: 'bg-zinc-800 text-zinc-400', }, } export function ApiGatewayModal({ isOpen, onClose, title, description, sections, blocking = false, }: ApiGatewayModalProps) { const [values, setValues] = useState>({ ltx: '', fal: '' }) const [isSaving, setIsSaving] = useState>({ ltx: false, fal: false }) const [errors, setErrors] = useState>({ ltx: null, fal: null }) useEffect(() => { if (!isOpen) return setValues({ ltx: '', fal: '' }) setIsSaving({ ltx: false, fal: false }) setErrors({ ltx: null, fal: null }) }, [isOpen]) const allRequiredConfigured = useMemo(() => { const requiredSections = sections.filter((section) => section.required) if (requiredSections.length === 0) return true return requiredSections.every((section) => section.isConfigured) }, [sections]) useEffect(() => { if (!isOpen) return if (!allRequiredConfigured) return if (sections.length <= 1) { onClose() } }, [isOpen, onClose, sections.length, allRequiredConfigured]) useEffect(() => { if (!isOpen) return const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape' && !blocking) { onClose() } } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [blocking, isOpen, onClose]) const handleSave = async (section: ApiGatewaySection) => { const keyType = section.keyType const trimmedKey = (values[keyType] ?? '').trim() if (!trimmedKey) { setErrors((prev) => ({ ...prev, [keyType]: `Please enter a valid ${section.inputLabel}.` })) return } setIsSaving((prev) => ({ ...prev, [keyType]: true })) setErrors((prev) => ({ ...prev, [keyType]: null })) try { await section.onSave(trimmedKey) setValues((prev) => ({ ...prev, [keyType]: '' })) } catch (err) { if (err instanceof Error && err.message.trim()) { setErrors((prev) => ({ ...prev, [keyType]: err.message })) } else { setErrors((prev) => ({ ...prev, [keyType]: 'Failed to save API key.' })) } } finally { setIsSaving((prev) => ({ ...prev, [keyType]: false })) } } const requiredMissing = useMemo(() => sections.some((section) => section.required && !section.isConfigured), [sections]) if (!isOpen) return null return (

{title}

{!blocking && ( )}

{description}

{sections.map((section) => { const meta = KEY_TYPE_META[section.keyType] const Icon = meta.icon const configured = section.isConfigured const saving = isSaving[section.keyType] const value = values[section.keyType] ?? '' const error = errors[section.keyType] const canSubmit = value.trim().length > 0 && !saving return (

{section.title}

{section.required ? 'Required' : 'Optional'}

{section.description}

{configured ? 'Configured' : 'Not set'}
{!configured && (
setValues((prev) => ({ ...prev, [section.keyType]: event.target.value }))} placeholder={section.placeholder ?? 'Paste your API key'} className="flex-1" />
)} {error && (
{error}
)}
) })}
{blocking && requiredMissing && (
Required API keys are missing. Add them to continue.
)} {sections.length > 1 && allRequiredConfigured && (
)}
) }