import { useEffect, useState } from 'react'; import { toast } from 'sonner'; import { useClient } from 'hooks/useClient'; 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'; // Atelier's providers: which adapter, which model, whose key. // // A PANEL, in the plugin, not a section in Officer's settings. It was the latter for one day, because the // extraction brief said so — and `IntegrationsSettings/index.tsx` already carried the opposite rule, // written when email was extracted: "A plugin contributes panels, not settings sections, so it now lives // inside the email workspace itself." // // The brief was wrong and the precedent was right. Settings in the platform mean the plugin is not // self-contained: it cannot be configured on an older platform, and uninstalling leaves a dead section // behind that someone has to delete by hand. Neither is true of a panel. // (which adapter, which model, whose key), the same shape as the Apify token above rather than // something worth a dedicated working panel. Talks to /api/atelier/providers over HTTP through // useClient(), same as every other authenticated screen — this does not import anything from // plugins/atelier/, so it stays on the right side of "a plugin importing platform code is fine; the // reverse is not". type ProviderStatus = { id: string; name: string; adapter: string; model: string; enabled: boolean; is_default: boolean; configured: boolean; key_source: 'database' | 'environment' | null; env_var: string; }; type Adapter = { name: string; label: string; models: string[]; default_model: string; env_var: string; }; export const Providers = () => { const client = useClient(); const [providers, setProviders] = useState([]); const [adapters, setAdapters] = useState([]); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(null); const [keyDrafts, setKeyDrafts] = useState>({}); const [newAdapter, setNewAdapter] = useState(''); const [newModel, setNewModel] = useState(''); useEffect(() => { Promise.all([ client.get<{ data: ProviderStatus[] }>('/atelier/providers').then((r) => r.data), client.get<{ data: Adapter[] }>('/atelier/providers/adapters').then((r) => r.data), ]) .then(([rows, catalogue]) => { setProviders(rows); setAdapters(catalogue); if (catalogue[0]) { setNewAdapter(catalogue[0].name); setNewModel(catalogue[0].default_model); } }) .catch(() => toast.error('Failed to load Atelier providers')) .finally(() => setLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const run = async (key: string, action: () => Promise<{ data: ProviderStatus[] }>) => { setBusy(key); try { setProviders((await action()).data); } catch { toast.error('That did not work — check the server logs'); } finally { setBusy(null); } }; const modelsFor = (adapterName: string) => adapters.find((a) => a.name === adapterName)?.models ?? []; const patch = (id: string, values: Record) => run(id, () => client.patch<{ data: ProviderStatus[] }>(`/atelier/providers/${id}`, values)); const saveKey = async (id: string) => { const apiKey = keyDrafts[id]?.trim(); if (!apiKey) return; await patch(id, { apiKey }); setKeyDrafts((current) => ({ ...current, [id]: '' })); toast.success('Key saved'); }; const add = () => run('new', () => client.post<{ data: ProviderStatus[] }>('/atelier/providers', { adapter: newAdapter, model: newModel }), ); const remove = (id: string) => run(id, () => client.delete<{ data: ProviderStatus[] }>(`/atelier/providers/${id}`)); if (loading) return null; // `content-start`: a grid told to fill its height stretches its rows, which would pull the two cards // tall and leave their text floating in the middle of them. return (

Add a provider

An adapter and a model. The same adapter can back several providers.

{providers.length === 0 ? (

No providers yet. Add one above.

) : ( providers.map((provider) => { const busyHere = busy === provider.id; return (

{provider.name} {provider.is_default && ( default )}

{describe(provider)}

{!provider.is_default && ( )}
{/* 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 — which reads as "the key did not save". TODO.md tracks nineteen panels with this same defect; this is not the twentieth. */}
{ ev.preventDefault(); void saveKey(provider.id); }} > setKeyDrafts((current) => ({ ...current, [provider.id]: ev.target.value }))} />

Stored encrypted. The server never sends a key back, so this field always starts empty.

); }) )}
); }; function describe(provider: ProviderStatus): string { const base = `${provider.adapter} · ${provider.model}`; if (!provider.configured) return `${base} — no key. Set one here, or as ${provider.env_var}.`; if (provider.key_source === 'environment') return `${base} — key from ${provider.env_var}.`; return `${base} — key stored and encrypted.`; }