One wide modal, a card per group, and a group that opens into a node list beside that node/'s parameters, saved a group at a time. Groups travel on the slot, so a pipeline keeps its own arrangement and a new group is a slot claiming a name nobody used. Values are drafted until Save; adding, removing, replacing and moving a node go to the server as they happen, because only it can say whether the result still wires up. An unset dropdown reads "not set" rather than showing the first file as though it had been chosen -- v4 recorded that trap, and picking the entry the box already shows fires no change, so the one click that looks like the fix does nothing. New: region.stack, a frame header, a modal footer that can be refilled. Not yet: wiring. A socket says so where a control would be. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
3.7 KiB
JavaScript
91 lines
3.7 KiB
JavaScript
// What the server would queue, asked for rather than assembled here.
|
|
//
|
|
// The graph is built by core from the pipeline's slots, so the app never
|
|
// composes one: two builders would be two answers to "what runs", and the one
|
|
// the user sees would be whichever was written last. The app asks, and gets
|
|
// either a prompt or the reasons there is not one.
|
|
|
|
const ENDPOINT = "/funpack/api/pipeline";
|
|
const NODES = "/funpack/api/nodes";
|
|
|
|
/**
|
|
* check(values) -> { slots, refused, incomplete, queueable, prompt }
|
|
*
|
|
* `refused` and `incomplete` are different things and stay apart: refused means
|
|
* the edit did not happen, incomplete means it did and the pipeline still is
|
|
* not ready -- an unset file picker on a fresh install is the normal case, not
|
|
* a failure.
|
|
*/
|
|
export async function check({ fetch: doFetch = globalThis.fetch, slots,
|
|
action = "check", slot, node } = {}) {
|
|
// `slots` and an action travel together on purpose: a remove is "take this
|
|
// one out of THIS pipeline", and sending the action without the pipeline the
|
|
// user is looking at would apply it to the server's defaults instead.
|
|
const body = { action };
|
|
if (slots) body.slots = slots;
|
|
if (slot !== undefined) body.slot = slot;
|
|
if (node !== undefined) body.node = node;
|
|
|
|
const response = await doFetch(ENDPOINT, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
// A 400 here is a malformed REQUEST, which is the app's fault and not the
|
|
// user's -- so it is reported as a refusal rather than dressed up as an
|
|
// unfinished pipeline.
|
|
return {
|
|
slots: payload.slots || [],
|
|
refused: payload.problems || payload.refused || [`the pipeline could not be read (${response.status})`],
|
|
incomplete: [],
|
|
queueable: false,
|
|
prompt: null,
|
|
};
|
|
}
|
|
return {
|
|
slots: payload.slots || [],
|
|
refused: payload.refused || [],
|
|
incomplete: payload.incomplete || [],
|
|
queueable: Boolean(payload.queueable),
|
|
prompt: payload.prompt || null,
|
|
};
|
|
}
|
|
|
|
/** The pipeline as the server holds it, before anything has been edited. */
|
|
export async function load({ fetch: doFetch = globalThis.fetch } = {}) {
|
|
const response = await doFetch(ENDPOINT);
|
|
if (!response.ok) throw new Error(`the pipeline could not be read (${response.status})`);
|
|
const payload = await response.json();
|
|
return {
|
|
slots: payload.slots || [],
|
|
incomplete: payload.incomplete || [],
|
|
queueable: Boolean(payload.queueable),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* describe(classes) -> { [className]: description | null }
|
|
*
|
|
* A null is an answer, not a gap: the slot points at a node this install does
|
|
* not have. The caller asked about it, so it is in the reply saying so.
|
|
*/
|
|
export async function describe(classes, { fetch: doFetch = globalThis.fetch } = {}) {
|
|
const wanted = [...new Set(classes)].filter(Boolean);
|
|
if (!wanted.length) return {};
|
|
const response = await doFetch(`${NODES}?classes=${encodeURIComponent(wanted.join(","))}`);
|
|
if (!response.ok) throw new Error(`those nodes could not be described (${response.status})`);
|
|
return (await response.json()).nodes || {};
|
|
}
|
|
|
|
/** Installed nodes matching a query, and how many there were before the cut. */
|
|
export async function search(query, { fetch: doFetch = globalThis.fetch, limit = 40 } = {}) {
|
|
const response = await doFetch(
|
|
`${NODES}/search?q=${encodeURIComponent(query || "")}&limit=${limit}`);
|
|
if (!response.ok) throw new Error(`nodes could not be searched (${response.status})`);
|
|
const payload = await response.json();
|
|
return { nodes: payload.nodes || [], total: payload.total || 0 };
|
|
}
|