From 9f5abfb8b57211abe7f0fac3650f19fdc8d869fb Mon Sep 17 00:00:00 2001 From: Alex Furer Date: Mon, 15 Dec 2025 13:32:22 +0100 Subject: [PATCH] v1.1.0 - Added the "Model Switch" node - Updated README - Cleaned up the CHANGELOG to meet standard --- .gitignore | 5 +- CHANGELOG.md | 33 ++-- README.md | 104 +++++++++++- nodes.py | 130 ++++++++++++++- package.json | 4 +- pyproject.toml | 4 +- web/AF_Model_Switch.js | 347 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 592 insertions(+), 35 deletions(-) create mode 100644 web/AF_Model_Switch.js diff --git a/.gitignore b/.gitignore index 0ff6698..21a9ccd 100644 --- a/.gitignore +++ b/.gitignore @@ -88,8 +88,9 @@ yarn-error.log* # AF speciffic Status.txt -*.docx *.psd +*.docx +helpers/ nodes.zip node.zip -Docs/screenshots/ \ No newline at end of file +Docs/screenshots/ diff --git a/CHANGELOG.md b/CHANGELOG.md index be6aabf..10c68a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,43 +1,44 @@ # Changelog -## [1.0.9] - 2025-11-11 +### 1.1.0 +- Added the "Model Switch" node +- Updated README +- Cleaned up the CHANGELOG to meet standard + +### v1.0.9 - Added "Show Text" node documentation - Added a video about the "Edit Generated Prompt" node - Updated the workflow example to use my own "Show Text" node -## [1.0.8] - 2025-11-10 +### v1.0.8 - Fixed an error on the "Edit Generated Prompt" node ( invalid prompt: {'type': 'prompt_no_outputs', 'message': 'Prompt has no outputs', 'details': '', 'extra_info': {}} ) - Added a "Show Text" node, so folks don't have to install any other custom nodes to run the example workflow (Yeah ComfyUI does NOT have a simple show text node...) -## [1.0.7] - 2025-11-05 +### v1.0.7 - Changed Icon location to my own website as the ComfyUI Registry botched out on any GitHub URL I tried... -## [1.0.6] - 2025-10-30 +### v1.0.6 - Minor changes for registry compliance -## [1.0.5] - 2025-10-30 -### Changed +### v1.0.5 - Added screenshots - Added workflow examples - Updated README -## [1.0.4] - 2025-10-30 -### Changed +### v1.0.4 - Save Prompt History now only saves, when the prompt changed, eliminating duplicate prompts in the archive. - Added screenshots - Added workflow examples - Updated README -## [1.0.3] - 2025-10-29 -### Changed +### v1.0.3 - Fixed links in nodes.py to git - Added CHANGELOG.md - Simplified __init__.py - Added package.json for ComfyUI Manager compliance - Added pyproject.toml for registry compliance -## [1.0.2] - 2025-10-29 -### Changed +### v1.0.2 - Changed "project" to "filename" in load prompt node - Changed display on info node - Changed console output to include ID number @@ -45,11 +46,9 @@ - Fixed Versioning text - Made print out in console less prominent -## [1.0.1] - 2025-10-22 -### Added +### v1.0.1 - Added "project" back in as a tag that gets stored in the json -## [1.0.0] - 2025-10-20 -### Major Changes +### v1.0.0 - Merged AF Edit Generated Prompt and AF Prompt History Nodes into single pack -- Complete overhauled Load Prompt node \ No newline at end of file +- Complete overhauled Load Prompt node diff --git a/README.md b/README.md index 6c6d75a..e57484f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ A lightweight suite of ComfyUI custom nodes for AI prompt management and history **[AF - Edit Generated Prompt](#af---edit-generated-prompt)** - Receive LLM-generated prompts and optionally edit them manually **[AF - Save Prompt History](#af---save-prompt-history)** - Archive prompts to JSON files with timestamps **[AF - Load Prompt History](#af---load-prompt-history)** - Browse and load previous prompts by index -**[AF - Show Text](#af---show-text)** - SImple node that displays the connected text +**[AF - Show Text](#af---show-text)** - Simple node that displays the connected text +**[AF - Model Switch](#af---model-switch)** - A dynamic model/clip/vae switching node --- @@ -204,6 +205,18 @@ AF Load - ✓ Successfully loaded: ID: 126 | 2025-10-29 14:32:15 --- + +## 💡 Pro Tips + +- **Index 0 is your friend** - Always points to newest prompt +- **Info output shows everything** - See exactly what you loaded +- **Console logging** - Check ID and timestamp confirmation +- **Multiple load nodes** - Each can access different files/indices +- **Workflows persist** - Your selections save with the workflow +- **Project field** - Use it as a mental note when saving, great for manual inspection + +--- + ### AF - Show Text A very simple node that shows the text connected to the input of the Node @@ -239,14 +252,89 @@ A very simple node that shows the text connected to the input of the Node --- -## 💡 Pro Tips +## AF - Model Switch -- **Index 0 is your friend** - Always points to newest prompt -- **Info output shows everything** - See exactly what you loaded -- **Console logging** - Check ID and timestamp confirmation -- **Multiple load nodes** - Each can access different files/indices -- **Workflows persist** - Your selections save with the workflow -- **Project field** - Use it as a mental note when saving, great for manual inspection +A dynamic model switching node that allows you to toggle between multiple model/CLIP/VAE combinations without rewiring your workflow. + +### Features + +- **Dynamic Rails**: Add 2-10 model rails on demand +- **Smart AUTO Mode**: Automatically selects the first connected rail +- **1-Based Indexing**: Intuitive numbering (Rail 1, Rail 2, Rail 3...) +- **Permanent Base Rails**: Rails 1-2 never lose connections when adjusting rail count +- **Real-Time Status**: Color-coded display shows active rail instantly +- **Flexible Sizing**: Resizable node that preserves your layout preferences + +### How It Works + +The Model Switch acts as a gateway for your model pipeline. Instead of maintaining multiple workflow branches or manually reconnecting loaders, you can: + +1. Connect multiple checkpoint loaders (or AIO loaders) to different rails +2. Choose which rail to use via the `selected_rail` parameter +3. The node outputs the MODEL, CLIP, and VAE from your selected rail + +### Parameters + +| Parameter | Type | Range | Description | +|-----------|------|-------|-------------| +| `number_of_rails` | INT | 2-10 | Number of visible model rails | +| `selected_rail` | INT | 0-10 | Active rail: `0` = AUTO (first connected), `1-10` = manual selection | +| `status` | STRING | - | Read-only display showing current active rail | + +### Status Display Colors + +- 🟠 **Orange**: AUTO mode - automatically using first connected rail +- 🔵 **Blue**: Manual mode - specific rail selected +- 🔴 **Red**: Error - no models connected + +### Usage Example + +**Basic Setup:** +``` +Checkpoint Loader A → model_1, clip_1, vae_1 +Checkpoint Loader B → model_2, clip_2, vae_2 + ↓ + AF Model Switch + (selected_rail: 0) + ↓ + → model, clip, vae +``` + +**A/B Testing Workflow:** +1. Set `number_of_rails: 2` +2. Connect two different models to Rails 1 and 2 +3. Set `selected_rail: 1` to use first model, `2` for second model +4. Set `selected_rail: 0` for AUTO mode (uses first connected) + +**Advanced: Quality vs Speed:** +1. Set `number_of_rails: 3` +2. Rail 1: High-quality slow model +3. Rail 2: Balanced model +4. Rail 3: Fast draft model +5. Switch between them by changing `selected_rail` + +### Tips + +- **Rails 1-2 are permanent**: These connections are never lost when you adjust `number_of_rails` +- **Rails 3-10 are dynamic**: These are added/removed as needed +- **Use with rgthree Group Bypasser**: Bypass entire model loading groups to save memory - only the selected rail's loaders will execute +- **AUTO mode is smart**: It will find the first rail with actual data, skipping any bypassed/muted nodes +- **Status updates instantly**: No need to run the workflow to see which rail is selected + +### Integration with Other Nodes + +**Works great with:** +- `rgthree Context Switch` - for full pipeline switching +- `rgthree Group Bypasser` - to disable unused model loaders +- `AF - Save Prompt History` - save which model was used with each prompt +- Any checkpoint loader or AIO loader node + +### Technical Notes + +- Input names are 1-based: `model_1`, `clip_1`, `vae_1`, etc. +- Internally consistent: no confusing 0-based conversions +- Minimum node width: 160px +- Height auto-adjusts based on number of rails --- diff --git a/nodes.py b/nodes.py index 8ec20e4..8fe3b5a 100644 --- a/nodes.py +++ b/nodes.py @@ -475,21 +475,143 @@ class AF_Show_Text: "ui": {"text": [str(text)]}, "result": (text,) } - + # ============================================================================= -# Node Mappings +# AF - Model Switch Node (1-based Rails, 0=AUTO) # ============================================================================= +class AF_Model_Switch: + """ + Switch between model configurations with dynamic input visibility. + Uses 1-based indexing: Rail 1, Rail 2, Rail 3, etc. + Rails 1 and 2 are permanent (connections never lost). + Rails 3-10 are dynamically added/removed. + + Selected rail: 0 = AUTO (first connected), 1-10 = manual selection + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "number_of_rails": ("INT", { + "default": 2, + "min": 2, + "max": 10, + "step": 1, + "display": "number", + "tooltip": "Number of visible rails (2-10)" + }), + "selected_rail": ("INT", { + "default": 0, + "min": 0, + "max": 10, + "step": 1, + "display": "number", + "tooltip": "Active rail: 0=AUTO (first connected), 1-10=manual selection" + }), + "status": ("STRING", { + "default": "💡 Active Rail: auto", + "multiline": True, + "dynamicPrompts": False + }), + }, + "optional": { + # Rail 1 and Rail 2 are PERMANENT - never removed + "model_1": ("MODEL",), + "clip_1": ("CLIP",), + "vae_1": ("VAE",), + "model_2": ("MODEL",), + "clip_2": ("CLIP",), + "vae_2": ("VAE",), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + } + } + + RETURN_TYPES = ("MODEL", "CLIP", "VAE") + RETURN_NAMES = ("model", "clip", "vae") + FUNCTION = "switch_model" + CATEGORY = "AF - Nodes/AF - Prompt Pack" + OUTPUT_NODE = True + + def switch_model(self, number_of_rails, selected_rail, status="", unique_id=None, extra_pnginfo=None, **kwargs): + # Clamp values to valid ranges + number_of_rails = max(2, min(10, number_of_rails)) + selected_rail = max(0, min(number_of_rails, selected_rail)) + + # Determine actual active rail (1-based throughout!) + actual_rail = None + is_auto_mode = (selected_rail == 0) + + if is_auto_mode: + # AUTO mode - find first rail with data + for i in range(1, number_of_rails + 1): # Rails 1 to number_of_rails + test_model = kwargs.get(f"model_{i}", None) + if test_model is not None: + actual_rail = i + print(f"AF Model Switch - AUTO → Rail {actual_rail}") + break + + if actual_rail is None: + # No rails have data, default to rail 1 + actual_rail = 1 + print(f"AF Model Switch - AUTO → No data found, defaulting to Rail 1") + + status_text = f"💡 Active Rail: auto → {actual_rail}" + else: + # Manual mode - user selected specific rail (1-based) + actual_rail = selected_rail + status_text = f"🎯 Active Rail: {actual_rail}" + print(f"AF Model Switch - Manual → Rail {actual_rail}") + + # Get selected components using 1-based rail number directly + model = kwargs.get(f"model_{actual_rail}", None) + clip = kwargs.get(f"clip_{actual_rail}", None) + vae = kwargs.get(f"vae_{actual_rail}", None) + + # Final fallback if still no model + if model is None: + for i in range(1, number_of_rails + 1): + test_model = kwargs.get(f"model_{i}", None) + if test_model is not None: + model = test_model + clip = kwargs.get(f"clip_{i}", None) + vae = kwargs.get(f"vae_{i}", None) + print(f"AF Model Switch - Fallback to rail {i}") + break + + if model is not None: + print(f"AF Model Switch - ✓ Outputting Rail {actual_rail} (of 1-{number_of_rails})") + else: + print(f"AF Model Switch - ⚠️ No models connected") + status_text = "⚠️ Active Rail: none (no connections)" + + return { + "ui": {"status": [status_text]}, + "result": (model, clip, vae) + } + + @classmethod + def IS_CHANGED(cls, number_of_rails, selected_rail, **kwargs): + # Force re-evaluation when inputs change + return f"{number_of_rails}-{selected_rail}" + +# Update NODE_CLASS_MAPPINGS NODE_CLASS_MAPPINGS = { "AF_Edit_Generated_Prompt": AF_Edit_Generated_Prompt, "AF_Save_Prompt_History": AF_Save_Prompt_History, "AF_Load_Prompt_History": AF_Load_Prompt_History, "AF_Show_Text": AF_Show_Text, + "AF_Model_Switch": AF_Model_Switch, } NODE_DISPLAY_NAME_MAPPINGS = { "AF_Edit_Generated_Prompt": "AF - Edit Generated Prompt", - "AF_Save_Prompt_History": "AF - Save Prompt History", + "AF_Save_Prompt_History": "AF - Save Prompt History", "AF_Load_Prompt_History": "AF - Load Prompt History", "AF_Show_Text": "AF - Show Text", -} \ No newline at end of file + "AF_Model_Switch": "AF - Model Switch", +} diff --git a/package.json b/package.json index de28ea2..cc4551c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "comfyui-af-pack-prompt-nodes", - "version": "1.0.9", + "version": "1.1.0", "description": "A lightweight suite of ComfyUI custom nodes for AI prompt management and history tracking.", "author": "Alex Furer", "license": "MIT", @@ -9,4 +9,4 @@ "url": "https://github.com/alFrame/ComfyUI-AF-Pack-Prompt-Nodes.git" }, "nodepath": "custom_nodes/ComfyUI-AF-Pack-Prompt-Nodes" -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index 35c6f46..4bc9fbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-af-pack-prompt-nodes" description = "A lightweight suite of ComfyUI custom nodes for AI prompt management and history tracking." -version = "1.0.9" +version = "1.1.0" license = {file = "LICENSE"} dependencies = [] @@ -11,4 +11,4 @@ Repository = "https://github.com/alFrame/ComfyUI-AF-Pack-Prompt-Nodes" [tool.comfy] PublisherId = "alframe" DisplayName = "ComfyUI AF - Pack-Prompt-Nodes" -Icon = "https://fullframestudios.ch/ComfyUI-Icon/icon.png" \ No newline at end of file +Icon = "https://fullframestudios.ch/ComfyUI-Icon/icon.png" diff --git a/web/AF_Model_Switch.js b/web/AF_Model_Switch.js new file mode 100644 index 0000000..e3f86aa --- /dev/null +++ b/web/AF_Model_Switch.js @@ -0,0 +1,347 @@ +import { app } from "/scripts/app.js"; + +app.registerExtension({ + name: "AF.ModelSwitch", + + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name === "AF_Model_Switch") { + + // Hook into onExecuted to update status after workflow runs + const originalOnExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function(message) { + const r = originalOnExecuted?.apply(this, arguments); + + if (message?.status && Array.isArray(message.status) && message.status.length > 0) { + const statusText = message.status[0]; + + const statusWidget = this.widgets?.find(w => w.name === "status"); + if (statusWidget) { + statusWidget.value = statusText; + + if (statusWidget.inputEl) { + statusWidget.inputEl.value = statusText; + + if (statusText.includes("auto")) { + statusWidget.inputEl.style.backgroundColor = "#2a2a1a"; + statusWidget.inputEl.style.color = "#ffaa44"; + statusWidget.inputEl.style.borderColor = "#cc8833"; + } else if (statusText.includes("none")) { + statusWidget.inputEl.style.backgroundColor = "#2a1a1a"; + statusWidget.inputEl.style.color = "#ff6666"; + statusWidget.inputEl.style.borderColor = "#cc4444"; + } else { + statusWidget.inputEl.style.backgroundColor = "#1a2a3a"; + statusWidget.inputEl.style.color = "#88ccff"; + statusWidget.inputEl.style.borderColor = "#4488cc"; + } + } + + this.setDirtyCanvas(true, false); + } + } + + return r; + }; + + // Store original onNodeCreated + const originalOnNodeCreated = nodeType.prototype.onNodeCreated; + + nodeType.prototype.onNodeCreated = function() { + const result = originalOnNodeCreated?.apply(this, arguments); + + // Rails 1 and 2 are PERMANENT (defined in Python INPUT_TYPES) + // So we start counting from rail 2 as the baseline + // _currentRailCount represents the TOTAL number of rails visible + + // Detect existing rail count from loaded inputs + // Check for the highest rail number present (rails 3-10 are dynamic) + let detectedRailCount = 2; // Always have at least rails 1 and 2 + if (this.inputs) { + for (let i = 10; i >= 3; i--) { + if (this.inputs.find(inp => inp.name === `model_${i}`)) { + detectedRailCount = i; + break; + } + } + } + + // Initialize tracking + this._currentRailCount = detectedRailCount; + this._userResized = false; + this._isInitializing = true; // Flag to prevent duplicate initialization + + // Setup callbacks with delay to ensure widgets exist + setTimeout(() => { + const railCountWidget = this.widgets?.find(w => w.name === "number_of_rails"); + const selectWidget = this.widgets?.find(w => w.name === "selected_rail"); + + // Rail count callback + if (railCountWidget) { + railCountWidget.callback = (value) => { + const count = Math.max(2, Math.min(10, parseInt(value) || 2)); + this.updateRailInputs(count); + }; + } + + // Selected rail callback + if (selectWidget) { + selectWidget.callback = (value) => { + this.updateStatusDisplay(parseInt(value)); + }; + } + + this.updateStatusDisplay(0); + this._isInitializing = false; + }, 100); + + return result; + }; + + // Update status display (UI only, no execution) + nodeType.prototype.updateStatusDisplay = function(selectedRail) { + const statusWidget = this.widgets?.find(w => w.name === "status"); + if (!statusWidget) return; + + let statusText, bgColor, textColor, borderColor; + + // 0 = AUTO, 1-10 = manual rail selection + if (selectedRail === 0 || isNaN(selectedRail)) { + statusText = "💡 Active Rail: auto"; + bgColor = "#2a2a1a"; + textColor = "#ffaa44"; + borderColor = "#cc8833"; + } else { + // Display 1-based rail number + statusText = `🎯 Active Rail: ${selectedRail}`; + bgColor = "#1a2a3a"; + textColor = "#88ccff"; + borderColor = "#4488cc"; + } + + statusWidget.value = statusText; + + if (statusWidget.inputEl) { + statusWidget.inputEl.value = statusText; + statusWidget.inputEl.style.backgroundColor = bgColor; + statusWidget.inputEl.style.color = textColor; + statusWidget.inputEl.style.borderColor = borderColor; + } + + this.setDirtyCanvas(true, false); + }; + + // Update rail inputs dynamically + nodeType.prototype.updateRailInputs = function(railCount) { + railCount = Math.max(2, Math.min(10, railCount)); + + if (this._currentRailCount === railCount) return; + + const oldCount = this._currentRailCount; + const newCount = railCount; + + console.log(`AF Model Switch - Updating rails from ${oldCount} to ${newCount}`); + + if (newCount > oldCount) { + // Adding rails: model_3, model_4, etc. (1-based!) + // Rails 1 and 2 are permanent, so we only add 3+ + for (let i = oldCount + 1; i <= newCount; i++) { + // Check if this input already exists before adding + const modelExists = this.inputs.find(inp => inp.name === `model_${i}`); + const clipExists = this.inputs.find(inp => inp.name === `clip_${i}`); + const vaeExists = this.inputs.find(inp => inp.name === `vae_${i}`); + + if (!modelExists) { + console.log(`AF Model Switch - Adding model_${i}`); + this.addInput(`model_${i}`, "MODEL"); + } + if (!clipExists) { + console.log(`AF Model Switch - Adding clip_${i}`); + this.addInput(`clip_${i}`, "CLIP"); + } + if (!vaeExists) { + console.log(`AF Model Switch - Adding vae_${i}`); + this.addInput(`vae_${i}`, "VAE"); + } + } + } else { + // Removing rails from the end + // NEVER remove rails 1 or 2 (they're permanent) + for (let i = oldCount; i > newCount && i > 2; i--) { + console.log(`AF Model Switch - Removing rail ${i}`); + for (let j = this.inputs.length - 1; j >= 0; j--) { + const input = this.inputs[j]; + if (input.name === `vae_${i}` || + input.name === `clip_${i}` || + input.name === `model_${i}`) { + this.removeInput(j); + } + } + } + } + + this._currentRailCount = newCount; + + // Update select widget: 0=AUTO, 1 to newCount for rails + const selectWidget = this.widgets?.find(w => w.name === "selected_rail"); + if (selectWidget) { + const maxRail = newCount; + selectWidget.options = selectWidget.options || {}; + selectWidget.options.min = 0; + selectWidget.options.max = maxRail; + + // Clamp current selection to valid range + const currentValue = parseInt(selectWidget.value); + if (currentValue > maxRail) { + selectWidget.value = maxRail; + this.updateStatusDisplay(maxRail); + } + + // Update the input element's attributes + if (selectWidget.inputEl) { + selectWidget.inputEl.max = maxRail; + selectWidget.inputEl.min = 0; + } + } + + // Only adjust height, preserve width + const widgetHeight = 100; + const inputHeight = 20; + const totalInputs = this._currentRailCount * 3; + const newHeight = widgetHeight + (totalInputs * inputHeight); + + // Preserve current width, only update height + if (this.size) { + this.size[1] = newHeight; + } else { + this.size = [160, newHeight]; + } + + this.setDirtyCanvas(true, true); + }; + + // Override computeSize - minimal intervention, let LiteGraph work + const originalComputeSize = nodeType.prototype.computeSize; + nodeType.prototype.computeSize = function(out) { + // If user hasn't resized, calculate proper height + if (!this._userResized) { + let height = 200; + + if (this._currentRailCount) { + const widgetHeight = 100; + const inputHeight = 20; + const totalInputs = this._currentRailCount * 3; + height = widgetHeight + (totalInputs * inputHeight); + } + + const size = [160, height]; + + if (out) { + out[0] = size[0]; + out[1] = size[1]; + return out; + } + + return size; + } + + // User has resized - let LiteGraph handle it normally + return originalComputeSize?.apply(this, arguments); + }; + + // Track when user manually resizes + const originalOnResize = nodeType.prototype.onResize; + nodeType.prototype.onResize = function(size) { + this._userResized = true; + return originalOnResize?.apply(this, arguments); + }; + } + }, + + async nodeCreated(node) { + if (node.comfyClass === "AF_Model_Switch") { + setTimeout(() => { + // Style rail count widget + const railCountWidget = node.widgets?.find(w => w.name === "number_of_rails"); + if (railCountWidget?.inputEl) { + railCountWidget.inputEl.style.border = "1px solid #4488cc"; + railCountWidget.inputEl.style.backgroundColor = "#1a2a3a"; + railCountWidget.inputEl.style.padding = "4px"; + railCountWidget.inputEl.style.borderRadius = "3px"; + } + + // Style selected rail widget + const selectWidget = node.widgets?.find(w => w.name === "selected_rail"); + if (selectWidget?.inputEl) { + selectWidget.inputEl.style.border = "1px solid #88cc44"; + selectWidget.inputEl.style.backgroundColor = "#1a3a2a"; + selectWidget.inputEl.style.padding = "4px"; + selectWidget.inputEl.style.borderRadius = "3px"; + selectWidget.inputEl.style.fontWeight = "bold"; + } + + // Style status widget + const statusWidget = node.widgets?.find(w => w.name === "status"); + if (statusWidget?.inputEl) { + statusWidget.inputEl.readOnly = true; + statusWidget.inputEl.style.fontFamily = "monospace"; + statusWidget.inputEl.style.fontSize = "13px"; + statusWidget.inputEl.style.fontWeight = "bold"; + statusWidget.inputEl.style.cursor = "default"; + statusWidget.inputEl.style.padding = "6px 8px"; + statusWidget.inputEl.style.textAlign = "center"; + statusWidget.inputEl.style.userSelect = "none"; + statusWidget.inputEl.style.resize = "none"; + statusWidget.inputEl.style.minHeight = "32px"; + statusWidget.inputEl.style.maxHeight = "32px"; + statusWidget.inputEl.style.height = "32px"; + statusWidget.inputEl.style.overflow = "hidden"; + statusWidget.inputEl.style.width = "100%"; + statusWidget.inputEl.style.boxSizing = "border-box"; + + // Set initial AUTO color (orange) + statusWidget.inputEl.style.backgroundColor = "#2a2a1a"; + statusWidget.inputEl.style.color = "#ffaa44"; + statusWidget.inputEl.style.border = "1px solid #cc8833"; + } + }, 200); + } + }, + + async loadedGraphNode(node) { + if (node.comfyClass === "AF_Model_Switch") { + setTimeout(() => { + // Mark as user-resized if node has custom size stored + if (node.size && node.size[0] !== 160) { + node._userResized = true; + } + + // Detect current rail count from actual inputs + let actualRailCount = 2; // Minimum (rails 1 and 2 are permanent) + if (node.inputs) { + for (let i = 10; i >= 3; i--) { + if (node.inputs.find(inp => inp.name === `model_${i}`)) { + actualRailCount = i; + break; + } + } + } + + // Update _currentRailCount to match actual state + node._currentRailCount = actualRailCount; + + const railCountWidget = node.widgets?.find(w => w.name === "number_of_rails"); + if (railCountWidget) { + const widgetValue = parseInt(railCountWidget.value) || 2; + + console.log(`AF Model Switch - Loaded: widget=${widgetValue}, actual inputs=${actualRailCount}`); + + // Sync: if widget and actual don't match, update to match widget + if (widgetValue !== actualRailCount && node.updateRailInputs) { + console.log(`AF Model Switch - Syncing rails to ${widgetValue}`); + node.updateRailInputs(widgetValue); + } + } + }, 100); + } + } +});