fix: Improve widget handling in lora_loader, lora_stacker, and wanvideo_lora_select, and ensuring expanded state preservation in loras_widget

This commit is contained in:
Will Miao
2025-08-19 22:31:11 +08:00
parent ee84571bdb
commit 3d3c66e12f
6 changed files with 487 additions and 448 deletions

View File

@@ -296,3 +296,6 @@ Join our Discord community for support, discussions, and updates:
[Discord Server](https://discord.gg/vcqNrWVFvM) [Discord Server](https://discord.gg/vcqNrWVFvM)
--- ---
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=willmiao/ComfyUI-Lora-Manager&type=Date)](https://star-history.com/#willmiao/ComfyUI-Lora-Manager&Date)

View File

@@ -1,209 +1,225 @@
import { app } from "../../scripts/app.js"; import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"; import { api } from "../../scripts/api.js";
import { import {
LORA_PATTERN, LORA_PATTERN,
collectActiveLorasFromChain, collectActiveLorasFromChain,
updateConnectedTriggerWords, updateConnectedTriggerWords,
chainCallback, chainCallback,
mergeLoras, mergeLoras,
setupInputWidgetWithAutocomplete setupInputWidgetWithAutocomplete,
} from "./utils.js"; } from "./utils.js";
import { addLorasWidget } from "./loras_widget.js"; import { addLorasWidget } from "./loras_widget.js";
app.registerExtension({ app.registerExtension({
name: "LoraManager.LoraLoader", name: "LoraManager.LoraLoader",
setup() { setup() {
// Add message handler to listen for messages from Python // Add message handler to listen for messages from Python
api.addEventListener("lora_code_update", (event) => { api.addEventListener("lora_code_update", (event) => {
const { id, lora_code, mode } = event.detail; const { id, lora_code, mode } = event.detail;
this.handleLoraCodeUpdate(id, lora_code, mode); this.handleLoraCodeUpdate(id, lora_code, mode);
});
},
// Handle lora code updates from Python
handleLoraCodeUpdate(id, loraCode, mode) {
// Handle broadcast mode (for Desktop/non-browser support)
if (id === -1) {
// Find all Lora Loader nodes in the current graph
const loraLoaderNodes = [];
for (const nodeId in app.graph._nodes_by_id) {
const node = app.graph._nodes_by_id[nodeId];
if (node.comfyClass === "Lora Loader (LoraManager)") {
loraLoaderNodes.push(node);
}
}
// Update each Lora Loader node found
if (loraLoaderNodes.length > 0) {
loraLoaderNodes.forEach((node) => {
this.updateNodeLoraCode(node, loraCode, mode);
}); });
}, console.log(
`Updated ${loraLoaderNodes.length} Lora Loader nodes in broadcast mode`
// Handle lora code updates from Python );
handleLoraCodeUpdate(id, loraCode, mode) { } else {
// Handle broadcast mode (for Desktop/non-browser support) console.warn(
if (id === -1) { "No Lora Loader nodes found in the workflow for broadcast update"
// Find all Lora Loader nodes in the current graph );
const loraLoaderNodes = []; }
for (const nodeId in app.graph._nodes_by_id) {
const node = app.graph._nodes_by_id[nodeId]; return;
if (node.comfyClass === "Lora Loader (LoraManager)") { }
loraLoaderNodes.push(node);
// Standard mode - update a specific node
const node = app.graph.getNodeById(+id);
if (
!node ||
(node.comfyClass !== "Lora Loader (LoraManager)" &&
node.comfyClass !== "Lora Stacker (LoraManager)" &&
node.comfyClass !== "WanVideo Lora Select (LoraManager)")
) {
console.warn("Node not found or not a LoraLoader:", id);
return;
}
this.updateNodeLoraCode(node, loraCode, mode);
},
// Helper method to update a single node's lora code
updateNodeLoraCode(node, loraCode, mode) {
// Update the input widget with new lora code
const inputWidget = node.inputWidget;
if (!inputWidget) return;
// Get the current lora code
const currentValue = inputWidget.value || "";
// Update based on mode (replace or append)
if (mode === "replace") {
inputWidget.value = loraCode;
} else {
// Append mode - add a space if the current value isn't empty
inputWidget.value = currentValue.trim()
? `${currentValue.trim()} ${loraCode}`
: loraCode;
}
// Trigger the callback to update the loras widget
if (typeof inputWidget.callback === "function") {
inputWidget.callback(inputWidget.value);
}
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass == "Lora Loader (LoraManager)") {
chainCallback(nodeType.prototype, "onNodeCreated", function () {
// Enable widget serialization
this.serialize_widgets = true;
this.addInput("clip", "CLIP", {
shape: 7,
});
this.addInput("lora_stack", "LORA_STACK", {
shape: 7, // 7 is the shape of the optional input
});
// Add flag to prevent callback loops
let isUpdating = false;
// Get the widget object directly from the returned object
this.lorasWidget = addLorasWidget(
this,
"loras",
{},
(value) => {
// Collect all active loras from this node and its input chain
const allActiveLoraNames = collectActiveLorasFromChain(this);
// Update trigger words for connected toggle nodes with the aggregated lora names
updateConnectedTriggerWords(this, allActiveLoraNames);
// Prevent recursive calls
if (isUpdating) return;
isUpdating = true;
try {
// Remove loras that are not in the value array
const inputWidget = this.widgets[0];
const currentLoras = value.map((l) => l.name);
// Use the constant pattern here as well
let newText = inputWidget.value.replace(
LORA_PATTERN,
(match, name, strength, clipStrength) => {
return currentLoras.includes(name) ? match : "";
} }
);
// Clean up multiple spaces, extra commas, and trim; remove trailing comma if it's the only content
newText = newText
.replace(/\s+/g, " ")
.replace(/,\s*,+/g, ",")
.trim();
if (newText === ",") newText = "";
inputWidget.value = newText;
} finally {
isUpdating = false;
} }
}
// Update each Lora Loader node found ).widget;
if (loraLoaderNodes.length > 0) {
loraLoaderNodes.forEach(node => {
this.updateNodeLoraCode(node, loraCode, mode);
});
console.log(`Updated ${loraLoaderNodes.length} Lora Loader nodes in broadcast mode`);
} else {
console.warn("No Lora Loader nodes found in the workflow for broadcast update");
}
return;
}
// Standard mode - update a specific node
const node = app.graph.getNodeById(+id);
if (!node || (node.comfyClass !== "Lora Loader (LoraManager)" &&
node.comfyClass !== "Lora Stacker (LoraManager)" &&
node.comfyClass !== "WanVideo Lora Select (LoraManager)")) {
console.warn("Node not found or not a LoraLoader:", id);
return;
}
this.updateNodeLoraCode(node, loraCode, mode);
},
// Helper method to update a single node's lora code // Update input widget callback
updateNodeLoraCode(node, loraCode, mode) { const inputWidget = this.widgets[0];
// Update the input widget with new lora code inputWidget.options.getMaxHeight = () => 100;
const inputWidget = node.inputWidget; this.inputWidget = inputWidget;
if (!inputWidget) return;
// Get the current lora code
const currentValue = inputWidget.value || '';
// Update based on mode (replace or append)
if (mode === 'replace') {
inputWidget.value = loraCode;
} else {
// Append mode - add a space if the current value isn't empty
inputWidget.value = currentValue.trim()
? `${currentValue.trim()} ${loraCode}`
: loraCode;
}
// Trigger the callback to update the loras widget
if (typeof inputWidget.callback === 'function') {
inputWidget.callback(inputWidget.value);
}
},
async beforeRegisterNodeDef(nodeType, nodeData, app) { const originalCallback = (value) => {
if (nodeType.comfyClass == "Lora Loader (LoraManager)") { if (isUpdating) return;
chainCallback(nodeType.prototype, "onNodeCreated", function () { isUpdating = true;
// Enable widget serialization
this.serialize_widgets = true;
this.addInput("clip", "CLIP", { try {
shape: 7, const currentLoras = this.lorasWidget.value || [];
}); const mergedLoras = mergeLoras(value, currentLoras);
this.addInput("lora_stack", "LORA_STACK", { this.lorasWidget.value = mergedLoras;
shape: 7, // 7 is the shape of the optional input } finally {
}); isUpdating = false;
}
};
// Restore saved value if exists // Setup input widget with autocomplete
let existingLoras = []; inputWidget.callback = setupInputWidgetWithAutocomplete(
if (this.widgets_values && this.widgets_values.length > 0) { this,
// 0 for input widget, 1 for loras widget inputWidget,
const savedValue = this.widgets_values[1]; originalCallback
existingLoras = savedValue || []; );
}
// Merge the loras data
const mergedLoras = mergeLoras(
this.widgets[0].value,
existingLoras
);
// Add flag to prevent callback loops // Register this node with the backend
let isUpdating = false; this.registerNode = async () => {
try {
// Get the widget object directly from the returned object await fetch("/api/register-node", {
this.lorasWidget = addLorasWidget( method: "POST",
this, headers: {
"loras", "Content-Type": "application/json",
{
defaultVal: mergedLoras, // Pass object directly
}, },
(value) => { body: JSON.stringify({
// Collect all active loras from this node and its input chain node_id: this.id,
const allActiveLoraNames = collectActiveLorasFromChain(this); bgcolor: this.bgcolor,
title: this.title,
graph_id: this.graph.id,
}),
});
} catch (error) {
console.warn("Failed to register node:", error);
}
};
// Update trigger words for connected toggle nodes with the aggregated lora names // Ensure the node is registered after creation
updateConnectedTriggerWords(this, allActiveLoraNames); // Call registration
// setTimeout(() => {
// this.registerNode();
// }, 0);
});
}
},
// Prevent recursive calls async nodeCreated(node) {
if (isUpdating) return; if (node.comfyClass == "Lora Loader (LoraManager)") {
isUpdating = true; requestAnimationFrame(async () => {
// Restore saved value if exists
try { let existingLoras = [];
// Remove loras that are not in the value array if (node.widgets_values && node.widgets_values.length > 0) {
const inputWidget = this.widgets[0]; // 0 for input widget, 1 for loras widget
const currentLoras = value.map((l) => l.name); const savedValue = node.widgets_values[1];
existingLoras = savedValue || [];
// Use the constant pattern here as well
let newText = inputWidget.value.replace(
LORA_PATTERN,
(match, name, strength, clipStrength) => {
return currentLoras.includes(name) ? match : "";
}
);
// Clean up multiple spaces, extra commas, and trim; remove trailing comma if it's the only content
newText = newText.replace(/\s+/g, " ").replace(/,\s*,+/g, ",").trim();
if (newText === ",") newText = "";
inputWidget.value = newText;
} finally {
isUpdating = false;
}
}
).widget;
// Update input widget callback
const inputWidget = this.widgets[0];
inputWidget.options.getMaxHeight = () => 100;
this.inputWidget = inputWidget;
const originalCallback = (value) => {
if (isUpdating) return;
isUpdating = true;
try {
const currentLoras = this.lorasWidget.value || [];
const mergedLoras = mergeLoras(value, currentLoras);
this.lorasWidget.value = mergedLoras;
} finally {
isUpdating = false;
}
};
// Setup input widget with autocomplete
inputWidget.callback = setupInputWidgetWithAutocomplete(this, inputWidget, originalCallback);
// Register this node with the backend
this.registerNode = async () => {
try {
await fetch('/api/register-node', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
node_id: this.id,
bgcolor: this.bgcolor,
title: this.title,
graph_id: this.graph.id
})
});
} catch (error) {
console.warn('Failed to register node:', error);
}
};
// Ensure the node is registered after creation
// Call registration
// setTimeout(() => {
// this.registerNode();
// }, 0);
});
} }
}, // Merge the loras data
}); const mergedLoras = mergeLoras(node.widgets[0].value, existingLoras);
node.lorasWidget.value = mergedLoras;
});
}
},
});

