import { app } from "../../scripts/app.js"; import { PROMPT_TAG_AUTOCOMPLETE_SETTING_ID, getPromptTagAutocompletePreference, setLoraManagerSettingValue, } from "./settings.js"; import { showToast } from "./utils.js"; /** * Extension for PromptLM node to support dynamic trigger_words inputs. * Based on the dynamic input pattern from Impact Pack's Switch (Any) node. */ app.registerExtension({ name: "Comfy.LoraManager.PromptLM", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name !== "Prompt (LoraManager)") { return; } const onConnectionsChange = nodeType.prototype.onConnectionsChange; nodeType.prototype.onConnectionsChange = function(type, index, connected, link_info) { const stackTrace = new Error().stack; // Skip during graph loading/pasting to avoid interference if (stackTrace.includes('loadGraphData') || stackTrace.includes('pasteFromClipboard')) { return onConnectionsChange?.apply?.(this, arguments); } // Skip subgraph operations if (stackTrace.includes('convertToSubgraph') || stackTrace.includes('Subgraph.configure')) { return onConnectionsChange?.apply?.(this, arguments); } if (!link_info) { return onConnectionsChange?.apply?.(this, arguments); } // Handle input connections (type === 1) if (type === 1) { const input = this.inputs[index]; // Only process trigger_words inputs if (!input || !input.name.startsWith('trigger_words')) { return onConnectionsChange?.apply?.(this, arguments); } // Count existing trigger_words inputs let triggerWordCount = 0; for (const inp of this.inputs) { if (inp.name.startsWith('trigger_words')) { triggerWordCount++; } } // Renumber all trigger_words inputs sequentially let slotIndex = 1; for (const inp of this.inputs) { if (inp.name.startsWith('trigger_words')) { inp.name = `trigger_words${slotIndex}`; slotIndex++; } } // Add new input slot if connected and this was the last one if (connected) { const lastTriggerIndex = triggerWordCount; if (index === lastTriggerIndex || index === this.inputs.findIndex(i => i.name === `trigger_words${lastTriggerIndex}`)) { this.addInput(`trigger_words${slotIndex}`, "STRING", { forceInput: true, tooltip: "Trigger words to prepend. Connect to add more inputs." }); } } // Remove disconnected empty input slots (but keep at least one) if (!connected && triggerWordCount > 1) { // Check if this input is now empty and can be removed const disconnectedInput = this.inputs[index]; if (disconnectedInput && disconnectedInput.name.startsWith('trigger_words')) { // Only remove if it has no link and is not the last trigger_words input const isLastTriggerSlot = index === this.inputs.findLastIndex(i => i.name.startsWith('trigger_words')); if (!isLastTriggerSlot && !disconnectedInput.link) { this.removeInput(index); // Renumber again after removal let newSlotIndex = 1; for (const inp of this.inputs) { if (inp.name.startsWith('trigger_words')) { inp.name = `trigger_words${newSlotIndex}`; newSlotIndex++; } } } } } } return onConnectionsChange?.apply?.(this, arguments); }; // Expose the tag autocomplete toggle in the node's right-click menu so // users can discover the switch where the behavior actually happens, // instead of only via slash commands or the global settings dialog. const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function(_, options) { getExtraMenuOptions?.apply?.(this, arguments); options.push(null); const autocompleteEnabled = getPromptTagAutocompletePreference(); options.push({ content: autocompleteEnabled ? "Tag Autocomplete: ON (/noautocomplete to disable)" : "Tag Autocomplete: OFF (/autocomplete to enable)", callback: async () => { const newValue = !autocompleteEnabled; try { const success = await setLoraManagerSettingValue(PROMPT_TAG_AUTOCOMPLETE_SETTING_ID, newValue); if (!success) { throw new Error("settings API unavailable"); } showToast({ severity: newValue ? 'success' : 'secondary', summary: newValue ? 'Autocomplete Enabled' : 'Autocomplete Disabled', detail: newValue ? 'Tag autocomplete is now ON. Type to see suggestions.' : 'Tag autocomplete is now OFF. Type /autocomplete in the prompt field to re-enable.', life: 3000 }); } catch (error) { console.error('[Lora Manager] Failed to toggle setting:', error); showToast({ severity: 'error', summary: 'Error', detail: 'Failed to toggle autocomplete setting', life: 3000 }); } } }); }; }, nodeCreated(node, app) { if (node.comfyClass !== "Prompt (LoraManager)") { return; } // Ensure at least one trigger_words input exists on creation const hasTriggerWords = node.inputs.some(inp => inp.name.startsWith('trigger_words')); if (!hasTriggerWords) { node.addInput("trigger_words1", "STRING", { forceInput: true, tooltip: "Trigger words to prepend. Connect to add more inputs." }); } } });