import { app } from "/scripts/app.js"; import { ComfyWidgets } from "/scripts/widgets.js"; const COLOR_THEMES = { QwenVL: { nodeColor: "#28403f", nodeBgColor: "#374539", width: 340 }, QwenVLGGUF: { nodeColor: "#474539", nodeBgColor: "#2c4045", width: 340 }, Tools: { nodeColor: "#28403f", nodeBgColor: "#233238", width: 300 }, Enhancer: { nodeColor: "#374445", nodeBgColor: "#474539", width: 340 }, }; const NODE_COLORS = { "AILab_QwenVL": "QwenVL", "AILab_QwenVL_Advanced": "QwenVL", "AILab_QwenVL_PromptEnhancer": "Enhancer", "AILab_QwenVL_GGUF": "QwenVLGGUF", "AILab_QwenVL_GGUF_Advanced": "QwenVLGGUF", "AILab_QwenVL_GGUF_PromptEnhancer": "Enhancer", "ThinkingLLM_QwenVL": "QwenVL", "ThinkingLLM_QwenVL_Advanced": "QwenVL", "ThinkingLLM_QwenVL_PromptEnhancer": "Enhancer", "ThinkingLLM_QwenVL_GGUF": "QwenVLGGUF", "ThinkingLLM_QwenVL_GGUF_Advanced": "QwenVLGGUF", "ThinkingLLM_Gemma4_Audio_GGUF": "QwenVLGGUF", "ThinkingLLM_Whisper_ASR": "QwenVLGGUF", "ThinkingLLM_QwenVL_GGUF_PromptEnhancer": "Enhancer", "AILab_QwenVL_PromptLibrary": "Tools", "VRAMCleanup": "Tools", "StorySplitNode": "Tools", }; const THINKINGLLM_NODE_NAMES = new Set([ "AILab_QwenVL", "AILab_QwenVL_Advanced", "AILab_QwenVL_PromptEnhancer", "AILab_QwenVL_GGUF", "AILab_QwenVL_GGUF_Advanced", "AILab_QwenVL_GGUF_PromptEnhancer", "ThinkingLLM_QwenVL", "ThinkingLLM_QwenVL_Advanced", "ThinkingLLM_QwenVL_PromptEnhancer", "ThinkingLLM_QwenVL_GGUF", "ThinkingLLM_QwenVL_GGUF_Advanced", "ThinkingLLM_Gemma4_Audio_GGUF", "ThinkingLLM_Whisper_ASR", "ThinkingLLM_QwenVL_GGUF_PromptEnhancer", ]); const AUDIO_CAPABLE_NODE_NAMES = new Set([ "ThinkingLLM_Gemma4_Audio_GGUF", "ThinkingLLM_Whisper_ASR", "AILab_QwenVL_GGUF", "AILab_QwenVL_GGUF_Advanced", "ThinkingLLM_QwenVL_GGUF", "ThinkingLLM_QwenVL_GGUF_Advanced", ]); const GGUF_ADVANCED_INTERNAL_WIDGETS = new Set([ "legacy_seed_mode", "legacy_unload_after_run", ]); const RECOMMENDATIONS_URL = new URL("../model_recommendations.json", import.meta.url); const PRESET_TOOLTIPS_URL = new URL("../preset_tooltips.json", import.meta.url); const RECOMMENDATION_WIDGET_NAME = "recommended_settings"; const RECOMMENDATION_PLACEHOLDER = "Select a model to see provider/community recommended settings. This note never changes your saved widget values."; const RECOMMENDATION_FIELDS = ["temperature", "top_p", "top_k", "max_tokens", "ctx_min", "n_batch", "image_max_tokens"]; const AUDIO_UNSUPPORTED_MESSAGE = "No curated audio support for the selected model. Use Gemma 4 Audio for audio understanding or Whisper ASR for transcription."; const PRESET_WIDGET_NAMES = ["preset_prompt", "enhancement_style", "preset_system_prompt"]; let recommendationsPromise = null; let presetTooltipsPromise = null; function loadRecommendations() { if (!recommendationsPromise) { recommendationsPromise = fetch(RECOMMENDATIONS_URL) .then((response) => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }) .then((payload) => Array.isArray(payload?.rules) ? payload.rules : []) .catch((error) => { console.warn("[ThinkingLLM] Failed to load model recommendations", error); return []; }); } return recommendationsPromise; } function loadPresetTooltips() { if (!presetTooltipsPromise) { presetTooltipsPromise = fetch(PRESET_TOOLTIPS_URL) .then((response) => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }) .catch((error) => { console.warn("[ThinkingLLM] Failed to load preset tooltips", error); return {}; }); } return presetTooltipsPromise; } function setNodeColors(node, theme) { if (!theme) { return; } if (!node.color && theme.nodeColor) { node.color = theme.nodeColor; } if (!node.bgcolor && theme.nodeBgColor) { node.bgcolor = theme.nodeBgColor; } if (theme.width) { node.size = node.size || [140, 80]; node.size[0] = theme.width; } } function findWidget(node, name) { return node.widgets?.find((widget) => widget.name === name); } function hideStableWidget(node, widget) { if (!widget || widget._thinkingllmHidden) { return; } widget._thinkingllmHidden = true; widget._thinkingllmOriginalType = widget.type; widget._thinkingllmOriginalComputeSize = widget.computeSize; widget.hidden = true; widget.type = `thinkingllm_hidden_${widget.type || "widget"}`; widget.computeSize = () => [0, 0]; widget.draw = () => {}; } function findWidgets(node, name) { return node.widgets?.filter((widget) => widget?.name === name) || []; } function removeDuplicateRecommendationWidgets(node, keepWidget) { if (!node.widgets?.length || !keepWidget) { return; } for (let index = node.widgets.length - 1; index >= 0; index -= 1) { const widget = node.widgets[index]; if (widget?.name !== RECOMMENDATION_WIDGET_NAME || widget === keepWidget) { continue; } hideStableWidget(node, widget); node.widgets.splice(index, 1); } } function hideGgufAdvancedInternals(node) { if (!["AILab_QwenVL_GGUF_Advanced", "ThinkingLLM_QwenVL_GGUF_Advanced"].includes(node.comfyClass)) { return; } for (const widgetName of GGUF_ADVANCED_INTERNAL_WIDGETS) { hideStableWidget(node, findWidget(node, widgetName)); } if (node.widgets?.length) { requestAnimationFrame(() => node.setSize([node.size[0], node.computeSize()[1]])); } } function clearHfTokenAfterExecution(node) { if (!THINKINGLLM_NODE_NAMES.has(node.comfyClass) || node._thinkingllmClearsToken) { return; } node._thinkingllmClearsToken = true; const originalOnExecuted = node.onExecuted; node.onExecuted = function (...args) { const result = originalOnExecuted?.apply(this, args); const tokenWidget = findWidget(this, "hf_token"); if (tokenWidget?.value) { tokenWidget.value = ""; app.graph?.setDirtyCanvas?.(true, true); } return result; }; } function recommendationScope(node) { if ([ "AILab_QwenVL_GGUF", "AILab_QwenVL_GGUF_Advanced", "ThinkingLLM_QwenVL_GGUF", "ThinkingLLM_QwenVL_GGUF_Advanced", "ThinkingLLM_Gemma4_Audio_GGUF", "ThinkingLLM_Whisper_ASR", "AILab_QwenVL_GGUF_PromptEnhancer", "ThinkingLLM_QwenVL_GGUF_PromptEnhancer", ].includes(node.comfyClass)) { return "gguf"; } return "hf"; } function matchRecommendationRule(modelName, rules) { const lowered = String(modelName || "").toLowerCase(); if (!lowered) { return null; } const ordered = [...(rules || [])].sort((left, right) => (right.priority || 0) - (left.priority || 0)); for (const rule of ordered) { const include = Array.isArray(rule?.match_any) ? rule.match_any : []; const exclude = Array.isArray(rule?.exclude_any) ? rule.exclude_any : []; const matches = include.some((token) => lowered.includes(String(token).toLowerCase())); const blocked = exclude.some((token) => lowered.includes(String(token).toLowerCase())); if (matches && !blocked) { return rule; } } return null; } function hasWidget(node, widgetName) { return Boolean(findWidget(node, widgetName)); } function hasAudioInput(node) { return AUDIO_CAPABLE_NODE_NAMES.has(node.comfyClass) || Boolean(node.inputs?.some((input) => input?.name === "audio")); } function formatSetting(fieldName, value) { if (typeof value !== "number") { return null; } if (fieldName === "ctx_min") { return `ctx>=${value}`; } return `${fieldName}=${value}`; } function buildModeSummary(node, settings) { const parts = []; for (const fieldName of RECOMMENDATION_FIELDS) { if (!(fieldName in (settings || {}))) { continue; } if (fieldName === "ctx_min") { if (!hasWidget(node, "ctx")) { continue; } } else if (fieldName === "top_k") { if (!hasWidget(node, "top_k")) { continue; } } else if (!hasWidget(node, fieldName)) { continue; } const formatted = formatSetting(fieldName, settings[fieldName]); if (formatted) { parts.push(formatted); } } return parts; } function buildRecommendationText(node, rules) { if (node.comfyClass === "ThinkingLLM_Whisper_ASR") { const modelSize = String(findWidget(node, "model_size")?.value || "small").trim() || "small"; return [ "Whisper ASR transcription", `Model: ${modelSize}`, "Audio: Use for speech-to-text from AUDIO or audio_file_path. M4A/MP3/WAV/FLAC are decoded with FFmpeg.", "Settings: small + CPU/int8 is the reliable Windows default; large-v3 is higher quality; vad_filter skips silence.", "Dependency: faster-whisper must be installed in the active ComfyUI Python.", ].join("\n"); } const modelName = String(findWidget(node, "model_name")?.value || "").trim(); if (!modelName || modelName.startsWith("(no ")) { return RECOMMENDATION_PLACEHOLDER; } const rule = matchRecommendationRule(modelName, rules); if (!rule) { return [ "Manual settings guide", `No curated recommendation is bundled yet for ${modelName}.`, "Your current widget values stay exactly as you set them.", ].join("\n"); } const thinkingEnabled = Boolean(findWidget(node, "enable_thinking")?.value); const activeSettings = thinkingEnabled ? rule.thinking_on : rule.thinking_off; const alternateSettings = thinkingEnabled ? rule.thinking_off : rule.thinking_on; const activeSummary = buildModeSummary(node, activeSettings); const alternateSummary = buildModeSummary(node, alternateSettings); const scope = recommendationScope(node); const lines = [ rule.family_label || "Manual settings guide", `Current toggle: ${thinkingEnabled ? "thinking ON" : "thinking OFF"}`, ]; const supportNote = rule.support_notes?.[scope]; if (supportNote) { lines.push(`Toggle: ${supportNote}`); } const audioNote = rule.support_notes?.audio; if (audioNote && hasAudioInput(node)) { lines.push(`Audio: ${audioNote}`); } else if (hasAudioInput(node)) { lines.push(`Audio: ${AUDIO_UNSUPPORTED_MESSAGE}`); } lines.push(`Thinking ON: ${(thinkingEnabled ? activeSummary : alternateSummary).join(" | ") || "use the provider defaults"}`); lines.push(`Thinking OFF: ${(thinkingEnabled ? alternateSummary : activeSummary).join(" | ") || "use the provider defaults"}`); const softSwitch = rule.support_notes?.soft_switch; if (softSwitch) { lines.push(`Note: ${softSwitch}`); } if (Array.isArray(rule.notes) && rule.notes.length) { lines.push(`Extra: ${rule.notes[0]}`); } if (Array.isArray(rule.sources) && rule.sources.length) { lines.push(`Sources: ${rule.sources.join(" + ")}`); } return lines.join("\n"); } function ensureRecommendationWidget(node) { if (!THINKINGLLM_NODE_NAMES.has(node.comfyClass)) { return null; } let widget = findWidgets(node, RECOMMENDATION_WIDGET_NAME) .find((candidate) => candidate?.inputEl) || findWidget(node, RECOMMENDATION_WIDGET_NAME); if (!widget) { widget = ComfyWidgets.STRING(node, RECOMMENDATION_WIDGET_NAME, ["STRING", { multiline: true }], app).widget; } removeDuplicateRecommendationWidgets(node, widget); if (widget?.inputEl) { widget.inputEl.readOnly = true; widget.inputEl.style.opacity = 0.78; widget.inputEl.style.fontSize = "12px"; widget.inputEl.style.lineHeight = "1.35"; widget.inputEl.rows = 6; } widget.options = { ...(widget.options || {}), serialize: false }; widget.serializeValue = async () => undefined; if (!widget.value) { widget.value = RECOMMENDATION_PLACEHOLDER; } return widget; } function resizeNode(node) { requestAnimationFrame(() => { const size = node.computeSize(); if (size[0] < node.size[0]) { size[0] = node.size[0]; } if (size[1] < node.size[1]) { size[1] = node.size[1]; } node.setSize(size); app.graph?.setDirtyCanvas?.(true, false); }); } function updateRecommendationWidget(node) { const widget = ensureRecommendationWidget(node); if (!widget) { return; } loadRecommendations().then((rules) => { widget.value = buildRecommendationText(node, rules); resizeNode(node); }); } function queueRecommendationRefresh(node) { if (!node || node._thinkingllmRecommendationQueued) { return; } node._thinkingllmRecommendationQueued = true; requestAnimationFrame(() => { node._thinkingllmRecommendationQueued = false; updateRecommendationWidget(node); }); } function summarizePresetTooltip(prompt) { const text = String(prompt || "").trim(); if (!text) { return "No preset prompt is active. Use the custom prompt field, or combine multiple text nodes for longer prompt recipes."; } return text.length > 1800 ? `${text.slice(0, 1800).trim()}...` : text; } function updatePresetTooltip(widget, promptMap) { if (!widget) { return; } const prompt = promptMap?.[widget.name]?.[widget.value]; const tooltip = summarizePresetTooltip(prompt); widget.options = { ...(widget.options || {}), tooltip }; widget.tooltip = tooltip; if (widget.inputEl) { widget.inputEl.title = tooltip; } } function hookPresetTooltipPreviews(node) { if (!THINKINGLLM_NODE_NAMES.has(node.comfyClass) || node._thinkingllmPresetTooltipsHooked) { return; } node._thinkingllmPresetTooltipsHooked = true; loadPresetTooltips().then((promptMap) => { const refresh = () => { for (const widgetName of PRESET_WIDGET_NAMES) { updatePresetTooltip(findWidget(node, widgetName), promptMap); } }; for (const widgetName of PRESET_WIDGET_NAMES) { const target = findWidget(node, widgetName); if (!target || target._thinkingllmPresetTooltipCallbackHooked) { continue; } target._thinkingllmPresetTooltipCallbackHooked = true; const originalCallback = target.callback; target.callback = function (...args) { const result = originalCallback?.apply(this, args); refresh(); return result; }; } const originalConfigure = node.onConfigure; node.onConfigure = function (...args) { const result = originalConfigure?.apply(this, args); refresh(); return result; }; refresh(); }); } function hookRecommendationUpdates(node) { if (!THINKINGLLM_NODE_NAMES.has(node.comfyClass) || node._thinkingllmRecommendationHooked) { return; } node._thinkingllmRecommendationHooked = true; ensureRecommendationWidget(node); for (const widgetName of ["model_name", "enable_thinking"]) { const target = findWidget(node, widgetName); if (!target || target._thinkingllmRecommendationCallbackHooked) { continue; } target._thinkingllmRecommendationCallbackHooked = true; const originalCallback = target.callback; target.callback = function (...args) { const result = originalCallback?.apply(this, args); queueRecommendationRefresh(node); return result; }; } const originalConfigure = node.onConfigure; node.onConfigure = function (...args) { const result = originalConfigure?.apply(this, args); queueRecommendationRefresh(this); return result; }; queueRecommendationRefresh(node); } const ext = { name: "QwenVL.appearance", nodeCreated(node) { const nclass = node.comfyClass; if (NODE_COLORS.hasOwnProperty(nclass)) { const colorKey = NODE_COLORS[nclass]; const theme = COLOR_THEMES[colorKey]; setNodeColors(node, theme); } hideGgufAdvancedInternals(node); hookRecommendationUpdates(node); hookPresetTooltipPreviews(node); clearHfTokenAfterExecution(node); } }; app.registerExtension(ext);