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

@@ -6,7 +6,7 @@ import {
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";
@@ -36,12 +36,16 @@ app.registerExtension({
// Update each Lora Loader node found // Update each Lora Loader node found
if (loraLoaderNodes.length > 0) { if (loraLoaderNodes.length > 0) {
loraLoaderNodes.forEach(node => { loraLoaderNodes.forEach((node) => {
this.updateNodeLoraCode(node, loraCode, mode); this.updateNodeLoraCode(node, loraCode, mode);
}); });
console.log(`Updated ${loraLoaderNodes.length} Lora Loader nodes in broadcast mode`); console.log(
`Updated ${loraLoaderNodes.length} Lora Loader nodes in broadcast mode`
);
} else { } else {
console.warn("No Lora Loader nodes found in the workflow for broadcast update"); console.warn(
"No Lora Loader nodes found in the workflow for broadcast update"
);
} }
return; return;
@@ -49,9 +53,12 @@ app.registerExtension({
// Standard mode - update a specific node // Standard mode - update a specific node
const node = app.graph.getNodeById(+id); const node = app.graph.getNodeById(+id);
if (!node || (node.comfyClass !== "Lora Loader (LoraManager)" && if (
!node ||
(node.comfyClass !== "Lora Loader (LoraManager)" &&
node.comfyClass !== "Lora Stacker (LoraManager)" && node.comfyClass !== "Lora Stacker (LoraManager)" &&
node.comfyClass !== "WanVideo Lora Select (LoraManager)")) { node.comfyClass !== "WanVideo Lora Select (LoraManager)")
) {
console.warn("Node not found or not a LoraLoader:", id); console.warn("Node not found or not a LoraLoader:", id);
return; return;
} }
@@ -66,10 +73,10 @@ app.registerExtension({
if (!inputWidget) return; if (!inputWidget) return;
// Get the current lora code // Get the current lora code
const currentValue = inputWidget.value || ''; const currentValue = inputWidget.value || "";
// Update based on mode (replace or append) // Update based on mode (replace or append)
if (mode === 'replace') { if (mode === "replace") {
inputWidget.value = loraCode; inputWidget.value = loraCode;
} else { } else {
// Append mode - add a space if the current value isn't empty // Append mode - add a space if the current value isn't empty
@@ -79,7 +86,7 @@ app.registerExtension({
} }
// Trigger the callback to update the loras widget // Trigger the callback to update the loras widget
if (typeof inputWidget.callback === 'function') { if (typeof inputWidget.callback === "function") {
inputWidget.callback(inputWidget.value); inputWidget.callback(inputWidget.value);
} }
}, },
@@ -98,19 +105,6 @@ app.registerExtension({
shape: 7, // 7 is the shape of the optional input shape: 7, // 7 is the shape of the optional input
}); });
// Restore saved value if exists
let existingLoras = [];
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 // Add flag to prevent callback loops
let isUpdating = false; let isUpdating = false;
@@ -118,9 +112,7 @@ app.registerExtension({
this.lorasWidget = addLorasWidget( this.lorasWidget = addLorasWidget(
this, this,
"loras", "loras",
{ {},
defaultVal: mergedLoras, // Pass object directly
},
(value) => { (value) => {
// Collect all active loras from this node and its input chain // Collect all active loras from this node and its input chain
const allActiveLoraNames = collectActiveLorasFromChain(this); const allActiveLoraNames = collectActiveLorasFromChain(this);
@@ -146,7 +138,10 @@ app.registerExtension({
); );
// Clean up multiple spaces, extra commas, and trim; remove trailing comma if it's the only content // 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(); newText = newText
.replace(/\s+/g, " ")
.replace(/,\s*,+/g, ",")
.trim();
if (newText === ",") newText = ""; if (newText === ",") newText = "";
inputWidget.value = newText; inputWidget.value = newText;
@@ -176,25 +171,29 @@ app.registerExtension({
}; };
// Setup input widget with autocomplete // Setup input widget with autocomplete
inputWidget.callback = setupInputWidgetWithAutocomplete(this, inputWidget, originalCallback); inputWidget.callback = setupInputWidgetWithAutocomplete(
this,
inputWidget,
originalCallback
);
// Register this node with the backend // Register this node with the backend
this.registerNode = async () => { this.registerNode = async () => {
try { try {
await fetch('/api/register-node', { await fetch("/api/register-node", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
node_id: this.id, node_id: this.id,
bgcolor: this.bgcolor, bgcolor: this.bgcolor,
title: this.title, title: this.title,
graph_id: this.graph.id graph_id: this.graph.id,
}) }),
}); });
} catch (error) { } catch (error) {
console.warn('Failed to register node:', error); console.warn("Failed to register node:", error);
} }
}; };
@@ -206,4 +205,21 @@ app.registerExtension({
}); });
} }
}, },
async nodeCreated(node) {
if (node.comfyClass == "Lora Loader (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;
});
}
},
}); });

