Each variant choice can now have an optional image (picked from the ComfyUI input browser) stored as variants.<group>.<choice>.image. The grid item's cover swaps to that image when the choice is selected on a Flake Combo or Flake Stack, falling back to the base cover otherwise. Served via new /flakes/variant_image endpoint resolving from the input directory or the LoRA tree.
185 lines
6.1 KiB
JavaScript
185 lines
6.1 KiB
JavaScript
// ---------- API ----------
|
|
|
|
let LIST_PROMISE = null;
|
|
let LIST_FAMILY = null;
|
|
|
|
export function invalidateList() { LIST_PROMISE = null; LIST_FAMILY = null; }
|
|
|
|
export async function fetchList(family = "") {
|
|
const query = family ? `?family=${encodeURIComponent(family)}` : "";
|
|
if (!LIST_PROMISE || LIST_FAMILY !== family) {
|
|
LIST_FAMILY = family;
|
|
LIST_PROMISE = fetch(`/flakes/list${query}`).then(r => r.json()).then(d => ({
|
|
flakes: Array.isArray(d.flakes) ? d.flakes : [],
|
|
directories: Array.isArray(d.directories) ? d.directories : [],
|
|
display_names: d.display_names || {},
|
|
}));
|
|
}
|
|
return LIST_PROMISE;
|
|
}
|
|
|
|
export async function fetchFlake(name) {
|
|
const r = await fetch(`/flakes/get?name=${encodeURIComponent(name)}`);
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({}));
|
|
throw new Error(err.error || `HTTP ${r.status}`);
|
|
}
|
|
return (await r.json()).data || {};
|
|
}
|
|
|
|
export async function saveFlakeApi(name, data, family = "") {
|
|
const r = await fetch("/flakes/save", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name, data, family: family || undefined }),
|
|
});
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({}));
|
|
throw new Error(err.error || `HTTP ${r.status}`);
|
|
}
|
|
invalidateList();
|
|
const body = await r.json().catch(() => ({}));
|
|
return body.name || name;
|
|
}
|
|
|
|
export async function deleteFlakeApi(name) {
|
|
const r = await fetch(`/flakes/delete?name=${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({}));
|
|
throw new Error(err.error || `HTTP ${r.status}`);
|
|
}
|
|
invalidateList();
|
|
}
|
|
|
|
// --- Autocomplete / assets ---
|
|
|
|
let LORAS_PROMISE = null;
|
|
export function fetchLoras() {
|
|
if (!LORAS_PROMISE) LORAS_PROMISE = fetch("/flakes/loras").then(r => r.json()).then(d => d.loras || []);
|
|
return LORAS_PROMISE;
|
|
}
|
|
|
|
let CNMODELS_PROMISE = null;
|
|
export function fetchCnModels() {
|
|
if (!CNMODELS_PROMISE) CNMODELS_PROMISE = fetch("/flakes/cnmodels").then(r => r.json()).then(d => d.controlnets || []);
|
|
return CNMODELS_PROMISE;
|
|
}
|
|
|
|
let CN_TYPES_PROMISE = null;
|
|
export function fetchCnTypes() {
|
|
if (!CN_TYPES_PROMISE) CN_TYPES_PROMISE = fetch("/flakes/cn_types").then(r => r.json()).then(d => d.types || []);
|
|
return CN_TYPES_PROMISE;
|
|
}
|
|
|
|
let INPUTS_PROMISE = null;
|
|
export function fetchInputs() {
|
|
if (!INPUTS_PROMISE) INPUTS_PROMISE = fetch("/flakes/inputs").then(r => r.json()).then(d => d.inputs || []);
|
|
return INPUTS_PROMISE;
|
|
}
|
|
|
|
let CKPTS_PROMISE = null;
|
|
export function fetchCheckpoints() {
|
|
if (!CKPTS_PROMISE) CKPTS_PROMISE = fetch("/flakes/checkpoints").then(r => r.json()).then(d => d.checkpoints || []);
|
|
return CKPTS_PROMISE;
|
|
}
|
|
|
|
let VAES_PROMISE = null;
|
|
export function fetchVaes() {
|
|
if (!VAES_PROMISE) VAES_PROMISE = fetch("/flakes/vaes").then(r => r.json()).then(d => d.vaes || []);
|
|
return VAES_PROMISE;
|
|
}
|
|
|
|
export async function fetchBrowse(type, path = "") {
|
|
const r = await fetch("/flakes/browse", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ type, path }),
|
|
});
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({}));
|
|
throw new Error(err.error || `HTTP ${r.status}`);
|
|
}
|
|
return (await r.json());
|
|
}
|
|
|
|
export async function fetchPreset(name) {
|
|
const r = await fetch(`/flakes/preset?name=${encodeURIComponent(name)}`);
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({}));
|
|
throw new Error(err.error || `HTTP ${r.status}`);
|
|
}
|
|
return (await r.json()).data || {};
|
|
}
|
|
|
|
export async function fetchPresets(family = "") {
|
|
const query = family ? `?family=${encodeURIComponent(family)}` : "";
|
|
const r = await fetch(`/flakes/presets${query}`, { cache: "no-store" });
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({}));
|
|
throw new Error(err.error || `HTTP ${r.status}`);
|
|
}
|
|
return (await r.json()).presets || [];
|
|
}
|
|
|
|
// --- Cover ---
|
|
|
|
export function getCoverUrl(name) {
|
|
return `/flakes/cover?name=${encodeURIComponent(name)}`;
|
|
}
|
|
|
|
export function getVariantImageUrl(name, group, choice) {
|
|
return `/flakes/variant_image?name=${encodeURIComponent(name)}&group=${encodeURIComponent(group)}&choice=${encodeURIComponent(choice)}`;
|
|
}
|
|
|
|
export function loraSiblingImageUrl(path) {
|
|
return `/flakes/lora_sibling_image?path=${encodeURIComponent(path)}`;
|
|
}
|
|
|
|
export async function fetchLoraSiblingImage(path) {
|
|
const r = await fetch(loraSiblingImageUrl(path));
|
|
if (!r.ok) return null;
|
|
const blob = await r.blob();
|
|
const ct = r.headers.get("Content-Type") || "image/png";
|
|
const extMap = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/gif": "gif" };
|
|
const ext = extMap[ct] || "png";
|
|
return { blob, ext, mime: ct };
|
|
}
|
|
|
|
export async function fetchLoraSiblingImagePath(loraPath) {
|
|
const r = await fetch(`/flakes/lora_sibling_image_path?path=${encodeURIComponent(loraPath)}`);
|
|
if (!r.ok) return null;
|
|
const d = await r.json().catch(() => ({}));
|
|
return d.path || null;
|
|
}
|
|
|
|
export async function uploadCover(name, file) {
|
|
const form = new FormData();
|
|
form.append("file", file);
|
|
const r = await fetch(`/flakes/cover?name=${encodeURIComponent(name)}`, {
|
|
method: "POST",
|
|
body: form,
|
|
});
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({}));
|
|
throw new Error(err.error || `HTTP ${r.status}`);
|
|
}
|
|
}
|
|
|
|
// --- Flake meta ---
|
|
|
|
const META_CACHE = {};
|
|
export async function fetchFlakeMeta(name) {
|
|
if (META_CACHE[name]) return META_CACHE[name];
|
|
const r = await fetch(`/flakes/meta?name=${encodeURIComponent(name)}`);
|
|
if (!r.ok) return {};
|
|
const d = await r.json();
|
|
META_CACHE[name] = d.variants || d.options || {};
|
|
return META_CACHE[name];
|
|
}
|
|
|
|
let EMBEDDINGS_PROMISE = null;
|
|
export function fetchEmbeddings() {
|
|
if (!EMBEDDINGS_PROMISE) EMBEDDINGS_PROMISE = fetch("/flakes/embeddings").then(r => r.json()).then(d => d.embeddings || []);
|
|
return EMBEDDINGS_PROMISE;
|
|
}
|