'use client'; import { useState, useEffect, useRef } from 'react'; import type { ProviderConfig, ProviderId } from '@/types'; import { PROVIDERS, PROVIDER_MAP } from '@/lib/providers'; type ConnectStatus = 'idle' | 'loading' | 'success' | 'error'; interface Props { config: ProviderConfig; onSave: (config: ProviderConfig) => void; onClose: () => void; } export default function ProviderSettings({ config, onSave, onClose }: Props) { const [provider, setProvider] = useState(config.provider); const [model, setModel] = useState(config.model); const [apiKey, setApiKey] = useState(config.apiKey ?? ''); const [baseUrl, setBaseUrl] = useState(config.baseUrl ?? ''); const [connectStatus, setConnectStatus] = useState('idle'); const [connectError, setConnectError] = useState(''); const [fetchedModels, setFetchedModels] = useState([]); const [modelFilter, setModelFilter] = useState(''); const filterRef = useRef(null); const def = PROVIDER_MAP[provider]; // Reset when provider changes useEffect(() => { const newDef = PROVIDER_MAP[provider]; setModel(newDef.defaultModel); setBaseUrl(newDef.hasCustomBaseUrl ? newDef.defaultBaseUrl : ''); if (!newDef.requiresApiKey) setApiKey(''); setConnectStatus('idle'); setFetchedModels([]); setModelFilter(''); }, [provider]); // Focus filter when models load useEffect(() => { if (connectStatus === 'success') { setTimeout(() => filterRef.current?.focus(), 50); } }, [connectStatus]); async function handleConnect() { setConnectStatus('loading'); setConnectError(''); setFetchedModels([]); try { const res = await fetch('/api/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider, apiKey: apiKey.trim() || undefined, baseUrl: baseUrl.trim() || undefined, }), }); const data = await res.json(); if (!res.ok || data.error) { setConnectError(data.error ?? `Error ${res.status}`); setConnectStatus('error'); return; } const models: string[] = data.models ?? []; setFetchedModels(models); setConnectStatus('success'); // Auto-select first model if current isn't in the fetched list if (models.length && !models.includes(model)) { setModel(models[0]); } } catch (err) { setConnectError(err instanceof Error ? err.message : 'Connection failed'); setConnectStatus('error'); } } function handleSave() { const saved: ProviderConfig = { provider, model: model.trim() || def.defaultModel, ...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}), ...(baseUrl.trim() && def.hasCustomBaseUrl ? { baseUrl: baseUrl.trim() } : {}), }; onSave(saved); onClose(); } const filteredModels = fetchedModels.filter((m) => m.toLowerCase().includes(modelFilter.toLowerCase()) ); return (
e.target === e.currentTarget && onClose()} >
{/* Header */}

AI Provider Settings

{/* Body */}
{/* Provider selector */}
{PROVIDERS.map((p) => ( ))}
{/* API Key — shown before model so user fills it first */} {def.requiresApiKey && (
{ setApiKey(e.target.value); setConnectStatus('idle'); setFetchedModels([]); }} placeholder={provider === 'anthropic' ? 'sk-ant-… (optional)' : 'sk-or-…'} autoComplete="off" className="w-full rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-xs text-zinc-200 placeholder-zinc-600 focus:border-blue-500 focus:outline-none" />
)} {/* Base URL */} {def.hasCustomBaseUrl && (
{ setBaseUrl(e.target.value); setConnectStatus('idle'); setFetchedModels([]); }} placeholder={def.defaultBaseUrl} className="w-full rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-xs text-zinc-200 placeholder-zinc-600 focus:border-blue-500 focus:outline-none" />

{provider === 'ollama' ? 'Ollama must be running with OLLAMA_ORIGINS=* for CORS.' : 'LM Studio server must be running on this address.'}

)} {/* Model */}
{/* Error */} {connectStatus === 'error' && (
{connectError}
)} {/* Live model list */} {connectStatus === 'success' && fetchedModels.length > 0 ? (
{fetchedModels.length > 8 && ( setModelFilter(e.target.value)} placeholder={`Filter ${fetchedModels.length} models…`} className="w-full rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-xs text-zinc-200 placeholder-zinc-600 focus:border-blue-500 focus:outline-none" /> )}
{filteredModels.length === 0 ? (

No models match “{modelFilter}”

) : ( filteredModels.map((m) => ( )) )}
{modelFilter && filteredModels.length < fetchedModels.length && (

{filteredModels.length} of {fetchedModels.length} shown

)}
) : ( /* Static fallback — shown before connecting or on error */
{def.modelSuggestions.length > 0 ? ( <> {!def.modelSuggestions.includes(model) && ( setModel(e.target.value)} placeholder={def.defaultModel} className="w-full rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-xs text-zinc-200 placeholder-zinc-600 focus:border-blue-500 focus:outline-none" /> )} ) : ( setModel(e.target.value)} placeholder={def.defaultModel} className="w-full rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-xs text-zinc-200 placeholder-zinc-600 focus:border-blue-500 focus:outline-none" /> )}

Hit Connect to load available models from {def.label}.

)} {connectStatus === 'success' && (

Selected: {model}

)}
{/* Footer */}
); }