`IntegrationsSettings/index.tsx` has carried the rule since email was extracted on 2026-08-18: a plugin contributes panels, not settings sections. the extraction brief said the opposite and the brief was wrong. settings living in the platform mean this plugin is not self-contained — it cannot be configured on an older platform, and uninstalling would leave a dead "Atelier" section in the settings screen for someone to delete by hand. so: a fourth panel, `atelier-providers`, and the default layout stacks it under Collections in the right column. smallest and last because it is configured once and then ignored — but it has to be REACHABLE, which is the whole point. Generate's empty state pointed at "Settings → Integrations → Atelier". it now points at the Providers panel, which is where the key actually goes.
109 lines
3.6 KiB
TypeScript
109 lines
3.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useAtelierRefresh } from 'officerdev';
|
|
import { useAtelierClient } from '../lib/api';
|
|
import type { JobResponse, ProviderStatus } from '../lib/api';
|
|
|
|
export const SIZES = ['auto', '1024x1024', '1536x1024', '1024x1536'] as const;
|
|
export const QUALITIES = ['auto', 'low', 'medium', 'high'] as const;
|
|
export const BACKGROUNDS = ['auto', 'transparent', 'opaque'] as const;
|
|
|
|
export const useGenerateManager = () => {
|
|
const client = useAtelierClient();
|
|
const [, bumpRefresh] = useAtelierRefresh();
|
|
|
|
const [providers, setProviders] = useState<ProviderStatus[]>([]);
|
|
const [model, setModel] = useState('');
|
|
const [prompt, setPrompt] = useState('');
|
|
const [n, setN] = useState(1);
|
|
const [size, setSize] = useState<string>('auto');
|
|
const [quality, setQuality] = useState<string>('auto');
|
|
const [background, setBackground] = useState<string>('auto');
|
|
|
|
const [job, setJob] = useState<JobResponse | null>(null);
|
|
const [running, setRunning] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
client
|
|
.listProviders()
|
|
.then((rows) => {
|
|
setProviders(rows);
|
|
const usable =
|
|
rows.find((row) => row.is_default && row.configured && row.enabled) ??
|
|
rows.find((row) => row.configured && row.enabled);
|
|
if (usable) setModel(usable.model);
|
|
})
|
|
.catch((err: Error) => setError(err.message));
|
|
// Loaded once on mount — the Providers panel is where they
|
|
// are managed, and this panel does not need to react to edits made there in real time.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// Derived in the hook, not in the component.
|
|
const usableProviders = providers.filter((provider) => provider.configured && provider.enabled);
|
|
// One model per configured provider, deduped: two providers on the same model is legitimate (two
|
|
// keys, two accounts) and the request picks between them by model alone.
|
|
const models = [...new Set(usableProviders.map((provider) => provider.model))];
|
|
const ready = Boolean(prompt.trim()) && Boolean(model) && !running;
|
|
|
|
async function generate() {
|
|
if (!ready) return;
|
|
|
|
setRunning(true);
|
|
setError(null);
|
|
setJob(null);
|
|
|
|
try {
|
|
// Only what the user actually chose. `auto` is the provider's own default, so sending it would
|
|
// be inventing a parameter they did not set.
|
|
const body: Record<string, unknown> = { prompt: prompt.trim(), model, n };
|
|
if (size !== 'auto') body.size = size;
|
|
if (quality !== 'auto') body.quality = quality;
|
|
if (background !== 'auto') body.background = background;
|
|
|
|
const { id } = await client.startGeneration(body);
|
|
|
|
// Long-poll in a loop: each call waits up to 30s server-side, so this is one request per half
|
|
// minute rather than a busy poll, and it survives a generation that takes longer.
|
|
for (;;) {
|
|
const result = await client.awaitGeneration(id, 30);
|
|
if (result.atelier && ['succeeded', 'failed', 'partial'].includes(result.atelier.status)) {
|
|
setJob(result);
|
|
bumpRefresh();
|
|
return;
|
|
}
|
|
setJob(result);
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : String(err));
|
|
} finally {
|
|
setRunning(false);
|
|
}
|
|
}
|
|
|
|
return {
|
|
providers,
|
|
usableProviders,
|
|
models,
|
|
model,
|
|
setModel,
|
|
prompt,
|
|
setPrompt,
|
|
n,
|
|
setN,
|
|
size,
|
|
setSize,
|
|
quality,
|
|
setQuality,
|
|
background,
|
|
setBackground,
|
|
job,
|
|
running,
|
|
error,
|
|
ready,
|
|
generate,
|
|
};
|
|
};
|
|
|
|
export type GenerateManager = ReturnType<typeof useGenerateManager>;
|