View File

@@ -6,7 +6,7 @@ import {
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";
@@ -19,26 +19,14 @@ app.registerExtension({
// Enable widget serialization // Enable widget serialization
this.serialize_widgets = true; this.serialize_widgets = true;
this.addInput("lora_stack", 'LORA_STACK', { this.addInput("lora_stack", "LORA_STACK", {
"shape": 7 // 7 is the shape of the optional input shape: 7, // 7 is the shape of the optional input
}); });
// Restore saved value if exists
let existingLoras = [];
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 // Add flag to prevent callback loops
let isUpdating = false; let isUpdating = false;
const result = addLorasWidget(this, "loras", { const result = addLorasWidget(this, "loras", {}, (value) => {
defaultVal: mergedLoras // Pass object directly
}, (value) => {
// Prevent recursive calls // Prevent recursive calls
if (isUpdating) return; if (isUpdating) return;
isUpdating = true; isUpdating = true;
@@ -46,22 +34,28 @@ app.registerExtension({
try { try {
// Remove loras that are not in the value array // Remove loras that are not in the value array
const inputWidget = this.widgets[0]; const inputWidget = this.widgets[0];
const currentLoras = value.map(l => l.name); const currentLoras = value.map((l) => l.name);
// Use the constant pattern here as well // Use the constant pattern here as well
let newText = inputWidget.value.replace(LORA_PATTERN, (match, name, strength) => { let newText = inputWidget.value.replace(
return currentLoras.includes(name) ? match : ''; 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 // 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(); newText = newText
.replace(/\s+/g, " ")
.replace(/,\s*,+/g, ",")
.trim();
if (newText === ",") newText = ""; if (newText === ",") newText = "";
inputWidget.value = newText; inputWidget.value = newText;
// Update this stacker's direct trigger toggles with its own active loras // Update this stacker's direct trigger toggles with its own active loras
const activeLoraNames = new Set(); const activeLoraNames = new Set();
value.forEach(lora => { value.forEach((lora) => {
if (lora.active) { if (lora.active) {
activeLoraNames.add(lora.name); activeLoraNames.add(lora.name);
} }
@@ -102,25 +96,29 @@ app.registerExtension({
isUpdating = false; isUpdating = false;
} }
}; };
inputWidget.callback = setupInputWidgetWithAutocomplete(this, inputWidget, originalCallback); inputWidget.callback = setupInputWidgetWithAutocomplete(
this,
inputWidget,
originalCallback
);
// Register this node with the backend // Register this node with the backend
this.registerNode = async () => { this.registerNode = async () => {
try { try {
await fetch('/api/register-node', { await fetch("/api/register-node", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
node_id: this.id, node_id: this.id,
bgcolor: this.bgcolor, bgcolor: this.bgcolor,
title: this.title, title: this.title,
graph_id: this.graph.id graph_id: this.graph.id,
}) }),
}); });
} catch (error) { } catch (error) {
console.warn('Failed to register node:', error); console.warn("Failed to register node:", error);
} }
}; };
@@ -131,6 +129,22 @@ app.registerExtension({
}); });
} }
}, },
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
@@ -148,12 +162,19 @@ function updateDownstreamLoaders(startNode, visited = new Set()) {
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 &&
targetNode.comfyClass === "Lora Loader (LoraManager)"
) {
const allActiveLoraNames =
collectActiveLorasFromChain(targetNode);
updateConnectedTriggerWords(targetNode, allActiveLoraNames); updateConnectedTriggerWords(targetNode, allActiveLoraNames);
} }
// If target is another Lora Stacker, recursively check its outputs // If target is another Lora Stacker, recursively check its outputs
else if (targetNode && targetNode.comfyClass === "Lora Stacker (LoraManager)") { else if (
targetNode &&
targetNode.comfyClass === "Lora Stacker (LoraManager)"
) {
updateDownstreamLoaders(targetNode, visited); updateDownstreamLoaders(targetNode, visited);
} }
} }

View File

@@ -676,24 +676,8 @@ export function addLorasWidget(node, name, opts, callback) {
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

@@ -5,7 +5,7 @@ import {
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";
@@ -19,30 +19,18 @@ app.registerExtension({
this.serialize_widgets = true; this.serialize_widgets = true;
// Add optional inputs // Add optional inputs
this.addInput("prev_lora", 'WANVIDLORA', { this.addInput("prev_lora", "WANVIDLORA", {
"shape": 7 // 7 is the shape of the optional input shape: 7, // 7 is the shape of the optional input
}); });
this.addInput("blocks", 'SELECTEDBLOCKS', { this.addInput("blocks", "SELECTEDBLOCKS", {
"shape": 7 // 7 is the shape of the optional input shape: 7, // 7 is the shape of the optional input
}); });
// Restore saved value if exists
let existingLoras = [];
if (this.widgets_values && this.widgets_values.length > 0) {
// 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 // Add flag to prevent callback loops
let isUpdating = false; let isUpdating = false;
const result = addLorasWidget(this, "loras", { const result = addLorasWidget(this, "loras", {}, (value) => {
defaultVal: mergedLoras // Pass object directly
}, (value) => {
// Prevent recursive calls // Prevent recursive calls
if (isUpdating) return; if (isUpdating) return;
isUpdating = true; isUpdating = true;
@@ -50,22 +38,28 @@ app.registerExtension({
try { try {
// Remove loras that are not in the value array // Remove loras that are not in the value array
const inputWidget = this.widgets[2]; const inputWidget = this.widgets[2];
const currentLoras = value.map(l => l.name); const currentLoras = value.map((l) => l.name);
// Use the constant pattern here as well // Use the constant pattern here as well
let newText = inputWidget.value.replace(LORA_PATTERN, (match, name, strength) => { let newText = inputWidget.value.replace(
return currentLoras.includes(name) ? match : ''; 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 // 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(); newText = newText
.replace(/\s+/g, " ")
.replace(/,\s*,+/g, ",")
.trim();
if (newText === ",") newText = ""; if (newText === ",") newText = "";
inputWidget.value = newText; inputWidget.value = newText;
// Update this node's direct trigger toggles with its own active loras // Update this node's direct trigger toggles with its own active loras
const activeLoraNames = new Set(); const activeLoraNames = new Set();
value.forEach(lora => { value.forEach((lora) => {
if (lora.active) { if (lora.active) {
activeLoraNames.add(lora.name); activeLoraNames.add(lora.name);
} }
@@ -100,7 +94,27 @@ app.registerExtension({
isUpdating = false; isUpdating = false;
} }
}; };
inputWidget.callback = setupInputWidgetWithAutocomplete(this, inputWidget, originalCallback); 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;
}); });
} }
}, },