import { useEffect, useState } from 'react'; import { KeyRound, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; 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'; /** * Provider and model, at the top of Generate. * * Two dropdowns and nothing else. The provider list is the curated file — one entry per vendor, * whether or not you have a key — and the models under it are that provider's entry in the same file. * * What this replaced was a menu of "connections": a row per adapter+model+key, which made choosing a * model an act of configuration, put a key icon and a delete button inside a menu item, and listed * five accounts nobody had created. A provider is a vendor; the key is a property of it; the model is * a choice you make each time. */ export const ProvidersBar = ({ manager, model, onModelChange, }: { manager: ProvidersManager; model: string; onModelChange: (model: string) => void; }) => { const [selected, setSelected] = useState(''); const [keyFor, setKeyFor] = useState(null); // Default to a provider we can actually use, once the list arrives. useEffect(() => { if (selected && manager.byName(selected)) return; const next = manager.firstUsable() ?? manager.providers[0]; if (next) setSelected(next.name); // eslint-disable-next-line react-hooks/exhaustive-deps }, [manager.providers]); const provider = manager.byName(selected); const models = provider?.model_details ?? []; // Keep the model honest: when the provider changes, or a refresh retires what was chosen, fall back // to that provider's default rather than leaving a model the vendor no longer lists selected. useEffect(() => { if (!provider) return; if (model && models.some((entry) => entry.id === model)) return; onModelChange(provider.default_model ?? models[0]?.id ?? ''); // eslint-disable-next-line react-hooks/exhaustive-deps }, [provider?.name, provider?.model_details]); return (
{/* Hidden rather than disabled for a keyless provider. A greyed-out "Add key" invites the question of what would enable it, and the answer is nothing — there is no key to add. */} {provider?.keyless ? ( Runs on this machine — no key needed ) : ( )} {manager.error && {manager.error}}
setKeyFor(null)} />
); }; /** "as shipped" is a fact worth showing: these models are exactly what the plugin was released with. */ const freshness = (updatedAt?: string | null): string => { if (!updatedAt) return 'as shipped'; const days = Math.floor((Date.now() - new Date(updatedAt).getTime()) / 86_400_000); return days <= 0 ? 'updated today' : `${days}d old`; }; const KeyDialog = ({ provider, manager, onClose, }: { provider: ProviderStatus | null; manager: ProvidersManager; onClose: () => void; }) => { const [apiKey, setApiKey] = useState(''); useEffect(() => { setApiKey(''); }, [provider]); const fromEnvironment = provider?.key_source === 'environment'; const save = async () => { if (!provider || !apiKey.trim()) return; if (await manager.setKey(provider.name, apiKey)) onClose(); }; return ( !next && onClose()}> {provider?.label} API key {fromEnvironment ? `Currently taken from ${provider?.env_var} in the environment. A key saved here takes precedence.` : 'Stored in Officer’s secret store, encrypted at rest and never sent back — so this field always starts empty.'} {/* A form, so Enter saves. Pasting a key and pressing Enter is the strongest habit there is in a password field, and outside a form it silently does nothing. */}
{ ev.preventDefault(); void save(); }} > setApiKey(ev.target.value)} /> {provider?.configured && !fromEnvironment ? ( ) : ( )}
); };