import { useEffect, useState } from 'react'; import { Check, KeyRound, Plus, RefreshCw, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import type { ProviderStatus } from '../lib/api'; import type { ProvidersManager } from './useProviders'; /** * Providers, as one line at the top of the Generate panel. * * It was a whole panel — a card per provider, each with its own key field and three buttons, filling * a quarter of the screen for something set once and then ignored. What it actually has to answer is * "which one is this generation going to use", and that is a dropdown. * * Adding one opens a dialog rather than expanding the menu: it is three fields, one of them a secret, * and a menu that turns into a form cannot be dismissed by clicking away without losing what was typed. */ export const ProvidersBar = ({ manager }: { manager: ProvidersManager }) => { const [dialog, setDialog] = useState(null); const active = manager.providers.find((row) => row.is_default && row.configured && row.enabled) ?? manager.providers.find((row) => row.configured && row.enabled) ?? manager.providers[0]; return (
{manager.providers.length === 0 && ( No providers configured )} {manager.providers.map((provider) => ( void manager.makeDefault(provider.id)} className="gap-2" > {provider.id === active?.id ? : } {label(provider)} {!provider.configured && ( )} ))} setDialog({ mode: 'add' })} className="gap-2"> Add provider {manager.error && {manager.error}} setDialog(null)} />
); }; const label = (provider: ProviderStatus) => `${provider.adapter} · ${provider.model}`; /** "never refreshed" is a fact worth showing: it means these models are exactly what the plugin shipped. */ const freshness = (updatedAt?: string | null): string => { if (!updatedAt) return 'as shipped'; const days = Math.floor((Date.now() - new Date(updatedAt).getTime()) / 86_400_000); if (days <= 0) return 'refreshed today'; return `${days}d old`; }; type DialogState = null | { mode: 'add' } | { mode: 'key'; provider: ProviderStatus }; /** Add a provider, or set the key on one that has none. Same dialog, since the key field is shared. */ const ProviderDialog = ({ state, manager, onClose, }: { state: DialogState; manager: ProvidersManager; onClose: () => void; }) => { const [adapter, setAdapter] = useState(''); const [model, setModel] = useState(''); const [apiKey, setApiKey] = useState(''); // Seeded when the dialog opens, not on every render: the adapter list arrives asynchronously, and // resetting on each render would fight the user's own choice as soon as it did. useEffect(() => { if (state?.mode !== 'add') return; const first = manager.adapters[0]; setAdapter(first?.name ?? ''); setModel(first?.default_model ?? ''); setApiKey(''); }, [state, manager.adapters]); useEffect(() => { if (state?.mode === 'key') setApiKey(''); }, [state]); const submit = async () => { if (!state) return; const ok = state.mode === 'add' ? await manager.add(adapter, model, apiKey) : apiKey.trim() ? await manager.setKey(state.provider.id, apiKey) : false; if (ok) onClose(); }; return ( !next && onClose()}> {state?.mode === 'key' ? 'Set API key' : 'Add a provider'} {state?.mode === 'key' ? `${label(state.provider)} — the key is stored encrypted and never sent back.` : 'An adapter and a model. The same adapter can back several providers — two keys, two accounts.'}
{ ev.preventDefault(); void submit(); }} > {state?.mode === 'add' && ( <> {/* The provider list is the CURATED FILE — the providers Officer decided are worth offering — and the models under it are that file's current answer for the one chosen. Neither is read from the vendor here: the file is the offer, and refreshing it is a deliberate act with its own button rather than something that happens on a timer. */} )}
); };