Files
ComfyUI-Lora-Manager/web/comfyui/trigger_word_highlight.js
Will Miao 388ff7f5b4 feat(ui): add trigger word highlighting for selected LoRAs
- Import applySelectionHighlight in lora_loader and lora_stacker
- Pass onSelectionChange callback to loras_widget to handle selection changes
- Implement selection tracking and payload building in loras_widget
- Emit selection changes when LoRA selection is modified
- Update tags_widget to support highlighted tag styling

This provides visual feedback when LoRAs are selected by highlighting associated trigger words in the interface.
2025-11-07 16:08:56 +08:00

167 lines
4.1 KiB
JavaScript

import { api } from "../../scripts/api.js";
import {
getConnectedTriggerToggleNodes,
getLinkFromGraph,
getNodeKey,
} from "./utils.js";
const TRIGGER_WORD_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
const triggerWordCache = new Map();
const LORA_NODE_CLASSES = new Set([
"Lora Loader (LoraManager)",
"Lora Stacker (LoraManager)",
"WanVideo Lora Select (LoraManager)",
]);
function normalizeTriggerWordList(triggerWords) {
if (!triggerWords) {
return [];
}
if (triggerWords instanceof Set) {
return Array.from(triggerWords)
.map((word) => (word == null ? "" : String(word)).trim())
.filter(Boolean);
}
if (!Array.isArray(triggerWords)) {
return [(triggerWords == null ? "" : String(triggerWords)).trim()].filter(
Boolean
);
}
return triggerWords
.map((word) => (word == null ? "" : String(word)).trim())
.filter(Boolean);
}
export async function fetchTriggerWordsForLora(loraName) {
if (!loraName) {
return [];
}
const cached = triggerWordCache.get(loraName);
if (cached && Date.now() - cached.timestamp < TRIGGER_WORD_CACHE_TTL) {
return cached.words;
}
const response = await api.fetchApi(
`/lm/loras/get-trigger-words?name=${encodeURIComponent(loraName)}`,
{ method: "GET" }
);
if (!response?.ok) {
const errorText = response ? await response.text().catch(() => "") : "";
throw new Error(errorText || `Failed to fetch trigger words for ${loraName}`);
}
const data = (await response.json().catch(() => ({}))) || {};
const triggerWords = Array.isArray(data.trigger_words)
? data.trigger_words.filter((word) => typeof word === "string")
: [];
const normalized = triggerWords
.map((word) => word.trim())
.filter((word) => word.length > 0);
triggerWordCache.set(loraName, {
words: normalized,
timestamp: Date.now(),
});
return normalized;
}
export function highlightTriggerWordsAlongChain(startNode, triggerWords) {
const normalizedWords = normalizeTriggerWordList(triggerWords);
highlightNodeRecursive(startNode, normalizedWords, new Set());
}
export async function applySelectionHighlight(node, selection) {
if (!node) {
return;
}
node.__lmSelectionHighlightToken =
(node.__lmSelectionHighlightToken || 0) + 1;
const requestId = node.__lmSelectionHighlightToken;
const loraName = selection?.name;
const isActive = !!selection?.active;
if (!loraName || !isActive) {
highlightTriggerWordsAlongChain(node, []);
return;
}
try {
const triggerWords = await fetchTriggerWordsForLora(loraName);
if (node.__lmSelectionHighlightToken !== requestId) {
return;
}
highlightTriggerWordsAlongChain(node, triggerWords);
} catch (error) {
console.error("Error fetching trigger words for highlight:", error);
if (node.__lmSelectionHighlightToken === requestId) {
highlightTriggerWordsAlongChain(node, []);
}
}
}
function highlightNodeRecursive(node, triggerWords, visited) {
if (!node) {
return;
}
const nodeKey = getNodeKey(node);
if (!nodeKey || visited.has(nodeKey)) {
return;
}
visited.add(nodeKey);
highlightTriggerWordsOnNode(node, triggerWords);
if (!node.outputs) {
return;
}
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) {
continue;
}
if (LORA_NODE_CLASSES.has(targetNode.comfyClass)) {
highlightNodeRecursive(targetNode, triggerWords, visited);
}
}
}
}
function highlightTriggerWordsOnNode(node, triggerWords) {
const connectedToggles = getConnectedTriggerToggleNodes(node);
if (!connectedToggles.length) {
return;
}
connectedToggles.forEach((toggleNode) => {
if (typeof toggleNode?.highlightTriggerWords === "function") {
toggleNode.highlightTriggerWords(triggerWords);
} else {
toggleNode.__pendingHighlightWords = Array.isArray(triggerWords)
? [...triggerWords]
: triggerWords;
}
});
}