import { useCallback, useEffect, useState } from 'react'; import { useAtelierClient } from '../lib/api'; import type { Adapter, ProviderStatus } from '../lib/api'; /** * Every provider, and the verbs that change them. * * Owned here rather than inside useGenerateManager because two things need the same list and it must * not exist twice: the dropdown at the top of the panel, and the form below it whose model choices are * derived from whichever providers actually have a key. Adding one used to mean reloading the page * before the form noticed. */ export const useProviders = () => { const client = useAtelierClient(); const [providers, setProviders] = useState([]); const [adapters, setAdapters] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [refreshing, setRefreshing] = useState(false); const refresh = useCallback(async () => { try { const [rows, catalogue] = await Promise.all([client.listProviders(), client.listAdapters()]); setProviders(rows); setAdapters(catalogue); setError(null); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { void refresh(); }, [refresh]); const guard = async (action: () => Promise) => { setBusy(true); try { setProviders(await action()); setError(null); return true; } catch (err) { setError(err instanceof Error ? err.message : String(err)); return false; } finally { setBusy(false); } }; /** * Create, then set the key if one was typed. * * Two calls because they are two things on the server: a provider row is public configuration, a key * is an encrypted secret that is never sent back. A provider added without a key is legitimate — the * key may live in the environment — so the second call is skipped rather than required. */ const add = async (adapter: string, model: string, apiKey?: string) => guard(async () => { const rows = await client.createProvider(adapter, model); const created = rows.find((row) => row.adapter === adapter && row.model === model); if (!apiKey?.trim() || !created) return rows; return client.updateProvider(created.id, { apiKey: apiKey.trim() }); }); /** * Re-read the catalogue from the vendor, then re-read our copy of it. * * Separate busy flag from `busy`: this one is a live fetch against OpenAI and takes seconds, where * everything else here is a local database write. Sharing one flag would grey out the whole dialog * for the length of a network round trip to a third party. */ const refreshCatalogue = async (provider?: string) => { setRefreshing(true); try { await client.refreshCatalogue(provider); setAdapters(await client.listAdapters()); setError(null); return true; } catch (err) { setError(err instanceof Error ? err.message : String(err)); return false; } finally { setRefreshing(false); } }; return { providers, adapters, loading, busy, refreshing, refreshCatalogue, error, refresh, add, setKey: (id: string, apiKey: string) => guard(() => client.updateProvider(id, { apiKey: apiKey.trim() })), /** The one a generation will use when the request names only a model. */ makeDefault: (id: string) => guard(() => client.updateProvider(id, { isDefault: true, enabled: true })), remove: (id: string) => guard(() => client.deleteProvider(id)), modelsFor: (adapter: string) => adapters.find((entry) => entry.name === adapter)?.models ?? [], adapterFor: (adapter: string) => adapters.find((entry) => entry.name === adapter), }; }; export type ProvidersManager = ReturnType;