Files

313 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { t } from "./i18n.js";
import { openLoraPresets } from "./lora_presets.js";
const NODE_NAME = "PromptWarehouseMultiLoraLoader";
const ROW_HEIGHT = 23;
function showStrengthEditor(initialValue, onSave) {
if (!document.getElementById("pw-lora-strength-style")) {
const style = document.createElement("style");
style.id = "pw-lora-strength-style";
style.textContent = `
.pw-lora-strength-backdrop{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:#05070a99;font-family:Inter,system-ui,sans-serif}
.pw-lora-strength-dialog{width:270px;padding:17px;background:#202226;color:#eceff4;border:1px solid #555b65;border-radius:10px;box-shadow:0 20px 60px #000a}
.pw-lora-strength-dialog label{display:block;margin-bottom:9px;color:#cdd2da;font-size:13px}
.pw-lora-strength-dialog input{box-sizing:border-box;width:100%;padding:9px 10px;background:#121417;color:#fff;border:1px solid #5c6470;border-radius:6px;outline:none;font-size:14px}
.pw-lora-strength-dialog input:focus{border-color:#8b9fbd;box-shadow:0 0 0 3px #7791b426}
.pw-lora-strength-error{min-height:17px;margin-top:5px;color:#ef8d8d;font-size:11px}
.pw-lora-strength-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}
.pw-lora-strength-actions button{padding:6px 13px;color:#e7eaf0;background:#343840;border:1px solid #555c67;border-radius:6px;cursor:pointer}
.pw-lora-strength-actions button[data-save]{background:#526f98;border-color:#6f89ad}
`;
document.head.append(style);
}
const root = document.createElement("div");
root.className = "pw-lora-strength-backdrop";
root.innerHTML = `
<div class="pw-lora-strength-dialog" role="dialog" aria-modal="true" aria-label="${t("strength")}">
<label>${t("strength")}</label>
<input type="number" step="0.05" inputmode="decimal" value="${Number(initialValue)}">
<div class="pw-lora-strength-error"></div>
<div class="pw-lora-strength-actions"><button data-cancel>${t("cancel")}</button><button data-save>${t("confirm")}</button></div>
</div>`;
document.body.append(root);
const input = root.querySelector("input");
const error = root.querySelector(".pw-lora-strength-error");
const close = () => root.remove();
const focusAndSelect = () => {
input.focus();
input.select();
};
const save = () => {
const value = Number(input.value);
if (!input.value.trim() || !Number.isFinite(value)) {
error.textContent = t("invalidNumber");
focusAndSelect();
return;
}
onSave(value);
close();
};
root.querySelector("[data-save]").onclick = save;
root.querySelector("[data-cancel]").onclick = close;
root.onclick = (event) => { if (event.target === root) close(); };
root.onkeydown = (event) => {
if (event.key === "Enter") save();
if (event.key === "Escape") close();
};
// The canvas restores its own focus after handling the click that opens this
// dialog. Waiting one frame ensures the input wins and its value stays selected.
requestAnimationFrame(focusAndSelect);
}
function parseConfig(widget) {
try {
const value = JSON.parse(widget?.value || "[]");
return Array.isArray(value) ? value : [];
} catch (_) {
return [];
}
}
function hideConfigWidget(widget) {
if (!widget || widget._pwHidden) return;
widget._pwHidden = true;
widget.hidden = true;
widget.type = "pw-hidden-lora-config";
widget.computeSize = () => [0, -4];
widget.draw = () => {};
for (const element of [widget.element, widget.inputEl, widget.inputElement]) {
if (element?.style) element.style.display = "none";
}
}
function shortName(name, limit) {
const value = String(name || `${t("selectLora")}…`).replace(/\\/g, "/");
return value.length > limit ? `${value.slice(0, Math.max(1, limit - 1))}…` : value;
}
function chooseLora(event, names, callback) {
const items = (names.length ? names : [t("noLora")]).map((name) => ({
content: name,
disabled: !names.length,
callback: () => callback(name),
}));
new LiteGraph.ContextMenu(items, { event, title: t("selectLora") });
}
function removeEditorWidgets(node) {
for (const widget of node._pwLoraWidgets || []) {
const index = node.widgets?.indexOf(widget) ?? -1;
if (index >= 0) node.widgets.splice(index, 1);
}
node._pwLoraWidgets = [];
}
function makeRowWidget(node, row, index, rows, names, save, rebuild) {
const widget = {
name: `lora_${index}`,
type: "pw-lora-row",
value: row,
options: { serialize: false },
computeSize(width) { return [width, ROW_HEIGHT]; },
draw(ctx, _node, width, y, height) {
this.last_y = y;
const midY = y + height / 2;
const plusX = width - 20;
const valueX = plusX - 40;
const minusX = valueX - 28;
const nameRight = minusX - 9;
this._hit = { nameRight, minusX, valueX, plusX };
ctx.save();
ctx.globalAlpha = row.enabled ? 1 : 0.45;
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR || "#222";
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR || "#555";
ctx.beginPath();
ctx.roundRect(10, y, width - 20, height - 1, 6);
ctx.fill();
ctx.stroke();
ctx.fillStyle = row.enabled ? "#68b36b" : "#555";
ctx.beginPath();
ctx.arc(21, midY, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR || "#ddd";
ctx.font = "12px sans-serif";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.fillText(shortName(row.name, Math.max(8, Math.floor((nameRight - 35) / 7))), 34, midY);
ctx.textAlign = "center";
ctx.globalAlpha *= 0.72;
ctx.fillText("−", minusX, midY);
ctx.globalAlpha = row.enabled ? 1 : 0.45;
ctx.fillText(Number(row.strength ?? 1).toFixed(2), valueX, midY);
ctx.globalAlpha *= 0.72;
ctx.fillText("+", plusX, midY);
ctx.restore();
},
mouse(event, pos) {
if (!this._hit || !["pointerdown", "mousedown"].includes(event.type) || event.button > 0) {
return false;
}
const x = pos[0];
if (x < 31) {
row.enabled = !row.enabled;
save();
} else if (x < this._hit.nameRight) {
chooseLora(event, names, (name) => { row.name = name; save(); });
} else if (x < (this._hit.minusX + this._hit.valueX) / 2) {
row.strength = Math.round((Number(row.strength ?? 1) - 0.05) * 100) / 100;
save();
} else if (x < (this._hit.valueX + this._hit.plusX) / 2) {
showStrengthEditor(row.strength ?? 1, (value) => {
row.strength = value;
save();
});
} else {
row.strength = Math.round((Number(row.strength ?? 1) + 0.05) * 100) / 100;
save();
}
node.setDirtyCanvas(true, true);
return true;
},
};
widget._pwContextOptions = () => [
{
content: row.enabled ? `⚫ ${t("toggleOff")}` : `🟢 ${t("toggleOn")}`,
callback: () => { row.enabled = !row.enabled; save(); },
},
null,
{
content: `⬆ ${t("moveUp")}`,
disabled: index === 0,
callback: () => {
if (index > 0) [rows[index - 1], rows[index]] = [rows[index], rows[index - 1]];
rebuild();
},
},
{
content: `⬇ ${t("moveDown")}`,
disabled: index === rows.length - 1,
callback: () => {
if (index < rows.length - 1) [rows[index], rows[index + 1]] = [rows[index + 1], rows[index]];
rebuild();
},
},
{
content: `🗑 ${t("remove")}`,
callback: () => { rows.splice(index, 1); rebuild(); },
},
];
return widget;
}
function renderEditor(node, names) {
const configWidget = node.widgets?.find((widget) => widget.name === "lora_config");
if (!configWidget) return;
hideConfigWidget(configWidget);
removeEditorWidgets(node);
const rows = parseConfig(configWidget).map((row) => ({
enabled: row.enabled ?? row.on ?? true,
name: row.name ?? row.lora ?? "",
strength: Number(row.strength ?? row.strength_model ?? 1),
}));
const added = node._pwLoraWidgets = [];
const save = () => {
configWidget.value = JSON.stringify(rows);
node.setDirtyCanvas(true, true);
};
const rebuild = () => {
configWidget.value = JSON.stringify(rows);
renderEditor(node, names);
};
rows.forEach((row, index) => {
const widget = makeRowWidget(node, row, index, rows, names, save, rebuild);
node.addCustomWidget(widget);
added.push(widget);
});
const button = node.addWidget("button", t("addLora"), null, (_value, _canvas, _node, _pos, event) => {
const add = (name) => {
rows.push({ enabled: true, name, strength: 1 });
rebuild();
};
if (event) chooseLora(event, names, add);
else add("");
});
button.options = { ...(button.options || {}), serialize: false };
added.push(button);
const divider = {
name: "lora_presets_divider",
type: "pw-lora-divider",
options: { serialize: false },
computeSize(width) { return [width, 10]; },
draw(ctx, _node, width, y) {
ctx.save();
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR || "#555";
ctx.beginPath();
ctx.moveTo(14, y + 5);
ctx.lineTo(width - 14, y + 5);
ctx.stroke();
ctx.restore();
},
};
node.addCustomWidget(divider);
added.push(divider);
const presets = node.addWidget("button", t("loraPresets"), null, () => {
openLoraPresets(node, () => parseConfig(configWidget), (presetRows) => {
configWidget.value = JSON.stringify(presetRows);
renderEditor(node, names);
});
});
presets.options = { ...(presets.options || {}), serialize: false };
added.push(presets);
node.setSize([Math.max(node.size[0], 355), Math.max(node.computeSize()[1], 105)]);
save();
}
app.registerExtension({
name: "D33MO.MultiLoraLoader",
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== NODE_NAME) return;
const names = nodeData.input?.required?.lora_config?.[1]?.lora_names || [];
const created = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = created?.apply(this, arguments);
this.size[0] = Math.max(355, (this.size?.[0] || 360) - 5);
renderEditor(this, names);
return result;
};
const configured = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const result = configured?.apply(this, arguments);
renderEditor(this, names);
return result;
};
const getSlot = nodeType.prototype.getSlotInPosition;
nodeType.prototype.getSlotInPosition = function (canvasX, canvasY) {
const slot = getSlot?.apply(this, arguments);
if (slot) return slot;
const localY = canvasY - this.pos[1];
const widget = this.widgets?.find((item) => item.type === "pw-lora-row"
&& localY >= item.last_y && localY <= item.last_y + ROW_HEIGHT);
return widget ? { widget, output: { type: "LORA" } } : slot;
};
const getMenu = nodeType.prototype.getSlotMenuOptions;
nodeType.prototype.getSlotMenuOptions = function (slot) {
if (slot?.widget?._pwContextOptions) return slot.widget._pwContextOptions();
return getMenu?.apply(this, arguments);
};
},
});