'use client'; 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, Download, } 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; /** Merge probed model ids into the provider's list; returns the count added. */ onModelsFetched?: (ids: string[]) => number; /** Optional explicit /models URL override (from a preset). */ modelsUrl?: string; 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, onModelsFetched, modelsUrl, 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); const [fetchStatus, setFetchStatus] = useState<'idle' | 'fetching' | 'success' | 'error'>('idle'); const [fetchMessage, setFetchMessage] = useState(''); // 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(''); setFetchStatus('idle'); setFetchMessage(''); }, [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 effectiveBaseUrl = baseUrl || provider.defaultBaseUrl || ''; // Probe the provider's /models endpoint and merge results into the model list. const handleFetchModels = useCallback(async () => { setFetchStatus('fetching'); setFetchMessage(''); try { const response = await fetch('/api/provider/probe-models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ baseUrl: effectiveBaseUrl, apiKey, modelsUrl }), }); const data = await response.json(); if (response.ok && data.success) { const ids: string[] = (data.models || []).map((m: { id: string }) => m.id); const added = onModelsFetched?.(ids) ?? 0; setFetchStatus('success'); setFetchMessage( t('settings.fetchModelsResult') .replace('{added}', String(added)) .replace('{total}', String(ids.length)), ); } else if (response.status === 404) { setFetchStatus('error'); setFetchMessage(t('settings.fetchModelsNoEndpoint')); } else if (response.status === 401) { setFetchStatus('error'); setFetchMessage(t('settings.fetchModelsAuthError')); } else { setFetchStatus('error'); setFetchMessage(data.error || t('settings.fetchModelsFailed')); } } catch { setFetchStatus('error'); setFetchMessage(t('settings.fetchModelsFailed')); } }, [apiKey, effectiveBaseUrl, modelsUrl, onModelsFetched, t]); const models = providersConfig[provider.id]?.models || []; const isServerConfigured = providersConfig[provider.id]?.isServerConfigured; // When the operator pins an allowed model list (MODELS env/yaml), the model // catalog is admin-managed too — view-only, no add/edit/delete. Without a // pinned list the server manages only credentials and the user curates models. const modelsLocked = !!providersConfig[provider.id]?.serverModels?.length; return (
{testMessage}
{t('settings.requestUrl')}: {fullUrl}
); })()}