mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-06 22:10:14 -03:00
Add a pure frontend node that shows filename and editable notes for a selected LoRA. Connect any output from a LoRA Loader/Stacker/Randomizer/ WanVideoSelect to the lora_source input — selecting a LoRA in the source widget updates the info display automatically. - Python node (LoraInfoLM): display-only, no workflow execution - Vue widget: filename label, auto-sizing notes textarea, save button with ComfyUI toast feedback on save - Frontend extension: wire-based selection propagation with stale-response race guard; clears display on wire disconnect - Backend: get-notes endpoint now returns file_path alongside notes; matching supports full-path lora syntax; fix NoneType crash in trigger words endpoint; document cache file_name invariant - Wired into all four lora widget nodes (Loader, Stacker, Randomizer, WanVideoSelect)
183 lines
5.3 KiB
JavaScript
183 lines
5.3 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
import {
|
|
getLinkFromGraph,
|
|
chainCallback,
|
|
} from "./utils.js";
|
|
|
|
const LORA_INFO_CLASS = "Lora Info (LoraManager)";
|
|
|
|
/**
|
|
* Find Lora Info nodes directly connected to the given node's outputs.
|
|
* Mirrors the getConnectedTriggerToggleNodes pattern from utils.js.
|
|
* @param {object} node - The source node to check outputs from
|
|
* @returns {object[]} Array of connected Lora Info node instances
|
|
*/
|
|
export function getConnectedLoraInfoNodes(node) {
|
|
const connectedNodes = [];
|
|
|
|
if (!node?.outputs) {
|
|
return connectedNodes;
|
|
}
|
|
|
|
for (const output of node.outputs) {
|
|
if (!output?.links?.length) {
|
|
continue;
|
|
}
|
|
|
|
for (const linkId of output.links) {
|
|
const link = getLinkFromGraph(node.graph, linkId);
|
|
if (!link) {
|
|
continue;
|
|
}
|
|
|
|
const targetNode = node.graph?.getNodeById?.(link.target_id);
|
|
if (targetNode && targetNode.comfyClass === LORA_INFO_CLASS) {
|
|
connectedNodes.push(targetNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
return connectedNodes;
|
|
}
|
|
|
|
/**
|
|
* Fetch notes for the selected lora and push them to all directly connected
|
|
* Lora Info nodes (no recursive chain traversal — only direct connections).
|
|
* @param {object} node - The source LoRA Loader/Stacker node
|
|
* @param {object|null} selection - The current lora selection {name, active, entry}
|
|
*/
|
|
export async function updateConnectedLoraInfoNodes(node, selection) {
|
|
if (!node) {
|
|
return;
|
|
}
|
|
|
|
const infoNodes = getConnectedLoraInfoNodes(node);
|
|
|
|
if (infoNodes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// No selection or inactive — clear the display on all connected info nodes
|
|
if (!selection?.name || !selection?.active) {
|
|
for (const infoNode of infoNodes) {
|
|
infoNode.__loraInfoReqId = (infoNode.__loraInfoReqId || 0) + 1;
|
|
if (typeof infoNode._setLoraInfo === "function") {
|
|
infoNode._setLoraInfo(null);
|
|
} else {
|
|
infoNode.__pendingLoraInfo = null;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Bump request token on each info node to guard against stale async responses
|
|
for (const infoNode of infoNodes) {
|
|
infoNode.__loraInfoReqId = (infoNode.__loraInfoReqId || 0) + 1;
|
|
}
|
|
const reqIdSnapshot = new Map();
|
|
for (const infoNode of infoNodes) {
|
|
reqIdSnapshot.set(infoNode, infoNode.__loraInfoReqId);
|
|
}
|
|
|
|
// Fetch notes for the selected lora
|
|
try {
|
|
const response = await api.fetchApi(
|
|
`/lm/loras/get-notes?name=${encodeURIComponent(selection.name)}`,
|
|
{ method: "GET" }
|
|
);
|
|
|
|
if (!response?.ok) {
|
|
throw new Error(`Failed to fetch notes for ${selection.name}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const infoData = {
|
|
name: selection.name,
|
|
notes: data?.notes || "",
|
|
filePath: data?.file_path || "",
|
|
};
|
|
|
|
for (const infoNode of infoNodes) {
|
|
// Discard if a newer request has been issued for this node
|
|
if (infoNode.__loraInfoReqId !== reqIdSnapshot.get(infoNode)) {
|
|
continue;
|
|
}
|
|
if (typeof infoNode._setLoraInfo === "function") {
|
|
infoNode._setLoraInfo(infoData);
|
|
} else {
|
|
infoNode.__pendingLoraInfo = infoData;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching notes for lora info:", error);
|
|
|
|
const errorData = {
|
|
name: selection.name,
|
|
notes: "[Error loading notes]",
|
|
filePath: "",
|
|
};
|
|
|
|
for (const infoNode of infoNodes) {
|
|
if (infoNode.__loraInfoReqId !== reqIdSnapshot.get(infoNode)) {
|
|
continue;
|
|
}
|
|
if (typeof infoNode._setLoraInfo === "function") {
|
|
infoNode._setLoraInfo(errorData);
|
|
} else {
|
|
infoNode.__pendingLoraInfo = errorData;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "LoraManager.LoraInfo",
|
|
|
|
beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name !== LORA_INFO_CLASS) {
|
|
return;
|
|
}
|
|
|
|
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
|
// Add wire-only input for receiving connections from LoRA nodes
|
|
this.addInput("lora_source", "*", { shape: 7 });
|
|
|
|
// Forward lora info data to the Vue widget when available.
|
|
this._setLoraInfo = function (data) {
|
|
const widget = this.widgets?.find(
|
|
(w) => w.type === "LORA_INFO_DISPLAY"
|
|
);
|
|
if (widget) {
|
|
if (typeof widget._setLoraInfo === "function") {
|
|
widget._setLoraInfo(data);
|
|
} else {
|
|
widget.__pendingLoraInfo = data;
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
// When the lora_source wire is disconnected, clear the display.
|
|
const origOnConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
|
|
if (origOnConnectionsChange) {
|
|
origOnConnectionsChange.apply(this, arguments);
|
|
}
|
|
// type 1 = input connection change; disconnected = !connected
|
|
if (type === 1 && !connected) {
|
|
const input = this.inputs?.[index];
|
|
if (input?.name === "lora_source") {
|
|
// Check if any lora_source input still has a connection
|
|
const hasLoraSourceConnection = this.inputs?.some(
|
|
(inp) => inp.name === "lora_source" && inp.link != null
|
|
);
|
|
if (!hasLoraSourceConnection) {
|
|
this._setLoraInfo?.(null);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
},
|
|
});
|