Three things. The prompt field had gone the colour of the node body. Making the textarea transparent is what lets the highlight layer show through, but nothing was painting a background behind it - textarea, layer and wrapper were all transparent - so the one thing that should look like a field did not. The layer covers exactly the textarea's box, so it carries the field's background now, and the strip takes the darker menu colour so the two read as separate things. "/" no longer offers LoRAs for a base model that is not the one wired in. The node walks back to whichever loader feeds it and asks the server what that file is. Only what is known not to fit is dropped: a file with no sidecar declares no base, and guessing would hide a working LoRA - 163 of 761 files in a real collection say nothing about theirs - so those sort after the ones that match instead. Measured on that collection: "photo" went from 19 suggestions to 3, with the matching one first. The strip names the base, so a short list is never a mystery. Finding the connected model was already written for the library browser. Rather than a second copy that could drift, both now share model_base.js. And the strip is laid out rather than assembled: everything actionable is the same chip - the weight included, which was loose text among bordered buttons - at one height, centred on one line, with the trigger words last where clipping them costs least. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
2.6 KiB
JavaScript
72 lines
2.6 KiB
JavaScript
// Which base model a node is wired to, and which one a LoRA belongs to.
|
|
//
|
|
// Both the prompt's completion and the library browser need this, and they must
|
|
// not be able to disagree about what matches what, so it lives in one place.
|
|
|
|
/** What a LoRA is filed under: what it says it is, else where it sits.
|
|
*
|
|
* base_model comes from the .civitai.info sidecar and means the same thing in
|
|
* anybody's collection. Folder names only mean something to whoever chose them,
|
|
* so they are the fallback rather than the rule.
|
|
*/
|
|
export function groupOf(entry) {
|
|
return entry.base_model || entry.folder || "Unsorted";
|
|
}
|
|
|
|
// "Flux.2 Klein 9B" and "flux2 klein 9b" are the same base written twice.
|
|
const key = (name) => (name || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
|
|
/** Do these two name the same base model? Unknown never matches anything. */
|
|
export function sameBase(a, b) {
|
|
const left = key(a);
|
|
return left !== "" && left === key(b);
|
|
}
|
|
|
|
/** The model file feeding this node, found by walking back up the graph. */
|
|
export function connectedModelName(node) {
|
|
const seen = new Set();
|
|
const walk = (current, depth) => {
|
|
if (!current || depth > 12 || seen.has(current.id)) return null;
|
|
seen.add(current.id);
|
|
for (const widget of current.widgets || []) {
|
|
const isModel = /^(unet_name|ckpt_name|model_name)$/.test(widget.name || "");
|
|
if (isModel && typeof widget.value === "string" && widget.value) return widget.value;
|
|
}
|
|
for (let slot = 0; slot < (current.inputs || []).length; slot++) {
|
|
const found = walk(current.getInputNode?.(slot), depth + 1);
|
|
if (found !== null) return found;
|
|
}
|
|
return null;
|
|
};
|
|
return walk(node, 0);
|
|
}
|
|
|
|
// Asked of the server once per file: the answer is a sidecar read, and the
|
|
// same checkpoint stays connected for as long as anyone keeps typing.
|
|
const asked = new Map();
|
|
|
|
/** The base model of the file wired into this node, asked of the server. */
|
|
export async function connectedBase(node) {
|
|
const name = connectedModelName(node);
|
|
if (!name) return null;
|
|
if (asked.has(name)) return asked.get(name);
|
|
|
|
let answer = null;
|
|
try {
|
|
const info = await fetch(`/warppipe/model/base?name=${encodeURIComponent(name)}`).then((r) =>
|
|
r.json()
|
|
);
|
|
answer = info.base_model || null;
|
|
} catch {
|
|
/* fall through to the folder */
|
|
}
|
|
if (!answer) {
|
|
// No sidecar: fall back to the folder, which is right for collections filed
|
|
// that way and simply matches nothing for the rest.
|
|
const parts = name.replace(/\\/g, "/").split("/");
|
|
answer = parts.length > 1 ? parts[0] : null;
|
|
}
|
|
asked.set(name, answer);
|
|
return answer;
|
|
}
|