View File

@@ -1,164 +1,185 @@
import { app } from "../../scripts/app.js"; import { app } from "../../scripts/app.js";
import { import {
LORA_PATTERN, LORA_PATTERN,
getActiveLorasFromNode, getActiveLorasFromNode,
collectActiveLorasFromChain, collectActiveLorasFromChain,
updateConnectedTriggerWords, updateConnectedTriggerWords,
chainCallback, chainCallback,
mergeLoras, mergeLoras,
setupInputWidgetWithAutocomplete setupInputWidgetWithAutocomplete,
} from "./utils.js"; } from "./utils.js";
import { addLorasWidget } from "./loras_widget.js"; import { addLorasWidget } from "./loras_widget.js";
app.registerExtension({ app.registerExtension({
name: "LoraManager.LoraStacker", name: "LoraManager.LoraStacker",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "Lora Stacker (LoraManager)") {
chainCallback(nodeType.prototype, "onNodeCreated", async function() {
// Enable widget serialization
this.serialize_widgets = true;
this.addInput("lora_stack", 'LORA_STACK', { async beforeRegisterNodeDef(nodeType, nodeData, app) {
"shape": 7 // 7 is the shape of the optional input if (nodeType.comfyClass === "Lora Stacker (LoraManager)") {
}); chainCallback(nodeType.prototype, "onNodeCreated", async function () {
// Enable widget serialization
this.serialize_widgets = true;
// Restore saved value if exists this.addInput("lora_stack", "LORA_STACK", {
let existingLoras = []; shape: 7, // 7 is the shape of the optional input
if (this.widgets_values && this.widgets_values.length > 0) { });
// 0 for input widget, 1 for loras widget
const savedValue = this.widgets_values[1];
existingLoras = savedValue || [];
}
// Merge the loras data
const mergedLoras = mergeLoras(this.widgets[0].value, existingLoras);
// Add flag to prevent callback loops
let isUpdating = false;
const result = addLorasWidget(this, "loras", {
defaultVal: mergedLoras // Pass object directly
}, (value) => {
// Prevent recursive calls
if (isUpdating) return;
isUpdating = true;
try {
// Remove loras that are not in the value array
const inputWidget = this.widgets[0];
const currentLoras = value.map(l => l.name);
// Use the constant pattern here as well
let newText = inputWidget.value.replace(LORA_PATTERN, (match, name, strength) => {
return currentLoras.includes(name) ? match : '';
});
// Clean up multiple spaces, extra commas, and trim; remove trailing comma if it's the only content
newText = newText.replace(/\s+/g, " ").replace(/,\s*,+/g, ",").trim();
if (newText === ",") newText = "";
inputWidget.value = newText;
// Update this stacker's direct trigger toggles with its own active loras
const activeLoraNames = new Set();
value.forEach(lora => {
if (lora.active) {
activeLoraNames.add(lora.name);
}
});
updateConnectedTriggerWords(this, activeLoraNames);
// Find all Lora Loader nodes in the chain that might need updates
updateDownstreamLoaders(this);
} finally {
isUpdating = false;
}
});
this.lorasWidget = result.widget;
// Update input widget callback // Add flag to prevent callback loops
const inputWidget = this.widgets[0]; let isUpdating = false;
inputWidget.options.getMaxHeight = () => 100;
this.inputWidget = inputWidget;
// Wrap the callback with autocomplete setup
const originalCallback = (value) => {
if (isUpdating) return;
isUpdating = true;
try {
const currentLoras = this.lorasWidget.value || [];
const mergedLoras = mergeLoras(value, currentLoras);
this.lorasWidget.value = mergedLoras;
// Update this stacker's direct trigger toggles with its own active loras
const activeLoraNames = getActiveLorasFromNode(this);
updateConnectedTriggerWords(this, activeLoraNames);
// Find all Lora Loader nodes in the chain that might need updates
updateDownstreamLoaders(this);
} finally {
isUpdating = false;
}
};
inputWidget.callback = setupInputWidgetWithAutocomplete(this, inputWidget, originalCallback);
// Register this node with the backend const result = addLorasWidget(this, "loras", {}, (value) => {
this.registerNode = async () => { // Prevent recursive calls
try { if (isUpdating) return;
await fetch('/api/register-node', { isUpdating = true;
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
node_id: this.id,
bgcolor: this.bgcolor,
title: this.title,
graph_id: this.graph.id
})
});
} catch (error) {
console.warn('Failed to register node:', error);
}
};
// Call registration try {
// setTimeout(() => { // Remove loras that are not in the value array
// this.registerNode(); const inputWidget = this.widgets[0];
// }, 0); const currentLoras = value.map((l) => l.name);
// Use the constant pattern here as well
let newText = inputWidget.value.replace(
LORA_PATTERN,
(match, name, strength) => {
return currentLoras.includes(name) ? match : "";
}
);
// Clean up multiple spaces, extra commas, and trim; remove trailing comma if it's the only content
newText = newText
.replace(/\s+/g, " ")
.replace(/,\s*,+/g, ",")
.trim();
if (newText === ",") newText = "";
inputWidget.value = newText;
// Update this stacker's direct trigger toggles with its own active loras
const activeLoraNames = new Set();
value.forEach((lora) => {
if (lora.active) {
activeLoraNames.add(lora.name);
}
}); });
updateConnectedTriggerWords(this, activeLoraNames);
// Find all Lora Loader nodes in the chain that might need updates
updateDownstreamLoaders(this);
} finally {
isUpdating = false;
}
});
this.lorasWidget = result.widget;
// Update input widget callback
const inputWidget = this.widgets[0];
inputWidget.options.getMaxHeight = () => 100;
this.inputWidget = inputWidget;
// Wrap the callback with autocomplete setup
const originalCallback = (value) => {
if (isUpdating) return;
isUpdating = true;
try {
const currentLoras = this.lorasWidget.value || [];
const mergedLoras = mergeLoras(value, currentLoras);
this.lorasWidget.value = mergedLoras;
// Update this stacker's direct trigger toggles with its own active loras
const activeLoraNames = getActiveLorasFromNode(this);
updateConnectedTriggerWords(this, activeLoraNames);
// Find all Lora Loader nodes in the chain that might need updates
updateDownstreamLoaders(this);
} finally {
isUpdating = false;
}
};
inputWidget.callback = setupInputWidgetWithAutocomplete(
this,
inputWidget,
originalCallback
);
// Register this node with the backend
this.registerNode = async () => {
try {
await fetch("/api/register-node", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
node_id: this.id,
bgcolor: this.bgcolor,
title: this.title,
graph_id: this.graph.id,
}),
});
} catch (error) {
console.warn("Failed to register node:", error);
}
};
// Call registration
// setTimeout(() => {
// this.registerNode();
// }, 0);
});
}
},
async nodeCreated(node) {
if (node.comfyClass == "Lora Stacker (LoraManager)") {
requestAnimationFrame(async () => {
// Restore saved value if exists
let existingLoras = [];
if (node.widgets_values && node.widgets_values.length > 0) {
// 0 for input widget, 1 for loras widget
const savedValue = node.widgets_values[1];
existingLoras = savedValue || [];
} }
}, // Merge the loras data
const mergedLoras = mergeLoras(node.widgets[0].value, existingLoras);
node.lorasWidget.value = mergedLoras;
});
}
},
}); });
// Helper function to find and update downstream Lora Loader nodes // Helper function to find and update downstream Lora Loader nodes
function updateDownstreamLoaders(startNode, visited = new Set()) { function updateDownstreamLoaders(startNode, visited = new Set()) {
if (visited.has(startNode.id)) return; if (visited.has(startNode.id)) return;
visited.add(startNode.id); visited.add(startNode.id);
// Check each output link // Check each output link
if (startNode.outputs) { if (startNode.outputs) {
for (const output of startNode.outputs) { for (const output of startNode.outputs) {
if (output.links) { if (output.links) {
for (const linkId of output.links) { for (const linkId of output.links) {
const link = app.graph.links[linkId]; const link = app.graph.links[linkId];
if (link) { if (link) {
const targetNode = app.graph.getNodeById(link.target_id); const targetNode = app.graph.getNodeById(link.target_id);
// If target is a Lora Loader, collect all active loras in the chain and update // If target is a Lora Loader, collect all active loras in the chain and update
if (targetNode && targetNode.comfyClass === "Lora Loader (LoraManager)") { if (
const allActiveLoraNames = collectActiveLorasFromChain(targetNode); targetNode &&
updateConnectedTriggerWords(targetNode, allActiveLoraNames); targetNode.comfyClass === "Lora Loader (LoraManager)"
} ) {
// If target is another Lora Stacker, recursively check its outputs const allActiveLoraNames =
else if (targetNode && targetNode.comfyClass === "Lora Stacker (LoraManager)") { collectActiveLorasFromChain(targetNode);
updateDownstreamLoaders(targetNode, visited); updateConnectedTriggerWords(targetNode, allActiveLoraNames);
}
}
}
} }
// If target is another Lora Stacker, recursively check its outputs
else if (
targetNode &&
targetNode.comfyClass === "Lora Stacker (LoraManager)"
) {
updateDownstreamLoaders(targetNode, visited);
}
}
} }
}
} }
} }
}

