diff --git a/config_builder_node.py b/config_builder_node.py index a001c68..7c7f3cf 100644 --- a/config_builder_node.py +++ b/config_builder_node.py @@ -1,6 +1,7 @@ """ -Ultimate Config Builder with Dynamic LoRA Arrays -Enhanced with session loading that populates all fields +Ultimate Config Builder - Complete HTML UI Version +ALL data stored in single widget (lora_config) +Python reads everything from that widget """ import os @@ -10,7 +11,8 @@ from typing import List, Dict, Any class UltimateConfigBuilder: """ - Config builder with dynamic LoRA array management and session loading. + Config builder with complete HTML UI. + All data is stored in the lora_config widget as a single JSON object. """ @classmethod @@ -19,7 +21,7 @@ class UltimateConfigBuilder: return { "required": { - # Session Management + # Session Management (hidden, controlled by HTML) "session_name": ("STRING", { "default": "my_test_session", "multiline": False @@ -28,72 +30,69 @@ class UltimateConfigBuilder: "default": sessions[0] if sessions else "None" }), - # Sampler Settings + # Sampler Settings (hidden, controlled by HTML) "samplers": ("STRING", { "default": "euler, dpmpp_2m", - "multiline": False, - "tooltip": "Comma-separated samplers" + "multiline": False }), "schedulers": ("STRING", { "default": "normal, karras", - "multiline": False, - "tooltip": "Comma-separated schedulers" + "multiline": False }), "steps": ("STRING", { "default": "20, 30", - "multiline": False, - "tooltip": "Comma-separated step counts" + "multiline": False }), "cfg": ("STRING", { "default": "7.0", - "multiline": False, - "tooltip": "Comma-separated CFG values" + "multiline": False }), - # Dynamic LoRA Configuration + # LoRA Configuration (ACTUAL DATA STORAGE - contains EVERYTHING) "lora_config": ("STRING", { "default": cls.get_default_config(), - "multiline": True, - "tooltip": "LoRA arrays configuration (managed by UI)" + "multiline": True }), - # Options + # Options (hidden, controlled by HTML) "include_none": ("BOOLEAN", { - "default": True, - "label_on": "Include 'None'", - "label_off": "Skip 'None'" + "default": True }), }, "optional": { "model": ("STRING", { "default": "", - "multiline": False, - "tooltip": "Optional model override" + "multiline": False }), } } - RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING") - RETURN_NAMES = ("configs_json", "session_name", "loaded_samplers", "loaded_schedulers", "loaded_steps", "loaded_cfg", "loaded_lora_config") + RETURN_TYPES = ("STRING", "STRING") + RETURN_NAMES = ("configs_json", "session_name") FUNCTION = "generate_config" CATEGORY = "sampling/testing" OUTPUT_NODE = True @staticmethod def get_default_config(): - """Return default LoRA configuration""" + """Return default complete configuration""" config = { - "arrays": [ + "session_name": "my_test_session", + "include_none": True, + "config_arrays": [ { - "name": "Array 1", - "combine": False, - "loras": [ - {"type": "lora", "name": "None", "str_model": 1.0, "str_clip": 1.0} - ] + "name": "Config 1", + "samplers": "euler, dpmpp_2m", + "schedulers": "normal, karras", + "steps": "20, 30", + "cfg": "7.0", + "model": "", + "loras": ["None"], + "combine": False } ] } - return json.dumps(config, indent=2) + return json.dumps(config, indent=2, ensure_ascii=False) @staticmethod def get_available_sessions() -> List[str]: @@ -132,194 +131,47 @@ class UltimateConfigBuilder: print(f"[ConfigBuilder] Warning: Could not parse '{item}'") return result - def build_lora_string(self, lora_type: str, name: str, str_model: float, str_clip: float) -> str: - """Build LoRA string""" - if name == "None" or not name: - return "None" + def process_lora_array(self, config_array: Dict, include_none: bool) -> List[str]: + """ + Process a SINGLE config array and return its lora strings. - if lora_type == "folder": - return name if name.endswith("/") else name + "/" + Args: + config_array: Single config array dict from config_arrays + include_none: Whether to include "None" in results + + Returns: + List of lora strings for this config array + """ + array_name = config_array.get("name", "Unnamed Config") + combine = config_array.get("combine", False) + loras = config_array.get("loras", []) - return f"{name}:{str_model}:{str_clip}" - - def load_session_data(self, session_name: str) -> Dict[str, Any]: - """Load configuration from an existing session""" - print("loading session data") - try: - output_dir = folder_paths.get_output_directory() - manifest_path = os.path.join( - output_dir, - "benchmarks", - session_name, - "manifest.json" - ) - - if not os.path.exists(manifest_path): - print(f"[ConfigBuilder] Session '{session_name}' not found") - return None - - with open(manifest_path, "r") as f: - manifest = json.load(f) - - meta = manifest.get("meta", {}) - - # Extract raw config data - raw_configs = meta.get("raw_configs", []) - configs_json = meta.get("configs_json", "") - print(raw_configs) - print(configs_json) - # Try to parse configs_json first, fall back to raw_configs - try: - if configs_json: - parsed_configs = json.loads(configs_json) - if isinstance(parsed_configs, list) and len(parsed_configs) > 0: - raw_configs = parsed_configs - except: - pass - - # Extract sampler settings - handle both single and list formats - def extract_value(key, default): - value = meta.get(key, default) - if isinstance(value, list): - return ", ".join(str(v) for v in value) - return str(value) - - # Extract settings - loaded_data = { - "samplers": extract_value("samplers", "euler"), - "schedulers": extract_value("schedulers", "normal"), - "steps": extract_value("steps", "20"), - "cfg": extract_value("cfg", "7.0"), - "model": meta.get("model", ""), - "raw_configs": raw_configs - } - - return loaded_data - - except Exception as e: - print(f"[ConfigBuilder] Error loading session: {e}") - import traceback - traceback.print_exc() - return None - - def convert_raw_configs_to_lora_config(self, raw_configs: List[Dict]) -> str: - """Convert raw configs from manifest into lora_config format""" - if not raw_configs: - return self.get_default_config() + # Loras are already strings + lora_strings = [str(lora) for lora in loras if lora and lora != "None"] - arrays = [] + # Add combined version if requested + if combine and len(lora_strings) > 1: + stackable = [s for s in lora_strings if not s.endswith("/")] + if len(stackable) > 1: + # When combine is true, ONLY return the combined version + combined = " + ".join(stackable) + lora_strings = [combined] + print(f"[ConfigBuilder] {array_name}: Using combined version only") - # Process each raw config - for idx, raw_config in enumerate(raw_configs): - lora_value = raw_config.get("lora", "None") - - # Parse the lora field - loras_list = [] - - if isinstance(lora_value, str): - lora_items = [lora_value] - elif isinstance(lora_value, list): - lora_items = lora_value - else: - lora_items = ["None"] - - # Convert each lora item - for lora_item in lora_items: - if lora_item == "None": - loras_list.append({ - "type": "lora", - "name": "None", - "str_model": 1.0, - "str_clip": 1.0 - }) - elif lora_item.endswith("/"): - # Folder - loras_list.append({ - "type": "folder", - "name": lora_item, - "str_model": 1.0, - "str_clip": 1.0 - }) - elif " + " in lora_item: - # Stacked LoRAs - split and parse each - stacked = lora_item.split(" + ") - for stacked_lora in stacked: - if ":" in stacked_lora: - parts = stacked_lora.split(":") - loras_list.append({ - "type": "lora", - "name": parts[0], - "str_model": float(parts[1]) if len(parts) > 1 else 1.0, - "str_clip": float(parts[2]) if len(parts) > 2 else 1.0 - }) - else: - loras_list.append({ - "type": "lora", - "name": stacked_lora, - "str_model": 1.0, - "str_clip": 1.0 - }) - elif ":" in lora_item: - # Parse lora:str_model:str_clip format - parts = lora_item.split(":") - loras_list.append({ - "type": "lora", - "name": parts[0], - "str_model": float(parts[1]) if len(parts) > 1 else 1.0, - "str_clip": float(parts[2]) if len(parts) > 2 else 1.0 - }) - else: - # Just a lora name - loras_list.append({ - "type": "lora", - "name": lora_item, - "str_model": 1.0, - "str_clip": 1.0 - }) - - if loras_list: - arrays.append({ - "name": f"Loaded Config {idx + 1}", - "combine": False, - "loras": loras_list - }) + # Handle None option + if include_none: + lora_strings.insert(0, "None") - config = {"arrays": arrays} - return json.dumps(config, indent=2) - - def process_lora_config(self, config_data: Dict) -> List[str]: - """Process the LoRA configuration and return list of LoRA strings""" - arrays = config_data.get("arrays", []) - all_configs = [] + # Remove duplicates while preserving order + seen = set() + unique_strings = [] + for item in lora_strings: + if item not in seen: + seen.add(item) + unique_strings.append(item) - for i, array in enumerate(arrays): - array_name = array.get("name", f"Array {i+1}") - combine = array.get("combine", False) - loras = array.get("loras", []) - - lora_strings = [] - - for lora in loras: - lora_type = lora.get("type", "lora") - name = lora.get("name", "None") - str_model = lora.get("str_model", 1.0) - str_clip = lora.get("str_clip", 1.0) - - lora_str = self.build_lora_string(lora_type, name, str_model, str_clip) - lora_strings.append(lora_str) - all_configs.append(lora_str) - - # Add combined version if requested - if combine and len(lora_strings) > 1: - stackable = [s for s in lora_strings if s != "None" and not s.endswith("/")] - if len(stackable) > 1: - combined = " + ".join(stackable) - all_configs.append(combined) - print(f"[ConfigBuilder] {array_name}: Added stacked combo") - - print(f"[ConfigBuilder] {array_name}: Processed {len(lora_strings)} LoRAs") - - return all_configs + print(f"[ConfigBuilder] {array_name}: Processed {len(unique_strings)} LoRA configs") + return unique_strings def generate_config( self, @@ -333,119 +185,93 @@ class UltimateConfigBuilder: include_none, model="" ): - """Generate configuration""" + """ + Generate configuration. - # Store original values for output - output_samplers = samplers - output_schedulers = schedulers - output_steps = steps - output_cfg = cfg - output_lora_config = lora_config - - # Check if we should load a session - if load_session and load_session != "None": - print(f"\n[ConfigBuilder] 🔄 Loading session '{load_session}'...") - loaded = self.load_session_data(load_session) - - if loaded: - print(f"[ConfigBuilder] ✅ Successfully loaded session '{load_session}'") - - # Override parameters with loaded values - output_samplers = loaded.get("samplers", samplers) - output_schedulers = loaded.get("schedulers", schedulers) - output_steps = str(loaded.get("steps", steps)) - output_cfg = str(loaded.get("cfg", cfg)) - model = loaded.get("model", model) - - # Convert raw configs to lora_config format - if loaded.get("raw_configs"): - output_lora_config = self.convert_raw_configs_to_lora_config(loaded["raw_configs"]) - print(f"[ConfigBuilder] 📋 Converted {len(loaded['raw_configs'])} config(s) to LoRA format") - - # Update session name to loaded session - session_name = load_session - - print(f"[ConfigBuilder] 📊 Loaded settings:") - print(f" Samplers: {output_samplers}") - print(f" Schedulers: {output_schedulers}") - print(f" Steps: {output_steps}") - print(f" CFG: {output_cfg}") - if model: - print(f" Model: {model}") - else: - print(f"[ConfigBuilder] ⚠️ Could not load session '{load_session}', using current values") - - # Use the (potentially loaded) values for processing - sampler_list = self.parse_comma_list(output_samplers) - scheduler_list = self.parse_comma_list(output_schedulers) - steps_list = self.parse_number_list(output_steps) - cfg_list = self.parse_number_list(output_cfg) - - # Parse LoRA configuration - try: - config_data = json.loads(output_lora_config) - except json.JSONDecodeError as e: - print(f"[ConfigBuilder] ⚠️ Error parsing LoRA config: {e}") - print(f"[ConfigBuilder] Using default configuration") - config_data = json.loads(self.get_default_config()) - - # Process LoRA arrays - all_lora_configs = self.process_lora_config(config_data) - - # Add None if requested - if include_none and "None" not in all_lora_configs: - all_lora_configs.insert(0, "None") - - # Remove duplicates - seen = set() - unique_configs = [] - for item in all_lora_configs: - if item not in seen: - seen.add(item) - unique_configs.append(item) - - # Build final config - config = { - "sampler": sampler_list if len(sampler_list) > 1 else sampler_list[0] if sampler_list else "euler", - "scheduler": scheduler_list if len(scheduler_list) > 1 else scheduler_list[0] if scheduler_list else "normal", - "steps": steps_list if len(steps_list) > 1 else steps_list[0] if steps_list else 20, - "cfg": cfg_list if len(cfg_list) > 1 else cfg_list[0] if cfg_list else 7.0, - "lora": unique_configs if len(unique_configs) > 1 else unique_configs[0] if unique_configs else "None" - } - - if model and model.strip(): - config["model"] = model.strip() - - configs_array = [config] - json_output = json.dumps(configs_array, indent=2) - - # Calculate totals - total = ( - len(sampler_list or [1]) * - len(scheduler_list or [1]) * - len(steps_list or [1]) * - len(cfg_list or [1]) * - len(unique_configs or [1]) - ) + NOTE: All widget parameters are IGNORED! + The actual data comes from the lora_config widget which contains everything. + """ print(f"\n{'='*80}") - print(f"[ConfigBuilder] 📋 Final Config for '{session_name}'") + print(f"[ConfigBuilder] 🎯 Generating Configuration") print(f"{'='*80}") - print(f"LoRA Arrays: {len(config_data.get('arrays', []))}") - print(f"Unique LoRA Configs: {len(unique_configs)}") - print(f"Total Combinations: {total}") + + # Parse the COMPLETE state from lora_config widget + try: + state = json.loads(lora_config) + except json.JSONDecodeError as e: + print(f"[ConfigBuilder] ⚠️ Error parsing lora_config: {e}") + print(f"[ConfigBuilder] Using default config") + state = json.loads(self.get_default_config()) + + # Extract values from state + actual_session_name = state.get("session_name", session_name) + actual_include_none = state.get("include_none", include_none) + config_arrays = state.get("config_arrays", []) + + if not config_arrays: + config_arrays = [{ + "name": "Config 1", + "samplers": "euler", + "schedulers": "normal", + "steps": "20", + "cfg": "7.0", + "model": "", + "loras": ["None"], + "combine": False + }] + + configs_output = [] + total_lora_configs = 0 + + for config_array in config_arrays: + # Parse values from this config array + sampler_list = self.parse_comma_list(config_array.get("samplers", "euler")) + scheduler_list = self.parse_comma_list(config_array.get("schedulers", "normal")) + steps_list = self.parse_number_list(config_array.get("steps", "20")) + cfg_list = self.parse_number_list(config_array.get("cfg", "7.0")) + config_model = config_array.get("model", "") + + # Process loras for this config + lora_strings = self.process_lora_array(config_array, actual_include_none) + total_lora_configs += len(lora_strings) + + # Create ONE config for this array + config = { + "sampler": sampler_list if len(sampler_list) > 1 else sampler_list[0] if sampler_list else "euler", + "scheduler": scheduler_list if len(scheduler_list) > 1 else scheduler_list[0] if scheduler_list else "normal", + "steps": steps_list if len(steps_list) > 1 else steps_list[0] if steps_list else 20, + "cfg": cfg_list if len(cfg_list) > 1 else cfg_list[0] if cfg_list else 7.0, + "lora": lora_strings if len(lora_strings) > 1 else lora_strings[0] if lora_strings else "None" + } + + if config_model and config_model.strip(): + config["model"] = config_model.strip() + + configs_output.append(config) + + json_output = json.dumps(configs_output, indent=2, ensure_ascii=False) + + # Calculate totals + total_combinations = 0 + for config in configs_output: + lora_count = len(config["lora"]) if isinstance(config["lora"], list) else 1 + sampler_count = len(config["sampler"]) if isinstance(config["sampler"], list) else 1 + scheduler_count = len(config["scheduler"]) if isinstance(config["scheduler"], list) else 1 + steps_count = len(config["steps"]) if isinstance(config["steps"], list) else 1 + cfg_count = len(config["cfg"]) if isinstance(config["cfg"], list) else 1 + + total_combinations += (sampler_count * scheduler_count * steps_count * cfg_count * lora_count) + + print(f"[ConfigBuilder] 📊 Configuration Summary:") + print(f" Session: {actual_session_name}") + print(f" Config Arrays: {len(config_arrays)}") + print(f" Total LoRA Configs: {total_lora_configs}") + print(f" Total Combinations: {total_combinations}") print(f"{'='*80}\n") - # Return loaded values so they can update the UI - return ( - json_output, - session_name, - output_samplers, - output_schedulers, - output_steps, - output_cfg, - output_lora_config - ) + # Return just the essentials + return (json_output, actual_session_name) NODE_CLASS_MAPPINGS = { diff --git a/pyproject.toml b/pyproject.toml index 2ee6dbc..36185bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ultimate-auto-sampler-config-grid-testing-suite" description = "ComfyUI Sampler testing & benchmarking tool for testing Stable Diffusion samplers, schedulers, CFG scales, prompts, img2img denoise values, and LoRA combinations. Features infinite-canvas dashboard image grids with virtual scrolling that can handle thousands of images, multi-model comparison, automatic resume on interrupt, and real-time visualization. Test entire checkpoint folders, stack multiple LoRAs, generate random seed variations, and export optimized configs. Includes fullscreen mode, keyboard navigation, smart filtering by parameters, and one-click regeneration workflow. Perfect for sampler output optimization and hyperparameter tuning." -version = "1.1.4" +version = "1.2.0" license = {file = "LICENSE"} classifiers = [ # # For OS-independent nodes (works on all operating systems) diff --git a/web/config_builder.js b/web/config_builder.js index 172d8fe..4de2cd4 100644 --- a/web/config_builder.js +++ b/web/config_builder.js @@ -1,53 +1,69 @@ /** - * Web extension for Ultimate Config Builder - OPTIMIZED - * Proper widget management and LoRA selection + * Ultimate Config Builder - COMPLETE HTML UI + * - ALL inputs in HTML/JS + * - Session loading updates everything + * - Live JSON preview + * - FLEX WRAP LAYOUT + * - DYNAMIC MODEL SELECTION (Multiple Models & Folders) */ import { app } from "../../scripts/app.js"; -// Cache for available LoRAs +// Cache for available resources let availableLoras = null; let loraFolders = null; +let availableModels = null; +let modelFolders = null; +let availableSessions = ["None"]; -// Fetch available LoRAs from ComfyUI +// Fetch available LoRAs async function getAvailableLoras() { if (availableLoras) return availableLoras; - try { const resp = await fetch("/object_info"); const objectInfo = await resp.json(); - - // Find LoRA selector from LoraLoader or similar nodes for (const nodeType in objectInfo) { const nodeDef = objectInfo[nodeType]; if (nodeDef.input?.required?.lora_name) { const loraInput = nodeDef.input.required.lora_name; if (Array.isArray(loraInput) && Array.isArray(loraInput[0])) { availableLoras = loraInput[0]; - console.log(`[ConfigBuilder] Found ${availableLoras.length} LoRAs`); return availableLoras; } } } - } catch (e) { - console.error("[ConfigBuilder] Error fetching LoRAs:", e); - } - + } catch (e) { console.error("[ConfigBuilder] Error fetching LoRAs:", e); } availableLoras = ["None"]; return availableLoras; } -// Extract unique folders from LoRA paths -async function getLoraFolders() { - if (loraFolders) return loraFolders; - - const loras = await getAvailableLoras(); - const folders = new Set(["/"]); // Root folder - - loras.forEach(lora => { - const parts = lora.split(/[\/\\]/); +// Fetch available Models (Checkpoints) +async function getAvailableModels() { + if (availableModels) return availableModels; + try { + const resp = await fetch("/object_info"); + const objectInfo = await resp.json(); + // Look for standard CheckpointLoaderSimple or similar + const loaderNode = objectInfo["CheckpointLoaderSimple"] || objectInfo["CheckpointLoader"]; + if (loaderNode?.input?.required?.ckpt_name) { + const modelInput = loaderNode.input.required.ckpt_name; + if (Array.isArray(modelInput) && Array.isArray(modelInput[0])) { + availableModels = modelInput[0]; + console.log(`[ConfigBuilder] Found ${availableModels.length} Models`); + return availableModels; + } + } + } catch (e) { console.error("[ConfigBuilder] Error fetching Models:", e); } + availableModels = ["None"]; + return availableModels; +} + +// Extract folders from paths (Generic) +function extractFolders(itemList) { + const folders = new Set(["/"]); + itemList.forEach(item => { + const parts = item.split(/[\/\\]/); if (parts.length > 1) { - // Build folder path progressively let currentPath = ""; for (let i = 0; i < parts.length - 1; i++) { currentPath += parts[i] + "/"; @@ -55,334 +71,655 @@ async function getLoraFolders() { } } }); - - loraFolders = Array.from(folders).sort(); - console.log(`[ConfigBuilder] Found ${loraFolders.length} folders`); + return Array.from(folders).sort(); +} + +async function getLoraFolders() { + if (loraFolders) return loraFolders; + const loras = await getAvailableLoras(); + loraFolders = extractFolders(loras); return loraFolders; } +async function getModelFolders() { + if (modelFolders) return modelFolders; + const models = await getAvailableModels(); + modelFolders = extractFolders(models); + return modelFolders; +} + +// Fetch available sessions +async function getAvailableSessions() { + try { + const resp = await fetch("/object_info"); + const objectInfo = await resp.json(); + for (const nodeType in objectInfo) { + const nodeDef = objectInfo[nodeType]; + if (nodeType === "UltimateConfigBuilder" && nodeDef.input?.required?.load_session) { + availableSessions = nodeDef.input.required.load_session[0]; + return availableSessions; + } + } + } catch (e) { console.error("[ConfigBuilder] Error fetching sessions:", e); } + return availableSessions; +} + +// Parse LoRA string +function parseLoraString(loraStr) { + if (!loraStr || loraStr === "None") return { name: "None", model_str: 1.0, clip_str: 1.0 }; + if (loraStr.endsWith("/")) return { name: loraStr, model_str: 1.0, clip_str: 1.0 }; + const parts = loraStr.split(":"); + return { + name: parts[0] || "None", + model_str: parts.length > 1 ? parseFloat(parts[1]) : 1.0, + clip_str: parts.length > 2 ? parseFloat(parts[2]) : 1.0 + }; +} + +function buildLoraString(name, modelStr, clipStr) { + if (!name || name === "None") return "None"; + if (name.endsWith("/")) return name; + if (modelStr === 1.0 && clipStr === 1.0) return name; + return `${name}:${modelStr.toFixed(2)}:${clipStr.toFixed(2)}`; +} + app.registerExtension({ - name: "UltimateConfigBuilder.Dynamic", + name: "UltimateConfigBuilder.CompleteHTML", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "UltimateConfigBuilder") { - console.log("[ConfigBuilder] Setting up optimized dynamic UI"); - const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = async function () { const result = onNodeCreated?.apply(this, arguments); - // Get the lora_config widget const configWidget = this.widgets?.find(w => w.name === "lora_config"); - if (!configWidget) { - console.error("[ConfigBuilder] lora_config widget not found!"); - return result; - } + if (!configWidget) return result; - // Pre-fetch LoRAs and folders + this.widgets.forEach(w => { + w.type = "converted-widget"; + w.computeSize = () => [0, -4]; + }); + + // Pre-fetch everything await getAvailableLoras(); await getLoraFolders(); + await getAvailableModels(); + await getModelFolders(); + await getAvailableSessions(); - console.log("[ConfigBuilder] Found lora_config widget, setting up UI"); - - // Store references this.configWidget = configWidget; - this.dynamicWidgets = []; // Track all dynamic widgets for cleanup - - // Helper to get config - this.getConfig = function () { - try { - return JSON.parse(configWidget.value); - } catch (e) { - console.error("[ConfigBuilder] Invalid JSON:", e); - return { arrays: [] }; - } + this.state = { + session_name: "my_test_session", + include_none: true, + config_arrays: [{ + name: "Config 1", + samplers: "euler, dpmpp_2m", + schedulers: "normal, karras", + steps: "20, 30", + cfg: "7.0", + models: ["None"], // Changed to array + loras: ["None"], + combine: false + }] }; - // Helper to save config - this.saveConfig = function (config) { - configWidget.value = JSON.stringify(config, null, 2); - }; - - // CRITICAL: Proper cleanup function - this.cleanupDynamicWidgets = function () { - // Remove all dynamic widgets - this.dynamicWidgets.forEach(widget => { - const idx = this.widgets.indexOf(widget); - if (idx > -1) { - this.widgets.splice(idx, 1); - } - }); - this.dynamicWidgets = []; - }; - - // Add a dynamic widget and track it - this.addDynamicWidget = function (type, name, value, callback, options) { - const widget = this.addWidget(type, name, value, callback, options); - this.dynamicWidgets.push(widget); - return widget; - }; - - // Main refresh function - this.refreshUI = function () { - console.log("[ConfigBuilder] Refreshing UI..."); - - // Clean up ALL dynamic widgets first - this.cleanupDynamicWidgets(); - - const config = this.getConfig(); - - // Add "Add Array" button at the top - this.addDynamicWidget("button", "➕ Add LoRA Array", "add_array", () => { - const config = this.getConfig(); - config.arrays.push({ - name: `Array ${config.arrays.length + 1}`, - combine: false, - loras: [] - }); - this.saveConfig(config); - this.refreshUI(); - }); - - // Render each array - config.arrays.forEach((array, arrayIdx) => { - this.renderArray(array, arrayIdx); - }); - - // Force node to recalculate size - this.setSize(this.computeSize()); - - console.log("[ConfigBuilder] UI refresh complete"); - }; - - // Render a single array - this.renderArray = function (array, arrayIdx) { - const config = this.getConfig(); - - // Separator - const sep = this.addDynamicWidget("button", "═══════════════════", `sep_top_${arrayIdx}`, () => {}); - sep.disabled = true; - - // Array name - const nameWidget = this.addDynamicWidget( - "text", - `📁 Array Name`, - array.name, - (value) => { - const config = this.getConfig(); - config.arrays[arrayIdx].name = value; - this.saveConfig(config); - }, - { multiline: false } - ); - - // Stack toggle - const stackWidget = this.addDynamicWidget( - "toggle", - `Stack All (Combine)`, - array.combine, - (value) => { - const config = this.getConfig(); - config.arrays[arrayIdx].combine = value; - this.saveConfig(config); - } - ); - - // Add LoRA button - this.addDynamicWidget("button", `➕ Add LoRA`, `add_lora_${arrayIdx}`, () => { - const config = this.getConfig(); - config.arrays[arrayIdx].loras.push({ - type: "lora", - name: "None", - str_model: 1.0, - str_clip: 1.0 - }); - this.saveConfig(config); - this.refreshUI(); - }); - - // Render each LoRA in this array - array.loras.forEach((lora, loraIdx) => { - this.renderLora(arrayIdx, loraIdx, lora); - }); - - // Delete array button - this.addDynamicWidget("button", `🗑️ Delete Array "${array.name}"`, `delete_array_${arrayIdx}`, () => { - const config = this.getConfig(); - config.arrays.splice(arrayIdx, 1); - this.saveConfig(config); - this.refreshUI(); - }); - }; - - // Render a single LoRA - this.renderLora = function (arrayIdx, loraIdx, lora) { - const config = this.getConfig(); - const loraLabel = ` LoRA ${loraIdx + 1}`; - - // Type selector - this.addDynamicWidget( - "combo", - `${loraLabel} Type`, - lora.type, - (value) => { - const config = this.getConfig(); - config.arrays[arrayIdx].loras[loraIdx].type = value; - // Update name to appropriate default - if (value === "folder") { - config.arrays[arrayIdx].loras[loraIdx].name = "/"; - } else { - config.arrays[arrayIdx].loras[loraIdx].name = "None"; + // Load state + try { + const existing = JSON.parse(configWidget.value); + if (existing.config_arrays) { + this.state = existing; + // Migration check: ensure models is an array + this.state.config_arrays.forEach(arr => { + if (arr.model && !arr.models) { + arr.models = [arr.model]; + delete arr.model; } - this.saveConfig(config); - this.refreshUI(); // Refresh to update name dropdown - }, - { values: ["lora", "folder"] } - ); - - // Name selector (dropdown with appropriate options) - if (lora.type === "folder") { - // Folder selector - this.addDynamicWidget( - "combo", - `${loraLabel} Folder`, - lora.name, - (value) => { - const config = this.getConfig(); - config.arrays[arrayIdx].loras[loraIdx].name = value; - this.saveConfig(config); - }, - { values: loraFolders } - ); - } else { - // LoRA file selector - this.addDynamicWidget( - "combo", - `${loraLabel} File`, - lora.name, - (value) => { - const config = this.getConfig(); - config.arrays[arrayIdx].loras[loraIdx].name = value; - this.saveConfig(config); - }, - { values: availableLoras } - ); + if (!arr.models) arr.models = ["None"]; + }); + } else if (existing.lora_config) { + this.state = this.migrateOldFormat(existing); } + } catch (e) {} - // Model strength slider - this.addDynamicWidget( - "number", - `${loraLabel} Model Strength`, - lora.str_model, - (value) => { - const config = this.getConfig(); - config.arrays[arrayIdx].loras[loraIdx].str_model = value; - this.saveConfig(config); - }, - { min: -10, max: 10, step: 0.01, precision: 2 } - ); - - // CLIP strength slider - this.addDynamicWidget( - "number", - `${loraLabel} CLIP Strength`, - lora.str_clip, - (value) => { - const config = this.getConfig(); - config.arrays[arrayIdx].loras[loraIdx].str_clip = value; - this.saveConfig(config); - }, - { min: -10, max: 10, step: 0.01, precision: 2 } - ); - - // Delete LoRA button - this.addDynamicWidget("button", `${loraLabel} 🗑️ Delete`, `delete_lora_${arrayIdx}_${loraIdx}`, () => { - const config = this.getConfig(); - config.arrays[arrayIdx].loras.splice(loraIdx, 1); - this.saveConfig(config); - this.refreshUI(); - }); - - // Mini separator - const miniSep = this.addDynamicWidget("button", " ─────", `sep_lora_${arrayIdx}_${loraIdx}`, () => {}); - miniSep.disabled = true; + this.migrateOldFormat = function(oldState) { + const arrays = oldState.lora_config?.arrays || []; + return { + session_name: oldState.session_name || "my_test_session", + include_none: oldState.include_none !== undefined ? oldState.include_none : true, + config_arrays: arrays.map(arr => ({ + name: arr.name, + samplers: oldState.samplers || "euler", + schedulers: oldState.schedulers || "normal", + steps: oldState.steps || "20", + cfg: oldState.cfg || "7.0", + models: oldState.model ? [oldState.model] : ["None"], // Handle legacy model string + loras: arr.loras || ["None"], + combine: arr.combine || false + })) + }; }; - // Initial render - this.refreshUI(); + this.saveState = function() { + configWidget.value = JSON.stringify(this.state, null, 2); + this.updatePreview(); + }; - // Watch for load_session changes - const loadSessionWidget = this.widgets?.find(w => w.name === "load_session"); - if (loadSessionWidget) { - const originalCallback = loadSessionWidget.callback; - loadSessionWidget.callback = function(value) { - if (originalCallback) { - originalCallback.call(this, value); - } - - // If a session is selected (not "None"), trigger node execution - if (value && value !== "None") { - console.log(`[ConfigBuilder] Load session changed to: ${value}`); - console.log("[ConfigBuilder] Triggering execution to load session data..."); + this.htmlContainer = document.createElement("div"); + this.htmlContainer.style.cssText = `width: 100%; height: 100%; background: #1a1a1a; display: flex; flex-direction: column;`; + this.addDOMWidget("config_ui", "div", this.htmlContainer, { serialize: false, hideOnZoom: false }); + + // --- UI RENDER --- + this.renderUI = function() { + this.htmlContainer.innerHTML = ` + +
+ `; - // Handle execution results to update widgets with loaded data - const onExecuted = this.onExecuted; - this.onExecuted = function(message) { - if (onExecuted) { - onExecuted.apply(this, arguments); - } + const root = this.htmlContainer.querySelector("#cb-root"); - // Check if we have loaded values in the output - if (message && message.length >= 7) { - const [configs_json, session_name, loaded_samplers, loaded_schedulers, loaded_steps, loaded_cfg, loaded_lora_config] = message; - - console.log("[ConfigBuilder] Received loaded session data, updating widgets..."); - - // Update widget values with loaded data - const samplersWidget = this.widgets?.find(w => w.name === "samplers"); - const schedulersWidget = this.widgets?.find(w => w.name === "schedulers"); - const stepsWidget = this.widgets?.find(w => w.name === "steps"); - const cfgWidget = this.widgets?.find(w => w.name === "cfg"); - const sessionNameWidget = this.widgets?.find(w => w.name === "session_name"); - - if (samplersWidget && loaded_samplers) { - samplersWidget.value = loaded_samplers; - console.log(`[ConfigBuilder] Updated samplers: ${loaded_samplers}`); - } - if (schedulersWidget && loaded_schedulers) { - schedulersWidget.value = loaded_schedulers; - console.log(`[ConfigBuilder] Updated schedulers: ${loaded_schedulers}`); - } - if (stepsWidget && loaded_steps) { - stepsWidget.value = loaded_steps; - console.log(`[ConfigBuilder] Updated steps: ${loaded_steps}`); - } - if (cfgWidget && loaded_cfg) { - cfgWidget.value = loaded_cfg; - console.log(`[ConfigBuilder] Updated cfg: ${loaded_cfg}`); - } - if (sessionNameWidget && session_name) { - sessionNameWidget.value = session_name; - console.log(`[ConfigBuilder] Updated session_name: ${session_name}`); - } - if (this.configWidget && loaded_lora_config) { - this.configWidget.value = loaded_lora_config; - console.log(`[ConfigBuilder] Updated lora_config`); - - // Refresh UI to show loaded LoRAs - this.refreshUI(); - } - } + // Top Section + const topRow = document.createElement("div"); + topRow.className = "cb-sections-row"; + this.renderSessionSection(topRow); + // this.renderOptionsSection(topRow); + root.appendChild(topRow); + + // Config Arrays + const configSection = document.createElement("div"); + configSection.className = "cb-section full-width"; + configSection.innerHTML = '