import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; const NODE_NAME = "FunPackApplyLoraWeights"; const LORA_TYPES = ["general", "action", "style", "quality", "character"]; const ADD_BUTTON_NAME = "+ Add LoRA"; const HEADER_WIDGET_NAME = "funpack_lora_header"; const LORA_LIST_WIDGET_NAME = "lora_list"; const BASE_WIDGET_NAMES = new Set(["positive_prompt", "refinement_key", "mode", "per_block"]); let cachedLoraValues = null; let latestNodeData = null; const trackedNodes = new Set(); let pendingRefresh = null; let activeLoraPicker = null; function fitString(ctx, text, maxWidth) { text = String(text ?? ""); if (ctx.measureText(text).width <= maxWidth) { return text; } const ellipsis = "..."; let low = 0; let high = text.length; while (low < high) { const mid = Math.ceil((low + high) / 2); if (ctx.measureText(text.slice(0, mid) + ellipsis).width <= maxWidth) { low = mid; } else { high = mid - 1; } } return text.slice(0, low) + ellipsis; } function roundRect(ctx, x, y, width, height, radius = height / 2) { ctx.beginPath(); ctx.roundRect(x, y, width, height, [radius]); } function drawRowBackground(ctx, x, y, width, height, active = true) { ctx.save(); roundRect(ctx, x, y, width, height, height / 2); ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR; ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR; if (!active) { ctx.globalAlpha = app.canvas.editor_alpha * 0.65; } ctx.fill(); ctx.stroke(); ctx.restore(); } function drawToggle(ctx, x, y, height, value) { const width = height * 1.5; const radius = height * 0.34; ctx.save(); ctx.globalAlpha = app.canvas.editor_alpha * 0.25; roundRect(ctx, x + 4, y + 4, width - 8, height - 8, height / 2); ctx.fillStyle = "rgba(255,255,255,0.45)"; ctx.fill(); ctx.globalAlpha = app.canvas.editor_alpha; ctx.fillStyle = value === true ? "#9ab" : "#888"; const knobX = value === true ? x + height : value === false ? x + height * 0.5 : x + height * 0.75; ctx.beginPath(); ctx.arc(knobX, y + height * 0.5, radius, 0, Math.PI * 2); ctx.fill(); ctx.restore(); return [x, width]; } function drawNumberControl(ctx, xRight, y, height, value) { const arrowWidth = 9; const inner = 3; const numberWidth = 36; const total = arrowWidth + inner + numberWidth + inner + arrowWidth; const x = xRight - total; const midY = y + height / 2; const arrowHeight = 10; ctx.save(); ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; ctx.fill(new Path2D(`M ${x} ${midY} l ${arrowWidth} ${arrowHeight / 2} l 0 -${arrowHeight} L ${x} ${midY} z`)); ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(Number(value ?? 1).toFixed(2), x + arrowWidth + inner + numberWidth / 2, midY); const rx = x + arrowWidth + inner + numberWidth + inner; ctx.fill(new Path2D(`M ${rx} ${midY - arrowHeight / 2} l ${arrowWidth} ${arrowHeight / 2} l -${arrowWidth} ${arrowHeight / 2} v -${arrowHeight} z`)); ctx.restore(); return { dec: [x, arrowWidth], value: [x + arrowWidth + inner, numberWidth], inc: [rx, arrowWidth], any: [x, total], x, total, }; } function inside(pos, bounds) { const x = bounds[0]; const width = bounds.length > 2 ? bounds[2] : bounds[1]; const hitX = pos[0] >= x && pos[0] <= x + width; if (bounds.length <= 2) { return hitX; } return hitX && pos[1] >= bounds[1] && pos[1] <= bounds[1] + bounds[3]; } function getLoraValues(nodeData) { const inputType = nodeData?.input?.optional?.lora_0?.[0]; const embeddedValues = Array.isArray(inputType) ? inputType : nodeData?.input?.optional?.lora_0?.[1]?.available_loras; return cachedLoraValues || (Array.isArray(embeddedValues) ? embeddedValues : ["None"]); } function rememberLoraValues(nodeData, values) { cachedLoraValues = values; const loraInput = nodeData?.input?.optional?.lora_0; if (Array.isArray(loraInput)) { loraInput[0] = values; } } async function fetchLoraValues(nodeData) { try { const response = await api.fetchApi(`/funpack/loras?cache_bust=${Date.now()}`, { cache: "no-store", }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const values = await response.json(); if (Array.isArray(values) && values.length) { rememberLoraValues(nodeData, values); return values; } } catch (error) { console.warn("FunPack: failed to refresh LoRA list", error); } rememberLoraValues(nodeData, getLoraValues(nodeData)); return cachedLoraValues; } function valuesWithCurrent(values, current) { if (!current || values.includes(current)) { return values; } return [...values, current]; } function normalizeSearchText(value) { return String(value ?? "").toLowerCase().replace(/[_\-./\\]+/g, " "); } function closeLoraPicker() { if (!activeLoraPicker) { return; } activeLoraPicker.remove(); activeLoraPicker = null; } function openSearchableLoraPicker(event, values, currentValue, onSelect) { closeLoraPicker(); const sourceValues = Array.from(new Set(valuesWithCurrent(values || ["None"], currentValue))); const root = document.createElement("div"); root.className = "funpack-lora-picker"; Object.assign(root.style, { position: "fixed", zIndex: 10000, minWidth: "320px", maxWidth: "520px", maxHeight: "420px", padding: "8px", border: "1px solid rgba(180, 190, 200, 0.35)", borderRadius: "8px", background: "rgba(30, 32, 36, 0.98)", boxShadow: "0 16px 40px rgba(0, 0, 0, 0.35)", color: "#ddd", font: "12px sans-serif", }); const input = document.createElement("input"); input.type = "search"; input.placeholder = "Search LoRA"; input.value = currentValue && currentValue !== "None" ? currentValue : ""; Object.assign(input.style, { boxSizing: "border-box", width: "100%", margin: "0 0 6px 0", padding: "7px 8px", border: "1px solid rgba(180, 190, 200, 0.35)", borderRadius: "5px", background: "#15171a", color: "#fff", outline: "none", }); const list = document.createElement("div"); Object.assign(list.style, { maxHeight: "350px", overflowY: "auto", }); const render = () => { const needle = normalizeSearchText(input.value); const parts = needle.split(/\s+/).filter(Boolean); const filtered = sourceValues .filter((value) => { if (!parts.length) { return true; } const haystack = normalizeSearchText(value); return parts.every((part) => haystack.includes(part)); }) .slice(0, 160); list.replaceChildren(); if (!filtered.length) { const empty = document.createElement("div"); empty.textContent = "No matching LoRAs"; Object.assign(empty.style, { padding: "10px 8px", opacity: "0.7", }); list.append(empty); return; } for (const value of filtered) { const row = document.createElement("button"); row.type = "button"; row.textContent = value; row.title = value; Object.assign(row.style, { display: "block", width: "100%", padding: "6px 8px", border: "0", borderRadius: "4px", background: value === currentValue ? "rgba(120, 150, 170, 0.32)" : "transparent", color: "#ddd", textAlign: "left", cursor: "pointer", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }); row.addEventListener("mouseenter", () => { row.style.background = "rgba(120, 150, 170, 0.24)"; }); row.addEventListener("mouseleave", () => { row.style.background = value === currentValue ? "rgba(120, 150, 170, 0.32)" : "transparent"; }); row.addEventListener("click", () => { onSelect(value); closeLoraPicker(); }); list.append(row); } }; input.addEventListener("input", render); input.addEventListener("keydown", (keyEvent) => { if (keyEvent.key === "Escape") { closeLoraPicker(); keyEvent.stopPropagation(); } else if (keyEvent.key === "Enter") { const first = list.querySelector("button"); if (first) { onSelect(first.textContent); closeLoraPicker(); } keyEvent.preventDefault(); keyEvent.stopPropagation(); } }); root.append(input, list); document.body.append(root); activeLoraPicker = root; const rect = root.getBoundingClientRect(); const x = Math.min(window.innerWidth - rect.width - 12, Math.max(12, event.clientX ?? 12)); const y = Math.min(window.innerHeight - rect.height - 12, Math.max(12, event.clientY ?? 12)); root.style.left = `${x}px`; root.style.top = `${y}px`; const outsideClick = (clickEvent) => { if (!root.contains(clickEvent.target)) { closeLoraPicker(); document.removeEventListener("pointerdown", outsideClick, true); } }; window.setTimeout(() => { document.addEventListener("pointerdown", outsideClick, true); }, 0); render(); input.focus(); input.select(); } function normalizeLoraType(value) { const type = value || "general"; if (type === "concept") { return "action"; } return LORA_TYPES.includes(type) ? type : "general"; } function normalizeRowValue(value = {}) { if (typeof value !== "object" || value === null) { return { on: true, lora: "None", type: "general", strength: 1.0, }; } const strength = Number(value.strength ?? value.base_weight ?? value.base_model_weight ?? 1.0); return { on: value.on !== false, lora: value.lora || value.name || "None", type: normalizeLoraType(value.type || value.lora_type), strength: Number.isFinite(strength) ? strength : 1.0, }; } class FunPackBaseWidget { constructor(name) { this.name = name; this.type = "custom"; this.options = { serialize: true }; this.hitAreas = {}; this.downHitAreas = []; this.clickHitAreas = []; this.mouseDown = false; this.last_y = 0; } serializeValue() { return this.value; } mouse(event, pos, node) { if (event.type === "pointerdown") { this.mouseDown = true; this.downHitAreas = []; this.clickHitAreas = []; let handled = false; for (const hitArea of Object.values(this.hitAreas)) { if (!inside(pos, hitArea.bounds)) { continue; } if (hitArea.onMove) { this.downHitAreas.push(hitArea); handled = true; } if (hitArea.onClick) { this.clickHitAreas.push(hitArea); handled = true; } if (hitArea.onDown) { handled = hitArea.onDown.call(this, event, pos, node, hitArea) === true || handled; } } return handled; } if (event.type === "pointermove") { if (!this.mouseDown) { return false; } for (const hitArea of this.downHitAreas) { hitArea.onMove.call(this, event, pos, node, hitArea); } return true; } if (event.type === "pointerup") { if (!this.mouseDown) { return false; } this.mouseDown = false; let handled = false; for (const hitArea of this.clickHitAreas) { if (inside(pos, hitArea.bounds)) { handled = hitArea.onClick.call(this, event, pos, node, hitArea) === true || handled; } } this.downHitAreas = []; this.clickHitAreas = []; if (!handled) { handled = this.onMouseClick?.(event, pos, node) === true; } return handled; } return false; } onMouseClick(_event, _pos, _node) { return false; } } class FunPackLoraHeaderWidget extends FunPackBaseWidget { constructor() { super(HEADER_WIDGET_NAME); this.options.serialize = false; this.value = {}; this.hitAreas = { toggle: { bounds: [0, 0], onDown: this.onToggleAll }, }; } computeSize(width) { return [width, LiteGraph.NODE_WIDGET_HEIGHT]; } draw(ctx, node, width, y, height) { if (!getLoraRows(node).length) { return; } const margin = 10; const midY = y + height / 2; let x = margin; this.hitAreas.toggle.bounds = drawToggle(ctx, x, y, height, allRowsState(node)); x += this.hitAreas.toggle.bounds[1] + 4; ctx.save(); ctx.globalAlpha = app.canvas.editor_alpha * 0.55; ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; ctx.textAlign = "left"; ctx.textBaseline = "middle"; ctx.fillText("Toggle All", x, midY); ctx.textAlign = "center"; ctx.fillText("Type", width - 106, midY); ctx.fillText("Strength", width - 38, midY); ctx.restore(); } onToggleAll(event, pos, node) { toggleAllRows(node); node.setDirtyCanvas(true, true); return true; } } class FunPackLoraListWidget extends FunPackBaseWidget { constructor(node) { super(LORA_LIST_WIDGET_NAME); this.node = node; this.value = "[]"; } computeSize(width) { return [width, 0]; } draw() {} serializeValue() { return JSON.stringify(getLoraRows(this.node).map((row) => row.serializeValue())); } } class FunPackLoraRowWidget extends FunPackBaseWidget { constructor(name, value, nodeData) { super(name); this.options.serialize = false; this.value = normalizeRowValue(value); this.nodeData = nodeData; this.draggedWeight = false; this.hitAreas = { toggle: { bounds: [0, 0], onDown: this.onToggle }, lora: { bounds: [0, 0], onClick: this.onLoraClick }, type: { bounds: [0, 0], onClick: this.onTypeClick }, weightDec: { bounds: [0, 0], onClick: this.onWeightDec }, weightValue: { bounds: [0, 0], onClick: this.onWeightValue }, weightInc: { bounds: [0, 0], onClick: this.onWeightInc }, weightAny: { bounds: [0, 0], onMove: this.onWeightMove }, }; } computeSize(width) { return [width, LiteGraph.NODE_WIDGET_HEIGHT]; } serializeValue() { return { on: this.value.on !== false, lora: this.value.lora || "None", type: normalizeLoraType(this.value.type), strength: Number(this.value.strength ?? 1.0), }; } updateLoraValues(values) { if (!values.includes(this.value.lora)) { this.value.lora = this.value.lora || "None"; } } draw(ctx, node, width, y, height) { const margin = 10; const inner = 4; const rowX = margin; const rowWidth = width - margin * 2; const midY = y + height / 2; drawRowBackground(ctx, rowX, y, rowWidth, height, this.value.on); let x = rowX; this.hitAreas.toggle.bounds = drawToggle(ctx, x, y, height, this.value.on); x += this.hitAreas.toggle.bounds[1] + inner; ctx.save(); if (!this.value.on) { ctx.globalAlpha = app.canvas.editor_alpha * 0.42; } ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; ctx.textBaseline = "middle"; const number = drawNumberControl(ctx, width - margin - inner, y, height, this.value.strength); this.hitAreas.weightDec.bounds = number.dec; this.hitAreas.weightValue.bounds = number.value; this.hitAreas.weightInc.bounds = number.inc; this.hitAreas.weightAny.bounds = number.any; const typeWidth = 70; const typeX = number.x - inner - typeWidth; roundRect(ctx, typeX, y + 3, typeWidth, height - 6, 5); ctx.fillStyle = "rgba(255,255,255,0.08)"; ctx.fill(); ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; ctx.textAlign = "center"; ctx.fillText(fitString(ctx, this.value.type || "general", typeWidth - 12), typeX + typeWidth / 2, midY); ctx.fillText("▾", typeX + typeWidth - 8, midY); this.hitAreas.type.bounds = [typeX, typeWidth]; const loraWidth = Math.max(40, typeX - inner - x); ctx.textAlign = "left"; ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; ctx.fillText(fitString(ctx, this.value.lora || "None", loraWidth), x, midY); this.hitAreas.lora.bounds = [x, loraWidth]; ctx.restore(); } onToggle(event, pos, node) { this.value.on = !this.value.on; node.setDirtyCanvas(true, true); return true; } onLoraClick(event, pos, node) { void fetchLoraValues(this.nodeData).then((freshValues) => { openSearchableLoraPicker(event, freshValues, this.value.lora, (value) => { if (typeof value === "string") { this.value.lora = value; node.setDirtyCanvas(true, true); } }); }); return true; } onTypeClick(event, pos, node) { new LiteGraph.ContextMenu(LORA_TYPES, { event, title: "LoRA Type", callback: (value) => { if (typeof value === "string") { this.value.type = value; node.setDirtyCanvas(true, true); } }, }); return true; } stepWeight(direction, node) { const current = Number(this.value.strength ?? 1.0); this.value.strength = Math.round(((Number.isFinite(current) ? current : 1.0) + direction * 0.05) * 100) / 100; node.setDirtyCanvas(true, true); } onWeightDec(event, pos, node) { this.stepWeight(-1, node); return true; } onWeightInc(event, pos, node) { this.stepWeight(1, node); return true; } onWeightMove(event, pos, node) { if (event.deltaX) { this.draggedWeight = true; const current = Number(this.value.strength ?? 1.0); this.value.strength = Math.round(((Number.isFinite(current) ? current : 1.0) + event.deltaX * 0.02) * 100) / 100; node.setDirtyCanvas(true, true); } return true; } onWeightValue(event, pos, node) { if (this.draggedWeight) { this.draggedWeight = false; return true; } app.canvas.prompt("Base weight", this.value.strength, (value) => { const parsed = Number(value); if (Number.isFinite(parsed)) { this.value.strength = parsed; node.setDirtyCanvas(true, true); } }, event); return true; } } class FunPackButtonWidget extends FunPackBaseWidget { constructor(name, label, callback) { super(name); this.options.serialize = false; this.value = ""; this.label = label; this.callback = callback; this.hitAreas = { button: { bounds: [0, 0], onClick: this.onButtonClick }, }; } computeSize(width) { return [width, LiteGraph.NODE_WIDGET_HEIGHT + 2]; } draw(ctx, node, width, y, height) { const x = 20; const w = width - 40; this.hitAreas.button.bounds = [x, y + 1, w, height - 2]; ctx.save(); roundRect(ctx, x, y + 1, w, height - 2, 3); ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR; ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR; ctx.fill(); ctx.stroke(); ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(this.label, width / 2, y + height / 2); ctx.restore(); } onMouseClick(event, pos, node) { return this.callback(event, pos, node); } onButtonClick(event, pos, node) { return this.callback(event, pos, node); } } function isDefaultLoraWidget(widget) { return /^lora_\d+(?:_type|_base_weight)?$/.test(widget.name || ""); } function isManagedWidget(widget) { return ( isDefaultLoraWidget(widget) || widget.name === LORA_LIST_WIDGET_NAME || widget.name === HEADER_WIDGET_NAME || widget.name === ADD_BUTTON_NAME || widget.__funpackLoraRow === true ); } function getLoraRows(node) { return (node.widgets || []).filter((widget) => widget.__funpackLoraRow === true); } function allRowsState(node) { const rows = getLoraRows(node); if (!rows.length) { return false; } const allOn = rows.every((row) => row.value.on !== false); const allOff = rows.every((row) => row.value.on === false); if (allOn) { return true; } if (allOff) { return false; } return null; } function toggleAllRows(node) { const rows = getLoraRows(node); const target = allRowsState(node) !== true; for (const row of rows) { row.value.on = target; } } function nextLoraIndex(node) { let next = 0; for (const widget of node.widgets || []) { const match = /^lora_(\d+)$/.exec(widget.name || ""); if (match) { next = Math.max(next, Number(match[1]) + 1); } } return next; } function resizeNode(node) { const computed = node.computeSize(); node.size[0] = Math.max(node.size[0], computed[0]); node.size[1] = computed[1]; } function addLoraRow(node, nodeData, value = {}, markDirty = true) { const row = new FunPackLoraRowWidget(`lora_${nextLoraIndex(node)}`, value, nodeData); row.__funpackLoraRow = true; node.addCustomWidget(row); moveButtonToEnd(node); resizeNode(node); if (markDirty) { node.setDirtyCanvas(true, true); } return row; } function moveButtonToEnd(node) { const buttonIndex = (node.widgets || []).findIndex((widget) => widget.name === ADD_BUTTON_NAME); if (buttonIndex < 0 || buttonIndex === node.widgets.length - 1) { return; } const [button] = node.widgets.splice(buttonIndex, 1); node.widgets.push(button); } function removeManagedWidgets(node) { node.widgets = (node.widgets || []).filter((widget) => !isManagedWidget(widget)); } function collectVisibleDefaultRows(node) { const rows = new Map(); for (const widget of node.widgets || []) { const match = /^lora_(\d+)(?:_(type|base_weight))?$/.exec(widget.name || ""); if (!match) { continue; } const index = Number(match[1]); const field = match[2] || "lora"; const row = rows.get(index) || { on: true, lora: "None", type: "general", strength: 1.0 }; if (field === "lora") { row.lora = widget.value || "None"; } else if (field === "type") { row.type = normalizeLoraType(widget.value); } else { row.strength = Number(widget.value ?? 1.0); } rows.set(index, row); } return [...rows.keys()].sort((a, b) => a - b).map((index) => rows.get(index)); } function serializedBaseWidgetCount(node) { let count = 0; for (const widget of node.widgets || []) { if (widget.serialize === false || widget.options?.serialize === false || isDefaultLoraWidget(widget)) { continue; } if (BASE_WIDGET_NAMES.has(widget.name)) { count += 1; } } return count; } function parseSerializedRows(values, baseCount) { if (!Array.isArray(values)) { return []; } const rowValues = values.slice(baseCount); for (const value of rowValues) { if (typeof value !== "string") { continue; } try { const parsed = JSON.parse(value); if (Array.isArray(parsed)) { const rows = parsed.map(normalizeRowValue); if (rows.length) { return rows; } } } catch (_error) { // Older workflows stored rows as individual widget values. } } const objectRows = rowValues .filter((value) => value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, "lora")) .map(normalizeRowValue); if (objectRows.length) { return objectRows; } const rows = []; for (let offset = 0; offset + 2 < rowValues.length; offset += 3) { rows.push(normalizeRowValue({ lora: rowValues[offset], type: rowValues[offset + 1], strength: rowValues[offset + 2], })); } return rows; } function ensureCompactLoraUi(node, nodeData, info = null) { trackNode(node); const defaultRows = collectVisibleDefaultRows(node); const baseCount = serializedBaseWidgetCount(node); const serializedRows = parseSerializedRows(info?.widgets_values, baseCount); const rows = serializedRows.length ? serializedRows : defaultRows; removeManagedWidgets(node); node.addCustomWidget(new FunPackLoraListWidget(node)); node.addCustomWidget(new FunPackLoraHeaderWidget()); if (rows.length) { for (const row of rows) { addLoraRow(node, nodeData, row, false); } } else { addLoraRow(node, nodeData, {}, false); } node.addCustomWidget(new FunPackButtonWidget(ADD_BUTTON_NAME, "+ Add LoRA", (event, pos, currentNode) => { void fetchLoraValues(nodeData).then((values) => { openSearchableLoraPicker(event, values, "None", (value) => { if (typeof value === "string" && value !== "None") { addLoraRow(currentNode, nodeData, { lora: value }); } else { addLoraRow(currentNode, nodeData, {}); } }); }); return true; })); moveButtonToEnd(node); resizeNode(node); } function updateLoraWidgets(node, values) { for (const widget of getLoraRows(node)) { widget.updateLoraValues(values); } node.setDirtyCanvas(true, true); } async function refreshLoraWidgets(node, nodeData) { updateLoraWidgets(node, await fetchLoraValues(nodeData)); } function trackNode(node) { trackedNodes.add(node); } async function refreshTrackedLoraWidgets(nodeData = latestNodeData) { if (!nodeData || pendingRefresh) { return pendingRefresh; } pendingRefresh = (async () => { const values = await fetchLoraValues(nodeData); for (const node of [...trackedNodes]) { if (!node?.graph) { trackedNodes.delete(node); continue; } updateLoraWidgets(node, values); } })().finally(() => { pendingRefresh = null; }); return pendingRefresh; } function scheduleTrackedRefresh(nodeData = latestNodeData) { window.setTimeout(() => { void refreshTrackedLoraWidgets(nodeData); }, 100); } function wrapRefreshFunction(functionName) { const original = app[functionName]; if (typeof original !== "function" || original.__funpackLoraWrapped) { return; } const wrapped = function () { const result = original.apply(this, arguments); Promise.resolve(result).finally(() => scheduleTrackedRefresh()); return result; }; wrapped.__funpackLoraWrapped = true; app[functionName] = wrapped; } function hookComfyRefreshControls() { for (const functionName of ["refreshComboInNodes", "refreshComboInNode", "refreshNodeDefs"]) { wrapRefreshFunction(functionName); } } function isRefreshControl(element) { for (let current = element; current && current !== document.body; current = current.parentElement) { const text = [ current.title, current.ariaLabel, current.textContent, ].filter(Boolean).join(" ").toLowerCase(); if (text.includes("refresh")) { return true; } } return false; } app.registerExtension({ name: "funpack.loraWeights", setup() { hookComfyRefreshControls(); window.setTimeout(hookComfyRefreshControls, 0); window.setTimeout(hookComfyRefreshControls, 1000); document.addEventListener("click", (event) => { if (isRefreshControl(event.target)) { scheduleTrackedRefresh(); } }, true); }, beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== NODE_NAME) { return; } latestNodeData = nodeData; hookComfyRefreshControls(); const originalOnNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { originalOnNodeCreated?.apply(this, arguments); ensureCompactLoraUi(this, nodeData); void refreshLoraWidgets(this, nodeData); }; const originalConfigure = nodeType.prototype.configure; nodeType.prototype.configure = function (info) { originalConfigure?.apply(this, arguments); ensureCompactLoraUi(this, nodeData, info); void refreshLoraWidgets(this, nodeData); }; }, });