View File

@@ -675,25 +675,9 @@ export function addLorasWidget(node, name, opts, callback) {
// Add the current lora // Add the current lora
return [...filtered, lora]; return [...filtered, lora];
}, []); }, []);
// Preserve clip strengths and expanded state when updating the value
const oldLoras = parseLoraValue(widgetValue);
// Apply existing clip strength values and transfer them to the new value // Apply existing clip strength values and transfer them to the new value
const updatedValue = uniqueValue.map(lora => { const updatedValue = uniqueValue.map(lora => {
const existingLora = oldLoras.find(oldLora => oldLora.name === lora.name);
// If there's an existing lora with the same name, preserve its clip strength and expanded state
if (existingLora) {
return {
...lora,
clipStrength: existingLora.clipStrength || lora.strength,
expanded: existingLora.hasOwnProperty('expanded') ?
existingLora.expanded :
Number(existingLora.clipStrength || lora.strength) !== Number(lora.strength)
};
}
// For new loras, default clip strength to model strength and expanded to false // For new loras, default clip strength to model strength and expanded to false
// unless clipStrength is already different from strength // unless clipStrength is already different from strength
const clipStrength = lora.clipStrength || lora.strength; const clipStrength = lora.clipStrength || lora.strength;

View File

@@ -193,6 +193,7 @@ export function mergeLoras(lorasText, lorasArr) {
name: lora.name, name: lora.name,
strength: lora.strength !== undefined ? lora.strength : parsedLoras[lora.name].strength, strength: lora.strength !== undefined ? lora.strength : parsedLoras[lora.name].strength,
active: lora.active !== undefined ? lora.active : true, active: lora.active !== undefined ? lora.active : true,
expanded: lora.expanded !== undefined ? lora.expanded : false,
clipStrength: lora.clipStrength !== undefined ? lora.clipStrength : parsedLoras[lora.name].clipStrength, clipStrength: lora.clipStrength !== undefined ? lora.clipStrength : parsedLoras[lora.name].clipStrength,
}); });
usedNames.add(lora.name); usedNames.add(lora.name);

View File

@@ -1,107 +1,121 @@
import { app } from "../../scripts/app.js"; import { app } from "../../scripts/app.js";
import { import {
LORA_PATTERN, LORA_PATTERN,
getActiveLorasFromNode, getActiveLorasFromNode,
updateConnectedTriggerWords, updateConnectedTriggerWords,
chainCallback, chainCallback,
mergeLoras, mergeLoras,
setupInputWidgetWithAutocomplete setupInputWidgetWithAutocomplete,
} from "./utils.js"; } from "./utils.js";
import { addLorasWidget } from "./loras_widget.js"; import { addLorasWidget } from "./loras_widget.js";
app.registerExtension({ app.registerExtension({
name: "LoraManager.WanVideoLoraSelect", name: "LoraManager.WanVideoLoraSelect",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "WanVideo Lora Select (LoraManager)") {
chainCallback(nodeType.prototype, "onNodeCreated", async function() {
// Enable widget serialization
this.serialize_widgets = true;
// Add optional inputs async beforeRegisterNodeDef(nodeType, nodeData, app) {
this.addInput("prev_lora", 'WANVIDLORA', { if (nodeType.comfyClass === "WanVideo Lora Select (LoraManager)") {
"shape": 7 // 7 is the shape of the optional input chainCallback(nodeType.prototype, "onNodeCreated", async function () {
}); // Enable widget serialization
this.serialize_widgets = true;
this.addInput("blocks", 'SELECTEDBLOCKS', {
"shape": 7 // 7 is the shape of the optional input
});
// Restore saved value if exists // Add optional inputs
let existingLoras = []; this.addInput("prev_lora", "WANVIDLORA", {
if (this.widgets_values && this.widgets_values.length > 0) { shape: 7, // 7 is the shape of the optional input
// 0 for low_mem_load, 1 for merge_loras, 2 for text widget, 3 for loras widget });
const savedValue = this.widgets_values[3];
existingLoras = savedValue || [];
}
// Merge the loras data
const mergedLoras = mergeLoras(this.widgets[2].value, existingLoras);
// Add flag to prevent callback loops
let isUpdating = false;
const result = addLorasWidget(this, "loras", {
defaultVal: mergedLoras // Pass object directly
}, (value) => {
// Prevent recursive calls
if (isUpdating) return;
isUpdating = true;
try {
// Remove loras that are not in the value array
const inputWidget = this.widgets[2];
const currentLoras = value.map(l => l.name);
// Use the constant pattern here as well
let newText = inputWidget.value.replace(LORA_PATTERN, (match, name, strength) => {
return currentLoras.includes(name) ? match : '';
});
// Clean up multiple spaces, extra commas, and trim; remove trailing comma if it's the only content
newText = newText.replace(/\s+/g, " ").replace(/,\s*,+/g, ",").trim();
if (newText === ",") newText = "";
inputWidget.value = newText;
// Update this node's direct trigger toggles with its own active loras
const activeLoraNames = new Set();
value.forEach(lora => {
if (lora.active) {
activeLoraNames.add(lora.name);
}
});
updateConnectedTriggerWords(this, activeLoraNames);
} finally {
isUpdating = false;
}
});
this.lorasWidget = result.widget;
// Update input widget callback this.addInput("blocks", "SELECTEDBLOCKS", {
const inputWidget = this.widgets[2]; shape: 7, // 7 is the shape of the optional input
inputWidget.options.getMaxHeight = () => 100; });
this.inputWidget = inputWidget;
// Wrap the callback with autocomplete setup // Add flag to prevent callback loops
const originalCallback = (value) => { let isUpdating = false;
if (isUpdating) return;
isUpdating = true; const result = addLorasWidget(this, "loras", {}, (value) => {
// Prevent recursive calls
try { if (isUpdating) return;
const currentLoras = this.lorasWidget.value || []; isUpdating = true;
const mergedLoras = mergeLoras(value, currentLoras);
try {
this.lorasWidget.value = mergedLoras; // Remove loras that are not in the value array
const inputWidget = this.widgets[2];
// Update this node's direct trigger toggles with its own active loras const currentLoras = value.map((l) => l.name);
const activeLoraNames = getActiveLorasFromNode(this);
updateConnectedTriggerWords(this, activeLoraNames); // Use the constant pattern here as well
} finally { let newText = inputWidget.value.replace(
isUpdating = false; LORA_PATTERN,
} (match, name, strength) => {
}; return currentLoras.includes(name) ? match : "";
inputWidget.callback = setupInputWidgetWithAutocomplete(this, inputWidget, originalCallback); }
);
// Clean up multiple spaces, extra commas, and trim; remove trailing comma if it's the only content
newText = newText
.replace(/\s+/g, " ")
.replace(/,\s*,+/g, ",")
.trim();
if (newText === ",") newText = "";
inputWidget.value = newText;
// Update this node's direct trigger toggles with its own active loras
const activeLoraNames = new Set();
value.forEach((lora) => {
if (lora.active) {
activeLoraNames.add(lora.name);
}
}); });
updateConnectedTriggerWords(this, activeLoraNames);
} finally {
isUpdating = false;
}
});
this.lorasWidget = result.widget;
// Update input widget callback
const inputWidget = this.widgets[2];
inputWidget.options.getMaxHeight = () => 100;
this.inputWidget = inputWidget;
// Wrap the callback with autocomplete setup
const originalCallback = (value) => {
if (isUpdating) return;
isUpdating = true;
try {
const currentLoras = this.lorasWidget.value || [];
const mergedLoras = mergeLoras(value, currentLoras);
this.lorasWidget.value = mergedLoras;
// Update this node's direct trigger toggles with its own active loras
const activeLoraNames = getActiveLorasFromNode(this);
updateConnectedTriggerWords(this, activeLoraNames);
} finally {
isUpdating = false;
}
};
inputWidget.callback = setupInputWidgetWithAutocomplete(
this,
inputWidget,
originalCallback
);
});
}
},
async nodeCreated(node) {
if (node.comfyClass == "WanVideo Lora Select (LoraManager)") {
requestAnimationFrame(async () => {
// Restore saved value if exists
let existingLoras = [];
if (node.widgets_values && node.widgets_values.length > 0) {
// 0 for low_mem_load, 1 for merge_loras, 2 for text widget, 3 for loras widget
const savedValue = node.widgets_values[3];
existingLoras = savedValue || [];
} }
}, // Merge the loras data
const mergedLoras = mergeLoras(node.widgets[2].value, existingLoras);
node.lorasWidget.value = mergedLoras;
});
}
},
}); });