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 configured connection: adapter + model + key. Several may share one adapter. */
export 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;
created_at: string;
updated_at: string;
};
/** The code side — what a new provider can be built on. */
export type Adapter = {
name: string;
label: string;
models: string[];
default_model: string;
env_var: string;
supports_edit: boolean;
};
/** 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),
listAdapters: () => client.get<{ data: Adapter[] }>('providers/adapters').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}`),
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),
deleteCollection: (id: string) => client.delete<{ data: unknown }>(`collections/${id}`),
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;