230 lines
10 KiB
JavaScript
230 lines
10 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
||
|
||
app.registerExtension({
|
||
name: "Forge.Lora.Stack.UI",
|
||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||
if (nodeData.name !== "ForgeLoraStack") return;
|
||
|
||
nodeType.prototype.applyVisualStyles = function() {
|
||
// Lora-only visual labelling: name-less widget labels are prettified
|
||
this.widgets.forEach(w => {
|
||
if (!w.name || !w.name.startsWith("lora_")) return;
|
||
const parts = w.name.split("_");
|
||
if (w.type === "button") w.label = "❌ Remove LoRA";
|
||
else w.label = parts.slice(2).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
|
||
});
|
||
};
|
||
|
||
// Initialize a background sync once to copy forge_url from a ForgeSampler
|
||
// to ForgeLoraStack nodes when available.
|
||
if (!app._forgeSamplerUrlSyncInitialized) {
|
||
app._forgeSamplerUrlSyncInitialized = true;
|
||
setInterval(() => {
|
||
try {
|
||
const findNodes = () => {
|
||
if (app.canvas && Array.isArray(app.canvas.nodes)) return app.canvas.nodes;
|
||
if (Array.isArray(app.nodes)) return app.nodes;
|
||
if (app.canvas && app.canvas.graph && Array.isArray(app.canvas.graph.nodes)) return app.canvas.graph.nodes;
|
||
return [];
|
||
};
|
||
|
||
const nodes = findNodes();
|
||
if (!nodes || !nodes.length) return;
|
||
|
||
const sampler = nodes.find(n => n.type === "ForgeSampler" || n.name === "ForgeSampler");
|
||
const samplerFW = sampler?.widgets && sampler.widgets.find(w => w.name === "forge_url");
|
||
const samplerVal = samplerFW?.value || samplerFW?.default || null;
|
||
if (!samplerVal) return;
|
||
|
||
for (const n of nodes) {
|
||
if (n.type !== "ForgeLoraStack" && n.name !== "ForgeLoraStack") continue;
|
||
const myW = n.widgets && n.widgets.find(w => w.name === "forge_url");
|
||
let wrote = false;
|
||
if (myW) {
|
||
if (!myW.value || myW.value === myW.default || myW.value === "http://127.0.0.1:7860") {
|
||
myW.value = samplerVal;
|
||
wrote = true;
|
||
}
|
||
} else {
|
||
try {
|
||
n.values = n.values || {};
|
||
if (!n.values["forge_url"] || n.values["forge_url"] === "http://127.0.0.1:7860") {
|
||
n.values["forge_url"] = samplerVal;
|
||
wrote = true;
|
||
}
|
||
} catch (e) {}
|
||
try {
|
||
if (!n._forge_url) {
|
||
n._forge_url = samplerVal;
|
||
wrote = true;
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
if (wrote) {
|
||
n.applyVisualStyles?.();
|
||
app.canvas.setDirty(true, true);
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
}, 1200);
|
||
}
|
||
|
||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||
nodeType.prototype.onNodeCreated = function () {
|
||
onNodeCreated?.apply(this, arguments);
|
||
|
||
// Remove any auto-created lora_* widgets BEFORE adding buttons
|
||
if (this.widgets) {
|
||
this.widgets = this.widgets.filter(w => !w.name || !w.name.startsWith("lora_"));
|
||
}
|
||
|
||
this.addWidget("button", "➕ Add LoRA", null, () => { this.addLoraSlot(); });
|
||
|
||
this.addWidget("button", "🗑️ Clear All", null, () => {
|
||
const base = ["enabled", "forge_url"];
|
||
this.widgets = this.widgets.filter(w => base.includes(w.name) || (w.type === "button" && !w.name.includes("_remove")));
|
||
this.applyVisualStyles();
|
||
this.autoSizeHeight();
|
||
});
|
||
|
||
// Hide the forge_url widget visually while keeping it for backend use
|
||
try {
|
||
const fw = this.widgets.find(w => w.name === "forge_url");
|
||
if (fw) {
|
||
fw.label = "";
|
||
fw.hidden = true;
|
||
fw.visible = false;
|
||
fw._hidden = true;
|
||
try { if (fw.dom) fw.dom.style.display = "none"; } catch (e) {}
|
||
try { if (fw.domElement) fw.domElement.style.display = "none"; } catch (e) {}
|
||
}
|
||
} catch (e) {}
|
||
|
||
this.applyVisualStyles();
|
||
this.autoSizeHeight();
|
||
};
|
||
|
||
// Serialize function - saves the state
|
||
nodeType.prototype.onSerialize = function(o) {
|
||
o.lora_slots = [];
|
||
const loraWidgets = this.widgets.filter(w => w.name && w.name.startsWith("lora_"));
|
||
|
||
// Group by LoRA index
|
||
const indices = new Set();
|
||
loraWidgets.forEach(w => {
|
||
const m = w.name.match(/^lora_(\d+)_/);
|
||
if (m) indices.add(m[1]);
|
||
});
|
||
|
||
indices.forEach(idx => {
|
||
const slot = { index: idx };
|
||
loraWidgets.forEach(w => {
|
||
if (w.name.startsWith(`lora_${idx}_`)) {
|
||
const param = w.name.replace(`lora_${idx}_`, '');
|
||
if (param !== 'remove') {
|
||
slot[param] = w.value;
|
||
}
|
||
}
|
||
});
|
||
o.lora_slots.push(slot);
|
||
});
|
||
};
|
||
|
||
// Deserialize function - restores the state
|
||
nodeType.prototype.onConfigure = function(o) {
|
||
if (o.lora_slots && Array.isArray(o.lora_slots)) {
|
||
// Clear existing LoRA slots first
|
||
this.widgets = this.widgets.filter(w => !w.name || !w.name.startsWith("lora_"));
|
||
|
||
// Restore each slot
|
||
o.lora_slots.forEach(slot => {
|
||
const idx = slot.index;
|
||
const id = `lora_${idx}`;
|
||
|
||
const nameWidget = this.addWidget("combo", `${id}_name`, slot.name || "None", () => {}, {
|
||
values: ["None"]
|
||
});
|
||
|
||
this.addWidget("number", `${id}_strength`, slot.strength !== undefined ? slot.strength : 1.0, () => {}, {
|
||
min: -10.0, max: 10.0, step: 0.05, precision: 2
|
||
});
|
||
|
||
try {
|
||
const base = `${location.protocol}//127.0.0.1:7860`;
|
||
fetch(`${base}/sdapi/v1/loras`).then(r => {
|
||
if (!r.ok) return null;
|
||
return r.json();
|
||
}).then(data => {
|
||
if (!data) return;
|
||
const vals = ["None"].concat((Array.isArray(data) ? data.map(d => d.name) : []).filter(Boolean));
|
||
nameWidget.options = nameWidget.options || {};
|
||
nameWidget.options.values = vals;
|
||
this.applyVisualStyles();
|
||
app.canvas.setDirty(true, true);
|
||
}).catch(() => {});
|
||
} catch (e) {}
|
||
|
||
this.addWidget("button", `${id}_remove`, null, () => {
|
||
this.widgets = this.widgets.filter(w => !w.name.startsWith(id));
|
||
this.applyVisualStyles();
|
||
this.autoSizeHeight();
|
||
});
|
||
});
|
||
|
||
this.finalizeAddition();
|
||
}
|
||
};
|
||
|
||
nodeType.prototype.addLoraSlot = function() {
|
||
const existing = this.widgets.filter(w => w.name && w.name.startsWith("lora_"));
|
||
let maxIndex = 0;
|
||
for (const w of existing) {
|
||
const m = w.name.match(/^lora_(\d+)_/);
|
||
if (m) {
|
||
const v = parseInt(m[1], 10);
|
||
if (!Number.isNaN(v) && v > maxIndex) maxIndex = v;
|
||
}
|
||
}
|
||
const index = maxIndex + 1;
|
||
const id = `lora_${index}`;
|
||
|
||
const nameWidget = this.addWidget("combo", `${id}_name`, "None", () => {}, { values: ["None"] });
|
||
this.addWidget("number", `${id}_strength`, 1.0, () => {}, { min: -10.0, max: 10.0, step: 0.05, precision: 2 });
|
||
|
||
try {
|
||
const base = `${location.protocol}//127.0.0.1:7860`;
|
||
fetch(`${base}/sdapi/v1/loras`).then(r => {
|
||
if (!r.ok) return null;
|
||
return r.json();
|
||
}).then(data => {
|
||
if (!data) return;
|
||
const vals = ["None"].concat((Array.isArray(data) ? data.map(d => d.name) : []).filter(Boolean));
|
||
nameWidget.options = nameWidget.options || {};
|
||
nameWidget.options.values = vals;
|
||
this.applyVisualStyles();
|
||
app.canvas.setDirty(true, true);
|
||
}).catch(() => {});
|
||
} catch (e) {}
|
||
|
||
this.addWidget("button", `${id}_remove`, null, () => {
|
||
this.widgets = this.widgets.filter(w => !w.name.startsWith(id));
|
||
this.applyVisualStyles();
|
||
this.autoSizeHeight();
|
||
});
|
||
this.finalizeAddition();
|
||
};
|
||
|
||
nodeType.prototype.finalizeAddition = function() {
|
||
this.applyVisualStyles();
|
||
const btns = this.widgets.filter(w => w.type === "button" && (w.name?.includes("➕") || w.name === "🗑️ Clear All"));
|
||
this.widgets = this.widgets.filter(w => !btns.includes(w)).concat(btns);
|
||
this.autoSizeHeight();
|
||
};
|
||
|
||
nodeType.prototype.autoSizeHeight = function() {
|
||
const size = this.computeSize();
|
||
this.setSize([this.size[0], size[1]]);
|
||
app.canvas.setDirty(true, true);
|
||
};
|
||
}
|
||
}); |