When the user selects a LoRA on a flake and no cover has been picked yet, fetch the LoRA's sibling image (same basename, .png/.jpg/.webp/...) from models/loras/ and use it as the flake's default cover. Triggers from all three LoRA selection paths (file browser, dropdown change, dropdown blur) but only for the first LoRA on the flake, and only when no cover file has been chosen by the user. Adds a server endpoint /flakes/lora_sibling_image mirroring the existing checkpoint sibling endpoint, with a shared helper.
165 lines
5.3 KiB
JavaScript
165 lines
5.3 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 : [],
|
|
}));
|
|
}
|
|
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();
|
|
}
|
|
|
|
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 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 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 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.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;
|
|
}
|