The daily job is gone. A refresh only matters at one moment — you are picking a provider and want to know its models are current — and at that moment a button answers the question, where a nightly pass can only have already answered it, invisibly, or not. It also bought a standing obligation: something hitting a vendor from every install forever whether anyone opens Atelier or not. So the add dialog is now the curated file made visible. Provider comes from the catalogue, models come from that provider's entry in it, and two buttons re-read it from the vendor: one for the provider in front of you, one for all of them. `POST /providers/catalogue/refresh?provider=` is the only trigger there is. The dialog also says how old the answer is — "as shipped" until a refresh has ever run, which is worth showing, because it means the models listed are exactly what the plugin was released with. A provider with no refresher answers 400 rather than a silent success: from a button, "nothing happened" and "nothing could happen" look identical.
111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
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<ProviderStatus[]>([]);
|
|
const [adapters, setAdapters] = useState<Adapter[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(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<ProviderStatus[]>) => {
|
|
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<typeof useProviders>;
|