import { useState, useCallback, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Loader2, CheckCircle2, XCircle, Eye, EyeOff, RotateCcw, Plus, Zap, Settings2, Trash2, Sparkles, Wrench, FileText, Send, } from 'lucide-react'; import { useI18n } from '@/lib/hooks/use-i18n'; import type { ProviderConfig } from '@/lib/ai/providers'; import type { ProvidersConfig } from '@/lib/types/settings'; import { createVerifyModelRequest, formatContextWindow } from './utils'; import { cn } from '@/lib/utils'; interface ProviderConfigPanelProps { provider: ProviderConfig; initialApiKey: string; initialBaseUrl: string; initialRequiresApiKey: boolean; providersConfig: ProvidersConfig; onConfigChange: (apiKey: string, baseUrl: string, requiresApiKey: boolean) => void; onSave: () => void; // Auto-save on blur onEditModel: (index: number) => void; onDeleteModel: (index: number) => void; onAddModel: () => void; onResetToDefault?: () => void; // Reset provider to default configuration isBuiltIn: boolean; // To determine if reset button should be shown } export function ProviderConfigPanel({ provider, initialApiKey, initialBaseUrl, initialRequiresApiKey, providersConfig, onConfigChange, onSave, onEditModel, onDeleteModel, onAddModel, onResetToDefault, isBuiltIn, }: ProviderConfigPanelProps) { const { t } = useI18n(); // Local state for this provider const [apiKey, setApiKey] = useState(initialApiKey); const [baseUrl, setBaseUrl] = useState(initialBaseUrl); const [requiresApiKey, setRequiresApiKey] = useState(initialRequiresApiKey); const [showApiKey, setShowApiKey] = useState(false); const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle'); const [testMessage, setTestMessage] = useState(''); const [showResetDialog, setShowResetDialog] = useState(false); // Update local state when provider changes or initial values change useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- Sync local state from props on provider change setApiKey(initialApiKey); setBaseUrl(initialBaseUrl); setRequiresApiKey(initialRequiresApiKey); setTestStatus('idle'); setTestMessage(''); }, [provider.id, initialApiKey, initialBaseUrl, initialRequiresApiKey]); // Notify parent of changes const handleApiKeyChange = (key: string) => { setApiKey(key); onConfigChange(key, baseUrl, requiresApiKey); }; const handleBaseUrlChange = (url: string) => { setBaseUrl(url); onConfigChange(apiKey, url, requiresApiKey); }; const handleRequiresApiKeyChange = (requires: boolean) => { setRequiresApiKey(requires); onConfigChange(apiKey, baseUrl, requires); }; const handleTestApi = useCallback(async () => { setTestStatus('testing'); setTestMessage(''); const availableModels = providersConfig[provider.id]?.models || []; if (availableModels.length === 0) { setTestStatus('error'); setTestMessage(t('settings.noModelsAvailable') || 'No models available for testing'); return; } const testModelId = availableModels[0].id; try { const response = await fetch('/api/verify-model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( createVerifyModelRequest({ providerId: provider.id, modelId: testModelId, apiKey, baseUrl, providerType: provider.type, requiresApiKey, }), ), }); const data = await response.json(); if (data.success) { setTestStatus('success'); setTestMessage(t('settings.connectionSuccess')); } else { setTestStatus('error'); setTestMessage(data.error || t('settings.connectionFailed')); } } catch (_error) { setTestStatus('error'); setTestMessage(t('settings.connectionFailed')); } }, [apiKey, baseUrl, provider.id, provider.type, requiresApiKey, providersConfig, t]); const models = providersConfig[provider.id]?.models || []; const isServerConfigured = providersConfig[provider.id]?.isServerConfigured; return (
{/* Server-configured notice */} {isServerConfigured && (
{t('settings.serverConfiguredNotice')}
)} {/* API Key */}
handleApiKeyChange(e.target.value)} onBlur={onSave} disabled={!requiresApiKey && !isServerConfigured} className="h-8 pr-8" />
{testMessage && (
{testStatus === 'success' && } {testStatus === 'error' && }

{testMessage}

)}
{ handleRequiresApiKeyChange(checked as boolean); onSave(); }} />
{/* API Host */}
handleBaseUrlChange(e.target.value)} onBlur={onSave} className="h-8" /> {provider.alternateBaseUrls && provider.alternateBaseUrls.length > 0 && (
{provider.alternateBaseUrls.map((alt) => { const active = (baseUrl || provider.defaultBaseUrl) === alt.url; return ( ); })}
)} {(() => { const effectiveBaseUrl = baseUrl || provider.defaultBaseUrl || ''; if (!effectiveBaseUrl) return null; // Generate endpoint path based on provider type let endpointPath = ''; switch (provider.type) { case 'openai': endpointPath = '/chat/completions'; break; case 'anthropic': endpointPath = '/messages'; break; case 'google': endpointPath = '/models/[model]'; break; default: endpointPath = ''; } const fullUrl = effectiveBaseUrl + endpointPath; return (

{t('settings.requestUrl')}: {fullUrl}

); })()}
{/* Models - No selection state, just list for management */}
{isBuiltIn && onResetToDefault && ( )}
{models.map((model, index) => { return (
{model.name}
{/* Capabilities */}
{model.capabilities?.vision && (
)} {model.capabilities?.tools && (
)} {model.capabilities?.streaming && (
)}
{/* Context Window */} {model.contextWindow && ( {formatContextWindow(model.contextWindow)} )} {/* Output Window */} {model.outputWindow && ( {formatContextWindow(model.outputWindow)} )}
{/* Edit/Delete Buttons */}
); })}
{/* Reset Confirmation Dialog */} {t('settings.resetToDefault')} {t('settings.resetConfirmDescription')} {t('settings.cancelEdit')} { setShowResetDialog(false); onResetToDefault?.(); }} > {t('settings.confirmReset')}
); }