diff --git a/README.md b/README.md index 51a71bd4..f9c38008 100644 --- a/README.md +++ b/README.md @@ -296,3 +296,6 @@ Join our Discord community for support, discussions, and updates: [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) diff --git a/web/comfyui/lora_loader.js b/web/comfyui/lora_loader.js index 550d4a3f..aa62252f 100644 --- a/web/comfyui/lora_loader.js +++ b/web/comfyui/lora_loader.js @@ -1,209 +1,225 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; -import { - LORA_PATTERN, - collectActiveLorasFromChain, - updateConnectedTriggerWords, - chainCallback, - mergeLoras, - setupInputWidgetWithAutocomplete +import { + LORA_PATTERN, + collectActiveLorasFromChain, + updateConnectedTriggerWords, + chainCallback, + mergeLoras, + setupInputWidgetWithAutocomplete, } from "./utils.js"; import { addLorasWidget } from "./loras_widget.js"; app.registerExtension({ - name: "LoraManager.LoraLoader", - - setup() { - // Add message handler to listen for messages from Python - api.addEventListener("lora_code_update", (event) => { - const { id, lora_code, mode } = event.detail; - this.handleLoraCodeUpdate(id, lora_code, mode); + name: "LoraManager.LoraLoader", + + setup() { + // Add message handler to listen for messages from Python + api.addEventListener("lora_code_update", (event) => { + const { id, lora_code, mode } = event.detail; + 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); }); - }, - - // 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); + 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 + 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 - 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); - }, + } + ).widget; - // 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); - } - }, + // Update input widget callback + const inputWidget = this.widgets[0]; + inputWidget.options.getMaxHeight = () => 100; + this.inputWidget = inputWidget; - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeType.comfyClass == "Lora Loader (LoraManager)") { - chainCallback(nodeType.prototype, "onNodeCreated", function () { - // Enable widget serialization - this.serialize_widgets = true; + const originalCallback = (value) => { + if (isUpdating) return; + isUpdating = true; - this.addInput("clip", "CLIP", { - shape: 7, - }); + try { + const currentLoras = this.lorasWidget.value || []; + const mergedLoras = mergeLoras(value, currentLoras); - this.addInput("lora_stack", "LORA_STACK", { - shape: 7, // 7 is the shape of the optional input - }); + this.lorasWidget.value = mergedLoras; + } finally { + isUpdating = false; + } + }; - // 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 - ); + // Setup input widget with autocomplete + inputWidget.callback = setupInputWidgetWithAutocomplete( + this, + inputWidget, + originalCallback + ); - // Add flag to prevent callback loops - let isUpdating = false; - - // Get the widget object directly from the returned object - this.lorasWidget = addLorasWidget( - this, - "loras", - { - defaultVal: mergedLoras, // Pass object directly + // Register this node with the backend + this.registerNode = async () => { + try { + await fetch("/api/register-node", { + method: "POST", + headers: { + "Content-Type": "application/json", }, - (value) => { - // Collect all active loras from this node and its input chain - const allActiveLoraNames = collectActiveLorasFromChain(this); + 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); + } + }; - // Update trigger words for connected toggle nodes with the aggregated lora names - updateConnectedTriggerWords(this, allActiveLoraNames); + // Ensure the node is registered after creation + // Call registration + // setTimeout(() => { + // this.registerNode(); + // }, 0); + }); + } + }, - // 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; - } - } - ).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); - }); + 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 || []; } - }, -}); \ No newline at end of file + // Merge the loras data + const mergedLoras = mergeLoras(node.widgets[0].value, existingLoras); + node.lorasWidget.value = mergedLoras; + }); + } + }, +}); diff --git a/web/comfyui/lora_stacker.js b/web/comfyui/lora_stacker.js index f979332e..9b812926 100644 --- a/web/comfyui/lora_stacker.js +++ b/web/comfyui/lora_stacker.js @@ -1,164 +1,185 @@ import { app } from "../../scripts/app.js"; -import { - LORA_PATTERN, - getActiveLorasFromNode, - collectActiveLorasFromChain, - updateConnectedTriggerWords, - chainCallback, - mergeLoras, - setupInputWidgetWithAutocomplete +import { + LORA_PATTERN, + getActiveLorasFromNode, + collectActiveLorasFromChain, + updateConnectedTriggerWords, + chainCallback, + mergeLoras, + setupInputWidgetWithAutocomplete, } from "./utils.js"; import { addLorasWidget } from "./loras_widget.js"; app.registerExtension({ - 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; + name: "LoraManager.LoraStacker", - this.addInput("lora_stack", 'LORA_STACK', { - "shape": 7 // 7 is the shape of the optional input - }); + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeType.comfyClass === "Lora Stacker (LoraManager)") { + chainCallback(nodeType.prototype, "onNodeCreated", async function () { + // Enable widget serialization + this.serialize_widgets = true; - // 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 - 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; + this.addInput("lora_stack", "LORA_STACK", { + shape: 7, // 7 is the shape of the optional input + }); - // 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); + // Add flag to prevent callback loops + let isUpdating = false; - // 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); - } - }; + const result = addLorasWidget(this, "loras", {}, (value) => { + // Prevent recursive calls + if (isUpdating) return; + isUpdating = true; - // Call registration - // setTimeout(() => { - // this.registerNode(); - // }, 0); + 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 + 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 function updateDownstreamLoaders(startNode, visited = new Set()) { - if (visited.has(startNode.id)) return; - visited.add(startNode.id); - - // Check each output link - if (startNode.outputs) { - for (const output of startNode.outputs) { - if (output.links) { - for (const linkId of output.links) { - const link = app.graph.links[linkId]; - if (link) { - const targetNode = app.graph.getNodeById(link.target_id); - - // If target is a Lora Loader, collect all active loras in the chain and update - if (targetNode && targetNode.comfyClass === "Lora Loader (LoraManager)") { - const allActiveLoraNames = collectActiveLorasFromChain(targetNode); - updateConnectedTriggerWords(targetNode, allActiveLoraNames); - } - // If target is another Lora Stacker, recursively check its outputs - else if (targetNode && targetNode.comfyClass === "Lora Stacker (LoraManager)") { - updateDownstreamLoaders(targetNode, visited); - } - } - } + if (visited.has(startNode.id)) return; + visited.add(startNode.id); + + // Check each output link + if (startNode.outputs) { + for (const output of startNode.outputs) { + if (output.links) { + for (const linkId of output.links) { + const link = app.graph.links[linkId]; + if (link) { + const targetNode = app.graph.getNodeById(link.target_id); + + // If target is a Lora Loader, collect all active loras in the chain and update + if ( + targetNode && + targetNode.comfyClass === "Lora Loader (LoraManager)" + ) { + const allActiveLoraNames = + collectActiveLorasFromChain(targetNode); + updateConnectedTriggerWords(targetNode, allActiveLoraNames); } + // If target is another Lora Stacker, recursively check its outputs + else if ( + targetNode && + targetNode.comfyClass === "Lora Stacker (LoraManager)" + ) { + updateDownstreamLoaders(targetNode, visited); + } + } } + } } -} \ No newline at end of file + } +} diff --git a/web/comfyui/loras_widget.js b/web/comfyui/loras_widget.js index 41ca9ea7..fb6efaeb 100644 --- a/web/comfyui/loras_widget.js +++ b/web/comfyui/loras_widget.js @@ -675,25 +675,9 @@ export function addLorasWidget(node, name, opts, callback) { // Add the current 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 - 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) - }; - } - + const updatedValue = uniqueValue.map(lora => { // For new loras, default clip strength to model strength and expanded to false // unless clipStrength is already different from strength const clipStrength = lora.clipStrength || lora.strength; diff --git a/web/comfyui/utils.js b/web/comfyui/utils.js index dd4e8132..4d1341c8 100644 --- a/web/comfyui/utils.js +++ b/web/comfyui/utils.js @@ -193,6 +193,7 @@ export function mergeLoras(lorasText, lorasArr) { name: lora.name, strength: lora.strength !== undefined ? lora.strength : parsedLoras[lora.name].strength, active: lora.active !== undefined ? lora.active : true, + expanded: lora.expanded !== undefined ? lora.expanded : false, clipStrength: lora.clipStrength !== undefined ? lora.clipStrength : parsedLoras[lora.name].clipStrength, }); usedNames.add(lora.name); diff --git a/web/comfyui/wanvideo_lora_select.js b/web/comfyui/wanvideo_lora_select.js index 5c5237b3..52532860 100644 --- a/web/comfyui/wanvideo_lora_select.js +++ b/web/comfyui/wanvideo_lora_select.js @@ -1,107 +1,121 @@ import { app } from "../../scripts/app.js"; -import { - LORA_PATTERN, - getActiveLorasFromNode, - updateConnectedTriggerWords, - chainCallback, - mergeLoras, - setupInputWidgetWithAutocomplete +import { + LORA_PATTERN, + getActiveLorasFromNode, + updateConnectedTriggerWords, + chainCallback, + mergeLoras, + setupInputWidgetWithAutocomplete, } from "./utils.js"; import { addLorasWidget } from "./loras_widget.js"; app.registerExtension({ - 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; + name: "LoraManager.WanVideoLoraSelect", - // Add optional inputs - this.addInput("prev_lora", 'WANVIDLORA', { - "shape": 7 // 7 is the shape of the optional input - }); - - this.addInput("blocks", 'SELECTEDBLOCKS', { - "shape": 7 // 7 is the shape of the optional input - }); + 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; - // 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 - 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; + // Add optional inputs + this.addInput("prev_lora", "WANVIDLORA", { + shape: 7, // 7 is the shape of the optional input + }); - // 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); + this.addInput("blocks", "SELECTEDBLOCKS", { + shape: 7, // 7 is the shape of the optional input + }); + + // Add flag to prevent callback loops + let isUpdating = false; + + const result = addLorasWidget(this, "loras", {}, (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 + 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; + }); + } + }, });