/** * ProviderEditor Component * * Provider configuration editor: API Key, Base URL, enabled state, custom models */ import { useState, useEffect } from 'react'; import { X, Save, Server, Key, Globe, AlertCircle, Plus, Trash2, Eye, EyeOff, Cpu, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { toast } from 'sonner'; import { cn } from '../utils/cn'; import { modalOverlay, modalContent, smoothTransition } from '../utils/animations'; import { Button } from '../primitives/Button'; import { Input } from '../primitives/Input'; import { getProvider, updateProviderConfig, addProviderModel, deleteProviderModel, type ProviderDetail, type ProviderConfig, type ModelInfo, } from '../api/client.js'; interface ProviderEditorProps { /** Provider ID to edit */ providerId: string; onClose: () => void; onSave: () => void; /** Enable responsive layout */ responsive?: boolean; } export function ProviderEditor({ providerId, onClose, onSave, responsive = false, }: ProviderEditorProps) { // Provider data const [provider, setProvider] = useState(null); // Form state const [apiKey, setApiKey] = useState(''); const [baseUrl, setBaseUrl] = useState(''); const [enabled, setEnabled] = useState(true); // Custom model form const [showAddModel, setShowAddModel] = useState(false); const [newModelId, setNewModelId] = useState(''); const [newModelName, setNewModelName] = useState(''); const [newModelVision, setNewModelVision] = useState(false); const [newModelTools, setNewModelTools] = useState(false); // UI state const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [showApiKey, setShowApiKey] = useState(false); // Load provider data useEffect(() => { async function loadProvider() { setLoading(true); setError(null); try { const result = await getProvider(providerId); if (result.success && result.data) { setProvider(result.data); // Initialize form with current config const config = result.data.config; // API key is not returned for security, but we show if it's configured via hasApiKey setApiKey(''); setBaseUrl(config.baseUrl || result.data.baseUrl || ''); setEnabled(config.enabled !== false); } else { setError(result.error || 'Failed to load provider'); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load provider'); } finally { setLoading(false); } } loadProvider(); }, [providerId]); // Save configuration const handleSave = async () => { setSaving(true); setError(null); try { const config: Partial = { enabled, }; // Only include changed values if (apiKey.trim()) { config.apiKey = apiKey; } if (baseUrl.trim() && baseUrl !== provider?.baseUrl) { config.baseUrl = baseUrl; } const result = await updateProviderConfig(providerId, config); if (result.success) { toast.success(`Provider "${providerId}" updated`); onSave(); } else { setError(result.error || 'Failed to save'); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save'); } finally { setSaving(false); } }; // Add custom model const handleAddModel = async () => { if (!newModelId.trim() || !newModelName.trim()) { toast.error('Model ID and Name are required'); return; } try { const model: ModelInfo = { id: newModelId.trim(), name: newModelName.trim(), capabilities: { vision: newModelVision, functionCalling: newModelTools, }, }; const result = await addProviderModel(providerId, model); if (result.success) { toast.success(`Model "${newModelId}" added`); // Reset form setNewModelId(''); setNewModelName(''); setNewModelVision(false); setNewModelTools(false); setShowAddModel(false); // Reload provider const updated = await getProvider(providerId); if (updated.success && updated.data) { setProvider(updated.data); } } else { toast.error(result.error || 'Failed to add model'); } } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to add model'); } }; // Delete custom model const handleDeleteModel = async (modelId: string) => { if (!confirm(`Delete model "${modelId}"?`)) return; try { const result = await deleteProviderModel(providerId, modelId); if (result.success) { toast.success(`Model "${modelId}" deleted`); // Reload provider const updated = await getProvider(providerId); if (updated.success && updated.data) { setProvider(updated.data); } } else { toast.error(result.error || 'Failed to delete model'); } } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to delete model'); } }; return ( e.stopPropagation()} className={cn( 'bg-surface-subtle max-h-[90vh] overflow-hidden flex flex-col', responsive ? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg' : 'rounded-lg w-full max-w-lg mx-4' )} > {/* Header */}
{responsive && (
)}

{loading ? 'Loading...' : provider?.name || providerId}

{provider && (

{provider.builtin ? 'Built-in Provider' : 'Custom Provider'}

)}
{/* Content */}
{loading ? (
) : ( <> {/* Error */} {error && (
{error}
)} {/* Enabled Toggle */}
Enabled

Use this provider for model selection

{/* API Key Section */}

API Key Configuration

{/* API Key Input */}
setApiKey(e.target.value)} placeholder={provider?.config.hasApiKey ? '••••••••' : 'Enter API key'} className="pr-10" />
{provider?.config.hasApiKey && (

API key is configured

)}
{/* Base URL Section */}

Endpoint Configuration

setBaseUrl(e.target.value)} placeholder={provider?.baseUrl || 'https://api.provider.com/v1'} /> {provider?.builtin && (

Leave empty to use default endpoint

)}
{/* Custom Models Section */} {provider?.allowCustomModels && (

Custom Models ({provider.config.customModels.length})

{/* Add Model Form */} {showAddModel && (
setNewModelId(e.target.value)} placeholder="gpt-4o-custom" className="text-sm" />
setNewModelName(e.target.value)} placeholder="GPT-4o Custom" className="text-sm" />
)}
{/* Custom Models List */} {provider.config.customModels.length > 0 ? (
{provider.config.customModels.map((model) => (
{model.name} ({model.id})
{model.capabilities?.vision && ( Vision )} {model.capabilities?.functionCalling && ( Tools )}
))}
) : (

No custom models added yet

)}
)} {/* Built-in Models Info */} {provider && (

Built-in Models:

{provider.models.slice(0, 5).map((m) => ( {m.name} ))} {provider.models.length > 5 && ( +{provider.models.length - 5} more )}
)} )}
{/* Footer */}
); }