import { getHeaders, useClient } from 'hooks/useClient'; // Every call the panels make to this plugin's own sidecar, through `/api/atelier`. // // `useClient('/api/atelier')` gives typed verbs for JSON; `fileUrl` below is the one thing a client // call cannot do — put a bearer token on an ``. const ATELIER_API = '/api/atelier'; /** * A provider: a vendor Officer curates, and whether we hold a key for it. * * Every provider in the catalogue is listed, set up or not — a vendor you have no key for is exactly * what you need to see in order to add one. There is no "configured provider" as a separate thing; * `configured` is a fact about the key, and the model is chosen per generation. */ export type ProviderStatus = { name: string; label: string; configured: boolean; key_source: 'store' | 'environment' | null; env_var: string; /** False when the catalogue lists a vendor this build has no adapter for. */ usable: boolean; supports_edit: boolean; refreshable: boolean; default_model: string | null; models: string[]; model_details: CatalogueModel[]; updated_at: string | null; }; /** One model in the catalogue — what it makes, what it accepts, what it costs. */ export type CatalogueModel = { id: string; label?: string; kind: 'image' | 'video'; sizes?: string[]; qualities?: string[]; transparency?: boolean; edit?: boolean; pricing?: Record; status?: 'current' | 'deprecated'; source?: 'shipped' | 'api' | 'scraped'; }; /** Snake case, matching the server — one spelling per field across every endpoint. */ export type GenerationRow = { id: string; request_id: string; index: number; operation: 'generate' | 'edit'; status: 'queued' | 'running' | 'succeeded' | 'failed'; provider: string; model: string; prompt: string; options: Record | null; input_images: string[] | null; mask_image: string | null; image_path: string | null; url: string | null; width: number | null; height: number | null; has_alpha: boolean | null; opaque: boolean | null; cost_usd: string | null; duration_ms: number | null; error: string | null; created_at: string; }; export type JobResponse = { id?: string; status?: string; created?: number; data?: { url: string; revised_prompt?: string; b64_json?: string }[]; atelier?: { id: string; status: 'queued' | 'running' | 'succeeded' | 'failed' | 'partial'; cost_usd: string | null; duration_ms: number; images: { id: string; status: string; url: string | null; error: string | null }[]; }; error?: { message: string; param?: string | null }; }; export type Collection = { id: string; name: string; description: string | null; created_at: string; updated_at: string; }; export type CollectionSummary = Collection & { item_count: number; cover_image_path: string | null; cover_url: string | null }; export type CollectionDetail = Collection & { images: GenerationRow[] }; /** * `row.url` (e.g. `/files/`) with this session's bearer token appended, for an ``. * * Browsers cannot set an `Authorization` header on an `` request, so the token travels in the * query string instead — the same escape hatch `FileViewer/file-types.ts` and the music plugin's * `MusicPlayerHost` already use for exactly this reason (`permission-gate.ts` and `user-middleware.ts` * both read `?token=` as well as the header). */ export function fileUrl(path: string | null): string | null { if (!path) return null; const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? ''; return `${ATELIER_API}${path}?token=${encodeURIComponent(token)}`; } export function useAtelierClient() { const client = useClient(ATELIER_API); return { ...client, listProviders: () => client.get<{ data: ProviderStatus[] }>('providers').then((r) => r.data), /** Save a provider's key. Goes to the platform's secret store; nothing ever reads it back. */ setProviderKey: (name: string, apiKey: string) => client.put<{ data: ProviderStatus[] }>(`providers/${name}/key`, { apiKey }).then((r) => r.data), /** Forget a stored key. Refused for a key that came from the environment — that one is the install's. */ clearProviderKey: (name: string) => client.delete<{ data: ProviderStatus[] }>(`providers/${name}/key`).then((r) => r.data), /** * Re-read one provider's models and prices from the vendor, or every provider when omitted. * * The only thing that updates the catalogue — there is no schedule. It runs a live fetch, so it is * as slow as the vendor is and the caller should show that it is working. */ refreshCatalogue: (provider?: string) => client .post<{ data: { updated_at: string | null; providers: ProviderStatus[]; changed: boolean } }>( `providers/catalogue/refresh${provider ? `?provider=${encodeURIComponent(provider)}` : ''}`, {}, ) .then((r) => r.data), listGenerations: (limit = 60) => client.get<{ data: GenerationRow[] }>(`images?limit=${limit}`).then((r) => r.data), startGeneration: (body: Record) => client.post<{ id: string }>('images/generations', body), /** Long-polls up to `waitSeconds` server-side, so the panel does not run its own retry loop. */ awaitGeneration: (id: string, waitSeconds = 30) => client.get(`images/generations/${id}?wait=${waitSeconds}`), /** A provider row is adapter + model; the key is set separately, and never comes back. */ createProvider: (adapter: string, model: string) => client.post<{ data: ProviderStatus[] }>('providers', { adapter, model }).then((r) => r.data), updateProvider: (id: string, values: Record) => client.patch<{ data: ProviderStatus[] }>(`providers/${id}`, values).then((r) => r.data), deleteProvider: (id: string) => client.delete<{ data: ProviderStatus[] }>(`providers/${id}`).then((r) => r.data), listCollections: () => client.get<{ data: CollectionSummary[] }>('collections').then((r) => r.data), getCollection: (id: string) => client.get<{ data: CollectionDetail }>(`collections/${id}`).then((r) => r.data), createCollection: (name: string) => client.post<{ data: Collection }>('collections', { name }).then((r) => r.data), renameCollection: (id: string, name: string) => client.patch<{ data: Collection }>(`collections/${id}`, { name }).then((r) => r.data), /** * `deleteImages` is the difference between filing something wrongly and regretting the whole * afternoon — the server keeps the images unless it is asked, by name, not to. */ deleteCollection: (id: string, deleteImages = false) => client .delete<{ data: { id: string; deleted: boolean; deletedImages: number } }>( `collections/${id}${deleteImages ? '?images=delete' : ''}`, ) .then((r) => r.data), addToCollection: (id: string, generationIds: string[]) => client .post<{ data: { added: number; images: GenerationRow[] } }>(`collections/${id}/items`, { generationIds }) .then((r) => r.data), removeFromCollection: (id: string, generationId: string) => client.delete<{ data: { images: GenerationRow[] } }>(`collections/${id}/items/${generationId}`).then((r) => r.data), }; } export type UseAtelierClientType = ReturnType;