mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Add test coverage for trigger word refresh functionality when LoRA syntax is edited in the input widget. The test verifies that after modifying LoRA syntax in the input field, the connected trigger words are properly updated to reflect the active LoRAs. Additionally, implement the actual trigger word update logic in lora_loader.js by calling updateConnectedTriggerWords after merging LoRAs, ensuring the UI stays synchronized with the current LoRA state.
216 lines
6.4 KiB
JavaScript
216 lines
6.4 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
import {
|
|
collectActiveLorasFromChain,
|
|
updateConnectedTriggerWords,
|
|
chainCallback,
|
|
mergeLoras,
|
|
setupInputWidgetWithAutocomplete,
|
|
getAllGraphNodes,
|
|
getNodeFromGraph,
|
|
} from "./utils.js";
|
|
import { addLorasWidget } from "./loras_widget.js";
|
|
import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js";
|
|
|
|
app.registerExtension({
|
|
name: "LoraManager.LoraLoader",
|
|
|
|
setup() {
|
|
// Add message handler to listen for messages from Python
|
|
api.addEventListener("lora_code_update", (event) => {
|
|
this.handleLoraCodeUpdate(event.detail || {});
|
|
});
|
|
},
|
|
|
|
// Handle lora code updates from Python
|
|
handleLoraCodeUpdate(message) {
|
|
const nodeId = message?.node_id ?? message?.id;
|
|
const graphId = message?.graph_id;
|
|
const loraCode = message?.lora_code ?? "";
|
|
const mode = message?.mode ?? "append";
|
|
|
|
const numericNodeId =
|
|
typeof nodeId === "string" ? Number(nodeId) : nodeId;
|
|
|
|
// Handle broadcast mode (for Desktop/non-browser support)
|
|
if (numericNodeId === -1) {
|
|
// Find all Lora Loader nodes in the current graph
|
|
const loraLoaderNodes = getAllGraphNodes(app.graph)
|
|
.map(({ node }) => node)
|
|
.filter((node) => node?.comfyClass === "Lora Loader (LoraManager)");
|
|
|
|
// 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 = getNodeFromGraph(graphId, numericNodeId);
|
|
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:",
|
|
graphId ?? "root",
|
|
nodeId
|
|
);
|
|
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 flags to prevent callback loops
|
|
let isUpdating = false;
|
|
let isSyncingInput = false;
|
|
|
|
const inputWidget = this.widgets[0];
|
|
inputWidget.options.getMaxHeight = () => 100;
|
|
this.inputWidget = inputWidget;
|
|
|
|
const scheduleInputSync = debounce((lorasValue) => {
|
|
if (isSyncingInput) {
|
|
return;
|
|
}
|
|
|
|
isSyncingInput = true;
|
|
isUpdating = true;
|
|
|
|
try {
|
|
const nextText = applyLoraValuesToText(
|
|
inputWidget.value,
|
|
lorasValue
|
|
);
|
|
|
|
if (inputWidget.value !== nextText) {
|
|
inputWidget.value = nextText;
|
|
}
|
|
} finally {
|
|
isUpdating = false;
|
|
isSyncingInput = false;
|
|
}
|
|
});
|
|
|
|
// Get the widget object directly from the returned object
|
|
this.lorasWidget = addLorasWidget(
|
|
this,
|
|
"loras",
|
|
{},
|
|
(value) => {
|
|
// Prevent recursive calls
|
|
if (isUpdating) return;
|
|
isUpdating = true;
|
|
|
|
try {
|
|
// 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);
|
|
} finally {
|
|
isUpdating = false;
|
|
}
|
|
|
|
scheduleInputSync(value);
|
|
}
|
|
).widget;
|
|
|
|
const originalCallback = (value) => {
|
|
if (isUpdating) return;
|
|
isUpdating = true;
|
|
|
|
try {
|
|
const currentLoras = this.lorasWidget.value || [];
|
|
const mergedLoras = mergeLoras(value, currentLoras);
|
|
|
|
this.lorasWidget.value = mergedLoras;
|
|
|
|
const allActiveLoraNames = collectActiveLorasFromChain(this);
|
|
updateConnectedTriggerWords(this, allActiveLoraNames);
|
|
} finally {
|
|
isUpdating = false;
|
|
}
|
|
};
|
|
|
|
// Setup input widget with autocomplete
|
|
inputWidget.callback = setupInputWidgetWithAutocomplete(
|
|
this,
|
|
inputWidget,
|
|
originalCallback
|
|
);
|
|
});
|
|
}
|
|
},
|
|
|
|
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;
|
|
});
|
|
}
|
|
},
|
|
});
|