From 6d1fcdb87f69ab5fa572c66013b22100c994c145 Mon Sep 17 00:00:00 2001 From: JasonHoku Date: Mon, 9 Mar 2026 15:32:37 -0700 Subject: [PATCH] chore: gitignore local-only docs and remove from tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProjectStructure.md, README.md, Roadmap.md, AI-CONTEXT.md, and docs/ are local development references — not needed in the repo. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 7 +- ProjectStructure.md | 722 -------- README.md | 1530 ----------------- Roadmap.md | 292 ---- ...26-02-28-builder-ui-improvements-design.md | 224 --- .../2026-02-28-builder-ui-improvements.md | 780 --------- .../2026-03-01-builder-ui-fixes-design.md | 113 -- docs/plans/2026-03-01-builder-ui-fixes.md | 850 --------- ...2026-03-01-builder-ui-navigation-design.md | 134 -- .../plans/2026-03-01-builder-ui-navigation.md | 784 --------- ...026-03-01-per-config-resolutions-design.md | 114 -- .../2026-03-01-per-config-resolutions.md | 455 ----- ...026-03-02-sync-models-to-workers-design.md | 179 -- .../2026-03-02-sync-models-to-workers.md | 650 ------- ...5-builder-dashboard-improvements-design.md | 67 - ...26-03-05-builder-dashboard-improvements.md | 35 - ...026-03-08-seed-flicker-upscale-cooldown.md | 955 ---------- ...03-09-favorites-workflow-packing-design.md | 63 - .../2026-03-09-favorites-workflow-packing.md | 765 --------- 19 files changed, 6 insertions(+), 8713 deletions(-) delete mode 100644 ProjectStructure.md delete mode 100644 README.md delete mode 100644 Roadmap.md delete mode 100644 docs/plans/2026-02-28-builder-ui-improvements-design.md delete mode 100644 docs/plans/2026-02-28-builder-ui-improvements.md delete mode 100644 docs/plans/2026-03-01-builder-ui-fixes-design.md delete mode 100644 docs/plans/2026-03-01-builder-ui-fixes.md delete mode 100644 docs/plans/2026-03-01-builder-ui-navigation-design.md delete mode 100644 docs/plans/2026-03-01-builder-ui-navigation.md delete mode 100644 docs/plans/2026-03-01-per-config-resolutions-design.md delete mode 100644 docs/plans/2026-03-01-per-config-resolutions.md delete mode 100644 docs/plans/2026-03-02-sync-models-to-workers-design.md delete mode 100644 docs/plans/2026-03-02-sync-models-to-workers.md delete mode 100644 docs/plans/2026-03-05-builder-dashboard-improvements-design.md delete mode 100644 docs/plans/2026-03-05-builder-dashboard-improvements.md delete mode 100644 docs/plans/2026-03-08-seed-flicker-upscale-cooldown.md delete mode 100644 docs/plans/2026-03-09-favorites-workflow-packing-design.md delete mode 100644 docs/plans/2026-03-09-favorites-workflow-packing.md diff --git a/.gitignore b/.gitignore index 37e52e9..3459afd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,9 @@ .claude_data/ .claude/ /.claude_data/ -/.claude/ \ No newline at end of file +/.claude/ +AI-CONTEXT.md +ProjectStructure.md +README.md +Roadmap.md +docs/ \ No newline at end of file diff --git a/ProjectStructure.md b/ProjectStructure.md deleted file mode 100644 index 0522cf8..0000000 --- a/ProjectStructure.md +++ /dev/null @@ -1,722 +0,0 @@ -### **ComfyUI Registry Security Requirements** - -This node is published to the ComfyUI Registry. Every version is scanned by an automated -security scanner. Versions that fail the scan receive `NodeVersionStatusFlagged` status and -are hidden from ComfyUI Manager. **All contributors must follow these rules:** - -1. **No `import requests`** — Use `urllib.request` (stdlib) instead. The scanner flags - outbound HTTP libraries as potential data exfiltration vectors. -2. **No `subprocess`, `os.system`, `eval()`, `exec()`** — These are blocked as arbitrary - code execution risks. -3. **No runtime `pip install`** — Package installation must go through `requirements.txt` - only, never called at runtime. -4. **No code obfuscation** — No encoded strings, multi-statement tricks, or undefined - variable patterns. -5. **No custom file-serving endpoints** — Use ComfyUI's built-in `WEB_DIRECTORY` for JS and - the built-in `/view` endpoint for images. Never use `web.FileResponse` or create custom - routes that serve files from disk. For external images, create symlinks into the output - directory and use `/view?filename=X&type=output&subfolder=Y`. -6. **Path containment on all filesystem operations** — Every endpoint that touches the - filesystem must validate that resolved paths stay within the expected base directory - (e.g., `output/benchmarks/`). Use `os.path.realpath()` + `startswith()` checks. -7. **Sanitize all user-supplied path components** — Session names, filenames, etc. must be - sanitized with `re.sub(r'[^\w\-]', '', name)` or `os.path.basename()` before use. - -Reference: https://blog.comfy.org/p/comfyui-2025-jan-security-update - ---- - -### **Updated Project File Structure** - -```text -ComfyUI-Ultimate-Sampler-Grid/ -├── __init__.py # Server Routes & Node Mappings -├── sampler_node.py # Main "Sampler Grid" Node -├── dashboard_node.py # "Dashboard Viewer" Node -├── config_builder_node.py # "Config Builder" Node -├── json_text_node.py # "Smart JSON" Node -│ -├── generation_orchestrator.py # The "Conductor" (Loop Logic, ~1700 lines) -├── image_generation.py # KSampler & VAE Wrappers -├── model_loader.py # Checkpoint/LoRA/VAE/GGUF Loading Logic -├── model_cache.py # 3-Tier Model Caching System -├── remote_vae.py # Remote VAE Offloading -│ -├── batch_encoding.py # Batch CLIP Encoding with Combinator Support -├── conditioning_cache.py # Disk-based Conditioning Cache -├── lora_utils.py # LoRA Searching & Validation -├── trigger_words.py # CivitAI Trigger Word Fetching & Prompt Assembly -├── config_utils.py # Config Parsing, Expansion & Cartesian Products -├── directory_scanner.py # External Directory Session Scanner -├── metadata_packer.py # PNG/WebP Metadata Embedding for Exports -├── manifest_utils.py # Manifest Read/Write/Merge Helpers -├── html_generator.py # Reads /resources/ to build the Dashboard HTML -│ -├── distribution_manager.py # Distributed Job Queue Coordinator (Master) -├── distribution_worker.py # Worker Thread for Remote Processing -├── distribution_routes.py # Distribution REST API Endpoints -│ -├── web/ # [ComfyUI Integration Layer] -│ ├── dashboard.js # Registers Dashboard Node & Message Handling -│ ├── smart_json_text.js # Registers JSON Node & Syntax Highlighting -│ └── conf_builder/ # [Config Builder UI Modules] -│ ├── conf-builder-main.js # Main entry, widget registration -│ ├── conf-builder-ui-components.js # UI rendering (dropdowns, sliders, cards) -│ ├── conf-builder-config-management.js # Save/Load/Duplicate config logic -│ ├── conf-builder-utilities.js # State-to-JSON conversion, iteration counting -│ └── conf-builder-distribution.js # Distribution settings UI (workers, toggles) -│ -└── resources/ # [The Dashboard SPA Core] - ├── template.html # HTML Skeleton for the generated report - ├── report.css # Styling (Infinite Canvas, Cards, Modals) - ├── logic_init.js # Bootstrapper (Load JSON, Init State) - ├── logic_state.js # State Store (Redux-style: filters, favorites) - ├── logic_pipeline.js # Data Processing (Filter -> Sort -> Layout) - ├── logic_virtual.js # Virtual DOM / Infinite Scroller Engine - ├── logic_ui.js # UI Renderer (DOM creation, Modal handling) - ├── logic_events.js # Interaction Handler (Hotkeys, API calls) - └── logic_utils.js # Helpers (Debounce, Formatters) - -``` - -**Output Directory Structure (Generated at Runtime):** -```text -ComfyUI/output/ -├── ultimate-configs/ # Saved config presets (NOT under benchmarks/) -│ └── {name}.json -└── benchmarks/ - ├── loras_tags.json # LoRA trigger word cache (shared) - ├── model_hashes.json # SHA256 hash cache for CivitAI lookups - ├── model-data/ # CivitAI metadata cache per model - │ └── {model_name}/metadata.json - └── {session_name}/ - ├── manifest.json # Generation metadata + user annotations - ├── images/ - │ └── img_{n}.webp - └── favorites/ # Created by export_favorites endpoint -``` - ---- - -### **Project Architecture Overview** - -**ComfyUI-Ultimate-Sampler-Grid** is a hybrid application. - -1. **The Backend (Python):** Runs inside ComfyUI. It orchestrates the grid generation, manages resources (caching/loading), and serves the API. -2. **The Frontend Bridge (JS):** Runs in the ComfyUI browser tab. It registers the custom nodes and widgets. -3. **The Dashboard SPA (HTML/JS/CSS):** A standalone, virtualized "IDE" for viewing results. It is generated by the backend but runs entirely in the client's browser, communicating back to the server only when saving changes. - ---- - -### **Node Class Mappings** - -| Internal Name | Python Class | Purpose | -|---|---|---| -| `UltimateSamplerGrid` | `SamplerGridTester` | Main generation node — receives configs, runs Cartesian grid | -| `UltimateGridDashboard` | `SamplerConfigDashboardViewer` | Dashboard viewer — renders iframe with results | -| `UltimateConfigBuilder` | `UltimateConfigBuilder` | Visual config builder — outputs `configs_json` string | -| `SmartJSONText` | `SmartJSONTextNode` | JSON text widget with syntax highlighting | - ---- - -### **API Routes Reference** - -All routes registered in `__init__.py` via `PromptServer.instance.routes`. - -**Config Builder APIs** (`/configbuilder/`): - -| Method | Route | Purpose | -|---|---|---| -| GET | `/configbuilder/list_configs` | List saved config preset filenames | -| POST | `/configbuilder/save_config` | Save config preset `{name, data}` | -| POST | `/configbuilder/load_config` | Load config preset `{name}` | -| POST | `/configbuilder/lookup_triggers` | Bulk LoRA trigger word lookup `{loras: [...]}` | -| POST | `/configbuilder/lookup_lora_metadata` | Full CivitAI metadata for LoRA `{lora_name}` | -| POST | `/configbuilder/lookup_model_metadata` | Full CivitAI metadata for model `{model_name, model_type}` | -| GET | `/configbuilder/model_lists` | All model/sampler/scheduler/VAE lists | -| POST | `/configbuilder/refresh_models` | Signal frontend cache clear | - -**Dashboard/Tester APIs** (`/config_tester/`): - -| Method | Route | Purpose | -|---|---|---| -| POST | `/config_tester/delete_session` | Delete session folder `{session_name}` | -| POST | `/config_tester/save_changes` | Delta save — only changed items `{session_name, changed_items}` | -| POST | `/config_tester/save_manifest` | Full merge save `{session_name, manifest}` | -| POST | `/config_tester/get_session_html` | Generate dashboard HTML on-demand `{session_name, node_id}` | -| POST | `/config_tester/export_favorites` | Export favorited images `{session_name, pack_metadata, organize_by_prompt, organize_by_lora}` | -| POST | `/config_tester/scan_directory` | Scan external dir, create symlinks, use built-in `/view` `{directory_path, session_name}` | - -**Distribution APIs** (`/distribution/`, registered in `distribution_routes.py`): - -| Method | Route | Purpose | -|---|---|---| -| GET | `/distribution/claim_job` | Worker claims next pending job (200 + job JSON, 204 = no jobs, 503 = inactive) | -| POST | `/distribution/submit_result` | Worker uploads completed image (multipart: {metadata, image}) | -| GET | `/distribution/status` | Get distribution status, worker stats, job counts | -| POST | `/distribution/register_worker` | Worker registration with master | -| GET | `/distribution/download_model` | Download model file for model sync `{category, filename}` | -| POST | `/distribution/heartbeat` | Worker keepalive signal | - ---- - -### **1. The Backend: Core Logic & Orchestration** - -*Located in the root directory.* - -* **`sampler_node.py`** (The Interface) -* **Role:** The primary ComfyUI node (`SamplerGridTester`). Validates inputs and passes control to the orchestrator. -* **Key Functions:** `run_tests()` (entry point → delegates to `generation_orchestrator.run_generation_loop()`), `find_existing_match()`, `get_latent_channels()` -* **Key Logic:** Disables caching features if "Optional" inputs are connected (since external changes can't be tracked). `IS_CHANGED()` returns `NaN` when optionals connected (forces re-execution), deterministic hash otherwise. -* **Required Inputs:** `ckpt_name`, `positive_text`, `negative_text`, `seed`, `denoise`, `configs_json`, `resolutions_json`, `session_name`, `overwrite_existing`, `flush_batch_every`, `add_random_seeds_to_gens`, `lora_triggerwords_mode`, `remote_vae_endpoint`, `save_conditioning_cache_to_file`, `enable_model_cache` -* **Optional Inputs:** `optional_model` (MODEL), `optional_clip` (CLIP), `optional_vae` (VAE), `optional_positive` (CONDITIONING), `optional_negative` (CONDITIONING), `optional_latent` (LATENT) - - -* **`generation_orchestrator.py`** (The Conductor — largest file, ~1700 lines) -* **Role:** Manages the generation loop, distributed processing, and master text encoding. -* **Key Functions:** `run_generation_loop()`, `check_if_job_completed()`, `setup_session_directories()`, `load_model_by_type()`, `get_model_cache_key()`, `calculate_clip_hash()`, `_conditioning_to_serializable()`, `_serializable_to_conditioning()`, `_preencode_all_conditionings()`, `_run_distributed_generation()`, `_send_distribution_status()` -* **Key Logic:** -* **Smart Skip:** `check_if_job_completed()` matches against manifest items by seed/resolution/sampler/scheduler/steps/cfg/denoise. Skips model/lora/prompt matching when optional inputs connected. -* **Lookahead:** Triggers `model_cache.preload_lora_model()` / `preload_base_model()` for the *next* job while the current one runs. -* **Interrupts:** Catches `InterruptProcessingException` during text encoding AND image generation. Flushes pending VAE batch and saves manifest before stopping. -* **ETA Tracking:** Sends `ultimate_grid.progress` events to dashboard via `PromptServer.instance.send_sync()`. -* **Node Change Detection:** Detects when connected upstream nodes have changed for smarter job resuming. -* **Pre-encoding:** Single model → batch-encode all unique prompts upfront. Multi-model → re-encode per model switch. -* **VAE Switching:** Flushes pending batch before switching VAE. Tracks `default_model_vae` for reverting to "Default". -* **Distribution:** `_run_distributed_generation()` manages the distributed pipeline — populates job queue, optionally pre-encodes conditionings, notifies workers, and processes master's share of jobs. -* **Master Text Encoding:** `_preencode_all_conditionings()` groups configs by (model, LoRA), loads each combination, encodes all unique prompts, and serializes via `_conditioning_to_serializable()`. Handles multi-entry conditionings (AND combinator). -* **Per-Worker Logging:** Final summary prints each worker's completed/failed job counts from `manager.get_status()["workers"]`. - - - - -* **`image_generation.py`** (The Worker) -* **Role:** Wraps KSampler and VAE Decode. -* **Key Logic:** Implements `flush_batch_with_vae` to save images and update the manifest in chunks (preventing VRAM overflow). - - -* **`config_builder_node.py`** (The Builder UI Backend) -* **Role:** `UltimateConfigBuilder` node. All state stored in single `lora_config` widget as JSON string. -* **Key Functions:** `generate_config()`, `process_lora_array()`, `get_available_sessions()`, `expand_lora_folders()`, `lookup_lora_triggers()` -* **Key Logic:** Python reads ONLY `lora_config` widget — all other INPUT_TYPES widgets (samplers, schedulers, steps, cfg) are vestigial and ignored. Also defines API routes for trigger/metadata lookup and model lists. -* **Distribution Output:** When distribution is enabled, wraps configs in `{"configs": [...], "_distribution": {...}}` with keys: `enabled`, `worker_urls`, `claim_timeout` (default 600), `use_master_encoding`, `sync_models_to_workers`. When disabled, still wraps in `{"configs": [...]}` — `sampler_node.py` must unwrap in both cases. - - -* **`html_generator.py`** (The Builder) -* **Role:** Compiles the SPA into a single inlined HTML string. -* **Key Functions:** `get_html_template(title, manifest_data, node_id)` -* **Key Logic:** Reads all files from `/resources/` in dependency order, injects them into `template.html`, embeds `manifest.json` as `fullManifest` JS variable. Replaces placeholders: `__CSS_CONTENT__`, `__JS_CONTENT__`, `__TITLE__`, `__NODE_ID__`. No separate HTTP requests — everything inlined. -* **JS Load Order:** `logic_state.js` → `logic_utils.js` → `logic_ui.js` → `logic_virtual.js` → `logic_pipeline.js` → `logic_events.js` → `logic_init.js` - - - ---- - -### **2. The Backend: Resource Management** - -*Located in the root directory.* - -* **`model_cache.py`** (The 3-Tier Cache) -* **Role:** Thread-safe resource manager. Single `ModelCache` class. -* **Tiers:** 1. LoRA File Cache (raw state_dicts in RAM), 2. Incremental States (partial LoRA stacks via `check_incremental_cache()`), 3. Final Patched Models (`get_lora_model()` / `put_lora_model()`). -* **Key Methods:** `get_base_model()`, `put_base_model()`, `preload_lora_model()`, `preload_base_model()`, `register_schedule()`, `set_current_step()`, `clear()`, `print_stats()` -* **Async Preloading:** `preload_*` methods run loaders in background threads via `concurrent.futures`, results retrieved later. - - -* **`conditioning_cache.py`** (The Prompt Cache) -* **Role:** Caches CLIP text encodings to disk. -* **Key Logic:** Hashes `(Prompt + CLIP State + LoRA Config)` to ensure validity. Auto-disabled when optional inputs connected. - - -* **`batch_encoding.py`** (The Accelerator) -* **Role:** Bulk prompt encoder with combinator support. -* **Key Functions:** `encode_prompt_with_combinators(clip, text, clip_skip)`, `batch_encode_prompts(clip, positives, negatives, cond_cache, clip_skip)` -* **Combinators:** `AND` (multi-cond with optional weights like `:1.5`), `CAT` (tensor concatenation), `AVG(weight)` (weighted average, default 0.5), `BREAK` (native tokenizer segment break at 77-token chunks). - - -* **`model_loader.py`** (The Loader) -* **Role:** Checkpoint, LoRA, VAE, GGUF, and diffusion model loading. -* **Key Functions:** `load_checkpoint()`, `load_loras()`, `load_vae_by_name()`, `load_diffusion_model_and_clip()`, `load_loras_for_preencoding()`, `cleanup_model_references()`, `print_incompatible_loras_summary()` - - -* **`trigger_words.py`** (Trigger Word Engine) -* **Role:** CivitAI trigger word fetching and prompt assembly. -* **Key Functions:** `collect_unique_prompts_with_triggers(expanded_configs, mode)`, `build_prompt_with_triggers(config, mode)`, `get_filtered_lora_triggers(lora_string, omit_list)` -* **Modes:** `"None"`, `"Append To End"`, `"Append To Start"`, `"Read From Config"`. Per-LoRA placement via `lora_triggerwords_append_settings`. Also applies `model_prompt_prefix` / `model_prompt_suffix`. - - -* **`config_utils.py`** (Expansion Engine) -* **Role:** Config parsing, Cartesian product expansion, folder expansion. -* **Key Functions:** `expand_configs()`, `parse_prompt_input_nested()`, `prepare_input_jobs()`, `parse_lora_definition()`, `get_files_from_folder()`, `sanitize_session_name()` -* **Key Logic:** `parse_prompt_input_nested()` supports arbitrarily deep nested arrays for Cartesian prompting. `"folder/"` = expand individually, `"folder/*"` = stack all together. - - -* **`manifest_utils.py`** (Manifest Persistence) -* **Key Functions:** `load_existing_manifest()`, `save_manifest()`, `merge_manifest_user_changes()` -* **Key Logic:** `save_manifest()` calls `merge_manifest_user_changes()` first — reloads from disk to preserve user favorites/rejected/notes that may have changed concurrently. - - -* **`directory_scanner.py`** (External Image Scanner) -* **Key Functions:** `scan_directory_for_images()`, `parse_a1111_parameters()` -* **Key Logic:** Parses A1111-style PNG metadata into dashboard-compatible manifest items. Preserves user tags on re-scan by merging with existing manifest. - - -* **`metadata_packer.py`** (Export Metadata) -* **Key Functions:** `pack_metadata_into_image()`, `extract_metadata_from_image()`, `calculate_file_hash()` -* **Key Logic:** Packs ComfyUI-compatible metadata into PNG for CivitAI uploads. Maintains SHA256 hash cache at `benchmarks/model_hashes.json`. - - ---- - -### **2b. The Backend: Distribution System** - -*Located in the root directory. Enables multi-machine parallel processing.* - -* **`distribution_manager.py`** (The Job Coordinator) -* **Role:** Thread-safe distributed job queue running on the master. Manages job lifecycle: PENDING → CLAIMED → COMPLETED/FAILED. -* **Classes:** `JobState` (constants), `DistributedJob` (individual job), `DistributionManager` (coordinator) -* **Key Methods:** `populate_jobs()`, `claim_job(worker_id)`, `complete_job(job_id)`, `get_stats()`, `set_encoded_conditionings()`, `get_encoded_conditionings_for_job()` -* **Key Logic:** - * Thread-safe with `RLock` for concurrent worker access - * Job timeout handling (default 600s) — re-queues timed-out jobs as PENDING - * `MAX_FAIL_COUNT = 3` — jobs that fail 3 times are abandoned - * `_job_to_dict()` attaches pre-encoded conditionings to job claim response if available - * Tracks per-worker stats (completed, failed, last_seen) - -* **`distribution_worker.py`** (The Remote Worker) -* **Role:** Background thread running on remote ComfyUI instances. Polls master for jobs, processes locally, uploads results. -* **Class:** `WorkerThread(threading.Thread)` — daemon thread -* **Key Methods:** `run()` (main loop), `_process_job()`, `_download_model_from_master()`, `_prefetch_models()`, `stop()` -* **Key Logic:** - * Polls `/distribution/claim_job` every 2 seconds - * Caches model/CLIP/VAE between jobs (reuses when same model) - * Checks for `encoded_positive`/`encoded_negative` in job data — uses master conditionings if available, falls back to local CLIP encoding otherwise - * Model sync: downloads models from master via `/distribution/download_model` if enabled - * Exits after 30 consecutive empty polls or 503 response (master finished) - -* **`distribution_routes.py`** (The API Layer) -* **Role:** Registers `/distribution/*` API endpoints on the PromptServer. -* **Global State:** `_distribution_manager` (active manager, master mode), `_worker_thread` (active worker, worker mode) — both thread-safe via getters/setters -* **Key Endpoints:** `claim_job` (GET), `submit_result` (POST multipart), `status` (GET), `register_worker` (POST), `download_model` (GET), `heartbeat` (POST) - ---- - -### **Key Data Shapes** - -**Config Array State** (`node.state.config_arrays[n]` in JS, parsed from `lora_config` widget in Python): -```json -{ - "name": "Config 1", - "samplers": ["euler", "dpmpp_2m"], - "schedulers": ["normal", "karras"], - "steps": "20, 30", - "cfg": "7.0", - "models": [{"path": "model.safetensors", "type": "checkpoint"}], - "vaes": ["None"], - "loras": ["lora.safetensors:1.00:1.00"], - "lora_bypass_states": {"lora_name": true}, - "model_bypass_states": {"model_path": true}, - "lora_omit_triggers": [], - "lora_triggerwords_append_settings": {}, - "lora_strength_lock": {}, - "combine": false, - "positive_prompt_groups": [], - "negative_prompt": "", - "use_custom_prompts": false, - "model_prompt_prefix": "", - "model_prompt_suffix": "", - "attention_modes": ["default"], - "resolutions": [], - "seed_behavior": "fixed", - "text_encoders": [], - "clip_type": "stable_diffusion", - "gguf_options": {}, - "model_sampling_override": "None", - "model_sampling_shift": 3.0, - "use_advanced_sampling": false, - "advanced_guider": "Basic", - "use_flux_guidance": false, - "flux_guidance_value": 3.5 -} -``` - -**Distribution Config** (embedded in `config_builder_node.py` output as `_distribution` key): -```json -{ - "enabled": true, - "worker_urls": ["http://192.168.1.100:8188", "http://192.168.1.101:8188"], - "claim_timeout": 600, - "use_master_encoding": false, - "sync_models_to_workers": false -} -``` - -**Top-level state fields** (in `conf-builder-main.js` default state, outside `config_arrays`): -```json -{ - "session_name": "my_session", - "config_name": "my_config", - "auto_save": false, - "include_none": true, - "label_mode": "full", - "global_positive_groups": [], - "global_negative": "", - "distribution_enabled": false, - "worker_urls": [], - "claim_timeout": 600, - "use_master_encoding": false, - "sync_models_to_workers": false -} -``` - -**Manifest Item** (`manifest.json` `items[n]`): -```json -{ - "id": "uuid-string", - "seed": 12345, "cfg": 7.0, "steps": 20, - "sampler": "euler", "scheduler": "normal", - "model": "model.safetensors", "lora": "lora:1.0:1.0", - "vae": "vae.safetensors", "attention_mode": "default", - "positive": "prompt text", "negative": "negative text", - "denoise": 1.0, "width": 1024, "height": 1024, - "batch_idx": 0, "generation_time": 45.2, - "file": "/view?filename=img_0.webp&type=output&subfolder=benchmarks/session/images", - "favorite": false, "rejected": false, "notes": "" -} -``` - ---- - -### **Communication Patterns** - -**Widget Bridge (Config Builder → Python):** -``` -JS node.state → JSON.stringify() → lora_config widget.value → Python json.loads(lora_config) -``` -All other widget params in INPUT_TYPES (samplers, schedulers, steps, cfg) are ignored by Python. - -**Live Dashboard Updates (Server → Dashboard iframe):** -``` -generation_orchestrator → PromptServer.send_sync("ultimate_grid.update") - → dashboard.js api.addEventListener → postMessage({type: 'update_data'}) - → iframe logic_events.js message handler -``` - -**Progress Updates (Server → Dashboard iframe):** -``` -generation_orchestrator → PromptServer.send_sync("ultimate_grid.progress") - → dashboard.js → postMessage({type: 'progress_update'}) - → iframe #eta-bar update -``` - -**Dashboard → Parent Frame:** -``` -iframe postMessage({type: 'toggle_fullscreen', node_id}) - → dashboard.js listener → toggle .dashboard-fullscreen class -``` - -**Distribution Status Updates (Server → Dashboard iframe):** -``` -generation_orchestrator → PromptServer.send_sync("ultimate_grid.distribution_status") - → dashboard.js api.addEventListener → postMessage({type: 'distribution_status'}) - → iframe displays worker activity -``` - -**Distribution Job Flow (Master ↔ Worker):** -``` -Master: - 1. generation_orchestrator → DistributionManager.populate_jobs() - 2. [Optional] _preencode_all_conditionings() → manager.set_encoded_conditionings() - 3. POST /distribution/register_worker on each worker - 4. Workers poll GET /distribution/claim_job - 5. manager._job_to_dict() attaches encoded_positive/encoded_negative if available - 6. Worker processes job → POST /distribution/submit_result (multipart: metadata + image) - 7. Master saves image + updates manifest -``` - ---- - -### **3. The Frontend: ComfyUI Integration** - -*Located in `/web/`.* - -* **`dashboard.js`** -* **Role:** Registers the Dashboard node (`UltimateGridDashboard`). Listens for `ultimate_grid.update`, `ultimate_grid.progress`, and `ultimate_grid.distribution_status` server events. -* **Key Logic:** Forwards events to matching iframe via `postMessage()`. Auto-loads session when update received for unloaded session. Handles fullscreen toggle via `.dashboard-fullscreen` CSS class. **Auto-loads on node creation** with 500ms delay via `forceLoadSession()` so the dashboard shows content immediately (landing page or session grid) instead of a blank black box. -* **Widgets:** "RELOAD / SHOW SESSION" button → `forceLoadSession()`, "DELETE SESSION" button → POST `/config_tester/delete_session` -* **Event Listeners (Singletons):** Each uses `window.__ultimateGrid*ListenerInstalled` guard to prevent duplicate registration: - * `ultimate_grid.update` — Live data updates to iframe - * `ultimate_grid.progress` — ETA/progress bar updates - * `ultimate_grid.distribution_status` — Distribution worker status - * `window "message"` — Fullscreen toggle from iframe - - -* **`config_builder.js`** (entry point, loads `conf_builder/` modules) -* **Role:** Registers the Builder node (`UltimateConfigBuilder`). Entry point for the modular Config Builder UI. -* **Key Logic:** `onNodeCreated` **MUST be synchronous** (async init runs in fire-and-forget IIFE). Uses `Date.now()` cache-busting on module `import()` calls. Hooks `app.refreshComboInNodes()` to detect model/LoRA changes. -* **Node Methods:** `node.saveState()`, `node.renderUI()`, `node.loadSession(sessionName)`, `node.saveConfigToBackend()`, `node.loadConfigFromBackend(filename)`, `node.triggerAutoSave()` (2s debounce) - - -* **`smart_json_text.js`** -* **Role:** Registers the JSON node (`SmartJSONText`). Adds syntax highlighting and validation. - - -* **`conf_builder/conf-builder-utilities.js`** (The Data Layer) -* **Key Exports:** `convertStateToConfigs()`, `getIterationCount()`, `parseLoraString()`, `buildLoraString()`, `refreshAllConfigBuilders()`, `clearAllCaches()`, `normalizePath()`, `getAvailableLoras()`, `getModelLists()` -* **Global Caches:** `availableLoras`, `availableModels`, `availableVAEs`, `availableSamplers`, `availableSchedulers`, `availableSessions`, `availableConfigs` - - -* **`conf_builder/conf-builder-config-management.js`** (The UI Builder) -* **Key Exports:** `renderUI()`, `updatePreview()`, `renderConfigSection()`, `renderSessionSection()`, `createConfigArrayElement()`, `createModelElement()` - - -* **`conf_builder/conf-builder-ui-components.js`** (The Widget Library) -* **Key Exports:** `createSearchableSelect()`, `createSlider()`, `createInputGroup()`, `createTopBar()`, `createSidebar()`, `createSectionHeader()`, `getStyles()` - - -* **`conf_builder/conf-builder-distribution.js`** (Distribution Settings UI) -* **Role:** Renders the distribution settings section in the Config Builder. -* **Key Exports:** `renderDistributionSection()`, `renderDistributionSettingsSection()` -* **Features:** Worker URL management (add/remove), connection testing via `/distribution/test_worker`, status indicators (green/yellow/red dots), claim timeout slider (30-900s, default 600), toggles for "Use Master Text Encoding" and "Sync Models to Workers". - - ---- - -### **4. The Frontend: The Dashboard SPA** - -*Located in `/resources/`. This is the "App" users interact with.* - -* **`logic_init.js`** (The Bootstrapper) -* **Role:** Main Entry Point. -* **Key Logic:** -* **`initDashboard()`:** The first function called. -* **Data Injection:** detects if `fullManifest` exists (injected by Python). If not, it attempts to fetch it (View Mode). -* **Lifecycle:** Calls `initLogicState()` -> `applyPipeline()` -> `initVirtualScroller()` -> `initGlobalEvents()`. - - - - -* **`logic_state.js`** (The Brain) -* **Role:** Central State Store (Redux-style). -* **Key Logic:** -* **`State` Object:** Holds `items` (all images), `filteredItems` (visible), `selection` (active card), and `favorites`. -* **Triple JSON Lists:** Manages the logic for the "Accepted", "Favorites", and "Rejected" JSON bars at the bottom of the UI. -* **Persistence:** Saves user preferences (Sort Order, Column Count) to `localStorage` so they persist across reloads. - - - - -* **`logic_events.js`** (The Handler) -* **Role:** Interaction & API Bridge. -* **Key Logic:** -* **Keyboard Shortcuts:** Maps `Arrow Keys` (Pan), `Space` (Scroll), `+/-` (Zoom), `F` (Fit), `Escape` (close modals with priority chain). -* **API Calls:** Uses `fetch` to hit endpoints like `/config_tester/save_manifest` (when you star/reject an image) and `/config_tester/delete_session`. -* **Live Updates:** Listens for the `executed` event from ComfyUI to trigger a soft-refresh of the image list. - - - - -* **`logic_virtual.js`** (The Engine) -* **Role:** Virtual Scroller. -* **Key Logic:** Calculates viewport geometry to render only the ~20 visible cards out of thousands, enabling high performance. - - -* **`logic_pipeline.js`** (The Processor) -* **Role:** Filtering & Sorting. -* **Key Logic:** Transforms the raw item list based on active filters (Model, Sampler, etc.) before passing it to the virtual scroller. - - -* **`logic_ui.js`** (The Renderer) -* **Role:** DOM Manipulation. -* **Key Logic:** Generates the HTML for individual cards, the "Revise" modal, and the unified Settings panel (gear icon). Manages modal open/close with overlay and body overflow lock. - - -* **`logic_utils.js`** (The Helpers) -* **Role:** Utilities and server communication. -* **Key Functions:** `loadSession()`, `saveState()`, `exportFavorites()`, `scanDirectory()`, `triggerGen()`, `toggleFullscreen()`, `rejectItem()` - - - ---- - -### **Key Functions Quick Reference** - -**Python (by file):** - -| File | Key Functions | -|---|---| -| `sampler_node.py` | `run_tests()`, `find_existing_match()`, `get_latent_channels()`, `IS_CHANGED()` | -| `config_builder_node.py` | `generate_config()`, `process_lora_array()`, `get_available_sessions()`, `expand_lora_folders()` | -| `generation_orchestrator.py` | `run_generation_loop()`, `check_if_job_completed()`, `setup_session_directories()`, `load_model_by_type()`, `_preencode_all_conditionings()`, `_conditioning_to_serializable()`, `_serializable_to_conditioning()`, `_run_distributed_generation()` | -| `batch_encoding.py` | `encode_prompt_with_combinators()`, `batch_encode_prompts()` | -| `config_utils.py` | `expand_configs()`, `parse_prompt_input_nested()`, `prepare_input_jobs()`, `parse_lora_definition()` | -| `model_loader.py` | `load_checkpoint()`, `load_loras()`, `load_vae_by_name()`, `load_diffusion_model_and_clip()` | -| `model_cache.py` | `ModelCache.get_base_model()`, `.put_base_model()`, `.preload_lora_model()`, `.register_schedule()` | -| `trigger_words.py` | `collect_unique_prompts_with_triggers()`, `build_prompt_with_triggers()`, `get_filtered_lora_triggers()` | -| `manifest_utils.py` | `load_existing_manifest()`, `save_manifest()`, `merge_manifest_user_changes()` | -| `directory_scanner.py` | `scan_directory_for_images()`, `parse_a1111_parameters()` | -| `metadata_packer.py` | `pack_metadata_into_image()`, `extract_metadata_from_image()`, `calculate_file_hash()` | -| `distribution_manager.py` | `DistributionManager.populate_jobs()`, `.claim_job()`, `.complete_job()`, `.get_stats()`, `.set_encoded_conditionings()`, `.get_encoded_conditionings_for_job()` | -| `distribution_worker.py` | `WorkerThread.run()`, `._process_job()`, `._download_model_from_master()`, `._prefetch_models()`, `.stop()` | -| `distribution_routes.py` | `get_distribution_manager()`, `set_distribution_manager()`, endpoint handlers for `/distribution/*` | - -**JavaScript (by file):** - -| File | Key Functions | -|---|---| -| `conf-builder-utilities.js` | `convertStateToConfigs()`, `getIterationCount()`, `parseLoraString()`, `buildLoraString()`, `refreshAllConfigBuilders()` | -| `conf-builder-config-management.js` | `renderUI()`, `updatePreview()`, `createConfigArrayElement()`, `createModelElement()` | -| `conf-builder-ui-components.js` | `createSearchableSelect()`, `createSlider()`, `getStyles()` | -| `conf-builder-main.js` | `ensureModulesLoaded()`, `node.saveState()`, `node.renderUI()`, `node.loadSession()`, `node.triggerAutoSave()` | -| `conf-builder-distribution.js` | `renderDistributionSection()`, `renderDistributionSettingsSection()` | -| `logic_ui.js` | `toggleCogMenu()`, `toggleFiltersPopup()`, `initFilters()` | -| `logic_utils.js` | `loadSession()`, `exportFavorites()`, `scanDirectory()`, `triggerGen()`, `toggleFullscreen()` | -| `logic_virtual.js` | `renderDOM()`, `updateVisibleItems()`, `calculateVisibleRange()`, `autoFitZoom()`, `goToImage()` | -| `logic_pipeline.js` | `updateDataPipeline()`, `executePipeline()`, `processNewData()`, `incrementalFilter()` | -| `logic_events.js` | Message handler for `progress_update` and `update_data` types | - ---- - -### **Summary of Data Flow** - -1. **User Config:** You set parameters in the **Config Builder** (visual UI) or **Sampler Node** (JSON text). -2. **Config Output:** `config_builder_node.py` wraps configs in `{"configs": [...], "_distribution": {...}}`. The `sampler_node.py` unwraps the `"configs"` key and extracts distribution settings. -3. **Expansion:** `generation_orchestrator.py` expands configs into a full list of jobs (Cartesian product). -4. **Distribution (optional):** If enabled, `_run_distributed_generation()` populates the job queue, optionally pre-encodes conditionings, and notifies workers. Workers poll for jobs, process locally, and upload results. -5. **Generation:** The backend loops through its share of jobs, using **ModelCache** and **ConditioningCache** for speed. -6. **Storage:** Images are saved to disk; metadata is appended to `manifest.json`. -7. **Compilation:** `html_generator.py` reads the `manifest.json` and all `.js` files in `/resources/`, baking them into a single `dashboard_html` string. -8. **Display:** The **Dashboard Node** (JS) renders this HTML in an iframe. Auto-loads on node creation. -9. **Boot:** `logic_init.js` calls `init()` which calls `showSessionLandingIfEmpty()`. If no data, the session landing page is shown. If data exists, `logic_virtual.js` renders the grid. -10. **Interaction:** You click "Favorite" in the UI -> `logic_events.js` sends an API request to `__init__.py` -> Python updates `manifest.json` on disk. - ---- - -### **Critical Development Gotchas** - -> ⚠️ **GOLDEN RULE: DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY.** - -1. **Dual Data Path** — Frontend `convertStateToConfigs()` (in `conf-builder-utilities.js`) and Python `generate_config()` (in `config_builder_node.py`) are **independent implementations** that both transform `node.state` into config JSON. Both must stay in sync when adding new config fields or the UI preview will mismatch the actual output. - -2. **Widget Bridge** — The `lora_config` widget (STRING type) holds the **entire** node state as serialized JSON. Python `generate_config()` ignores all other widget params and reads only `lora_config`. The `samplers`, `schedulers`, `steps`, `cfg` widgets in `INPUT_TYPES` are vestigial (kept for ComfyUI schema compliance only). - -3. **onNodeCreated Must Be Synchronous** — In `conf-builder-main.js`, `onNodeCreated` MUST remain synchronous. Async initialization runs inside a fire-and-forget `(async () => { ... })()` IIFE. Making `onNodeCreated` itself async breaks widget registration. - -4. **Resources Are Inlined** — `html_generator.py` reads all `/resources/` files and bakes them into one HTML string. Zero external HTTP requests. Changes to dashboard JS/CSS require re-generating the HTML (re-run the sampler node or call `/config_tester/get_session_html`). - -5. **Manifest Merge on Save** — `manifest_utils.py` `save_manifest()` calls `merge_manifest_user_changes()` which reloads from disk before saving. This preserves user favorites/rejected/notes that may have been changed concurrently while generation was running. - -6. **Optional Inputs Disable Caches** — `sampler_node.py` disables conditioning cache when `optional_model`/`optional_clip`/`optional_positive`/`optional_negative` are connected. `IS_CHANGED()` returns `NaN` (forces re-execution). `check_if_job_completed()` skips model/lora/prompt matching. - -7. **Folder Expansion Syntax** — `"folder/"` = expand to individual entries (one combination each). `"folder/*"` = stack ALL files in folder together as a single combined entry. Weight array syntax `"lora:[0.5, 0.8]:1.0"` creates grid search over strengths. No trailing slash = single file match. - -8. **LoRA String Format** — `"name:model_str:clip_str"`, stacked with `" + "` separator. `parseLoraString()` in JS, `parse_lora_definition()` in Python. Default strengths: 1.0 for both if colon-separated parts are missing. - -9. **Cache Busting** — `conf-builder-main.js` uses `Date.now()` timestamp on `import()` calls for module loading. Also intercepts `window.fetch` to detect `/object_info` calls and trigger cache refresh. Static JS served via ComfyUI's `/extensions/` path. - -10. **Config Presets Path** — Saved to `output/ultimate-configs/` (defined in `__init__.py`), NOT `output/benchmarks/configs/`. Session data goes under `output/benchmarks/{session_name}/`. - -11. **Field Name Inconsistency: `favorite` vs `favorited`** — `manifest_utils.py` `merge_manifest_user_changes()` uses the key `"favorite"`. But `__init__.py` routes like `save_manifest` and `export_favorites` check `"favorited"`. When touching favorite/unfavorite logic, check which key name the specific code path uses. - -12. **Prompt Expansion is Recursive** — `parse_prompt_input_nested()` in `config_utils.py` supports arbitrarily deep nested arrays. Flat list = OR (options). List containing sub-lists = AND (Cartesian product). This is used by both the node text inputs and per-config prompt groups from the builder UI. - -13. **Config Builder Output is ALWAYS Wrapped** — `config_builder_node.py` always outputs `{"configs": [...]}` (with optional `"_distribution"` key). The `sampler_node.py` MUST unwrap the `"configs"` key before passing to `expand_configs()`. Both the `_distribution` present AND absent cases must be handled (see lines 269-286 of `sampler_node.py`). - -14. **Dashboard Auto-Load on Creation** — `web/dashboard.js` calls `forceLoadSession()` with 500ms delay when a dashboard node is created. The `get_session_html` endpoint in `__init__.py` returns a full HTML template with empty manifest for non-existent sessions (NOT a 404). This ensures the landing page shows immediately. - -15. **`logic_pipeline.js` Legacy Init Guard** — Lines 489-505 have a `DOMContentLoaded` handler that calls `loadSession()` ONLY when `fullManifest.items.length > 0`. Without this guard, `loadSession()` triggers an alert popup for non-existent sessions. The proper initialization path is `logic_init.js` → `init()` → `showSessionLandingIfEmpty()`. - -16. **Distribution Default Values Must Stay in Sync** — `claim_timeout` default (600) appears in: `distribution_manager.py` (constructor), `config_builder_node.py` (fallback), `conf-builder-distribution.js` (slider default), `conf-builder-main.js` (default state). All four must match. Same for `use_master_encoding` default (false). - -17. **Conditioning Serialization Uses np.copy()** — `_serializable_to_conditioning()` in `generation_orchestrator.py` must call `np.frombuffer(...).copy()` before `torch.from_numpy()` because `np.frombuffer()` returns read-only arrays that are incompatible with GPU tensor operations. - -18. **Multi-Entry Conditioning (AND combinator)** — The AND combinator creates multiple `[tensor, {pooled_output}]` entries in a conditioning list. Serialization in `_conditioning_to_serializable()` must iterate ALL entries, not just `[0]`. Deserialization in `_serializable_to_conditioning()` must reconstruct the full list. - -19. **State Migration for New Fields** — When adding new fields to the Config Builder default state in `conf-builder-main.js` (around line 89), also add a migration check (around line 363) to backfill the default for existing saved workflows. Without migration, old workflows will have `undefined` for the new field. - -20. **ComfyUI Registry Security** — No `import requests`, no `subprocess`/`os.system`/`eval()`/`exec()`, no runtime `pip install`, no custom file-serving endpoints, no code obfuscation. Use `urllib.request` for HTTP. Use ComfyUI's `/view` endpoint for serving files. Sanitize all user-supplied paths. See top of this file for full rules. - ---- - -### **Notes for AI Development** - -This section provides critical context for AI assistants working on this codebase. - -**Architecture Mental Model:** -``` -┌─────────────────────────────────────────────────────────────┐ -│ ComfyUI Browser Tab │ -│ ┌──────────────────┐ ┌──────────────────────────────────┐│ -│ │ Config Builder │ │ Dashboard Node ││ -│ │ (conf-builder-*) │ │ (dashboard.js) ││ -│ │ Direct DOM widget │ │ ┌──────────────────────────────┐││ -│ │ │ │ │ iframe (srcdoc) │││ -│ │ │ │ │ Dashboard SPA │││ -│ │ │ │ │ (resources/logic_*.js) │││ -│ │ │ │ │ postMessage() ↕ │││ -│ │ │ │ └──────────────────────────────┘││ -│ └──────────────────┘ └──────────────────────────────────┘│ -│ ↕ widget value ↕ server events │ -├─────────────────────────────────────────────────────────────┤ -│ ComfyUI Server (Python) │ -│ ┌──────────────┐ ┌────────────────┐ ┌────────────────┐ │ -│ │config_builder│ │sampler_node.py │ │__init__.py │ │ -│ │_node.py │→ │ │ │(API endpoints) │ │ -│ └──────────────┘ │ ↓ │ └────────────────┘ │ -│ │generation_ │ │ -│ │orchestrator.py │ │ -│ │ ↓ │ │ -│ ┌─────────┐ │model_loader + │ ┌─────────────────┐ │ -│ │ModelCache│←────│image_generation│ │DistributionMgr │ │ -│ └─────────┘ │ ↓ │ │(master jobs) │ │ -│ │html_generator │ └─────────────────┘ │ -│ └────────────────┘ ↕ HTTP │ -│ ┌─────────────────┐ │ -│ │WorkerThread │ │ -│ │(remote workers) │ │ -│ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -**File Dependency Graph (Python):** -``` -sampler_node.py - → generation_orchestrator.py (main entry) - → config_utils.py (expand_configs, prepare_input_jobs) - → model_loader.py (load_checkpoint, load_loras) - → image_generation.py (generate_image, flush_batch) - → batch_encoding.py (encode_prompt_with_combinators) - → trigger_words.py (build_prompt_with_triggers) - → manifest_utils.py (save_manifest, load_existing_manifest) - → model_cache.py (ModelCache) - → conditioning_cache.py (ConditioningCache) - → remote_vae.py (RemoteVAEDecodeWorker) - → html_generator.py (get_html_template) - → distribution_manager.py (DistributionManager) - → distribution_worker.py (WorkerThread) - -config_builder_node.py - → lora_utils.py (trigger word lookup, CivitAI API) - -__init__.py - → all node classes (sampler, dashboard, config_builder, json_text) - → distribution_routes.py (distribution API endpoints) - → html_generator.py (on-demand HTML generation) - → metadata_packer.py (export favorites) - → directory_scanner.py (scan external directories) - → manifest_utils.py (save/load manifests) -``` - -**Server Event Types:** - -| Event Name | Sender | Data Fields | Purpose | -|---|---|---|---| -| `ultimate_grid.update` | `generation_orchestrator.py` | `session_name, node, manifest, meta, new_items` | New images generated | -| `ultimate_grid.progress` | `generation_orchestrator.py` | `session_name, progress_pct, eta_str, current_job, total_jobs, avg_duration, total_generated, total_elapsed, finish_time` | ETA/progress bar | -| `ultimate_grid.distribution_status` | `generation_orchestrator.py` | `session_name, workers, stats` | Distribution worker status | - -**Testing Changes:** -- **Python backend files:** Restart ComfyUI -- **`web/` JS files:** Refresh browser (Ctrl+F5) -- **`resources/` JS/CSS/HTML files:** Dashboard HTML must be regenerated — re-run the sampler node or call `/config_tester/get_session_html` -- **Config Builder state fields:** Also update migration checks in `conf-builder-main.js` \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 67a89a0..0000000 --- a/README.md +++ /dev/null @@ -1,1530 +0,0 @@ -# ComfyUI Ultimate Auto Sampler Config Grid Testing Suite - -
-Want to support development of this project? Buy me a coffee on Ko-fi: -

- - Support Me on Ko-fi - -

- -**A professional-grade benchmarking and "IDE-like" testing suite for ComfyUI.** - -Stop guessing which Sampler, Scheduler, Prompt, Denoise, Model, Lora or CFG value works best. This custom node suite allows you to generate massive Cartesian product grids, view them in an interactive infinite-canvas dashboard, and refine your settings with a "Revise & Generate" workflow without ever leaving the interface. - -image - ---- - -## 📑 Table of Contents - -- [Key Features](#-key-features) - - [Powerful Grid Generation](#-powerful-grid-generation) - - [Interactive Dashboard (The "IDE")](#-interactive-dashboard-the-ide) - - [The "Revise & Generate" Workflow](#-the-revise--generate-workflow) - - [Curation & JSON Export](#-curation--json-export) - - [Config Builder Node (Visual UI For Creating Runs)](#-config-builder-node-visual-ui) - - [Basic Setup](#basic-setup) - - [Understanding the Interface](#understanding-the-interface) - - [Session Management](#session-management) - - [Config Management (Save/Load Presets)](#config-management-saveload-presets) - - [Config Arrays](#config-arrays) - - [Fast CivitAI Info Lookup](#fast-civitai-info-lookup) -- [Installation](#-installation) -- [Getting Started](#getting-started) - - [The Nodes](#1-the-nodes) - - [Generator Node Parameters](#2-generator-node-parameters) - - [The JSON Configuration](#3-the-json-configuration) - - [Hybrid Inputs (Optional)](#4-hybrid-inputs-optional) - - - [Basic Parameters](#basic-parameters) - - [Iteration Count Display](#iteration-count-display) - - [Config Array Controls](#config-array-controls) - - [Models Section](#models-section) - - [LoRAs Section](#loras-section) - - [LoRA Trigger Words](#lora-trigger-words) - - [Trigger Word Lookup Modal](#trigger-word-lookup-modal) - - [Trigger Word Placement Control](#trigger-word-placement-control) - - [LoRA Bypass States](#lora-bypass-states) - - [Strength Locking](#strength-locking) - - [Folder Expansion (Models & LoRAs)](#folder-expansion-models--loras) - - [LoRA Stacking](#lora-stacking) - - [VAEs Section](#vaes-section) - - [Attention Modes](#attention-modes) - - [Model-Specific Prompts](#model-specific-prompts) - - [Recursive Cartesian Prompt Builder](#recursive-cartesian-prompt-builder) -- [Dashboard Interface](#-dashboard-interface) - - [Header Bar](#header-bar) - - [Toolbar](#toolbar) - - [Navigation & Controls](#navigation--controls) - - [Card Overlays](#card-overlays) - - [JSON Bars](#json-bars-bottom---horizontal-layout) - - [Revision Modal](#revision-modal) -- [Example Workflows](#-example-workflows) - - [Quick Quality Test](#quick-quality-test-40-images) - - [Multi-Model Comparison](#multi-model-comparison-9-images) - - [LoRA Stack Testing](#lora-stack-testing-24-images) - - [Random LoRA Selection](#random-lora-selection) - - [Full Model Folder Test](#full-model-folder-test) - - [CLIP Skip for Anime Models](#clip-skip-for-anime-models) - - [LoRA Trigger Word Filtering](#lora-trigger-word-filtering) - - [Model Folder Expansion](#model-folder-expansion) - - [LoRA Strength Sweep](#lora-strength-sweep) - - [Quality Enhancement Stack](#quality-enhancement-stack) -- [Preset Configs](#-preset-configs) -- [Performance Tips](#-performance-tips) -- [Tips & Best Practices](#-tips--best-practices) -- [Troubleshooting](#-troubleshooting) - - [Generation Issues](#generation-issues) - - [Dashboard Issues](#dashboard-issues) - - [LoRA & Trigger Word Issues](#lora--trigger-word-issues) - - [Model & CLIP Issues](#model--clip-issues) - - [Config Builder UI Issues](#config-builder-ui-issues) - - [Remote VAE Issues](#remote-vae-issues) - - [Interrupt/Cancel Issues](#interruptcancel-issues) - - [Browser Compatibility](#browser-compatibility) -- [JSON Output Format Reference](#json-output-format-reference) -- [API Integration (Config Builder)](#api-integration-config-builder) -- [File Locations](#file-locations) -- [Keyboard Shortcuts](#keyboard-shortcuts) -- [Distributed Processing](#-distributed-processing) - - [Setting Up Distribution](#setting-up-distribution) - - [Master Text Encoding](#master-text-encoding) - - [Model Sync](#model-sync) - - [Distribution Troubleshooting](#distribution-troubleshooting) -- [Notes for AI Development](#-notes-for-ai-development) -- [Changelog](#-changelog) -- [License](#-license) -- [Credits](#-credits) - ---- - -## 🌟 Key Features - -### 🚀 Powerful Grid Generation - -image - - -* **Cartesian Product Engine:** Automatically generates every permutation of your input settings. Test unlimited Samplers, Schedulers, CFG scales, Sizes, Prompts, LoRA combinations all in one go. -* **Visual Config Builder:** A point-and-click GUI for building complex sampler configurations — no JSON editing required. Searchable dropdowns, drag sliders, folder expansion, LoRA stacking, and trigger word management all built in. -* **Non-Standard Model Support:** Full support for SD3, Flux, Z-Image, and other non-standard architectures with automatic latent channel detection. -* **Multi-Model Support:** Test multiple checkpoints in a single run by passing an array of model names or folder paths. -* **Model Folder Expansion:** Use `"model": "FolderName/"` to test all checkpoints in a folder automatically — perfect for comparing different versions or architectures. -* **CLIP Skip Support:** Control which CLIP layer to use for text encoding with the `clip_skip` parameter — essential for anime models (typically -2) vs realistic models (typically 0). -* **Intelligent CLIP Encoding:** Automatically detects when multiple models are used and handles CLIP encoding correctly per-model to ensure accurate results. -* **Batch Encoding on Model Switch:** When switching between models, all prompts are batch-encoded at once rather than per-generation, resulting in 3-6x faster encoding for multi-model workflows. -* **Multi-LoRA Stacking:** Layer multiple LoRAs with custom strengths using the `+` separator. Supports folder expansion for testing entire LoRA directories. -* **LoRA Trigger Word Filtering:** Use `lora_omit_triggers` to exclude specific trigger words from auto-appended LoRA triggers, giving you fine control over prompts. -* **Random LoRA Selection:** Randomly select LoRAs from folders with `[count,strength]` or `[count,strength,random]` syntax. Supports both reproducible (seed-based) and truly random selection modes. -* **Auto LoRA Trigger Words:** Automatically fetches and appends LoRA trigger words from CivitAI API using SHA256 hash lookup. Results are cached locally for offline use. -* **Multi-Seed Generation:** Add extra random variations per config with the `add_random_seeds_to_gens` parameter — perfect for evaluating consistency. -* **Smart Caching:** Intelligently skips model and LoRA reloading when consecutive runs share the same resources, making generation instant for parameter tweaks. -* **Stop & Resume:** Intelligent skip detection — if you stop a generation mid-run, resuming will skip already-generated images and continue where you left off. -* **Advanced Skip Logic:** Uses conditioning tensor hashing to detect prompt changes even when using pre-encoded conditioning from CLIP nodes. -* **LoRA Compatibility Detection:** Automatically detects and skips incompatible LoRAs with detailed error reporting, preventing log spam. -* **Graceful Interruption:** Cancel button now stops ALL remaining jobs (not just current generation) and saves all completed work including pending remote VAE decodes. -* **VAE Batching:** Includes a `vae_batch_size` input to batch decode images, significantly speeding up large grid runs. -* **Live Dashboard Updates:** Configure `flush_batch_every` to update the dashboard incrementally (e.g., every 4 images) instead of waiting for the entire batch to complete. -* **Remote VAE Support:** Offload VAE decoding to remote servers (HuggingFace endpoints or local) for 20-30% faster generation and lower VRAM usage. -* **VAE Selection & Iteration:** Test multiple VAEs per config — add individual VAEs from a searchable dropdown or expand entire VAE folders. Each VAE becomes a dimension in the Cartesian grid. -* **Attention Mode Testing:** Grid-test different attention implementations (`default`, `sageattn`, `xformers`, etc.) as a combinatorial dimension. Find the optimal attention backend for your hardware. -* **CLIP Encoding Combinators:** Advanced prompt syntax with `AND`, `CAT`, `AVG(weight)`, and `BREAK` keywords for multi-segment conditioning. `AND` creates separate conditioning entries, `CAT` concatenates token contexts, `AVG(0.5)` blends prompts, and `BREAK` forces new 77-token chunks. -* **Model-Specific Prompts:** Per-config prompt prefix and suffix fields for model-appropriate quality tags (e.g., prepend `"masterpiece, best quality"` for anime models, append `"4k, photorealistic"` for realistic models). -* **Recursive Cartesian Prompt Builder:** Visual chip-based prompt editor with `{option1|option2|option3}` syntax that expands into all combinations. Nest multiple groups for exponential prompt variation. -* **Model & LoRA On/Off Switches:** Toggle individual models and LoRAs on/off without removing them from the config. Bypassed entries are omitted from both the preview JSON and the generated config output. - -### 🌐 Distributed Processing (Multi-Machine) -* **Multi-Worker Distribution:** Split large grid runs across multiple ComfyUI instances. A master node coordinates job distribution while workers process images in parallel and upload results back. -* **One-Click Setup:** Enable distribution in the Config Builder's Distribution Settings section. Add worker URLs (e.g., `http://192.168.1.100:8188`) and toggle "Enable Distribution." -* **Worker Connection Testing:** Test connectivity to each worker with a single click — status dots show green (reachable), yellow (testing), or red (error). -* **Master Text Encoding:** Optional "Use Master Text Encoding" toggle — the master pre-encodes ALL unique prompts (including LoRA-specific combinations) and sends the encoded conditionings to workers. Workers skip CLIP encoding entirely, saving significant time on prompt-heavy grids. -* **Model Sync:** Optional "Sync Models to Workers" toggle — automatically transfers checkpoint and LoRA files from the master to workers that don't have them. -* **Smart Job Queue:** Thread-safe job distribution with automatic re-queuing on worker timeout (configurable, default 600s) and max 3 retries before abandoning a job. -* **Per-Worker Logging:** Final generation summary shows each worker's completed/failed job counts for full visibility into the distributed run. -* **Live Distribution Status:** Dashboard receives real-time distribution status updates showing worker activity and progress. -* **Backward Compatible:** Workers gracefully fall back to local CLIP encoding if master-encoded conditionings aren't available. - -### 🎨 Interactive Dashboard (The "IDE") -* **Infinite Canvas with Pan/Zoom:** Google Maps-style navigation with mouse drag, mousewheel zoom, and keyboard shortcuts. -* **Virtual Scrolling:** Ultra-optimized rendering handles thousands of images smoothly by only loading visible items — scroll through 5000+ images without lag. -* **Mobile Touch Support:** Full pinch-to-zoom and pan gestures on mobile devices with optimized touch controls. -* **Fullscreen Mode:** Click the fullscreen button (⛶) to expand the dashboard to fill your entire screen. -* **Favorites System:** Star your best images with a ⭐ button — favorites are collected in a separate gold JSON bar for easy export. -* **Smart Filtering:** Toggle visibility by Model, Sampler, Scheduler, Denoise, or LoRA type. **Shift+Click** to isolate a single filter for quick A/B testing. -* **Intelligent Sorting:** Instantly sort your grid by **Oldest**, **Newest**, or **Fastest** (generation time). Your preference is saved to localStorage. -* **Go to Image #:** Jump directly to any image number with the "Go to #" input field in the header. -* **Auto-Load Sessions:** Dashboard automatically loads when generation starts — no manual session name entry needed. -* **Session Management:** Save and load previous testing sessions directly from the UI. -* **Keyboard Navigation:** `Space` to scroll rows, `Arrow Keys` to pan, `+/-` to zoom, `0` to reset, `F` to auto-fit. -* **Real-Time ETA Progress Bar:** Shows estimated time remaining during generation with a progress bar in the dashboard header. -* **Unified Settings Panel:** Gear icon (⚙️) opens a consolidated settings panel with Session management, Grid Display controls, Export Favorites, and Scan External Directory — replacing the old separate SESSION button. -* **Scan External Directory:** Load image sessions from any folder (including other ComfyUI instances) into the dashboard for viewing and filtering. Reads PNG metadata for full config display. -* **Export Favorites:** Copy all favorited images to a dedicated export folder with optional CivitAI metadata packing for sharing. - -### ⚡ The "Revise & Generate" Workflow -* **One-Click Revision:** Click "REVISE" on any image to open a detail view. -* **Complete Metadata View:** Shows model, seed, prompts (with trigger words), and all generation parameters. -* **Instant Tweak:** Adjust CFG, Steps, or Sampler for *just that specific image*. -* **Generate New:** A "GENERATE NEW" button queues the new variation immediately without needing to disconnect wires or change the main batch. -* **Similarity Reel:** The revision modal shows a side-scrolling reel of all other images that share the same seed, allowing for perfect A/B comparison. -* **Multiple Prompts:** Use an array to run multiple prompts in one run without re-running your entire workflow: `["picture of a forest", "mountains at night", "masterpiece, painting of dog"]` - -### 🧹 Curation & JSON Export -* **Rejection System:** Click the red **"✕"** on bad generations to hide them. -* **Triple JSON Bars (Horizontal Layout):** - * **Green Bar (Left):** Automatically groups all *accepted* configs into a clean, optimized JSON array ready for copy-pasting. - * **Gold Bar (Center):** Contains all *favorited* configs — your best-performing settings. - * **Red Bar (Right):** Collects all *rejected* configs so you know exactly what settings to avoid. - ---- - -## 📦 Installation - -1. Navigate to your ComfyUI `custom_nodes` directory: - ```bash - cd ComfyUI/custom_nodes/ - ``` - -2. Clone this repository: - ```bash - git clone https://github.com/YOUR_USERNAME/ComfyUI-Ultimate-Auto-Sampler-Config-Grid-Testing-Suite.git - ``` - -3. Restart ComfyUI. - -The Config Builder node is included automatically — no separate installation is required. - -**Requirements:** -- ComfyUI (latest version recommended) - -**File Structure:** -``` -ComfyUI/custom_nodes/ComfyUI-Ultimate-Auto-Sampler/ -├── __init__.py # Server routes, API endpoints, node mappings -├── sampler_node.py # Main "Sampler Grid" node -├── dashboard_node.py # "Dashboard Viewer" node -├── config_builder_node.py # "Config Builder" node backend -├── json_text_node.py # "Smart JSON" node -│ -├── generation_orchestrator.py # Main orchestration layer & generation loop -├── image_generation.py # KSampler & VAE wrappers -├── model_loader.py # Checkpoint/LoRA/VAE loading and patching -├── model_cache.py # 3-tier model caching system -├── remote_vae.py # Remote VAE offloading -│ -├── batch_encoding.py # CLIP batch encoding with combinator support -├── conditioning_cache.py # Disk-based conditioning cache -├── lora_utils.py # LoRA searching & validation -├── trigger_words.py # CivitAI trigger word fetching & prompt assembly -├── config_utils.py # Config parsing, expansion & Cartesian products -├── directory_scanner.py # External directory session scanner -├── metadata_packer.py # PNG/WebP metadata embedding for exports -├── manifest_utils.py # Manifest read/write/merge helpers -├── html_generator.py # Reads /resources/ to build dashboard HTML -│ -├── distribution_manager.py # Distributed job queue coordinator (master) -├── distribution_worker.py # Worker thread for remote processing -├── distribution_routes.py # Distribution API endpoints -│ -├── web/ # [ComfyUI Integration Layer] -│ ├── dashboard.js # Dashboard node registration & messaging -│ ├── smart_json_text.js # JSON node & syntax highlighting -│ └── conf_builder/ # [Config Builder UI Modules] -│ ├── conf-builder-main.js # Entry point, widget registration -│ ├── conf-builder-ui-components.js # Reusable UI (dropdowns, sliders, cards) -│ ├── conf-builder-config-management.js # Save/Load/state rendering logic -│ ├── conf-builder-utilities.js # Data fetching, caching, calculations -│ └── conf-builder-distribution.js # Distribution settings UI -│ -└── resources/ # [Dashboard SPA Core] - ├── template.html # HTML skeleton for dashboard - ├── report.css # Styling (infinite canvas, cards, modals) - ├── logic_state.js # Global state variables - ├── logic_utils.js # API & helper functions - ├── logic_ui.js # UI renderer (DOM creation, modals) - ├── logic_virtual.js # Virtual scroll engine & viewport - ├── logic_pipeline.js # Sorting & filtering pipeline - ├── logic_events.js # Event listeners (messages, keyboard) - └── logic_init.js # Initialization & startup -``` - ---- - -## Getting Started - -### 1. The Nodes -This suite consists of three nodes found under the `sampling/testing` category: - -1. **Ultimate Sampler Grid (Generator):** The engine. It handles model loading, grid generation, and saving. -2. **Ultimate Grid Dashboard (Viewer):** The display. It renders the HTML output. -3. **Ultimate Config Builder:** The visual GUI for building configs without writing JSON. - -**Basic Setup:** -* Add the **Generator** node. -* Connect your Checkpoint, CLIP, and VAE (optional, see "Hybrid Inputs" below). -* Add the **Viewer** node. -* Connect the `dashboard_html` output from the Generator to the input of the Viewer. -* *(Optional)* Add the **Config Builder** node and connect its `configs_json` output to the Generator's `configs_json` input. - -### 2. Generator Node Parameters - -#### Core Settings -* **`ckpt_name`**: Default checkpoint to use (can be overridden by `model` in JSON or optional input). -* **`positive_text`** / **`negative_text`**: Prompts. Supports arrays: `["prompt 1", "prompt 2"]` or JSON arrays. -* **`seed`**: Base seed. Each config uses this seed unless overridden. -* **`denoise`**: Denoise strength(s). Supports single value, array, or comma-separated: `"1.0"` or `"0.8, 0.9, 1.0"`. - -#### Performance Settings -* **`vae_batch_size`**: How many images to decode at once. - - `4` (default): Good for 8-12GB VRAM - - `12-24`: For 24GB+ VRAM - - `-1`: Decode all images at once (fastest, but risky) - -* **`flush_batch_every`**: Update dashboard every X images (0 = use VAE batch size). - - `0`: Wait until VAE batch is full - - `4`: Update dashboard every 4 images (recommended for live monitoring) - -#### Advanced Settings -* **`overwrite_existing`**: - - `False` (default): Skip already-generated images (resume mode) - - `True`: Re-generate everything - -* **`add_random_seeds_to_gens`**: Generate X extra random variations per config. - - `0` (default): Only use base seed - - `3`: Generate 3 additional random seed variations per config - - Random seeds are deterministic per base seed — changing base seed generates new random variations - -* **`lookup_and_append_lora_triggerwords`**: Automatically fetch and append LoRA trigger words. - - `False` (default): Use prompts as-is - - `True`: Calculate SHA256 hash of each LoRA, query CivitAI API for trigger words, cache results locally, and prepend to prompts - - Cache stored in `loras_tags.json` for offline use - -* **`session_name`**: Folder name where results are saved (`ComfyUI/output/benchmarks/{session_name}/`). - -### 3. The JSON Configuration -The `configs_json` widget determines your grid. It accepts an array of objects. All fields support single values or arrays. You can write JSON by hand or use the [Config Builder Node](#-config-builder-node-visual-ui) to build it visually. - -**Basic Example:** -```json -[ - { - "sampler": ["euler", "dpmpp_2m"], - "scheduler": ["normal", "karras"], - "steps": [20, 30], - "cfg": [7.0, 8.0] - } -] -``` -*Generates 16 images (2 samplers × 2 schedulers × 2 steps × 2 cfg)* - -**Advanced Features:** - -#### Multi-Model Testing -```json -[ - { - "sampler": "euler", - "scheduler": "normal", - "steps": 20, - "cfg": 7.0, - "model": [ - "sd_xl_base_1.0.safetensors", - "juggernautXL_v9.safetensors", - "ponyDiffusionV6XL_v6.safetensors" - ] - } -] -``` -*Tests 3 different models with the same settings* - -#### Folder Expansion -```json -[ - { - "sampler": "euler", - "scheduler": "normal", - "steps": 20, - "cfg": 7.0, - "model": "sdxl_models/" - } -] -``` -*Trailing `/` tests ALL models in the folder.* - -#### Multi-LoRA with Stacking -```json -[ - { - "sampler": "euler", - "scheduler": "normal", - "steps": 20, - "cfg": 7.0, - "lora": [ - "None", - "style_lora.safetensors:0.8:1.0", - "style_lora.safetensors:0.5:1.0 + detail_lora.safetensors:1.0:1.0", - "loras_folder/" - ] - } -] -``` -*LoRA format: `filename:strength_model:strength_clip`* -*Stack with `+` separator: `lora1:0.8:1.0 + lora2:1.0:1.0`* - -#### Resolution Grid -```json -// In resolutions_json input: -[ - [1024, 1024], - [1024, 1536], - [1536, 1024] -] -``` - -#### Multiple Prompts -```json -// In positive_text input (as JSON array): -[ - "a beautiful landscape, mountains, sunset", - "cyberpunk city at night, neon lights", - "portrait of a warrior, detailed armor" -] -``` - -### 4. Hybrid Inputs (Optional) -The Generator node features built-in widgets for Model Selection and Prompts, but also has **Optional Inputs** for flexibility: -* **Standalone Mode:** Use the dropdown menu to select a checkpoint and type prompts into the text boxes. -* **Hybrid Mode:** Connect a `MODEL`, `CLIP`, `VAE`, or `CONDITIONING` wire. The node will automatically ignore the internal widget and use the connected input instead. -* **Non-Standard Models:** For SD3, Flux, Z-Image, and other architectures: - - Connect `optional_model`, `optional_clip`, and `optional_vae` from your model loader - - Connect `optional_positive` and `optional_negative` for pre-encoded conditioning - - The node automatically detects latent channel count (4 for SD1.5/SDXL, 16 for SD3/Flux/Z-Image) - - Skip detection uses conditioning tensor hashing to properly detect prompt changes -* **Latent Input:** Connect a `LATENT` to use img2img or upscaling workflows. - - For SD3/Flux: Use `EmptySD3LatentImage` instead of `EmptyLatentImage` - - Latent dimensions are automatically preserved - ---- - -## 🔧 Config Builder Node (Visual UI) - -image - -The Config Builder node replaces manual JSON editing with an intuitive visual interface. Instead of writing configuration arrays by hand, you can point and click to select models and LoRAs, drag sliders to adjust strengths, expand entire folders, stack multiple LoRAs, manage trigger words, and preview JSON output in real-time. - -All configuration data is automatically synchronized with the Ultimate Sampler Grid node. - -### Basic Setup - -1. **Add the Node:** Right-click on the canvas → `Add Node` → `sampling/testing` → `Ultimate Config Builder` -2. **Connect to Sampler:** - - Connect the `configs_json` output to the `configs_json` input of the `Ultimate Sampler Grid (Generator)` node - - Connect the `session_name` output to the `session_name` input of the generator (optional) -3. **Start Building:** The Config Builder UI will appear in the node with one config array ready to customize. - -### Understanding the Interface - -``` -┌─────────────────────────────────────────────────┐ -│ 📁 Session Management │ -│ (Session name, Load session, Refresh button) │ -├─────────────────────────────────────────────────┤ -│ 💾 Config Management │ -│ (Save/Load JSON presets, Auto-save toggle) │ -├─────────────────────────────────────────────────┤ -│ ⚙️ Config Arrays │ -│ ┌─────────────────────────────────────────┐ │ -│ │ Config 1 │ │ -│ │ - Samplers, Schedulers, Steps, CFG │ │ -│ │ - Models Section (expandable) │ │ -│ │ - LoRAs Section (expandable) │ │ -│ │ - Omit Triggers Section │ │ -│ └─────────────────────────────────────────┘ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ Config 2... │ │ -│ └─────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────┤ -│ 📋 JSON Preview │ -│ (Real-time preview of generated config) │ -└─────────────────────────────────────────────────┘ -``` - -### Session Management - -Located at the top of the interface, this section controls the output session name for your tests. - -* **Session Name:** Sets the folder name where results are saved (`ComfyUI/output/benchmarks/{session_name}/`). Enter a descriptive name like `"sdxl_quality_test"` or `"character_lora_comparison"`. Changes automatically update the connected Sampler Grid node. -* **Load Session:** Load a previously saved session's metadata from a searchable dropdown. -* **Refresh Models/LoRAs:** Reload the list of available models and LoRAs from disk. Use when you've added new files or the dropdowns are showing stale data. - -### Config Management (Save/Load Presets) - -Save and load configuration presets as JSON files for reuse across projects. - -* **Config Name (Filename):** Name for the JSON preset file. Stored at `ComfyUI/output/benchmarks/configs/{config_name}.json`. -* **Load Saved Config:** Searchable dropdown showing all available `.json` config files. Replaces current config arrays with the loaded preset. -* **Save Config Now:** Manual save button to persist current configuration to disk. Creates a reusable JSON file containing all config arrays, models, LoRAs, trigger settings, and strength locks. -* **Auto-Save (2s):** Toggle to automatically save the config file every 2 seconds. Recommended for long editing sessions. - -### Config Arrays - -Config Arrays are the building blocks of your testing grid. Each array defines a set of parameters that will be multiplied into all possible combinations. - -### Fast CivitAI Info Lookup - -image - -image - -image - -#### Basic Parameters - -* **Config Name:** Label for this config group (purely organizational, not used in generation). -* **Samplers:** Comma-separated list of samplers to test (e.g., `"euler, dpmpp_2m, dpmpp_2m_sde"`). -* **Schedulers:** Comma-separated list of schedulers (e.g., `"normal, karras, exponential"`). -* **Steps:** Comma-separated list of step counts (e.g., `"20, 30, 40"`). -* **CFG:** Comma-separated list of CFG scale values (e.g., `"6.0, 7.0, 8.0"`). -* **Samplers Dropdown:** Searchable dropdown auto-populated with all available samplers from the connected sampler node. Select multiple to add them as a comma-separated list. -* **Schedulers Dropdown:** Searchable dropdown auto-populated with all available schedulers. Works the same as the samplers dropdown. -* **Attention Mode:** Dropdown selector for attention implementations (`default`, `sageattn`, `xformers`, etc.). Select multiple to test as a grid dimension. - -#### Iteration Count Display - -``` -⏱️ Iterations: 144 -``` - -Shows the total number of images this config array will generate: -**Formula:** Models × LoRAs × Samplers × Schedulers × Steps × CFG -Updates in real-time as you add/remove items. - -#### Config Array Controls - -* **➕ Add Model** — Adds a new model slot to the Models section. -* **➕ Add LoRA** — Adds a new LoRA slot to the LoRAs section. -* **📋 Duplicate Config** — Creates an exact copy of this config array (useful for variations). -* **🗑️ Delete Config** — Removes this entire config array. -* **▼ Collapse/Expand** — Toggle visibility of Models and LoRAs sections. -* **➕ Add VAE** — Adds a new VAE slot to the VAEs section for testing multiple VAE decoders. - -### Models Section - -Each model card includes: - -* **Model Name (Searchable):** Dropdown with fuzzy search through all available checkpoints. Supports folder structure display (e.g., `"subfolder/model.safetensors"`). -* **Type Selection:** - - **Single File (default):** Test just this specific model. - - **Folder (Separate):** Expand folder — each model becomes a separate test. Multiplies iteration count by number of models in folder. - - **Folder (Combined):** *Reserved for future use.* -* **🗑️ Remove Model:** Deletes this model from the config (at least one model is always required). -* **🔘 On/Off (Bypass) Switch:** Toggle a model on or off without removing it. Bypassed models are grayed out and excluded from the generated config output. -* **🔍 CivitAI Model Lookup:** Opens a metadata panel showing model info fetched from CivitAI (name, base model, tags, creator, description). Includes a direct link to the model page. - -### LoRAs Section - -Each LoRA card includes: - -* **LoRA Name (Searchable):** Dropdown with fuzzy search. Supports special syntax — end with `/` for folder expansion, end with `/*` for combined folder stack. -* **Type Selection:** - - **Single File:** Load this specific LoRA. - - **Folder (Separate):** Test each LoRA in the folder individually with the same strength settings. - - **Folder (Combined):** Stack ALL LoRAs in the folder into a single load. -* **Model Strength Slider:** Range 0.0–2.0 (default 1.0). Controls how strongly the LoRA affects the model. Number input accepts values beyond slider range. -* **CLIP Strength Slider:** Range 0.0–2.0 (default 1.0). Controls how strongly the LoRA affects text encoding. Independent from model strength. -* **🔗 Lock Strengths:** Toggle to link model and CLIP strengths so both sliders move together. -* **🔍 Lookup Trigger Words:** Opens the Trigger Word Lookup Modal (fetches from CivitAI API). -* **🚫 Bypass Trigger Fetch:** Toggle to prevent automatic trigger word lookup for this LoRA. -* **🗑️ Remove LoRA:** Deletes this LoRA from the config (at least one is always required). -* **🔘 On/Off (Bypass) Switch:** Toggle a LoRA on or off without removing it. Bypassed LoRAs are grayed out and excluded from both the preview JSON and the Python node's `config_json` output. - -### LoRA Trigger Words - -The Config Builder integrates with CivitAI's API to automatically fetch and manage trigger words for your LoRAs. - -**How It Works:** -1. When you select a LoRA, the node calculates its SHA256 hash. -2. The hash is sent to CivitAI to retrieve metadata. -3. Results are cached in `ComfyUI/output/benchmarks/loras_tags.json`. -4. Cached data is reused on subsequent loads (offline-capable). - -#### Trigger Word Lookup Modal - -Click the **🔍 Lookup Trigger Words** button to open the modal: - -``` -╔════════════════════════════════════════════╗ -║ LoRA Trigger Word Manager ║ -║ ║ -║ LoRA: character_style_v2.safetensors ║ -║ Model: Character Style LoRA v2 ║ -║ Base Model: SDXL 1.0 ║ -║ Creator: ArtistName ║ -║ ║ -║ Tags: character, anime, style ║ -║ ║ -║ Trigger Words: ║ -║ ☑️ character_name ║ -║ ☑️ red_hair ║ -║ ☑️ blue_eyes ║ -║ ☐ masterpiece (excluded) ║ -║ ☐ best quality (excluded) ║ -║ ║ -║ [✅ Save Selections] [❌ Cancel] ║ -╚════════════════════════════════════════════╝ -``` - -* **Checkboxes** to select which trigger words to exclude. -* **Metadata display** showing model info before deciding. -* **Persist selections** — saved exclusions apply to all future generations. -* **CivitAI link** to open the model page in a new tab. - -Excluded triggers are stored in the config array's `lora_omit_triggers` array and automatically filtered during generation. - -#### Trigger Word Placement Control - -Control exactly where trigger words are inserted in your prompts. For each LoRA with trigger words, you can choose: - -* **None (Default):** Trigger words are **prepended** to the beginning of the positive prompt. Best for character LoRAs and style LoRAs that need early influence. -* **Positive Start:** Explicitly prepend to positive prompt (same as None/default). -* **Positive End:** **Append** trigger words to the end of the positive prompt. Best for quality tags and detail LoRAs. -* **Negative Start:** **Prepend** trigger words to the negative prompt. Best for negative embeddings and "anti-style" LoRAs. -* **Negative End:** **Append** trigger words to the negative prompt. Best for quality-control negative tags. - -Set placement by opening the trigger word modal, selecting placement from the dropdown at the bottom, and saving. The placement is stored in `lora_triggerwords_append_settings`. - -### LoRA Bypass States - -Bypass states allow you to mark specific LoRAs to skip automatic trigger word fetching. - -**When to use:** LoRAs without triggers, offline work, custom workflows where you're manually adding triggers, or speed optimization for known LoRAs. - -Toggle the **🚫 Bypass Trigger Fetch** checkbox on a LoRA card. The card shows a ⚠️ indicator when bypassed. Bypass state is saved in the `lora_bypass_states` object. - -### Strength Locking - -Click the **🔗 Lock Strengths** button on a LoRA card to link model and CLIP strengths. When locked, both sliders move together maintaining the ratio. For example, if locked at 1.0/0.8 (ratio 1.25), changing model to 1.2 sets CLIP to 0.96. - -Useful for maintaining tested ratios and quickly scaling both strengths up/down together. - -### Folder Expansion (Models & LoRAs) - -Folder expansion allows you to test entire directories of models or LoRAs without manually adding each file. - -**Model Folder Expansion:** Select a folder path ending with `/` and set type to "Folder (Separate)." All `.safetensors` files in that folder are tested individually with the same settings. - -**LoRA Folder Expansion — Separate:** Path ending with `/`, type "Folder (Separate)." Each LoRA in the folder is tested individually. All use the same strength settings, and trigger words are fetched for each automatically. - -**LoRA Folder Expansion — Combined:** Path ending with `/*`, type "Folder (Combined)." ALL LoRAs in the folder are stacked into a single load. Creates one config with multiple LoRAs stacked at the same strength. Trigger words from all LoRAs are combined. Great for quality enhancement stacks, multi-concept combinations, or testing "everything at once" scenarios. - -### LoRA Stacking - -Combine multiple LoRAs into a single generation by adding multiple LoRA slots to a config array. - -**Manual Stacking:** Click **➕ Add LoRA** multiple times and configure each independently with different files, strengths, and trigger settings. All selected LoRAs are applied simultaneously. - -**Generated config format:** -```json -{ - "lora": "character_lora.safetensors:1.0:1.0 + style_lora.safetensors:0.8:0.8 + detail_lora.safetensors:0.5:0.5" -} -``` - -**Mixed Expansion + Manual:** You can combine folder expansion with manual stacking — for example, a combined quality folder plus a specific character LoRA stacked together. - -### VAEs Section - -Each VAE card includes: - -* **VAE Name (Searchable):** Dropdown with fuzzy search through all available VAE models. Supports folder structure display. -* **Type Selection:** - - **Single File:** Test just this specific VAE. - - **Folder (Separate):** Expand folder — each VAE becomes a separate iteration, multiplying the total grid size. -* **🗑️ Remove VAE:** Deletes this VAE from the config. - -VAEs are treated as a Cartesian dimension just like samplers or models. If you add 3 VAEs, your total iteration count is multiplied by 3. - -### Attention Modes - -Select which attention backend implementations to test. Available options depend on your system (e.g., `default`, `sageattn`, `xformers`). Like other parameters, you can select multiple modes and they become a grid dimension. - -### Model-Specific Prompts - -Per-config prompt prefix and suffix fields that are prepended/appended to all prompts generated by that config array: - -* **Prompt Prefix:** Text prepended to the positive prompt (e.g., `"masterpiece, best quality, "` for anime models). -* **Prompt Suffix:** Text appended to the positive prompt (e.g., `", 4k, photorealistic"` for realistic models). - -These fields are especially useful when testing multiple models that require different quality tags or style descriptors. - -### Recursive Cartesian Prompt Builder - -A visual chip-based prompt editor that supports variation groups using `{option1|option2|option3}` syntax: - -``` -a {photo|painting|sketch} of a {cat|dog} in a {forest|city} -``` - -This expands into all combinations (3 × 2 × 2 = 12 prompts): -- "a photo of a cat in a forest" -- "a photo of a cat in a city" -- "a painting of a dog in a forest" -- ... etc. - -The editor provides a visual chip interface for building and editing these variation groups. Each group is displayed as a clickable chip that can be expanded to add/remove options. Multiple groups can be nested for exponential prompt variation. - ---- - -## 🖥️ Dashboard Interface - -### Header Bar -* **Model/Prompt Info:** Shows current model and prompt metadata. -* **Go to Image #:** Jump directly to any image by entering its number (shown in bottom-left of cards). -* **Column Count:** Set fixed grid columns or leave at 0 for auto-sizing. -* **Zoom Controls:** `⊙` (reset), `−` (zoom out), `+` (zoom in). - -### Toolbar -* **⚙️ Settings Panel (Gear Icon):** Opens a unified settings panel with four sections: - - **Session:** Name input + SAVE/LOAD buttons. Dashboard auto-loads when connected to sampler and generation starts. - - **Grid Display:** Go to Image #, Column Count, Reset Zoom controls. - - **Export Favorites:** Export starred images to a dedicated folder with optional CivitAI metadata packing. - - **Scan External Directory:** Load image sessions from any folder path into the dashboard. - -* **ETA Progress Bar:** During generation, a real-time progress bar appears in the header showing estimated time remaining and completion percentage. - -* **Filter Groups:** Click colored buttons to toggle visibility: - - **Model** (Purple): Filter by checkpoint - - **Sampler** (Cyan): Filter by sampler type - - **Scheduler** (Blue): Filter by scheduler - - **Denoise** (Red): Filter by denoise value - - **LoRA** (Orange): Filter by LoRA configs - - **Shift+Click:** Isolate single filter (deselect all others) - -* **Sort Button:** Cycles between **Sort: Oldest** (default), **Sort: Newest**, and **Sort: Fastest** (by generation time). Preference is saved to localStorage. - -* **Fullscreen Button (⛶):** Expand dashboard to fill entire screen. - -### Navigation & Controls - -**Mouse:** -- Left-click or middle-click drag to pan -- Scroll wheel to zoom in/out -- Right-click on canvas to focus for keyboard controls - -**Touch (Mobile/Tablet):** -- Single finger drag to pan -- Two finger pinch/spread to zoom -- Tap card to reveal buttons - -**Keyboard:** -- `Space` — Scroll down one row -- `Shift+Space` — Scroll up one row -- `↑↓←→` — Pan in any direction -- `+/-` — Zoom in/out -- `0` — Reset zoom to 1:1 -- `F` — Auto-fit first row to viewport width - -### Card Overlays -* **Bottom Left:** Index number (#1, #2, etc.) — used for "Go to #" feature. -* **Bottom Right:** Generation time in seconds. -* **Top Left (on hover):** Red ✕ button to reject/hide image. -* **Top Right (on hover):** Gold ⭐ button to favorite image, and green "REVISE" button to open studio view. - -### JSON Bars (Bottom — Horizontal Layout) -* **Green Bar (Left — Accepted):** Contains optimized JSON of all currently visible images. Click to select all, then copy-paste back into the `configs_json` widget to refine your batch. -* **Gold Bar (Center — Favorites):** Contains configs of all images you starred with ⭐. Your best-performing settings in one place. -* **Red Bar (Right — Rejected):** Contains the configs of images you deleted with the ✕ button. Know what to avoid. - -### Revision Modal - -Clicking **REVISE** on a card opens the studio view: -1. **Left:** Full-resolution preview. -2. **Top Right — Read-Only Info:** Model used, seed number, positive prompt (with trigger words if applicable), negative prompt. -3. **Bottom Right — Adjustable Parameters:** Sampler, Scheduler, Steps, CFG, Denoise, LoRA. -4. **Bottom:** "Related Variants" reel showing other images with the same seed. -5. **GENERATE NEW:** Queues the specific config you just edited. - ---- - -## 🎯 Example Workflows - -### Quick Quality Test (40 images) -```json -[ - { - "sampler": ["dpmpp_2m", "euler"], - "scheduler": ["karras", "normal"], - "steps": [20, 30], - "cfg": [6.0, 7.0, 8.0], - "denoise": "1.0" - } -] -``` - -### Multi-Model Comparison (9 images) -```json -[ - { - "sampler": "euler", - "scheduler": "normal", - "steps": 25, - "cfg": 7.0, - "model": [ - "model_v1.safetensors", - "model_v2.safetensors", - "model_v3.safetensors" - ] - } -] -``` -Set `add_random_seeds_to_gens: 2` to get 3 variations per model (27 total images). - -### LoRA Stack Testing (24 images) -```json -[ - { - "sampler": "euler", - "scheduler": "normal", - "steps": 25, - "cfg": 7.0, - "lora": [ - "None", - "style.safetensors:0.6:1.0", - "style.safetensors:0.8:1.0", - "style.safetensors:1.0:1.0", - "style.safetensors:0.8:1.0 + detail.safetensors:0.5:1.0", - "style.safetensors:1.0:1.0 + detail.safetensors:1.0:1.0" - ] - } -] -``` - -### Random LoRA Selection - -Randomly select LoRAs from a folder for variety and experimentation: - -**Basic:** `"lora": "XL/Styles/[3,0.85]"` — Selects 3 random LoRAs at strength 0.85 (seed-based, reproducible). - -**Truly Random:** `"lora": "XL/Styles/[3,0.85,random]"` — Add `random` keyword for non-reproducible selection. - -**Dual Strength:** `"lora": "XL/Characters/[2,0.8,0.6]"` — Model strength 0.8, CLIP strength 0.6. - -**Combined with Regular LoRAs:** -```json -"lora": "XL/base.safetensors:1.0 + XL/Styles/[2,0.7] + XL/Details/[1,0.5,random]" -``` -Loads `base.safetensors` always, picks 2 reproducible style LoRAs, and picks 1 truly random detail LoRA. - -### Full Model Folder Test -```json -[ - { - "sampler": "dpmpp_2m", - "scheduler": "karras", - "steps": 25, - "cfg": 7.0, - "model": "realistic_models/" - } -] -``` - -### CLIP Skip for Anime Models -```json -[ - { - "sampler": "euler", - "scheduler": "normal", - "steps": 28, - "cfg": 7.0, - "model": "anime_model.safetensors", - "clip_skip": -2 - } -] -``` -- `clip_skip: 0` (default) — Use last layer (best for realistic models) -- `clip_skip: -2` — Skip 2 layers (best for anime/illustration models) - -Test multiple values: `"clip_skip": [0, -1, -2, -3]` - -### LoRA Trigger Word Filtering -```json -[ - { - "sampler": "euler", - "steps": 28, - "cfg": 7.0, - "lora": "style2.safetensors:0.8:0.6", - "lora_omit_triggers": ["style", "makeup", "jewelry"] - } -] -``` -Filters apply to ALL LoRAs in the stack. - -### Model Folder Expansion -```json -[ - { - "model": ["SD1.5/", "SDXL/", "Flux/"], - "sampler": "euler", - "steps": 28 - } -] -``` -Tests all models from all three folders with the same settings. - -### LoRA Strength Sweep - -Find the optimal strength for a new LoRA using the Config Builder: add the same LoRA 6 times with strengths from 0.4 to 1.5, keep everything else minimal (1 sampler, 1 scheduler), and compare the 6 images side by side. - -### Quality Enhancement Stack - -Create a consistent quality stack using the Config Builder: add 3 LoRAs manually (e.g., detail_enhancer at 1.0, color_boost at 0.6, sharpness at 0.5), use the trigger word modal to set all placements to **Positive End**, exclude generic tags like "masterpiece" and "best quality", then save as a reusable preset. - ---- - -## 📋 Preset Configs - -### 🏆 Group 1: The "Gold Standards" (Reliable Realism) - -*Tests the 5 most reliable industry-standard combinations.* -5 samplers × 2 schedulers × 2 step settings × 2 cfgs = **40 images** - -```json -[ - { - "sampler": ["dpmpp_2m", "dpmpp_2m_sde", "euler", "uni_pc", "heun"], - "scheduler": ["karras", "normal"], - "steps": [25, 30], - "cfg": [6.0, 7.0], - "lora": "None" - } -] -``` - -### 🎨 Group 2: Artistic & Painterly - -*Tests creative/soft combinations best for illustration and anime.* -2 samplers × 3 schedulers × 3 step settings × 2 cfgs = **36 images** - -```json -[ - { - "sampler": ["euler", "dpmpp_2m"], - "scheduler": ["simple", "beta", "normal"], - "steps": [20, 25, 30], - "cfg": [1.0, 4.5], - "lora": "None" - } -] -``` - -### ⚡ Group 3: Speed / Turbo / LCM - -*Tests ultra-fast configs. (Note: Ensure you are using a Turbo/LCM capable model or LoRA).* -4 samplers × 3 schedulers × 4 step settings × 2 cfgs = **96 images** - -```json -[ - { - "sampler": ["lcm", "euler", "dpmpp_sde", "euler_ancestral"], - "scheduler": ["simple", "sgm_uniform", "karras"], - "steps": [4, 5, 6, 8], - "cfg": [1.0, 1.5], - "lora": "None" - } -] -``` - -### 🦾 Group 4: Flux & SD3 Specials - -*Tests configs specifically tuned for newer Rectified Flow models like Flux and SD3.* -2 samplers × 3 schedulers × 3 step settings × 2 cfgs = **36 images** - -```json -[ - { - "sampler": ["euler", "dpmpp_2m"], - "scheduler": ["simple", "beta", "normal"], - "steps": [20, 25, 30], - "cfg": [1.0, 4.5], - "lora": "None" - } -] -``` - -### 🧪 Group 5: Experimental & Unique - -*Tests niche combinations for discovering unique textures.* -6 samplers × 4 schedulers × 5 step settings × 4 cfgs = **480 images** - -```json -[ - { - "sampler": ["dpmpp_3m_sde", "ddim", "ipndm", "heunpp2", "dpm_2_ancestral", "euler"], - "scheduler": ["exponential", "normal", "karras", "beta"], - "steps": [25, 30, 35, 40, 50], - "cfg": [4.5, 6.0, 7.0, 8.0], - "lora": "None" - } -] -``` - ---- - -## 🔧 Performance Tips - -### For Large Batches (1000+ images) -1. Set `flush_batch_every: 10-20` to see progress updates without overwhelming the browser. -2. Use `vae_batch_size: 8-12` (balance between speed and VRAM). -3. Enable `overwrite_existing: False` so you can stop/resume safely. - -### For Multi-Model Testing -1. Use identical LoRA/prompt settings across models for fair comparison. -2. Use `add_random_seeds_to_gens: 2-3` to evaluate model consistency. - -### For Memory-Constrained Systems -1. Lower `vae_batch_size` to 1-2. - ---- - -## 💡 Tips & Best Practices - -### Organization -- Use descriptive config names: `"SDXL_Quality_Test_v2"` instead of `"config1"`. -- Group related tests in separate config arrays. -- Save presets early using auto-save to prevent data loss. -- Version your configs with numbers in saved config names. - -### Config Builder Workflow Efficiency -- **Duplicate instead of recreate:** Use the Duplicate Config button for variations. -- **Load templates:** Keep a library of saved configs for common test scenarios. -- **Leverage the trigger word modal:** Bulk-exclude unwanted triggers once — they persist. -- **Lock strengths for ratios:** When you find good ratios (e.g., 1.0/0.8), lock them. -- **Collapse unused sections:** Minimize Models/LoRAs sections when not actively editing. - -### Trigger Word Management -- Exclude generic terms like "masterpiece", "best quality", "highres" — they often appear but may be redundant. -- Use placement strategically: character/style triggers → Positive Start, quality/detail triggers → Positive End, anti-style triggers → Negative Start. -- Bypass technical LoRAs — technique LoRAs (e.g., "detail tweaker") rarely have meaningful triggers. - -### Testing Strategy -- Start small: 1 model × 1 LoRA × 2 samplers = 2 images. -- Expand gradually after confirming the basics work. -- Use separate config arrays — don't mix unrelated tests in one array. -- Monitor iteration counts — the ⏱️ display updates in real-time. Watch for unexpected explosions. -- Save before large grids — always save your config before running 500+ image generations. - ---- - -## ⚠️ Troubleshooting - -Please check the [GitHub Issues list](https://github.com/YOUR_USERNAME/ComfyUI-Ultimate-Auto-Sampler-Config-Grid-Testing-Suite/issues) and post any issues there after searching. (Pro-tip: delete `state:open` from the search to see closed issues too.) - -### Generation Issues -* **"Session not found":** Ensure the `session_name` matches a folder inside `ComfyUI/output/benchmarks/`. -* **OOM Errors:** Lower the `vae_batch_size` to 1 or 2. -* **Images not resuming:** Make sure `overwrite_existing: False`. Check console for skip messages. -* **Random seeds different each run:** Intentional — random seeds are tied to the base seed. -* **"mat1 and mat2 shapes cannot be multiplied":** Model architecture mismatch. For SD3/Flux/Z-Image, ensure you connect ALL optional inputs. Check LoRA compatibility. -* **Dashboard not auto-loading:** Ensure the dashboard node is connected to the sampler's `dashboard_html` output. Try the "RELOAD / SHOW SESSION" button. - -### Dashboard Issues -* **Cards not appearing:** Click inside the viewport area first to give it focus. -* **Can't scroll/pan:** Right-click on the canvas area to focus it, or left-click drag. -* **Slow performance:** Virtual scrolling handles 5000+ images. If slow, close other tabs, reduce browser zoom to 100%, or clear localStorage. -* **Images not loading:** Scroll slower to give the lazy loader time to fetch. - -### LoRA & Trigger Word Issues -* **Trigger words not appearing:** Enable `lookup_and_append_lora_triggerwords` in the sampler node. Ensure internet connection for first-time lookup. Check `loras_tags.json` for cached results. -* **"INCOMPATIBLE LORA DETECTED":** Normal — the node automatically skips incompatible LoRAs. Ensure your LoRAs match your model architecture. -* **Random LoRA selection not working:** Ensure folder path includes `/` before the brackets (e.g., `"XL/Styles/[3,0.85]"`). Check console for debug messages. -* **Trigger words not filtered with `lora_omit_triggers`:** Ensure `lookup_and_append_lora_triggerwords` is enabled. Filtering is case-insensitive and handles trailing commas. - -### Model & CLIP Issues -* **Images look wrong with multiple models:** System handles this automatically. You should see `⚠️ Multiple models detected - pre-encoding DISABLED` in console. -* **CLIP skip not affecting results:** Verify the model supports it. Use 0 for realistic, -2 for anime. SDXL may be less sensitive than SD1.5. -* **"No files found in folder":** Ensure trailing `/` in path. Folder names are case-sensitive on Linux/Mac. - -### Config Builder UI Issues -* **Dropdowns not showing models/LoRAs:** Click the **🔄 Refresh Models/LoRAs** button. -* **UI not appearing or showing blank:** Check browser console (F12) for JavaScript errors. Ensure the node is "Ultimate Config Builder." Refresh the page. -* **Changes not saving:** Ensure auto-save is enabled OR manually click "Save Config Now." Check that the config name field is filled. Verify write permissions to `ComfyUI/output/benchmarks/configs/`. -* **"No triggers found" for a known LoRA:** LoRA may not be on CivitAI or hash mismatch. Manually add triggers or enable Bypass Trigger Fetch. -* **Trigger words not updating after exclusions:** Open the modal again and verify checkboxes. Click "Save Selections" (not just closing the modal). Check JSON preview for `lora_omit_triggers`. -* **API timeout or slow lookups:** First lookup requires hashing + API call (10-30 seconds). Subsequent lookups use cache (instant). Bypass trigger fetch for offline work. -* **Iteration count unexpectedly high:** Check for folder expansion in Models/LoRAs sections and accidental commas in CSV fields. -* **Config not loading from saved file:** Verify file exists in `ComfyUI/output/benchmarks/configs/`. Check JSON syntax. Try manually editing and re-saving. -* **LoRA stacking not working:** Check JSON preview — LoRAs should join with ` + `. Verify each has format `"name:model_str:clip_str"`. Don't mix "None" with actual LoRAs. -* **Sampler Grid not receiving configs:** Verify the `configs_json` wire is connected. Check JSON preview. Try disconnecting and reconnecting. Queue a prompt to force update. - -### Remote VAE Issues -* **Not working:** Check endpoint is set correctly. Verify server is running. Look for `🌐 Remote VAE worker started` in console. -* **Hangs on "Waiting for remote VAE":** Server may have crashed. Check server logs. Restart and try again. - -### Interrupt/Cancel Issues -* **Cancel doesn't stop generation:** Ensure latest version with interrupt handling. Should see `🛑 INTERRUPTED - Stopping all jobs` in console. -* **Work lost after canceling:** Manifest is saved on interrupt. Check `ComfyUI/output/benchmarks/{session_name}/manifest.json`. - -### Browser Compatibility -* **Chrome/Edge:** Full support ✅ -* **Firefox:** Full support ✅ -* **Safari:** Mostly works, some keyboard shortcuts may conflict -* **Mobile:** Full touch support ✅ (iOS Safari, Android Chrome tested) - ---- - -## JSON Output Format Reference - -The Config Builder generates JSON in the exact format required by the Ultimate Sampler Grid: - -```json -[ - { - "sampler": ["euler", "dpmpp_2m"], - "scheduler": "normal", - "steps": 25, - "cfg": [6.0, 7.0, 8.0], - "lora": "style.safetensors:0.8:0.8 + detail.safetensors:0.5:0.5", - "model": "base_xl.safetensors", - "vae": ["vae_v1.safetensors", "vae_v2.safetensors"], - "attention_mode": ["default", "sageattn"], - "prompt_prefix": "masterpiece, best quality, ", - "prompt_suffix": ", 4k, detailed", - "lora_omit_triggers": ["masterpiece", "best quality"], - "lora_triggerwords_append_settings": { - "style.safetensors": "positive_start", - "detail.safetensors": "positive_end" - }, - "lora_bypass_states": { - "technique_lora.safetensors": true - }, - "model_bypass_states": { - "old_model.safetensors": true - }, - "lora_strength_lock": { - "style.safetensors": true - } - } -] -``` - -**Field Types:** -- **Single value:** `"euler"` or `7.0` -- **Array:** `["euler", "dpmpp_2m"]` or `[6.0, 7.0, 8.0]` -- **LoRA string:** `"name:model_str:clip_str"` — stack with ` + ` -- **Folder:** `"path/"` (separate) or `"path/*"` (combined) -- **VAE:** Single VAE name or array of VAE names (each becomes a grid dimension) -- **Attention mode:** Single mode string or array of modes to test -- **Prompt prefix/suffix:** Strings prepended/appended to all prompts in this config -- **Bypass states:** Object mapping model/LoRA names to `true` (bypassed) or `false` (active) - ---- - -## API Integration (Config Builder) - -For advanced users, the Config Builder exposes several backend endpoints: - -### `/configbuilder/lookup_triggers` (POST) -Bulk lookup trigger words for multiple LoRAs. - -**Request:** -```json -{ - "loras": ["lora1.safetensors", "lora2.safetensors"] -} -``` - -**Response:** -```json -{ - "triggers": { - "lora1.safetensors": ["trigger1", "trigger2"], - "lora2.safetensors": ["trigger3"] - } -} -``` - -### `/configbuilder/lookup_lora_metadata` (POST) -Fetch complete metadata for a specific LoRA from CivitAI. - -**Request:** -```json -{ - "lora_name": "character_lora.safetensors" -} -``` - -**Response:** -```json -{ - "metadata": { - "name": "Character LoRA v2", - "model_name": "Character Series", - "trained_words": ["character", "outfit"], - "base_model": "SDXL 1.0", - "description": "...", - "tags": ["character", "anime"], - "url": "https://civitai.com/models/12345", - "hash": "abc123...", - "creator": "ArtistName" - } -} -``` - -### `/configbuilder/refresh_models` (POST) -Signal to clear frontend caches (called by ComfyUI when refreshing node definitions). - -### `/configbuilder/lookup_model_metadata` (POST) -Fetch metadata for a checkpoint model from CivitAI (same as LoRA metadata but for models). - -**Request:** -```json -{ - "model_name": "realistic_vision_v5.safetensors" -} -``` - -**Response:** -```json -{ - "metadata": { - "name": "Realistic Vision V5.1", - "model_name": "Realistic Vision", - "base_model": "SD 1.5", - "description": "...", - "tags": ["realistic", "photorealistic"], - "url": "https://civitai.com/models/12345", - "hash": "abc123...", - "creator": "SG161222" - } -} -``` - -### `/configbuilder/scan_directory` (POST) -Scan an external directory for image sessions and load them into the dashboard. - -**Request:** -```json -{ - "path": "D:/other_comfyui/output/benchmarks/my_session" -} -``` - -**Response:** -```json -{ - "success": true, - "images_found": 42, - "manifest": { ... } -} -``` - -### Distribution API (registered via `distribution_routes.py`) - -These endpoints are used internally by the distribution system: - -| Method | Route | Purpose | -|--------|-------|---------| -| GET | `/distribution/claim_job` | Worker claims next pending job (200 + job JSON, 204 = no jobs, 503 = inactive) | -| POST | `/distribution/submit_result` | Worker uploads completed image (multipart: metadata + image) | -| GET | `/distribution/status` | Get distribution status and worker stats | -| POST | `/distribution/register_worker` | Worker registration with master | -| GET | `/distribution/download_model` | Download model file for model sync | -| POST | `/distribution/heartbeat` | Worker keepalive signal | - ---- - -## File Locations - -### Generated Files - -**Config Presets:** -``` -ComfyUI/output/benchmarks/configs/ -├── my_quality_test.json -├── model_comparison.json -└── lora_strength_sweep.json -``` - -**Session Output:** -``` -ComfyUI/output/benchmarks/{session_name}/ -├── manifest.json -├── image_001.png -├── image_002.png -└── ... -``` - -**LoRA Trigger Cache:** -``` -ComfyUI/output/benchmarks/loras_tags.json -``` - -**LoRA Metadata:** -``` -ComfyUI/output/benchmarks/model-data/ -├── lora_name/ -│ └── metadata.json -``` - ---- - -## Keyboard Shortcuts - -### Dashboard -| Shortcut | Action | -|----------|--------| -| `Space` | Pan down one row | -| `Shift+Space` | Pan up one row | -| `↑↓←→` | Pan in any direction | -| `+/-` | Zoom in/out | -| `0` | Reset zoom to 1:1 | -| `F` | Auto-fit first row to viewport width | -| `Escape` | Close active modal/popup (priority: Revision > Filters > Settings > Fullscreen) | - -### Config Builder -| Shortcut | Action | -|----------|--------| -| `Ctrl+F / Cmd+F` | Search within searchable dropdowns (when focused) | -| `Enter` | Confirm selection in dropdown | -| `Escape` | Close dropdown without selecting | -| `Tab` | Navigate between input fields | - ---- - -## 🌐 Distributed Processing - -Distribute large grid runs across multiple ComfyUI instances for parallel image generation. - -### Setting Up Distribution - -1. **Ensure Workers Are Running:** Each worker must be a separate ComfyUI instance with this node suite installed and running. -2. **Open Config Builder:** In the Config Builder node, scroll to the **Distribution Settings** section. -3. **Add Worker URLs:** Enter the full URL of each worker (e.g., `http://192.168.1.100:8188`). Click **+ Add Worker** for additional workers. -4. **Test Connections:** Click the test button next to each worker URL. A green dot means the worker is reachable. -5. **Enable Distribution:** Toggle the distribution switch ON. -6. **Run Generation:** Queue the workflow. The master node will coordinate job distribution automatically. - -**How It Works:** -``` -Master (your machine): - 1. Expands configs into individual jobs - 2. [Optional] Pre-encodes all prompts (Master Text Encoding) - 3. Notifies workers to start polling - 4. Processes its own jobs while workers claim theirs - 5. Workers upload completed images back to master - 6. Master saves everything to the session manifest -``` - -### Master Text Encoding - -When enabled, the master pre-encodes ALL unique positive/negative prompts for every (model, LoRA) combination before distributing jobs. Workers receive the encoded conditionings in their job claim response and skip CLIP encoding entirely. - -**Benefits:** -- Workers don't need to load text encoders at all -- Eliminates duplicate CLIP encoding across workers -- Significant time savings for prompt-heavy grids - -**Enable:** Toggle "Use Master Text Encoding" in Distribution Settings. - -**Technical Notes:** -- Encoded conditionings are serialized as base64-encoded NumPy arrays (~640KB per job) -- Multi-entry conditionings (AND combinator) are fully supported -- Workers fall back to local encoding if pre-encoded data is unavailable (backward compatible) - -### Model Sync - -When enabled, the master automatically transfers checkpoint and LoRA files to workers that don't have them. - -**Enable:** Toggle "Sync Models to Workers" in Distribution Settings. - -### Distribution Troubleshooting - -* **Workers not connecting:** Verify the worker URL is correct (include port). Check firewalls. Both master and worker must have this node suite installed. -* **Jobs timing out:** Increase the "Claim Timeout" slider (default 600s). First-time model loads on workers can take several minutes. -* **Duplicate images:** If a worker times out but actually completes the job, duplicates may appear. Increase claim timeout to prevent this. -* **Workers not using pre-encoded prompts:** Ensure "Use Master Text Encoding" is enabled. Check worker console for `"Using master pre-encoded conditionings"` log message. -* **Model sync failing:** Ensure both master and worker have sufficient disk space. Check worker console for download errors. - ---- - -## 🤖 Notes for AI Development - -This section provides critical context for AI assistants (Claude, GPT, etc.) working on this codebase. - -### Critical Constraints - -**DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY.** This is the #1 rule for all code changes. The codebase has many interconnected parts and removing code often breaks things in unexpected ways. - -### Architecture Mental Model - -This is a **hybrid application** with three layers: -1. **Python Backend** — Runs inside ComfyUI's server process. Handles generation, caching, file I/O, and API endpoints. -2. **JS Frontend Bridge** (`web/` directory) — Runs in the ComfyUI browser tab. Registers custom nodes, manages widget state, and forwards server events. -3. **Dashboard SPA** (`resources/` directory) — Runs **inside an iframe** in the dashboard node. A standalone app with its own state management, virtual scrolling, and event handling. - -### Key Gotchas for AI Development - -1. **Config Builder output is ALWAYS wrapped** — `config_builder_node.py` outputs `{"configs": [...], "_distribution": {...}}`. The `sampler_node.py` MUST unwrap the `"configs"` key before passing to `expand_configs()`. If you add new top-level keys to the config output, update the extraction logic in `sampler_node.py` lines 269-286. - -2. **Two independent config-to-JSON implementations** — `convertStateToConfigs()` in `conf-builder-utilities.js` (JS preview) and `generate_config()` in `config_builder_node.py` (Python output) both transform `node.state` into config JSON. **Both must stay in sync** when adding new config fields. - -3. **Dashboard runs in an iframe** — The dashboard HTML is set via `iframe.srcdoc`. All JS/CSS is inlined (no external requests). Communication between the parent ComfyUI page and the iframe uses `postMessage()`. The `dashboard.js` file bridges server events to iframe messages. - -4. **Dashboard auto-loads on node creation** — `web/dashboard.js` calls `forceLoadSession()` with a 500ms delay when a dashboard node is created. The `get_session_html` API endpoint returns a full HTML template with empty manifest for non-existent sessions (NOT a 404). - -5. **`logic_pipeline.js` has a legacy init block** — Lines 489-505 have a `DOMContentLoaded` handler that calls `loadSession()` only when `fullManifest.items.length > 0`. The proper initialization happens in `logic_init.js` which calls `init()`. Don't add a second unconditional `loadSession()` call — it causes alert popups for empty sessions. - -6. **Manifest merge on save** — `save_manifest()` always reloads from disk first to preserve user favorites/rejected/notes. This is intentional for concurrent access (generation + user interaction). Don't bypass this. - -7. **`onNodeCreated` must be synchronous** — In `conf-builder-main.js`, async work runs inside fire-and-forget `(async () => { ... })()`. Making `onNodeCreated` async breaks ComfyUI widget registration. - -8. **Optional inputs disable caches** — When `optional_model`/`optional_clip`/`optional_positive`/`optional_negative` are connected to the sampler node, conditioning cache is disabled and `IS_CHANGED()` returns `NaN` (forces re-execution every time). - -9. **Distribution conditionings are serialized with np.copy()** — `np.frombuffer()` returns read-only arrays. Must call `.copy()` before `torch.from_numpy()` or GPU tensor operations will fail. - -10. **Default state migration** — When adding new fields to the Config Builder state (in `conf-builder-main.js`), also add a migration check in the `onNodeCreated` function (around line 363) to backfill the default value for existing saved workflows. - -11. **claim_timeout default is 600** — This value appears in `distribution_manager.py`, `config_builder_node.py`, `conf-builder-distribution.js`, and `conf-builder-main.js`. Keep all four in sync. - -12. **ComfyUI Registry security** — No `import requests`, no `subprocess`/`eval`/`exec`, no custom file-serving endpoints. Use `urllib.request` for HTTP and ComfyUI's `/view` endpoint for serving files. See `ProjectStructure.md` for full rules. - -### File Dependencies (Import Graph) - -``` -sampler_node.py - → generation_orchestrator.py (main entry point) - → config_utils.py (expand_configs, prepare_input_jobs) - → model_loader.py (load_checkpoint, load_loras) - → image_generation.py (generate_image, flush_batch) - → batch_encoding.py (encode_prompt_with_combinators) - → trigger_words.py (build_prompt_with_triggers) - → manifest_utils.py (save_manifest, load_existing_manifest) - → model_cache.py (ModelCache) - → conditioning_cache.py (ConditioningCache) - → remote_vae.py (RemoteVAEDecodeWorker) - → html_generator.py (get_html_template) - → distribution_manager.py (DistributionManager) - → distribution_worker.py (WorkerThread) - -config_builder_node.py - → lora_utils.py (trigger word lookup) - -__init__.py - → all node classes - → distribution_routes.py (distribution API endpoints) - → html_generator.py (on-demand HTML generation) - → metadata_packer.py (export favorites) - → directory_scanner.py (scan external directories) - → manifest_utils.py (save/load manifests) -``` - -### Server Event Types - -| Event Name | Sender | Data | Purpose | -|---|---|---|---| -| `ultimate_grid.update` | `generation_orchestrator.py` | `{session_name, node, manifest, meta, new_items}` | New images generated | -| `ultimate_grid.progress` | `generation_orchestrator.py` | `{session_name, progress_pct, eta_str, ...}` | ETA/progress updates | -| `ultimate_grid.distribution_status` | `generation_orchestrator.py` | `{session_name, workers, stats}` | Distribution status | - -### Testing Changes - -After modifying backend Python files, restart ComfyUI. After modifying `web/` JS files, refresh the browser (Ctrl+F5). After modifying `resources/` JS/CSS/HTML files, the dashboard HTML must be regenerated — either re-run the sampler node or call the `/config_tester/get_session_html` endpoint. - ---- - -## 📝 Changelog - -### Update 3/6/26 — Distributed Processing & Dashboard Improvements -* 🌐 **Multi-Worker Distribution:** Split grid runs across multiple ComfyUI instances with automatic job coordination, claiming, timeout handling, and result upload. -* 🧠 **Master Text Encoding:** Master pre-encodes all prompts and sends conditionings to workers, eliminating duplicate CLIP encoding. -* 📦 **Model Sync:** Optionally transfer checkpoint and LoRA files from master to workers automatically. -* 📊 **Per-Worker Logging:** Final summary shows each worker's completed/failed job counts. -* ⚙️ **Distribution Settings UI:** Config Builder section with worker URL management, connection testing, and toggles for text encoding and model sync. -* 🏠 **Session Landing Page:** Dashboard now auto-loads and shows a session picker on startup instead of a blank black box. -* 🔄 **Dashboard Auto-Load:** Dashboard iframe automatically loads session HTML on node creation with 500ms delay. -* 🛠️ **Non-Existent Session Handling:** `get_session_html` endpoint returns a full template with empty manifest instead of 404. -* ⏱️ **Claim Timeout Update:** Default claim timeout increased from 300s to 600s across all files. -* 🤖 **AI Development Notes:** Added comprehensive development notes to README and ProjectStructure for AI-assisted development. - -### Update 2/16/26 — Config Builder & Dashboard Overhaul -* 🎛️ **VAE Selection & Iteration:** Test multiple VAEs per config with searchable dropdown and folder expansion. VAEs are a full Cartesian dimension. -* 🧠 **Attention Mode Testing:** Grid-test different attention implementations (`default`, `sageattn`, `xformers`) as a combinatorial dimension. -* 🔗 **CLIP Encoding Combinators:** `AND`, `CAT`, `AVG(weight)`, and `BREAK` keywords for multi-segment conditioning and prompt blending. -* 📝 **Model-Specific Prompts:** Per-config prompt prefix and suffix fields for model-appropriate quality tags. -* 🔄 **Recursive Cartesian Prompt Builder:** Visual chip-based editor with `{option1|option2}` syntax expanding into all combinations. -* 🔘 **Model & LoRA On/Off Switches:** Toggle entries on/off without removing them. Bypassed entries excluded from preview and output. -* 🔍 **CivitAI Model Metadata Lookup:** Fetch model info from CivitAI directly in the Config Builder (same as existing LoRA lookup). -* 📋 **Sampler/Scheduler Dropdowns:** Searchable dropdowns auto-populated from the connected sampler node, replacing free-text CSV entry. -* ⚙️ **Unified Settings Panel:** SESSION button merged into a gear icon settings panel with Session, Grid Display, Export Favorites, and Scan Directory sections. -* 📊 **Real-Time ETA Progress Bar:** Shows estimated time remaining during generation in the dashboard header. -* 📂 **Scan External Directory:** Load image sessions from any folder into the dashboard for viewing and filtering. -* 📦 **Export Favorites:** Copy starred images to a dedicated folder with optional CivitAI metadata packing. -* 🛑 **Cancel During Text Encoding:** Properly cancels the entire run when interrupted during the text encoding step. -* 🔄 **Connected Node Change Detection:** Detects when upstream nodes have changed for smarter job resuming. -* ⌨️ **Close Modal Hotkeys:** Escape key closes modals in priority order (Revision > Filters > Settings > Fullscreen). -* 🧹 **Dashboard Topbar Consolidation:** Streamlined header with condensed controls. - -### Update 2/5/26 — Code Refactoring & Performance Improvements -* 🏗️ **Major Code Refactoring:** Reorganized codebase into 6 modular files for better maintainability (`trigger_words.py`, `batch_encoding.py`, `manifest_utils.py`, `model_loader.py`, `image_generation.py`, `generation_orchestrator.py`). -* 🎯 **CLIP Skip Support:** Control CLIP layer usage with `clip_skip` parameter. Essential for anime models (-2) vs realistic models (0). Supports arrays for testing multiple values. -* 🧠 **Intelligent CLIP Encoding:** Multi-model workflows now handle CLIP correctly with automatic detection and per-model encoding. -* ⚡ **Batch Encoding on Model Switch:** 3-6x faster encoding for multi-model workflows. -* 🗑️ **LoRA Trigger Word Filtering:** New `lora_omit_triggers` parameter to exclude specific trigger words. -* 📁 **Model Folder Expansion:** Use `"model": "FolderName/"` to test all checkpoints in a folder. -* 🛑 **Graceful Interruption:** Cancel now stops ALL jobs, flushes pending batches, waits for remote VAE, and saves manifest. -* 🌐 **Remote VAE Fixes:** Fixed initialization, job queuing, and interrupt handling. -* 🔧 **str_model/str_clip Removal:** Deprecated config fields removed. Strengths now specified in LoRA string. - -### Update 2/5/26 — Random LoRA Selection -* 🎲 **Random LoRA Selection:** `[count,strength]` syntax for seed-based selection, `[count,strength,random]` for truly random, dual strength support, cross-platform paths, and full trigger word integration. - -### Update 1/14/26 — Major Feature Update -* 🎯 **Non-Standard Model Support:** SD3, Flux, Z-Image with automatic latent channel detection. -* ⭐ **Favorites System:** Star images with dedicated gold JSON bar. -* 🎨 **Horizontal JSON Bars:** Redesigned three-bar layout. -* 🔍 **LoRA Auto Trigger Words:** CivitAI API integration with local caching. -* 🚫 **LoRA Compatibility Detection:** Automatic skip with clear error messages. -* ⌨️ **Shift+Click Filter Isolation.** -* 🎯 **Go to Image #.** -* 🚀 **Dashboard Auto-Load.** -* 📱 **Mobile Touch Support.** -* 🔐 **Conditioning Change Detection.** -* 📋 **Enhanced Revise Modal.** - -### Update 1/11/26 — Major Overhaul -* ✨ **Virtual Scrolling:** Handles 5000+ images. -* 🖼️ **Fullscreen Mode.** -* 🔄 **Multi-Model Support** with folder expansion. -* 🎨 **Multi-LoRA Stacking** with `+` separator. -* 🎲 **Multi-Seed Generation.** -* ⏸️ **Stop & Resume.** -* ⌨️ **Keyboard Navigation.** -* 📊 **Live Updates** with `flush_batch_every`. -* 💾 **Persistent Settings.** -* 🎯 **Auto-Fit Zoom.** -* ⚡ **Massive performance refactoring.** - ---- - -## 📜 License - -MIT License. Feel free to use, modify, and distribute. - ---- - -## 🙏 Credits - -Created for the ComfyUI community. Special thanks to all contributors and testers who helped refine this tool. - -**Special Features Powered By:** -- CivitAI API for trigger word lookups -- ComfyUI's object_info system for model/LoRA discovery - -**Star this repo if you find it useful!** ⭐ - -**Support development:** [Buy me a coffee on Ko-fi](https://ko-fi.com/jasonhoku) ☕ - -**Version:** 3.0 -**Last Updated:** March 2026 diff --git a/Roadmap.md b/Roadmap.md deleted file mode 100644 index 44ea6ef..0000000 --- a/Roadmap.md +++ /dev/null @@ -1,292 +0,0 @@ - - -99% of this project's near 10k lines of code was written by either Gemini or Claude. Sorry if that upsets you, I've been a programmer for 15 years and it's simply a much faster means of developing. - -With that said, here is a guide on: - -Easyily Get AI To Add Features To This Project: - -Gemini Pro & Claude Sonnet 4.5 both work great but they sometimes make mistakes. The key is to really get a very clear prompt built out. - -Step 1. -Send the AI the ProjectStructure.md and README.md files and your task and ask it to tell you which files need to be edited to get your task completed. - -Step 2. -Start a new chat and attach the files it mentioned, along with the following prompt or something similar with your task request prompt in the middle: - -Prompt Fill-in: - -Help me update my ComfyUI Custom Node. When updating files, DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY. - -PUT YOUR TASK HERE - -Check ProjectStructure.md to get an idea of file structure and contents, check README.md to get an idea of the project as a whole. - -step 3 -????? - -step 4 -Profit!!! - -But seriously, after updating your code see if it works. If it didn't send the error message if there isnt one or your symptoms. If its still not working try a different AI or try breaking down your task in smaller pieces. - - ---- - -!!!! -Check Roadmap.md for some tasks and do them. If you think you can do the ones marked as New to-do items, needs more info/explaining/numbering those are good. - -There is info on the project in README.md and info on the file structure and notes on each files functions in the ProjectStructure.md -!!!! -ProjectStructure.md has notes to assist AI in developing this project -!!!! - -### **ComfyUI Ultimate Sampler Grid – Development Roadmap** - -## Builder UI Improve Navigation, Layout & Styles - Always on top, sticky, Top Bar & Sidebar: - -Top bar should have Session Name as a text input first, far left. Then let's add a Settings button with a dropdown with Load Session, Load Config, Save Config, Enable Distributed Processing - -Side Bar, Icons For Each Major Section, click to scroll to it. - -Layout & Styles fix: Many section headers are too small to find and read easily, and some are different sizes, lets make them uniform. - -Color issues, comfyui is dark mode, many buttons and headers and texts are too dark or darkly colored to be viewed easily for everyone, lets fix that. - - - -## Jobs don't generate prompts in the order they're written in the array, it's mildly inconvenient. Lets fix that and make it run in order from top to bottom of the array. - -## Custom Job Resume / Skipping - Start At Job # Option - -## Batch Encoding could use with implementing the smart, look ahead, caching system we built for lora swapping. - -## Add HF-RemoteVae list to BuilderUI Remote Vae (when Remote URL is selected, add a Presets: selection to choose which one from the list) - - -## bug fix, the Builder UI is very slow to update when first starting, adding models, adding loras, a few other things, we need to check it and optimize it to load and respond faster. - - -## Add on off switches in builder ui to text encoder selection and vae selection (nearly complete but vae on off switches don't work, also I'd prefer for simple omition from node output confg_json instead of adding uncessary on-off switches to the output) - -## In the Dashboard UI, I'd love to see a toggle option for view prompts without lora trigger-words. Currently it shows them all with them added and it causes a bit of a duplicate issue if we're just trying to see which prompts work best on variety of various loras. - - -## Models from UI aren't getting hashed properly for some reason, maybe diffusion models, maybe builder UI path / \ normalization? console logs: Using fallback hash for model: 559ad07aae - -## Add on off switches in builder ui to text encoder selection and vae selection that works exactly the same as the model on off switches - -## Add more info to the lora and model civitai lookups in the builderUI, option to show full API response, show full JSON response, save full JSON resposne, if lookup data is cached, show a big warning on the very top that says READ FROM DISK CACHE, LAST LOOKED UP ON {lookup date} - -## When you click a sampler or scheduler in the dropdown it should add it to the list right away. - -## In the builder ui civit-ai lookup modal, when it alerts the user the data is cached and read from disk there needs to be a button for re-check/lookup now so they can get updated data when they want. - -## Feature: Adding permanent/saving/editing custom lora trigger words to a lora, read from and edit the lora_triggers.json file, with nice ui to make it easy and convenient to edit, remove and add triggerwords. - - - -#### **12. Arrays in LoRA Weights** Integrated but needs adding to the Builder UI - -Builder UI could have a + button right next to model strength text input (on its right side) to make it easy to add a compare different lora strengths. If activated it should add an additonal slider, or two sliders if 🔒 Lock Model & CLIP Strength Together is deactivated, for each time they click +. If a lora has a multiple strength array, the preview json and py node json_output should duplicate the full config for that run for each entry with the only difference being each lora strength. If multiple loras have multiple arrays it needs to output a cartesian of an each for each of all lora strenth combos. - - - - -## Save/Load/Import (Merge) Prompts. (With settable unique naming option) Load should offer a searchable dropdown menu for all past saved prompts. Let's store this data in a outputs/benchmarks/PromptsData folder. Each prompt/save should be its own file. - - -## ~~Easy Feature: Add Esc key close to Revise modal and add X to close in top right of modal in Dashboard and the Lookup LoRA Metadata from CivitAI, and omit lora triggerwords modals in the Builder UI.~~ COMPLETED Also -Replace revise button in dashboard with edit emoji - - - -## Bug Fix: Batch Encoding Runs Before Job Skip/Continue/Resume check and will encode everything again even if it's already been completed. Also Continue/Resume needs optional inputs to be tracked. We need to track connected node changes from each of the optional inputs, we could also use this step to save the workflow to the benchmark/session folder and compare the last run workflow to the current to track node changes and determine changes and also integrate currenly missing from optional inputs such as model, loras, prompts, etc. - -## ~~Fix: Manifest doesn't need lora omit triggers list in every item~~ (DONE - already stripped in create_image_metadata via .pop()) - -# Needs Testing: Batch encoding doesnt seem to be working for optional inputs possibly, or maybe its very large models, maybe ggufs, I get loading messages after every single encoding instead of once per batch and it takes a long time (low priority) -Symptom: each encoding fills the GPU more and more and eventually it becomes 0 usable, 0 loaded all offloaded. -loaded partially; 5585.34 MB usable, 5543.55 MB loaded, 628.32 MB offloaded, 41.79 MB buffer reserved, lowvram patches: 0 -loaded partially; 5569.51 MB usable, 5527.72 MB loaded, 644.90 MB offloaded, 41.79 MB buffer reserved, lowvram patches: 0 -loaded partially; 5553.58 MB usable, 5511.79 MB loaded, 660.34 MB offloaded, 41.79 MB buffer reserved, lowvram patches: 0 -loaded partially; 5537.65 MB usable, 5495.86 MB loaded, 676.40 MB offloaded, 41.79 MB buffer reserved, lowvram patches: 0 -maybe we need to force encodings to offload to ram? - -## ~~Add attention options, xformers, sdpa, sage, flash, etc, option for test all, test all should clear ram & vram between each test.~~ COMPLETED -* **Status:** Added `attention_mode` config field supporting: default, xformers, pytorch, flash, sage, sage3, sub_quad, split. Use `"*"` to test all modes. Runtime switching via `transformer_options["optimized_attention_override"]` in ComfyUI's attention registry. Config Builder UI has dropdown+chips selector. Per-image metadata tracks which attention mode was used for dashboard filtering. Integrated into config expansion (Cartesian product with other params), skip/resume matching, and generation pipeline. - -# Deeper explained items to-do list - -#### **1. Skip Logic for Optional Inputs** COMPLETED - -* **Problem:** The `SamplerGridTester` node cannot reliably detect changes in optional inputs (`optional_model`, `optional_vae`, etc.) because standard `IS_CHANGED` logic relies on input hash comparisons, which don't update for passed objects in optional slots. -* **Status:** Implemented `IS_CHANGED` classmethod in `sampler_node.py` that returns `float("NaN")` when any optional input is connected (forces re-execution), or a deterministic hash when no optional inputs are used (allows caching). Updated `check_if_job_completed` in `generation_orchestrator.py` to skip model/lora/prompt matching when optional inputs are connected, since those values come from upstream nodes whose changes can't be tracked. Added warning when resume mode is used with optional inputs. -* **Target Files:** `sampler_node.py`, `generation_orchestrator.py` - - -#### **7. CivitAI Download Integration** (low priority) -A button in the builder UI to pack short sha256 into config with an explanation that it can be used to share or move an Ultimate Sampler Config Tester workflow and allow for downloading all models and loras in the workflow from civitAI with a few simple easy clicks. lora_utils has calculate civit model has function in it. dropdown configurable options for where to store each file type. - - -* **Target Files:** `__init__.py`, `lora_utils.py`, `web/config_builder.js` - - -#### **9. Tag/Token-Based Omit Logic** DONE - - -#### **10. Validation Warning (Omit vs Lookup) - Warn user if omits are added but lookup is off** (low priority) DONE - - -#### **11. Model-Specific Prompts** COMPLETED - -* **Problem:** Different models need different trigger words (e.g., "score_9, score_8" for Pony vs "masterpiece" for SD1.5). -* **Status:** Implemented as per-config `model_prompt_prefix` and `model_prompt_suffix` fields. These quality tags are prepended/appended to ALL prompts for a given config, wrapping around the entire prompt+triggers assembly. Added `_apply_model_prompt_affixes()` helper in `trigger_words.py`, integrated into both `build_prompt_with_triggers()` and `collect_unique_prompts_with_triggers()` for correct pre-encoding. Config Builder UI shows prefix/suffix text inputs in the Prompts section of each config. Fields persist through save/load and are passed through `expand_configs()` in `config_utils.py`. -* **Target Files:** `trigger_words.py`, `config_utils.py`, `conf-builder-config-management.js`, `conf-builder-utilities.js`, `conf-builder-main.js` - -#### **13. Real-Time ETA** COMPLETED - -* **Problem:** ETA only prints to server console. -* **Status:** Implemented real-time progress bar in Dashboard. Backend sends `ultimate_grid.progress` events via WebSocket after each job. Frontend displays an ETA bar below the header with job count, percentage, ETA, finish time, and avg duration. Shows completion summary and auto-hides after 30s. -* **Target Files:** `generation_orchestrator.py`, `web/dashboard.js`, `resources/logic_events.js`, `resources/template.html` - -#### **14. Cache Trigger Word Placement** (low priority) - -* **Problem:** `trigger_words.py` logic runs every loop iteration. -* **Instruction:** -1. **Modify `trigger_words.py`:** -2. **Apply:** Decorate `get_filtered_lora_triggers` - -* **Target Files:** `trigger_words.py` - - - - -#### **17. Menu Refactor (Cog Wheel)** COMPLETED - -* **Problem:** Header is cluttered. -* **Instruction:** -1. **Modify `resources/template.html`:** Replace the "Session" button with a `div` class "menu-container" containing an SVG gear icon. -2. **Dropdown:** Create a hidden `div` "menu-dropdown". Move "Cols", "Go To", and "Save/Load" inputs inside it. -3. **Modify `resources/logic_ui.js`:** Add logic to toggle visibility of "menu-dropdown" when the gear is clicked. - -Move COLS input into session popup modal, change session to cogwheel, change filters to filter icon, - -* **Target Files:** `resources/template.html`, `resources/logic_ui.js` -* **Status:** Moved Go To #, Cols, and Reset Zoom into a cog wheel (gear icon) dropdown menu. Added SVG gear icon button, CSS for dropdown menu with items, click-outside-to-close, and Esc key handling. -* **Target Files:** `resources/template.html`, `resources/logic_ui.js`, `resources/report.css` - -#### **18. Optionally Pack workflow into images ** (low priority) - -* **Problem:** Pack workflow into images optionally -* **Instruction:** -1. **Modify `image_generation.py`:** In `save_image_to_disk`. -2. **Implementation:** Use `PIL.PngImagePlugin.PngInfo`. Create a `PngInfo` object. Add `info.add_text("prompt", json.dumps(prompt))`. Pass this `pnginfo` to `image.save()`. -3. **Data Source:** Ensure the raw prompt/workflow is passed down from `sampler_node.py`. - - -* **Target Files:** `image_generation.py`, `generation_orchestrator.py` - - -#### **20. Hotkeys Reference List** COMPLETED - -* **Problem:** Users don't know shortcuts. -* **Status:** Already implemented in `resources/template.html` as a KEYBOARD SHORTCUTS section in the Filters & Info popup, with a table showing Space, Shift+Space, Arrow Keys, +/-, 0, F, and Shift+Click shortcuts. `0 Key Resets Zoom & Pan` Etc -* **Target Files:** `resources/template.html` - -Put the reference list at the bottom of the session popup modal - -#### **21. Virtual DOM Pan/Zoom (Canvas Builder)** (low priority) - -* **Problem:** The Config Builder UI (graph node) needs infinite canvas capabilities for large node graphs. -* **Instruction:** -1. **Scope Check:** If this refers to the *Dashboard*, it's done (`logic_virtual.js`). If this refers to the *Config Builder Node UI* (`web/config_builder.js`), it needs HTML5 Canvas implementation. -2. **Implementation:** In `web/config_builder.js`, wrap the main container in a parent `div` with `overflow: hidden`. Implement `mousedown` (start pan), `mousemove` (update transform translate), `wheel` (update transform scale) event listeners on the container. - - -* **Target Files:** `web/config_builder.js` - -#### **22. Import Configs (Merge)** (low priority) - -* **Problem:** Can only load full sessions, not merge snippets. -* **Instruction:** -1. **Backend:** `config_utils.py` handles parsing. -2. **Frontend (`web/config_builder.js`):** Add "Import JSON" button. -3. **Logic:** On click, open file picker. Read JSON. Iterate through arrays in JSON. Push them into `this.state.config_arrays`. Call `this.renderUI()`. - - -* **Target Files:** `web/config_builder.js` - -#### **23. Pseudo-JSON Nodes (Recursion)** (low priority) - -* **Problem:** Advanced. Running a raw JSON workflow as a sub-node. -* **Instruction:** -1. **Modify `json_text_node.py`:** This needs to interface with `comfy.nodes.GraphExecutor`. -2. **Logic:** Treat the input JSON as a "Group Node". Map the inputs of the `SamplerGridTester` to the inputs defined in the JSON. Execute the subgraph. Return the latent. - -More info needed on how this could work, would like to see a visual interface for it in the builder UI eventaully. (big job, very low priority) - -* **Target Files:** `json_text_node.py`, `sampler_node.py` - -#### **24. Combinatorial Randomization** - More Randomization tools - generate x configs from y possibilities and z prompts - (very low priority) - -* **Problem:** Feature. Combinatorial generation logic. Combine random prompts with random loras, fun! -* **Instruction:** -1. **Modify `config_builder_node.py`:** In `generate_config`. -2. **Logic:** Add a "Random Sample" mode. Instead of `itertools.product` (all combos), use `random.sample(all_combos, k=N)`. - - -* **Target Files:** `config_builder_node.py` - -#### **25. Double Click Filter (Isolate)** - (very low priority) - -* **Problem:** Tedious to uncheck all other filters. -* **Instruction:** -1. **Modify `resources/logic_ui.js`:** When generating filter tag buttons. -2. **Event:** Add `ondblclick`. -3. **Logic:** On double click, clear all other filters in that category and select only the clicked one. Call `logic_pipeline.update()`. - - -* **Target Files:** `resources/logic_ui.js` - -#### **26. Path Validation in Builder** (DONE) - - - - - -#### COMPLETED - -#### **27. Refresh Model List** COMPLETED - -#### **Show Rejected ASDFeature** COMPLETED -* **Problem:** Rejected images disappear; hard to undo. -3. **UI:** Added a toggle button for rejected in the Filters menu. - -#### **2. Session Load: Disable Auto-Save & Safe Filename** COMPLETED - -#### **6. Copy Favorites To Favorites Subfolders Based On LoRA Sets ** COMPLETED - -#### **4. Fix Lora Trigger Append Position** COMPLETED - -#### **15/16. Lookahead Caching Switch & Debug** COMPLETED - - -Add "Don't Append" option to Append Lora Triggerwords To: section. Adds all triggerwords to omit lora triggerwords list. COMPLETED - - -## Feature: LoRA Lookup From Builder UI. Get metadata, images, url, tags, & more to view quickly from builder in comfyui - -## Fix: Manifest doesn't need lora omit triggers list in every item. Save civitai lookup hashes when calculating lora short 256 and looking up lora trigger words. For use later with civit lookup lora / model info. Save all meta-data from lookup in a folder in output/benchmarks/model-data/{modelName} COMPLETED - - -Prompts manager section in config builder, browse & combine past prompts, analyze favorited tags, auto generate tag tests, COMPLETED - - -#### **3. Lora/Model Quick Toggle (Bypass)** COMPLETED - - -#### **8. Visualize Omitted Triggers** COMPLETED - - -## ~~Fix: Manifest doesn't need lora omit triggers list in every item~~ (DONE - already stripped in create_image_metadata via .pop()) diff --git a/docs/plans/2026-02-28-builder-ui-improvements-design.md b/docs/plans/2026-02-28-builder-ui-improvements-design.md deleted file mode 100644 index fe7e52f..0000000 --- a/docs/plans/2026-02-28-builder-ui-improvements-design.md +++ /dev/null @@ -1,224 +0,0 @@ -# Builder UI Improvements — Design Document - -**Date:** 2026-02-28 -**Scope:** 9 items — performance, bug fixes, and feature enhancements - -## Critical Constraint - -DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY. - ---- - -## 1. Performance Optimization (Builder UI Slow Everywhere) - -### Problem -`renderUI()` does a full DOM rebuild on every state change. There are 30+ call sites across `conf-builder-config-management.js` that trigger complete re-renders after single-item interactions (toggle, add, remove, dropdown change). This makes the UI sluggish on initial load AND on every interaction. - -### Solution: Eliminate unnecessary full re-renders -- **On/off toggles**: Already update DOM inline (opacity/filter). Remove any `node.renderUI()` calls following toggle actions — the visual update is already done. -- **Add model/lora/VAE**: After creating the new item in state, create its DOM element directly and `appendChild()` it to the container. Skip `renderUI()`. -- **Remove model/lora/VAE**: Remove the specific DOM element from the parent container. Skip `renderUI()`. -- **Dropdown changes** (model type, model selection, LoRA selection): Update only the affected card's content. Skip `renderUI()`. -- **Collapse/expand**: Already handled inline — verify no re-render follows. -- **Chip list builders** (samplers, schedulers): Already DON'T call `renderUI()` — no change needed. -- **Bulk operations** (load session, add config, delete config, duplicate config): Keep `renderUI()` — these legitimately change entire UI structure. - -### Approach for add/remove operations -Each `renderUI()` call after an add operation follows this pattern: -```javascript -// Current: add to state then rebuild everything -node.state.config_arrays[arrayIdx].models.push(newModel); -node.saveState(); -node.renderUI(); - -// New: add to state then append just the new element -node.state.config_arrays[arrayIdx].models.push(newModel); -node.saveState(); -const newElement = createModelElement(node, newModel, arrayIdx, newIdx, modelLists); -modelsContainer.appendChild(newElement); -``` - -For remove operations: -```javascript -// Current: remove from state then rebuild everything -node.state.config_arrays[arrayIdx].models.splice(idx, 1); -node.saveState(); -node.renderUI(); - -// New: remove from state then remove DOM element -node.state.config_arrays[arrayIdx].models.splice(idx, 1); -node.saveState(); -element.parentNode.removeChild(element); -``` - -### Key files -- `web/conf_builder/conf-builder-config-management.js` — all 30+ `renderUI()` call sites - ---- - -## 2. VAE & Text Encoder On/Off Switches (Bug Fix + Cleanup) - -### Problem -VAE on/off switches exist in the UI but user reports they "don't work." Additionally, `vae_bypass_states` and `te_bypass_states` are being output as separate fields in the `config_json` output (lines 586-594 in `conf-builder-utilities.js`), which is unnecessary — they should only control filtering. - -### Root Cause -The bypass states are internal UI state that controls which items are included. The filtering in `convertStateToConfigs()` at line 500 already works: -```javascript -const vaes = (configArray.vaes || []).filter(v => v && v !== "None" && !configArray.vae_bypass_states?.[v]); -``` -But the bypass state dicts are ALSO being added to the output config (lines 586-594), polluting the JSON output. Additionally, the bypass states may not be persisted properly across sessions. - -### Fix -1. **Remove** lines 586-594 in `conf-builder-utilities.js` that output `vae_bypass_states` and `te_bypass_states` to config JSON — these are UI-internal, not config parameters -2. **Verify** the bypass toggle onclick handler properly persists via `node.saveState()` -3. **Verify** bypass states survive load/save session cycles (check migration in `loadSession`) -4. Ensure `convertConfigsToConfigArrays()` preserves bypass states when loading - -### Key files -- `web/conf_builder/conf-builder-utilities.js` lines 586-594 (remove output), line 500 (VAE filter), line 545 (TE filter) -- `web/conf_builder/conf-builder-config-management.js` — VAE element (line 2277+), TE section (line 1664+) - ---- - -## 3. Model Hashing Bug Fix - -### Problem -Console shows `"Using fallback hash for model: 559ad07aae"` — models from the builder UI aren't getting proper SHA256 hashes. - -### Root Cause -In `metadata_packer.py` line 145-175, `find_model_file()` only searches `checkpoints` and `loras` folder paths. It **never checks `diffusion_models` or `unet` folders**. When a diffusion model is used, the file isn't found, and a filename-based hash is generated instead (line 268-272). - -### Fix -Add `diffusion_models` and `unet` to the search paths in `find_model_file()`: -```python -search_paths = [ - folder_paths.get_folder_paths("checkpoints")[0] if folder_paths.get_folder_paths("checkpoints") else None, - folder_paths.get_folder_paths("loras")[0] if folder_paths.get_folder_paths("loras") else None, - folder_paths.get_folder_paths("diffusion_models")[0] if folder_paths.get_folder_paths("diffusion_models") else None, - folder_paths.get_folder_paths("unet")[0] if folder_paths.get_folder_paths("unet") else None, -] -``` - -Also consider using `folder_paths.get_full_path()` which handles all model types, as a secondary fallback before the filename hash. - -### Key files -- `metadata_packer.py` lines 145-175 (`find_model_file()`), lines 260-272 (fallback hash) - ---- - -## 4. Dashboard Prompt Toggle (Hide Trigger Words) - -### Problem -Dashboard always shows prompts WITH lora trigger words appended. When comparing prompts across different loras, the trigger words create visual noise and apparent duplicates. - -### Solution -Store the raw config prompt (without triggers) alongside the final prompt in the manifest. Add a toggle in the dashboard to switch between views. - -### Implementation -1. **In `image_generation.py` `create_image_metadata()`**: Before overwriting `positive` with `actual_positive_prompt`, preserve the raw config prompt: - ```python - meta["config_positive"] = config.get("positive", "") - meta["config_negative"] = config.get("negative", "") - meta.update(update_dict) # This overwrites "positive" with actual_positive_prompt - ``` - -2. **In `resources/template.html`**: Add a toggle checkbox near the prompt filter area: - ```html - - ``` - -3. **In `resources/logic_ui.js`** line 836-837: Check toggle state when displaying prompts: - ```javascript - const showTriggers = document.getElementById('toggle-triggers')?.checked !== false; - if (posEl) posEl.value = showTriggers ? (d.positive || meta.positive || "") : (d.config_positive || d.positive || meta.positive || ""); - ``` - -4. **In filter buttons**: When toggle is off, filter by `config_positive` instead of `positive` - -### Key files -- `image_generation.py` line 356-367 (`create_image_metadata`) -- `resources/template.html` line 287-294 (filter area) -- `resources/logic_ui.js` line 831-837 (prompt display) - ---- - -## 5. CivitAI Lookup Enhancements - -### Current State -- Cache warning banner exists (lines 1186-1195) with orange styling -- Full JSON toggle exists (lines 1287-1300) for models and (lines 1517-1529) for loras -- Cache date shown from `data.cache_date` - -### Enhancements Needed -1. **Bigger cache warning**: Move to very top of modal, increase font size, add a red/orange gradient border -2. **Re-check button**: Add "Re-fetch from CivitAI" button inside the cache banner. Calls the same endpoint with `force_refresh=true` parameter -3. **Save JSON button**: Add "Save JSON Response" button next to the JSON toggle. Downloads the JSON as a `.json` file using `Blob` + `URL.createObjectURL()` -4. **Backend `force_refresh` support**: In the CivitAI lookup endpoint in `config_builder_node.py`, accept `force_refresh` parameter that skips disk cache and re-fetches from API - -### Key files -- `web/conf_builder/conf-builder-config-management.js` — model modal (line 1112+), lora modal (line 1375+) -- `config_builder_node.py` — CivitAI lookup endpoints - ---- - -## 6. Sampler/Scheduler Instant Add (Verification) - -### Current State -The `onchange` handler in `createChipListBuilder()` (line 239) already adds items immediately when a dropdown option is selected. The Add button is redundant. - -### Action Needed -Verify that no `renderUI()` call follows the chip addition that would cause perceived lag. The `createChipListBuilder` function correctly uses local `renderChips()` + `populateSelect()` without triggering a full re-render. If there's still lag, it's from the performance issue (Item 1) affecting other interactions. - -### Key files -- `web/conf_builder/conf-builder-config-management.js` lines 159-283 - ---- - -## 7. Custom Lora Trigger Word Editor - -### Design -A dedicated modal accessible from each lora card via an "Edit Triggers" button. - -### UI -- Button: "✏️ Edit Triggers" on each lora card, next to the existing "🔍 Lookup LoRA Metadata from CivitAI" button -- Modal contains: - - Header: "Edit Trigger Words — {lora_name}" - - Current triggers displayed as editable chips (each with × remove button) - - Text input field + Enter/Add button to add new trigger words - - "Save" button (saves to `loras_tags.json`) and "Cancel" button - - "Reset to CivitAI" button that re-fetches from API and replaces custom edits - -### Backend -- New endpoint: `POST /configbuilder/save_lora_triggers` - - Request body: `{ "lora_name": "...", "triggers": ["word1", "word2"] }` - - Reads `loras_tags.json`, updates the entry, writes back - - Clears the trigger word LRU cache after save -- New endpoint: `GET /configbuilder/get_lora_triggers?lora_name=...` - - Returns current triggers from `loras_tags.json` - -### Data flow -- `loras_tags.json` format: `{ "lora_name.safetensors": ["trigger1", "trigger2"] }` -- Editor reads from and writes to this file -- Trigger word cache (`@lru_cache`) cleared after save so changes take effect immediately - -### Key files -- `web/conf_builder/conf-builder-config-management.js` — new modal + button on lora card -- `config_builder_node.py` — new endpoints -- `lora_utils.py` — `loras_tags.json` read/write -- `trigger_words.py` — cache clearing - ---- - -## Implementation Order - -Recommended order (dependencies + quick wins first): - -1. **Model hashing bug** — smallest change, standalone fix -2. **VAE & TE on/off switches** — small fix, verifiable immediately -3. **Performance optimization** — foundational, makes everything else feel better -4. **Sampler/scheduler instant add** — verification only after perf fix -5. **CivitAI lookup enhancements** — moderate scope, self-contained -6. **Dashboard prompt toggle** — touches backend + frontend, needs manifest change -7. **Custom lora trigger word editor** — largest new feature, self-contained - -Total estimated scope: ~15-20 implementation tasks across 8-10 files. diff --git a/docs/plans/2026-02-28-builder-ui-improvements.md b/docs/plans/2026-02-28-builder-ui-improvements.md deleted file mode 100644 index 2097065..0000000 --- a/docs/plans/2026-02-28-builder-ui-improvements.md +++ /dev/null @@ -1,780 +0,0 @@ -# Builder UI Improvements Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Fix bugs (model hashing, VAE on/off switches), optimize builder UI performance, enhance CivitAI lookup modals, add dashboard prompt toggle, and build a lora trigger word editor. - -**Architecture:** Incremental changes across the existing JS/Python codebase. Performance fix eliminates unnecessary full DOM rebuilds. Bug fixes are targeted. New features follow existing modal/endpoint patterns. - -**Tech Stack:** Vanilla JavaScript (ComfyUI widget), Python aiohttp routes, JSON file storage. - -**Critical Constraint:** DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY. - ---- - -### Task 1: Fix Model Hashing Bug (metadata_packer.py) - -**Files:** -- Modify: `metadata_packer.py:145-175` (find_model_file function) - -**Step 1: Add diffusion_models and unet to search paths** - -In `find_model_file()` (line 156-163), the `search_paths` default only includes `checkpoints` and `loras`. Add `diffusion_models` and `unet`: - -```python -# Current (lines 156-163): -if search_paths is None: - import folder_paths - search_paths = [ - folder_paths.get_folder_paths("checkpoints")[0] if folder_paths.get_folder_paths("checkpoints") else None, - folder_paths.get_folder_paths("loras")[0] if folder_paths.get_folder_paths("loras") else None, - ] - search_paths = [p for p in search_paths if p] - -# New: -if search_paths is None: - import folder_paths - search_paths = [] - for folder_type in ["checkpoints", "loras", "diffusion_models", "unet"]: - try: - paths = folder_paths.get_folder_paths(folder_type) - if paths: - search_paths.extend(paths) - except Exception: - pass -``` - -Note: Use `extend(paths)` not just `[0]` — some folder types have multiple search directories. - -**Step 2: Add folder_paths.get_full_path fallback before filename hash** - -In lines 262-272, before the filename-based fallback hash, try `folder_paths.get_full_path()` for multiple model types: - -```python -# After line 263 (model_file = find_model_file(model, checkpoint_paths)): -# Add this BEFORE the existing fallback block (lines 268-272): -if not model_file: - # Try folder_paths.get_full_path for various model types - import folder_paths as fp - for model_type in ["checkpoints", "diffusion_models", "unet"]: - try: - resolved = fp.get_full_path(model_type, model) - if resolved and os.path.isfile(resolved): - model_file = resolved - break - except Exception: - pass -``` - -**Step 3: Verify syntax** - -Run: `python -c "import py_compile; py_compile.compile('metadata_packer.py', doraise=True)"` -Expected: No errors - -**Step 4: Commit** - -```bash -git add metadata_packer.py -git commit -m "Fix model hashing: search diffusion_models and unet folders" -``` - ---- - -### Task 2: Fix VAE & Text Encoder On/Off Switches - -**Files:** -- Modify: `web/conf_builder/conf-builder-utilities.js:586-594` - -**Step 1: Remove bypass state fields from config JSON output** - -The VAE/TE bypass states are internal UI state — they should filter the arrays (which they already do at lines 500 and 545) but NOT appear in the output config JSON. Remove lines 586-594: - -```javascript -// DELETE these lines (586-594) — bypass states are internal, not config output: - // Add vae_bypass_states if any are set - if (configArray.vae_bypass_states && Object.keys(configArray.vae_bypass_states).length > 0) { - config.vae_bypass_states = configArray.vae_bypass_states; - } - - // Add te_bypass_states if any are set - if (configArray.te_bypass_states && Object.keys(configArray.te_bypass_states).length > 0) { - config.te_bypass_states = configArray.te_bypass_states; - } -``` - -Wait — the constraint says DO NOT REMOVE ANY CODE. So instead, comment them out and add an explanation: - -```javascript - // vae_bypass_states and te_bypass_states are internal UI state only. - // They control filtering (lines 500, 545) but should NOT be in config output. - // Bypass state persistence is handled by node.saveState() separately. - // if (configArray.vae_bypass_states && Object.keys(configArray.vae_bypass_states).length > 0) { - // config.vae_bypass_states = configArray.vae_bypass_states; - // } - // if (configArray.te_bypass_states && Object.keys(configArray.te_bypass_states).length > 0) { - // config.te_bypass_states = configArray.te_bypass_states; - // } -``` - -**Step 2: Verify bypass states persist across sessions** - -Check that `conf-builder-main.js` migration block includes `vae_bypass_states` and `te_bypass_states` in the default state. They should already be there (line ~3486 in config-management.js sets defaults). Also verify `loadSession` and `loadConfigFromBackend` preserve them. - -**Step 3: Commit** - -```bash -git add web/conf_builder/conf-builder-utilities.js -git commit -m "Fix VAE/TE on/off switches: remove bypass states from config output" -``` - ---- - -### Task 3: Performance — Eliminate Unnecessary renderUI() Calls - -**Files:** -- Modify: `web/conf_builder/conf-builder-config-management.js` (many locations) - -This is the largest task. There are 30+ `node.renderUI()` calls after single-item interactions. Most can be replaced with targeted DOM updates. - -**Category A: Toggle/checkbox changes that DON'T need re-render** - -These already update DOM inline. Remove the `node.renderUI()` call: - -- Line 975: Strength lock toggle — BUT this one actually needs re-render to show/hide the CLIP slider. **KEEP this one.** -- Line 3307: Custom prompts toggle — needs re-render to show/hide prompt editors. **KEEP this one.** -- Line 3510: Label mode toggle — needs re-render to change display. **KEEP this one.** - -**Category B: Model type / selection changes that trigger re-render** - -These change the element structure (different dropdowns, different options). These are harder to optimize without a significant refactor. **KEEP these for now** but they're candidates for future optimization: - -- Line 591: Model type changed (checkpoint/diffusion/gguf) — changes available options -- Line 611: Model selection via searchable select -- Line 861: LoRA type changed -- Line 880: LoRA selection via searchable select - -**Category C: Add/Delete operations that CAN be optimized** - -Replace `renderUI()` with targeted DOM manipulation: - -**Step 1: Add Model button (line 384-389)** - -Replace the onclick handler. Instead of rebuilding everything, create and append the new model element: - -```javascript -// Line 384-389 — replace the onclick handler: -addModelBtn.onclick = () => { - if (!node.state.config_arrays[arrayIdx].models) node.state.config_arrays[arrayIdx].models = []; - node.state.config_arrays[arrayIdx].models.push("None"); - node.saveState(); - // Instead of node.renderUI(), append new element directly - const newIdx = node.state.config_arrays[arrayIdx].models.length - 1; - const modelsContainer = document.querySelector(`#config-array-${arrayIdx} .cb-models-container`); - if (modelsContainer) { - const newElement = createModelElement(node, "None", arrayIdx, newIdx, modelLists); - modelsContainer.appendChild(newElement); - } else { - node.renderUI(); // Fallback if container not found - } -}; -``` - -**IMPORTANT:** For this to work, we need to add an id or class to the models container in `renderModelsSection()`. Find where models are rendered and add `modelsContainer.className = "cb-models-container";` or similar. We also need `modelLists` to be accessible. - -**Given the complexity of making all add/delete operations work without renderUI, and the constraint "ONLY CHANGE WHAT IS NECESSARY", a better approach is debouncing:** - -**Step 2: Add debounced renderUI wrapper** - -At the top of conf-builder-config-management.js, add a debounced version: - -```javascript -// Debounced renderUI to batch rapid state changes -let _renderUITimer = null; -function debouncedRenderUI(node) { - if (_renderUITimer) clearTimeout(_renderUITimer); - _renderUITimer = setTimeout(() => { - _renderUITimer = null; - node.renderUI(); - }, 150); -} -``` - -Then replace `node.renderUI()` with `debouncedRenderUI(node)` at all 30+ call sites EXCEPT: -- Initial load / loadSession / loadConfig (these should render immediately) -- Add config / delete config / duplicate config (structural changes, render immediately) - -This batches rapid changes (e.g., strength slider dragging) without the risk of breaking targeted DOM updates. - -**Step 3: Optimize the main renderUI function for speed** - -In the `renderUI` function (conf-builder-config-management.js line ~3440), the scroll position save/restore is already there. Add one optimization: skip re-fetching model lists if they haven't changed. - -In `conf-builder-main.js` line 213, the `renderUI` function does `await utilities.getModelLists()` every time. This fetches from the backend. Cache it: - -```javascript -// In conf-builder-main.js, add caching for model lists: -let _cachedModelLists = null; -let _modelListsCacheTime = 0; - -this.renderUI = async function () { - if (!utilities || !configManagement) return; - - // Cache model lists for 5 seconds to avoid redundant fetches during rapid re-renders - const now = Date.now(); - if (!_cachedModelLists || (now - _modelListsCacheTime) > 5000) { - const availableLoras = await utilities.getAvailableLoras(); - const loraFolders = await utilities.getLoraFolders(); - await utilities.getModelLists(); - _cachedModelLists = { - // ... build modelLists object ... - }; - _modelListsCacheTime = now; - } - // ... rest of renderUI using _cachedModelLists ... -}; -``` - -Actually, the simplest high-impact change: the `renderUI` function in `conf-builder-main.js` makes multiple `await` calls to get model lists. These can be parallelized: - -```javascript -// Current: sequential awaits (slow) -const availableLoras = await utilities.getAvailableLoras(); -const loraFolders = await utilities.getLoraFolders(); -const availableSessions = await utilities.getAvailableSessions(); -const availableConfigs = await utilities.getAvailableConfigs(); -await utilities.getModelLists(); - -// New: parallel fetches (fast) -const [availableLoras, loraFolders, availableSessions, availableConfigs] = await Promise.all([ - utilities.getAvailableLoras(), - utilities.getLoraFolders(), - utilities.getAvailableSessions(), - utilities.getAvailableConfigs(), - utilities.getModelLists() -]); -``` - -**Step 4: Commit** - -```bash -git add web/conf_builder/conf-builder-config-management.js web/conf_builder/conf-builder-main.js -git commit -m "Optimize builder UI: debounce renderUI, parallelize model list fetches" -``` - ---- - -### Task 4: CivitAI Lookup Enhancements - -**Files:** -- Modify: `web/conf_builder/conf-builder-config-management.js` (both modals) -- Modify: `config_builder_node.py` (lookup endpoints) - -**Step 1: Add force_refresh support to backend endpoints** - -In `config_builder_node.py`, modify both lookup endpoints to accept `force_refresh`: - -For the model endpoint (line 816+), after line 820 (`data = await request.json()`): -```python - force_refresh = data.get("force_refresh", False) -``` - -Then wrap the cache check (lines 836-851) with: -```python - if os.path.exists(metadata_file) and not force_refresh: -``` - -Same pattern for the LoRA endpoint (line 703+). - -**Step 2: Add Re-fetch button to cache banner (both modals)** - -In the LoRA modal (line 1186-1195), enhance the cache banner: - -```javascript - if (data.cached) { - const cacheBanner = document.createElement("div"); - cacheBanner.style.cssText = "background: #553300; border: 2px solid #ffaa00; border-radius: 6px; padding: 10px 14px; margin-bottom: 15px; text-align: center;"; - cacheBanner.innerHTML = ` -
⚠️ READ FROM DISK CACHE
-
Last looked up on: ${data.cache_date || 'Unknown'}
- `; - // Re-fetch button - const refetchBtn = document.createElement("button"); - refetchBtn.className = "cb-button"; - refetchBtn.style.cssText = "margin-top: 8px; background: #664400; border: 1px solid #ffaa00; color: #ffcc44; font-size: 12px; padding: 6px 16px;"; - refetchBtn.textContent = "🔄 Re-fetch from CivitAI Now"; - refetchBtn.onclick = async () => { - refetchBtn.disabled = true; - refetchBtn.textContent = "🔄 Fetching..."; - closeModal(); - await showLoraMetadataModal(node, arrayIdx, loraName, true); // pass force_refresh - }; - cacheBanner.appendChild(refetchBtn); - content.appendChild(cacheBanner); - } -``` - -Update `showLoraMetadataModal` signature to accept `forceRefresh` parameter: -```javascript -async function showLoraMetadataModal(node, arrayIdx, loraName, forceRefresh = false) { -``` - -And update the fetch call (line 1163-1166) to include it: -```javascript - body: JSON.stringify({ lora_name: loraName, force_refresh: forceRefresh }) -``` - -Apply the SAME pattern to `showModelMetadataModal` (lines 1340+, 1416-1425). - -**Step 3: Add Save JSON button** - -Near the existing JSON toggle (line 1287-1300 for models, 1517-1529 for loras), add a save button: - -```javascript - // Save JSON button - const saveJsonBtn = document.createElement("button"); - saveJsonBtn.className = "cb-button"; - saveJsonBtn.style.cssText = "margin-bottom: 8px; margin-left: 8px; background: #333; border-left: 3px solid #44aa44;"; - saveJsonBtn.textContent = "💾 Save JSON Response"; - saveJsonBtn.onclick = () => { - const blob = new Blob([JSON.stringify(metadata, null, 2)], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${loraName.replace(/[/\\]/g, "_")}_civitai_metadata.json`; - a.click(); - URL.revokeObjectURL(url); - }; - jsonSection.appendChild(saveJsonBtn); -``` - -**Step 4: Commit** - -```bash -git add web/conf_builder/conf-builder-config-management.js config_builder_node.py -git commit -m "Enhance CivitAI modals: re-fetch button, save JSON, force_refresh" -``` - ---- - -### Task 5: Dashboard Prompt Toggle - -**Files:** -- Modify: `image_generation.py:356-367` (create_image_metadata) -- Modify: `resources/template.html:286-294` (add toggle) -- Modify: `resources/logic_ui.js:831-837` (prompt display) -- Modify: `resources/logic_pipeline.js:291-307` (filter logic) - -**Step 1: Store raw config prompt in metadata** - -In `image_generation.py` `create_image_metadata()`, preserve the raw config prompts before they get overwritten. Add before line 356 (the `update_dict`): - -```python - # Preserve raw config prompts (without trigger words) for dashboard toggle - meta["config_positive"] = config.get("positive", "") - meta["config_negative"] = config.get("negative", "") -``` - -These get added to the meta dict BEFORE `meta.update(update_dict)` overwrites `positive` and `negative` with the trigger-appended versions. - -**Step 2: Add toggle to dashboard template** - -In `resources/template.html`, after line 279 (the FILTERS section title), add a toggle: - -```html -
- -
-``` - -**Step 3: Update prompt display in logic_ui.js** - -In `resources/logic_ui.js`, modify lines 836-837: - -```javascript - // Check trigger word toggle - const showTriggers = document.getElementById('toggle-triggers')?.checked !== false; - if (posEl) posEl.value = showTriggers - ? (d.positive || meta.positive || "") - : (d.config_positive || d.positive || meta.positive || ""); - if (negEl) negEl.value = showTriggers - ? (d.negative || meta.negative || "") - : (d.config_negative || d.negative || meta.negative || ""); -``` - -**Step 4: Update filter logic in logic_pipeline.js** - -In `resources/logic_pipeline.js`, modify lines 304-305 to respect the toggle: - -```javascript - const showTriggers = document.getElementById('toggle-triggers')?.checked !== false; - if (filters.positive.size > 0) { - const posValue = showTriggers - ? (d.positive || meta.positive || "") - : (d.config_positive || d.positive || meta.positive || ""); - if (!filters.positive.has(posValue)) return false; - } - if (filters.negative.size > 0) { - const negValue = showTriggers - ? (d.negative || meta.negative || "") - : (d.config_negative || d.negative || meta.negative || ""); - if (!filters.negative.has(negValue)) return false; - } -``` - -**Step 5: Update filter button generation in logic_ui.js** - -In `resources/logic_ui.js` `initFilters()` (lines 145-152), update the positive/negative value extraction: - -```javascript - const showTriggers = document.getElementById('toggle-triggers')?.checked !== false; - // ... inside the forEach ... - if (key === 'positive') return showTriggers - ? (d.positive || meta.positive || "") - : (d.config_positive || d.positive || meta.positive || ""); - if (key === 'negative') return showTriggers - ? (d.negative || meta.negative || "") - : (d.config_negative || d.negative || meta.negative || ""); -``` - -**Step 6: Add toggleTriggerWords function in logic_ui.js** - -```javascript -function toggleTriggerWords(showTriggers) { - // Rebuild filters with new prompt mode - initFilters(); - updateDataPipeline(); - // If modal is open, refresh prompt display - if (window.currentModalId) { - openM(window.currentModalId); - } -} -``` - -**Step 7: Commit** - -```bash -git add image_generation.py resources/template.html resources/logic_ui.js resources/logic_pipeline.js -git commit -m "Add dashboard toggle to show/hide trigger words in prompts" -``` - ---- - -### Task 6: Custom Lora Trigger Word Editor - -**Files:** -- Modify: `web/conf_builder/conf-builder-config-management.js` (add button + modal) -- Modify: `config_builder_node.py` (new endpoints) - -**Step 1: Add backend endpoints** - -In `config_builder_node.py`, after the existing route handlers (after line ~940), add: - -```python -@server.PromptServer.instance.routes.get("/configbuilder/get_lora_triggers") -async def get_lora_triggers_endpoint(request): - """Get trigger words for a specific LoRA from loras_tags.json""" - try: - lora_name = request.query.get("lora_name", "") - if not lora_name: - return web.json_response({"error": "Missing lora_name"}, status=400) - - json_tags_path = os.path.join(folder_paths.get_output_directory(), "benchmarks/loras_tags.json") - triggers = [] - if os.path.exists(json_tags_path): - from .lora_utils import load_json_from_file - lora_tags = load_json_from_file(json_tags_path) or {} - # Try exact match, normalized, and backslash variants - normalized = lora_name.replace("\\", "/") - backslash = lora_name.replace("/", "\\") - triggers = lora_tags.get(lora_name, lora_tags.get(normalized, lora_tags.get(backslash, []))) - - return web.json_response({"lora_name": lora_name, "triggers": triggers}) - except Exception as e: - return web.json_response({"error": str(e)}, status=500) - - -@server.PromptServer.instance.routes.post("/configbuilder/save_lora_triggers") -async def save_lora_triggers_endpoint(request): - """Save edited trigger words for a LoRA to loras_tags.json""" - try: - data = await request.json() - lora_name = data.get("lora_name", "") - triggers = data.get("triggers", []) - - if not lora_name: - return web.json_response({"error": "Missing lora_name"}, status=400) - - json_tags_path = os.path.join(folder_paths.get_output_directory(), "benchmarks/loras_tags.json") - from .lora_utils import load_json_from_file, save_dict_to_json - lora_tags = {} - if os.path.exists(json_tags_path): - lora_tags = load_json_from_file(json_tags_path) or {} - - # Normalize the name for consistent storage - normalized = lora_name.replace("\\", "/") - lora_tags[normalized] = triggers - - save_dict_to_json(lora_tags, json_tags_path) - - # Clear the trigger word LRU cache so changes take effect immediately - from .trigger_words import clear_trigger_caches - clear_trigger_caches() - - print(f"[ConfigBuilder] ✏️ Saved {len(triggers)} trigger words for: {normalized}") - return web.json_response({"status": "saved", "lora_name": normalized, "triggers": triggers}) - except Exception as e: - return web.json_response({"error": str(e)}, status=500) -``` - -**Step 2: Add "Edit Triggers" button to lora card** - -In `conf-builder-config-management.js`, after line 1056 (the existing LoRA metadata button), add: - -```javascript - // 4. Edit Trigger Words Button - const editTriggersBtn = document.createElement("button"); - editTriggersBtn.className = "cb-button"; - editTriggersBtn.style.cssText = `width: 100%; background: linear-gradient(135deg, #336633, #446644); border-left: 4px solid #66cc66; margin-top: 4px;`; - editTriggersBtn.textContent = "✏️ Edit Trigger Words"; - editTriggersBtn.onclick = async () => await showEditTriggersModal(node, arrayIdx, parsed.name); - moreOptionsContent.appendChild(editTriggersBtn); -``` - -**Step 3: Create the trigger word editor modal** - -Add this function near the other modal functions (after `showLoraMetadataModal` or `showModelMetadataModal`): - -```javascript -async function showEditTriggersModal(node, arrayIdx, loraName) { - // Build modal overlay (same pattern as metadata modals) - const overlay = document.createElement("div"); - overlay.style.cssText = "position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.85); z-index: 10000; display: flex; align-items: center; justify-content: center;"; - - const modal = document.createElement("div"); - modal.style.cssText = "background: #1a1a1a; border: 2px solid #66cc66; border-radius: 12px; padding: 25px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; position: relative;"; - - const closeModal = () => { document.body.removeChild(overlay); }; - overlay.onclick = (e) => { if (e.target === overlay) closeModal(); }; - document.addEventListener("keydown", function escHandler(e) { - if (e.key === "Escape") { closeModal(); document.removeEventListener("keydown", escHandler); } - }); - - // Close X button - const closeX = document.createElement("button"); - closeX.textContent = "✕"; - closeX.style.cssText = "position: absolute; top: 10px; right: 15px; background: none; border: none; color: #ff4444; font-size: 20px; cursor: pointer;"; - closeX.onclick = closeModal; - modal.appendChild(closeX); - - // Title - const title = document.createElement("h3"); - title.textContent = "✏️ Edit Trigger Words"; - title.style.cssText = "margin: 0 0 5px 0; color: #66cc66;"; - modal.appendChild(title); - - const subtitle = document.createElement("div"); - subtitle.style.cssText = "font-size: 12px; color: #888; margin-bottom: 15px;"; - subtitle.textContent = loraName.split('/').pop(); - modal.appendChild(subtitle); - - const status = document.createElement("div"); - status.textContent = "🔄 Loading trigger words..."; - status.style.cssText = "margin-bottom: 15px; color: #aaa; font-size: 12px;"; - modal.appendChild(status); - - const chipsContainer = document.createElement("div"); - chipsContainer.style.cssText = "display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 15px; min-height: 30px; padding: 8px; background: #111; border: 1px solid #333; border-radius: 6px;"; - modal.appendChild(chipsContainer); - - // Input row - const inputRow = document.createElement("div"); - inputRow.style.cssText = "display: flex; gap: 6px; margin-bottom: 15px;"; - const input = document.createElement("input"); - input.className = "cb-input"; - input.type = "text"; - input.placeholder = "Add new trigger word..."; - input.style.cssText = "flex: 1; padding: 6px 10px; font-size: 12px;"; - const addWordBtn = document.createElement("button"); - addWordBtn.className = "cb-button primary"; - addWordBtn.textContent = "+ Add"; - addWordBtn.style.cssText = "padding: 6px 12px; font-size: 12px;"; - inputRow.appendChild(input); - inputRow.appendChild(addWordBtn); - modal.appendChild(inputRow); - - // Button row - const btnRow = document.createElement("div"); - btnRow.style.cssText = "display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px;"; - - const saveBtn = document.createElement("button"); - saveBtn.className = "cb-button"; - saveBtn.style.cssText = "background: #336633; border: 1px solid #66cc66; color: #88ff88; padding: 8px 20px;"; - saveBtn.textContent = "💾 Save"; - - const cancelBtn = document.createElement("button"); - cancelBtn.className = "cb-button"; - cancelBtn.style.cssText = "background: #333; color: #aaa; padding: 8px 20px;"; - cancelBtn.textContent = "Cancel"; - cancelBtn.onclick = closeModal; - - btnRow.appendChild(cancelBtn); - btnRow.appendChild(saveBtn); - modal.appendChild(btnRow); - - overlay.appendChild(modal); - document.body.appendChild(overlay); - - // State - let currentTriggers = []; - - function renderTriggerChips() { - chipsContainer.innerHTML = ""; - if (currentTriggers.length === 0) { - const empty = document.createElement("span"); - empty.style.cssText = "color: #666; font-size: 11px; font-style: italic;"; - empty.textContent = "No trigger words"; - chipsContainer.appendChild(empty); - return; - } - currentTriggers.forEach((trigger, idx) => { - const chip = document.createElement("span"); - chip.style.cssText = "background: #224422; border: 1px solid #448844; color: #aaffaa; padding: 3px 8px; border-radius: 12px; font-size: 11px; display: flex; align-items: center; gap: 4px;"; - chip.textContent = trigger; - const removeBtn = document.createElement("span"); - removeBtn.textContent = "×"; - removeBtn.style.cssText = "cursor: pointer; color: #ff6666; font-weight: bold; margin-left: 2px;"; - removeBtn.onclick = () => { - currentTriggers.splice(idx, 1); - renderTriggerChips(); - }; - chip.appendChild(removeBtn); - chipsContainer.appendChild(chip); - }); - } - - function addTriggerWord() { - const word = input.value.trim(); - if (word && !currentTriggers.includes(word)) { - currentTriggers.push(word); - renderTriggerChips(); - input.value = ""; - } - input.focus(); - } - - addWordBtn.onclick = addTriggerWord; - input.onkeydown = (e) => { if (e.key === "Enter") { e.preventDefault(); addTriggerWord(); } }; - - saveBtn.onclick = async () => { - saveBtn.disabled = true; - saveBtn.textContent = "💾 Saving..."; - try { - const resp = await fetch("/configbuilder/save_lora_triggers", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ lora_name: loraName, triggers: currentTriggers }) - }); - if (resp.ok) { - status.textContent = "✅ Trigger words saved!"; - status.style.color = "#66ff66"; - setTimeout(closeModal, 800); - } else { - const err = await resp.json(); - status.textContent = "❌ Error: " + (err.error || "Save failed"); - status.style.color = "#ff6666"; - saveBtn.disabled = false; - saveBtn.textContent = "💾 Save"; - } - } catch (e) { - status.textContent = "❌ Error: " + e.message; - status.style.color = "#ff6666"; - saveBtn.disabled = false; - saveBtn.textContent = "💾 Save"; - } - }; - - // Fetch current triggers - try { - const resp = await fetch(`/configbuilder/get_lora_triggers?lora_name=${encodeURIComponent(loraName)}`); - if (resp.ok) { - const data = await resp.json(); - currentTriggers = data.triggers || []; - status.textContent = `Loaded ${currentTriggers.length} trigger word(s)`; - status.style.color = "#88ff88"; - } else { - status.textContent = "⚠️ No triggers found (you can add new ones)"; - status.style.color = "#ffaa00"; - } - } catch (e) { - status.textContent = "⚠️ Could not load triggers: " + e.message; - status.style.color = "#ffaa00"; - } - renderTriggerChips(); - input.focus(); -} -``` - -**Step 4: Verify syntax** - -Run: `python -c "import py_compile; py_compile.compile('config_builder_node.py', doraise=True)"` - -**Step 5: Commit** - -```bash -git add web/conf_builder/conf-builder-config-management.js config_builder_node.py -git commit -m "Add custom lora trigger word editor with modal UI and backend endpoints" -``` - ---- - -### Task 7: Sampler/Scheduler Instant Add (Verification) - -**Files:** -- Verify: `web/conf_builder/conf-builder-config-management.js:239-246` - -**Step 1: Verify behavior** - -The `createChipListBuilder` function already adds items on dropdown `onchange` (line 239). It does NOT call `renderUI()`. Verify this still works after the debouncing changes from Task 3. No code changes expected — this is a verification task. - -**Step 2: Commit (only if changes needed)** - -No commit expected unless issues found. - ---- - -### Task 8: Sync to Main Working Directory - -**Step 1: Verify all changes compile** - -```bash -python -c "import py_compile; py_compile.compile('metadata_packer.py', doraise=True)" -python -c "import py_compile; py_compile.compile('config_builder_node.py', doraise=True)" -python -c "import py_compile; py_compile.compile('image_generation.py', doraise=True)" -``` - -**Step 2: Fast-forward merge to main** - -```bash -cd Z:\comfy_v0.12.3\ComfyUI\custom_nodes\ComfyUI-Ultimate-Auto-Sampler-Config-Grid-Testing-Suite -git merge claude/upbeat-jackson --ff-only -``` - ---- - -## Summary - -| Task | Scope | Files | Estimated Steps | -|------|-------|-------|----------------| -| 1. Model hashing bug | Small | metadata_packer.py | 4 | -| 2. VAE/TE on/off fix | Small | conf-builder-utilities.js | 3 | -| 3. Performance | Medium | conf-builder-config-management.js, conf-builder-main.js | 4 | -| 4. CivitAI enhancements | Medium | conf-builder-config-management.js, config_builder_node.py | 4 | -| 5. Dashboard prompt toggle | Medium | image_generation.py, template.html, logic_ui.js, logic_pipeline.js | 7 | -| 6. Trigger word editor | Large | conf-builder-config-management.js, config_builder_node.py | 5 | -| 7. Sampler/scheduler verify | Tiny | (verification only) | 1 | -| 8. Sync to main | Tiny | git operations | 2 | diff --git a/docs/plans/2026-03-01-builder-ui-fixes-design.md b/docs/plans/2026-03-01-builder-ui-fixes-design.md deleted file mode 100644 index bf3f02b..0000000 --- a/docs/plans/2026-03-01-builder-ui-fixes-design.md +++ /dev/null @@ -1,113 +0,0 @@ -# Builder UI Fixes & Enhancements Design - -**Date:** 2026-03-01 -**Status:** Approved - -## Summary - -Five targeted improvements to the Builder UI: fix VAE/TE bypass toggle JSON -output, add force_refresh tag comparison prompts, add Remote VAE presets -dropdown, add per-config sidebar navigation, and reorganize sections with -improved header contrast. - -## Item 1: VAE/TE On/Off Switch Fix - -**Problem:** Bypassed VAEs and TEs still appear in JSON Preview despite being -toggled off. The bypass toggle onclick handler updates visual state and calls -`node.saveState()` but does NOT call `updatePreview(node)`, so the JSON preview -shows stale data. - -**Root cause:** In `conf-builder-config-management.js`, the bypass toggle in -`createVAEElement()` (line ~2713-2721) and similar handlers for TE and model -bypass toggles do not trigger `updatePreview(node)`. - -**Fix:** -- Add `updatePreview(node)` call after `node.saveState()` in every bypass - toggle onclick handler (VAE, TE, model) -- Add `normalizePath()` safety to bypass state filter key lookups in - `convertStateToConfigs()` to guard against path separator mismatches - -**Files:** `conf-builder-config-management.js`, `conf-builder-utilities.js` - -## Item 2: force_refresh Lora Tags Update Prompt - -**Problem:** When force_refresh re-fetches CivitAI metadata, the system -overwrites without checking if tags changed. User wants to see what changed -and choose whether to update. - -**Backend changes:** -- In `config_builder_node.py` `lookup_lora_metadata_endpoint`: when - `force_refresh=True`, compare fresh `trainedWords` with stored tags from - `loras_tags.json` -- Return additional fields: `tags_changed: bool`, `old_tags: list`, - `new_tags: list` - -**Frontend changes:** -- In the CivitAI metadata modal re-fetch handler: check response for - `tags_changed` -- Show inline diff banner with old/new tags and Update/Keep buttons -- Update button writes new tags to `loras_tags.json` via existing save endpoint - -**Files:** `config_builder_node.py`, `lora_utils.py`, -`conf-builder-config-management.js` - -## Item 3: HF Remote VAE Presets Dropdown - -**Problem:** When "Remote URL" VAE type is selected, user must manually paste -HuggingFace URLs. The URLs are already defined in Python (`remote_vae.py` -`HF_ENDPOINTS`) but not exposed to the frontend. - -**Fix:** -- Define `REMOTE_VAE_PRESETS` constant in JS matching Python's `HF_ENDPOINTS`: - `{ "HF-SD": "url", "HF-SDXL": "url", "HF-Flux": "url", "HF-HunyuanVideo": "url" }` -- In `createVAEElement()`, when type selector changes to "Remote URL", show a - presets dropdown below the type selector -- Selecting a preset auto-fills the URL input -- "Custom" option leaves URL input empty for manual entry - -**Files:** `conf-builder-config-management.js` - -## Item 4: Per-Config Sidebar Navigation - -**Problem:** Sidebar only has one "Config Arrays" icon. With multiple config -arrays, there's no way to quickly jump to a specific config's sub-section. - -**Fix:** -- Below the main ⚙️ Config Arrays icon, show numbered cog icons (one per - config array), color-coded using `CONFIG_COLORS` -- Hovering a config cog icon expands the sidebar vertically to show sub-section - icons: Models, Text Encoders, VAE, LoRAs -- Clicking a sub-icon scrolls to that specific sub-section -- Add `id` attributes to sub-section containers: - `cb-config-{idx}-models`, `cb-config-{idx}-te`, `cb-config-{idx}-vae`, - `cb-config-{idx}-loras` -- Scroll spy highlights the active config cog - -**Files:** `conf-builder-ui-components.js`, `conf-builder-config-management.js` - -## Item 5: Section Organization & Header Fixes - -**Problem:** Section order and header contrast need improvement for dark mode. - -**Changes:** -- **Section order:** Distribution Settings (top) → Global Prompts → Config - Arrays → JSON Preview -- **Extra Model & Sampling Options:** Move inside Models section as a - sub-section, rendered just below the models list -- **Text Encoders header:** Brighter to match Models section header style -- **VAE header:** Lighter background (`#444` instead of `#3a3a3a`), brighter - text color (`#cc66ff`) -- **LoRAs header:** Brighter text (`#4499ff`) - -**Files:** `conf-builder-config-management.js`, `conf-builder-ui-components.js` - -## Files Changed Summary - -| File | Changes | -|------|---------| -| `conf-builder-config-management.js` | Bypass toggle fix (Item 1), tag diff UI (Item 2), Remote VAE presets (Item 3), sub-section IDs (Item 4), section reorder + header colors (Item 5) | -| `conf-builder-utilities.js` | Path normalization in bypass filter (Item 1) | -| `conf-builder-ui-components.js` | Per-config sidebar nav (Item 4), header style updates (Item 5) | -| `config_builder_node.py` | Tag comparison in force_refresh (Item 2) | -| `lora_utils.py` | Tag comparison helper (Item 2) | -| `conf-builder-distribution.js` | No changes (section already has standalone renderer) | diff --git a/docs/plans/2026-03-01-builder-ui-fixes.md b/docs/plans/2026-03-01-builder-ui-fixes.md deleted file mode 100644 index 1780e8d..0000000 --- a/docs/plans/2026-03-01-builder-ui-fixes.md +++ /dev/null @@ -1,850 +0,0 @@ -# Builder UI Fixes & Enhancements Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Fix bypass toggle JSON output, add force_refresh tag diff prompts, add Remote VAE presets dropdown, add per-config sidebar navigation, and reorganize sections with improved headers. - -**Architecture:** Five independent UI/backend improvements to the Config Builder node. Items 1, 3, and 5 are frontend-only. Item 2 spans backend (Python) and frontend (JS). Item 4 modifies sidebar and section rendering. All changes are additive — existing code must not be removed. - -**Tech Stack:** Vanilla JS (ComfyUI widget), Python (aiohttp server routes), JSON file I/O - -**Critical Constraint:** DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY. - ---- - -## Task 1: Fix Bypass Toggle JSON Preview Update - -All four bypass toggles (model, TE, VAE, LoRA) update visual state and call `node.saveState()` but never call `updatePreview(node)`, so the JSON Preview shows stale data when items are toggled off. - -**Files:** -- Modify: `web/conf_builder/conf-builder-config-management.js:696-704` (model bypass) -- Modify: `web/conf_builder/conf-builder-config-management.js:975-982` (LoRA bypass) -- Modify: `web/conf_builder/conf-builder-config-management.js:2131-2139` (TE bypass) -- Modify: `web/conf_builder/conf-builder-config-management.js:2713-2721` (VAE bypass) - -**Step 1: Add `updatePreview(node)` to model bypass toggle** - -In `conf-builder-config-management.js`, find the model bypass onclick handler at line ~696: - -```javascript -bypassCheck.onclick = (e) => { - e.stopPropagation(); // Prevent header collapse - const newBypassState = !bypassCheck.checked; - node.state.config_arrays[arrayIdx].model_bypass_states[modelPath] = newBypassState; - node.saveState(); - // Visual feedback - div.style.opacity = newBypassState ? "0.5" : "1.0"; - div.style.filter = newBypassState ? "grayscale(0.7)" : "none"; -}; -``` - -Add `updatePreview(node);` after `node.saveState();`: - -```javascript -bypassCheck.onclick = (e) => { - e.stopPropagation(); // Prevent header collapse - const newBypassState = !bypassCheck.checked; - node.state.config_arrays[arrayIdx].model_bypass_states[modelPath] = newBypassState; - node.saveState(); - updatePreview(node); - // Visual feedback - div.style.opacity = newBypassState ? "0.5" : "1.0"; - div.style.filter = newBypassState ? "grayscale(0.7)" : "none"; -}; -``` - -**Step 2: Add `updatePreview(node)` to LoRA bypass toggle** - -Find the LoRA bypass onclick at line ~975: - -```javascript -bypassCheck.onclick = (e) => { - e.stopPropagation(); // Prevent header collapse - const newBypassState = !bypassCheck.checked; - node.state.config_arrays[arrayIdx].lora_bypass_states[parsed.name] = newBypassState; - node.saveState(); - // Visual feedback - div.style.opacity = newBypassState ? "0.5" : "1.0"; - div.style.filter = newBypassState ? "grayscale(0.7)" : "none"; -}; -``` - -Add `updatePreview(node);` after `node.saveState();`: - -```javascript -bypassCheck.onclick = (e) => { - e.stopPropagation(); // Prevent header collapse - const newBypassState = !bypassCheck.checked; - node.state.config_arrays[arrayIdx].lora_bypass_states[parsed.name] = newBypassState; - node.saveState(); - updatePreview(node); - // Visual feedback - div.style.opacity = newBypassState ? "0.5" : "1.0"; - div.style.filter = newBypassState ? "grayscale(0.7)" : "none"; -}; -``` - -**Step 3: Add `updatePreview(node)` to TE bypass toggle** - -Find the TE bypass onclick at line ~2131: - -```javascript -teBypassCheck.onclick = (e) => { - e.stopPropagation(); - const newBypassState = !teBypassCheck.checked; - node.state.config_arrays[arrayIdx].te_bypass_states[tePath] = newBypassState; - node.saveState(); - // Visual feedback - teRow.style.opacity = newBypassState ? "0.5" : "1.0"; - teRow.style.filter = newBypassState ? "grayscale(0.7)" : "none"; -}; -``` - -Add `updatePreview(node);` after `node.saveState();`: - -```javascript -teBypassCheck.onclick = (e) => { - e.stopPropagation(); - const newBypassState = !teBypassCheck.checked; - node.state.config_arrays[arrayIdx].te_bypass_states[tePath] = newBypassState; - node.saveState(); - updatePreview(node); - // Visual feedback - teRow.style.opacity = newBypassState ? "0.5" : "1.0"; - teRow.style.filter = newBypassState ? "grayscale(0.7)" : "none"; -}; -``` - -**Step 4: Add `updatePreview(node)` to VAE bypass toggle** - -Find the VAE bypass onclick at line ~2713: - -```javascript -bypassCheck.onclick = (e) => { - e.stopPropagation(); // Prevent header collapse - const newBypassState = !bypassCheck.checked; - node.state.config_arrays[arrayIdx].vae_bypass_states[vaeName] = newBypassState; - node.saveState(); - // Visual feedback - div.style.opacity = newBypassState ? "0.5" : "1.0"; - div.style.filter = newBypassState ? "grayscale(0.7)" : "none"; -}; -``` - -Add `updatePreview(node);` after `node.saveState();`: - -```javascript -bypassCheck.onclick = (e) => { - e.stopPropagation(); // Prevent header collapse - const newBypassState = !bypassCheck.checked; - node.state.config_arrays[arrayIdx].vae_bypass_states[vaeName] = newBypassState; - node.saveState(); - updatePreview(node); - // Visual feedback - div.style.opacity = newBypassState ? "0.5" : "1.0"; - div.style.filter = newBypassState ? "grayscale(0.7)" : "none"; -}; -``` - -**Step 5: Verify and commit** - -Verify: Open the Builder UI, add a VAE, toggle it off via the "On" checkbox, and check the JSON Preview — the bypassed VAE should NOT appear. Repeat for models, TEs, and LoRAs. - -```bash -git add web/conf_builder/conf-builder-config-management.js -git commit -m "Fix bypass toggles not updating JSON Preview - -Add updatePreview(node) call after node.saveState() in all four -bypass toggle handlers (model, LoRA, TE, VAE) so the JSON Preview -immediately reflects bypass state changes." -``` - ---- - -## Task 2: force_refresh Tag Comparison & Update Prompt - -When force_refresh re-fetches CivitAI metadata, compare fresh `trainedWords` with stored tags and prompt the user if they differ. - -**Files:** -- Modify: `config_builder_node.py:764-805` (metadata endpoint response) -- Modify: `web/conf_builder/conf-builder-config-management.js:1384-1413` (metadata modal response handler) - -**Step 1: Add tag comparison to backend metadata endpoint** - -In `config_builder_node.py`, inside `lookup_lora_metadata_endpoint`, after the metadata dict is built (line ~779) and before saving (line ~798), add tag comparison logic. Find this code block: - -```python - # Save metadata to file - output_dir = folder_paths.get_output_directory() - model_data_dir = os.path.join(output_dir, "benchmarks", "model-data", lora_name.replace("/", "_").replace("\\", "_").replace(".safetensors", "")) - os.makedirs(model_data_dir, exist_ok=True) - - metadata_file = os.path.join(model_data_dir, "metadata.json") - save_dict_to_json(metadata, metadata_file) - - print(f"[ConfigBuilder] ✅ Metadata saved to: {metadata_file}") - - return web.json_response({ - "metadata": metadata, - "saved_to": metadata_file - }) -``` - -Replace the return statement only (keep everything above intact): - -```python - # Save metadata to file - output_dir = folder_paths.get_output_directory() - model_data_dir = os.path.join(output_dir, "benchmarks", "model-data", lora_name.replace("/", "_").replace("\\", "_").replace(".safetensors", "")) - os.makedirs(model_data_dir, exist_ok=True) - - metadata_file = os.path.join(model_data_dir, "metadata.json") - save_dict_to_json(metadata, metadata_file) - - print(f"[ConfigBuilder] ✅ Metadata saved to: {metadata_file}") - - # Compare fresh tags with stored loras_tags.json (only on force_refresh) - tags_changed = False - old_tags = [] - new_tags = metadata.get("trained_words", []) - if force_refresh and new_tags: - json_tags_path = os.path.join(output_dir, "benchmarks/loras_tags.json") - if os.path.exists(json_tags_path): - lora_tags = load_json_from_file(json_tags_path) or {} - normalized = lora_name.replace("\\", "/") - backslash = lora_name.replace("/", "\\") - old_tags = lora_tags.get(lora_name, lora_tags.get(normalized, lora_tags.get(backslash, []))) - if old_tags is None: - old_tags = [] - # Compare sorted lists to detect any difference - if sorted(old_tags) != sorted(new_tags): - tags_changed = True - print(f"[ConfigBuilder] ⚠️ Tags changed for {lora_name}: {old_tags} -> {new_tags}") - - return web.json_response({ - "metadata": metadata, - "saved_to": metadata_file, - "tags_changed": tags_changed, - "old_tags": old_tags, - "new_tags": new_tags - }) -``` - -**Step 2: Add tag diff banner to frontend metadata modal** - -In `conf-builder-config-management.js`, find the metadata modal response handling at line ~1384: - -```javascript - const data = await resp.json(); - const metadata = data.metadata; - - status.textContent = "✅ Metadata loaded successfully!"; - status.style.color = "#66ff66"; - - // Display metadata - content.innerHTML = ""; -``` - -Add a tag diff banner right after `content.innerHTML = "";` and before the cache warning banner block (line ~1393). Insert this code at line ~1392 (after `content.innerHTML = "";`): - -```javascript - // Tag diff banner (shown when force_refresh detects different tags) - if (data.tags_changed) { - const tagDiffBanner = document.createElement("div"); - tagDiffBanner.style.cssText = "background: #1a3300; border: 2px solid #44cc00; border-radius: 6px; padding: 10px 14px; margin-bottom: 15px;"; - - const tagDiffTitle = document.createElement("div"); - tagDiffTitle.style.cssText = "font-size: 14px; font-weight: bold; color: #44cc00; margin-bottom: 8px;"; - tagDiffTitle.textContent = "🔄 Trigger Words Changed"; - tagDiffBanner.appendChild(tagDiffTitle); - - const oldTagsDiv = document.createElement("div"); - oldTagsDiv.style.cssText = "font-size: 11px; color: #cc6666; margin-bottom: 4px;"; - oldTagsDiv.textContent = "Old: " + (data.old_tags.length > 0 ? data.old_tags.join(", ") : "(none)"); - tagDiffBanner.appendChild(oldTagsDiv); - - const newTagsDiv = document.createElement("div"); - newTagsDiv.style.cssText = "font-size: 11px; color: #66cc66; margin-bottom: 8px;"; - newTagsDiv.textContent = "New: " + data.new_tags.join(", "); - tagDiffBanner.appendChild(newTagsDiv); - - const tagBtnRow = document.createElement("div"); - tagBtnRow.style.cssText = "display: flex; gap: 8px;"; - - const updateTagsBtn = document.createElement("button"); - updateTagsBtn.className = "cb-button primary"; - updateTagsBtn.style.cssText = "flex: 1; background: #336600; border: 1px solid #44cc00; color: #88ff44;"; - updateTagsBtn.textContent = "✅ Update Tags"; - updateTagsBtn.onclick = async () => { - try { - const saveResp = await fetch("/configbuilder/save_lora_triggers", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ lora_name: loraName, triggers: data.new_tags }) - }); - if (saveResp.ok) { - updateTagsBtn.textContent = "✅ Updated!"; - updateTagsBtn.disabled = true; - keepTagsBtn.disabled = true; - } - } catch (e) { - updateTagsBtn.textContent = "❌ Error"; - } - }; - - const keepTagsBtn = document.createElement("button"); - keepTagsBtn.className = "cb-button"; - keepTagsBtn.style.cssText = "flex: 1;"; - keepTagsBtn.textContent = "Keep Current"; - keepTagsBtn.onclick = () => { - tagDiffBanner.style.display = "none"; - }; - - tagBtnRow.appendChild(updateTagsBtn); - tagBtnRow.appendChild(keepTagsBtn); - tagDiffBanner.appendChild(tagBtnRow); - content.appendChild(tagDiffBanner); - } -``` - -**Step 3: Commit** - -```bash -git add config_builder_node.py web/conf_builder/conf-builder-config-management.js -git commit -m "Add tag diff prompt on force_refresh metadata re-fetch - -Backend compares fresh CivitAI trainedWords with stored loras_tags.json -and returns tags_changed/old_tags/new_tags. Frontend shows diff banner -with Update/Keep buttons in the metadata modal." -``` - ---- - -## Task 3: HF Remote VAE Presets Dropdown - -When "Remote URL" is selected as VAE type, show a presets dropdown for known HuggingFace endpoints. - -**Files:** -- Modify: `web/conf_builder/conf-builder-config-management.js:2820-2841` (remote VAE URL section in `createVAEElement()`) - -**Step 1: Add REMOTE_VAE_PRESETS constant** - -At the top of `conf-builder-config-management.js`, near the other constants (e.g., `CONFIG_COLORS`), add: - -```javascript -// HF Remote VAE endpoint presets (mirrors remote_vae.py HF_ENDPOINTS) -const REMOTE_VAE_PRESETS = { - "Custom": "", - "HF-SD": "https://q1bj3bpq6kzilnsu.us-east-1.aws.endpoints.huggingface.cloud/", - "HF-SDXL": "https://x2dmsqunjd6k9prw.us-east-1.aws.endpoints.huggingface.cloud/", - "HF-Flux": "https://whhx50ex1aryqvw6.us-east-1.aws.endpoints.huggingface.cloud/", - "HF-HunyuanVideo": "https://o7ywnmrahorts457.us-east-1.aws.endpoints.huggingface.cloud/" -}; -``` - -Find the exact location: search for `const CONFIG_COLORS` and add the `REMOTE_VAE_PRESETS` constant right after the CONFIG_COLORS array declaration. - -**Step 2: Add presets dropdown in the Remote URL branch** - -In `createVAEElement()`, find the `if (isRemote)` block at line ~2820: - -```javascript - if (isRemote) { - // Text input for remote VAE endpoint URL - const urlInput = document.createElement("input"); -``` - -Replace the entire `if (isRemote) { ... }` block (lines 2820-2841, up to but NOT including the `} else {`) with: - -```javascript - if (isRemote) { - // Presets dropdown for known HF Remote VAE endpoints - const presetSelect = document.createElement("select"); - presetSelect.className = "cb-select"; - presetSelect.style.marginBottom = "4px"; - const currentUrl = vaeName.replace(/^remote:/, ""); - Object.entries(REMOTE_VAE_PRESETS).forEach(([label, url]) => { - const opt = document.createElement("option"); - opt.value = url; - opt.textContent = label; - // Select matching preset, or "Custom" if URL doesn't match any preset - if (url && currentUrl === url) opt.selected = true; - else if (!url && !Object.values(REMOTE_VAE_PRESETS).includes(currentUrl)) opt.selected = true; - presetSelect.appendChild(opt); - }); - - // Text input for remote VAE endpoint URL - const urlInput = document.createElement("input"); - urlInput.className = "cb-input"; - urlInput.type = "text"; - urlInput.placeholder = "http://192.168.1.100:8080/decode"; - urlInput.value = currentUrl; - urlInput.style.fontFamily = "monospace"; - urlInput.style.fontSize = "12px"; - urlInput.onchange = () => { - const url = urlInput.value.trim(); - node.state.config_arrays[arrayIdx].vaes[vaeIdx] = url ? `remote:${url}` : "remote:"; - node.saveState(); - // Update preset dropdown to match - const matchingPreset = Object.entries(REMOTE_VAE_PRESETS).find(([, u]) => u && u === url); - presetSelect.value = matchingPreset ? matchingPreset[1] : ""; - }; - urlInput.onblur = urlInput.onchange; - - presetSelect.onchange = () => { - const selectedUrl = presetSelect.value; - if (selectedUrl) { - urlInput.value = selectedUrl; - node.state.config_arrays[arrayIdx].vaes[vaeIdx] = `remote:${selectedUrl}`; - node.saveState(); - } else { - // "Custom" selected - clear URL for manual entry - urlInput.value = ""; - urlInput.focus(); - } - }; - - contentDiv.appendChild(presetSelect); - contentDiv.appendChild(urlInput); - - // Helper text - const helperText = document.createElement("div"); - helperText.style.cssText = "font-size: 9px; color: #666; padding: 2px 4px;"; - helperText.textContent = "Select a preset or enter a custom endpoint URL for your remote VAE decode server"; - contentDiv.appendChild(helperText); - } else { -``` - -Note: The `} else {` line that follows must remain unchanged — only the content inside `if (isRemote) { ... }` is replaced. - -**Step 3: Commit** - -```bash -git add web/conf_builder/conf-builder-config-management.js -git commit -m "Add Remote VAE presets dropdown for HF endpoints - -When Remote URL type is selected in VAE element, show a presets -dropdown with known HuggingFace VAE endpoints (SD, SDXL, Flux, -HunyuanVideo). Selecting a preset auto-fills the URL input." -``` - ---- - -## Task 4: Per-Config Sidebar Navigation - -Add per-config-array cog icons in the sidebar that expand on hover to show sub-section navigation (Models, TE, VAE, LoRAs). - -**Files:** -- Modify: `web/conf_builder/conf-builder-ui-components.js:723-731` (sidebar nav icons section) -- Modify: `web/conf_builder/conf-builder-ui-components.js:801` (scroll spy section IDs) -- Modify: `web/conf_builder/conf-builder-config-management.js:1964` (models section — add ID) -- Modify: `web/conf_builder/conf-builder-config-management.js:2064-2066` (TE section — add ID) -- Modify: `web/conf_builder/conf-builder-config-management.js:2263-2272` (VAE section — add ID) -- Modify: `web/conf_builder/conf-builder-config-management.js:2888-2896` (LoRAs section — add ID) -- Modify: `web/conf_builder/conf-builder-ui-components.js:184-424` (getStyles — add sidebar expansion CSS) - -**Step 1: Add sub-section IDs to config sections** - -Each sub-section renderer needs a unique ID for sidebar scroll-to targeting. - -In `renderModelsSection()` at line ~1969, after `const modelGrid = document.createElement("div");` (line ~1969-1970), add: - -```javascript - modelGrid.id = `cb-config-${arrayIdx}-models`; -``` - -In `renderTextEncodersSection()` at line ~2065, after `const section = document.createElement("div");`, add: - -```javascript - section.id = `cb-config-${arrayIdx}-te`; -``` - -In `renderVAEsSection()` at line ~2271, after `const vaeGrid = document.createElement("div");` (line ~2271-2272), add: - -```javascript - vaeGrid.id = `cb-config-${arrayIdx}-vae`; -``` - -In `renderLorasSection()` at line ~2893, after `const loraGrid = document.createElement("div");` (line ~2893-2894), add: - -```javascript - loraGrid.id = `cb-config-${arrayIdx}-loras`; -``` - -**Step 2: Add CSS for sidebar config expansion** - -In `getStyles()` in `conf-builder-ui-components.js`, find the closing `` tag at line ~423. Just BEFORE the closing ``, add: - -```css - /* Per-config sidebar navigation */ - .cb-sidebar-config-group { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - } - .cb-sidebar-config-icon { - width: 28px; - height: 28px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 11px; - font-weight: bold; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - color: #999; - transition: background 0.15s; - } - .cb-sidebar-config-icon:hover { - background: #333; - } - .cb-sidebar-config-icon.active { - background: #333; - color: #fff; - } - .cb-sidebar-sub-icons { - display: none; - flex-direction: column; - align-items: center; - gap: 2px; - padding: 2px 0; - } - .cb-sidebar-config-group:hover .cb-sidebar-sub-icons { - display: flex; - } - .cb-sidebar-sub-icon { - width: 28px; - height: 22px; - border: none; - border-radius: 3px; - cursor: pointer; - font-size: 9px; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - color: #888; - transition: background 0.15s; - } - .cb-sidebar-sub-icon:hover { - background: #444; - color: #fff; - } -``` - -**Step 3: Add per-config icons to sidebar** - -In `createSidebar()` in `conf-builder-ui-components.js`, find the line that adds the ⚙️ Config Arrays nav icon at line ~724: - -```javascript - addNavIcon("⚙️", "Config Arrays", "cb-sec-configs"); -``` - -After this line, add the per-config cog icons. Import `CONFIG_COLORS` at the top of the file if needed (check if it's already imported — it is defined in `conf-builder-config-management.js`). Since `CONFIG_COLORS` is in a different module, we'll define a local copy or pass it via the node state. The simplest approach: read from `node.state.config_arrays` directly since the sidebar already receives `node`. - -Add after `addNavIcon("⚙️", "Config Arrays", "cb-sec-configs");`: - -```javascript - // Per-config array navigation icons - const configColors = ["#0088ff", "#ff6600", "#00cc66", "#cc44cc", "#ffaa00", "#44cccc", "#ff4466", "#88aa00"]; - if (node.state.config_arrays && node.state.config_arrays.length > 0) { - node.state.config_arrays.forEach((ca, idx) => { - const color = configColors[idx % configColors.length]; - const group = document.createElement("div"); - group.className = "cb-sidebar-config-group"; - - const cogBtn = document.createElement("button"); - cogBtn.className = "cb-sidebar-config-icon"; - cogBtn.style.color = color; - cogBtn.style.borderLeft = `2px solid ${color}`; - cogBtn.textContent = `${idx + 1}`; - cogBtn.title = ca.name || `Config ${idx + 1}`; - cogBtn.dataset.targetId = `cb-config-${idx}`; - cogBtn.onclick = () => { - const target = mainContent.querySelector(`#cb-config-${idx}`); - if (target) target.scrollIntoView({ behavior: "smooth", block: "start" }); - }; - icons.push(cogBtn); - group.appendChild(cogBtn); - - // Sub-icons container (shown on hover) - const subIcons = document.createElement("div"); - subIcons.className = "cb-sidebar-sub-icons"; - - const subSections = [ - { emoji: "🧊", label: "Models", id: `cb-config-${idx}-models` }, - { emoji: "📎", label: "Text Enc", id: `cb-config-${idx}-te` }, - { emoji: "🎨", label: "VAE", id: `cb-config-${idx}-vae` }, - { emoji: "🔗", label: "LoRAs", id: `cb-config-${idx}-loras` } - ]; - - subSections.forEach(sub => { - const subBtn = document.createElement("button"); - subBtn.className = "cb-sidebar-sub-icon"; - subBtn.textContent = sub.emoji; - subBtn.title = sub.label; - subBtn.onclick = () => { - const target = mainContent.querySelector(`#${sub.id}`); - if (target) target.scrollIntoView({ behavior: "smooth", block: "start" }); - }; - subIcons.appendChild(subBtn); - }); - - group.appendChild(subIcons); - sidebar.appendChild(group); - }); - } -``` - -**Step 4: Update scroll spy to include config IDs** - -In `createSidebar()`, find the scroll spy `sectionIds` array at line ~801: - -```javascript - const sectionIds = ["cb-sec-prompts", "cb-sec-configs", "cb-sec-distribution", "cb-sec-preview"]; -``` - -Add per-config IDs dynamically. Replace this line with: - -```javascript - const sectionIds = ["cb-sec-prompts", "cb-sec-configs"]; - // Add per-config section IDs for scroll spy - if (node.state.config_arrays) { - node.state.config_arrays.forEach((_, idx) => { - sectionIds.push(`cb-config-${idx}`); - }); - } - sectionIds.push("cb-sec-distribution", "cb-sec-preview"); -``` - -**Step 5: Commit** - -```bash -git add web/conf_builder/conf-builder-ui-components.js web/conf_builder/conf-builder-config-management.js -git commit -m "Add per-config sidebar navigation with hover sub-sections - -Each config array gets a numbered cog icon in the sidebar, color-coded -with CONFIG_COLORS. Hovering expands sub-icons for Models, TE, VAE, -and LoRAs sub-section navigation. Adds section IDs for scroll targets." -``` - ---- - -## Task 5: Section Organization & Header Fixes - -Reorder sections, move Extra Model Sampling inside Models, and improve header contrast. - -**Files:** -- Modify: `web/conf_builder/conf-builder-config-management.js:3885-3991` (renderUI section order) -- Modify: `web/conf_builder/conf-builder-config-management.js:3977-3981` (per-config sub-section order) -- Modify: `web/conf_builder/conf-builder-config-management.js:2064-2070` (TE header style) -- Modify: `web/conf_builder/conf-builder-config-management.js:2274-2276` (VAE header style) -- Modify: `web/conf_builder/conf-builder-config-management.js:2898-2900` (LoRAs header style) -- Modify: `web/conf_builder/conf-builder-ui-components.js:723-731` (sidebar icon order) - -**Step 1: Reorder main content sections in renderUI()** - -In `renderUI()`, find the section rendering order at lines ~3884-3991. Change the order to: Distribution → Global Prompts → Config Arrays → Preview. - -Find this block: - -```javascript - // === MAIN CONTENT SECTIONS === - - // Global Prompts Section - renderGlobalPromptsSection(node, mainContent); - - // Config Arrays Section - const configSection = document.createElement("div"); -``` - -Change it to: - -```javascript - // === MAIN CONTENT SECTIONS === - - // Distribution Settings Section (when enabled, appears first) - if (node.state.distribution_enabled) { - renderDistributionSettingsSection(node, mainContent); - } - - // Global Prompts Section - renderGlobalPromptsSection(node, mainContent); - - // Config Arrays Section - const configSection = document.createElement("div"); -``` - -And REMOVE the old distribution block at lines ~3988-3991 (after `mainContent.appendChild(configSection);`): - -```javascript - // Distribution Settings Section (only when enabled) - if (node.state.distribution_enabled) { - renderDistributionSettingsSection(node, mainContent); - } -``` - -Delete those 4 lines (the comment and the if block). The distribution section is now rendered at the top instead. - -**Step 2: Move Extra Model Sampling inside Models section** - -In `renderUI()`, find the per-config sub-section rendering at lines ~3977-3981: - -```javascript - renderConfigPromptsSection(node, arrayElement, configArray, arrayIdx); - renderModelsSection(node, arrayElement, configArray, arrayIdx, modelLists); - renderExtraModelSamplingSection(node, arrayElement, configArray, arrayIdx); - renderVAEsSection(node, arrayElement, configArray, arrayIdx, modelLists); - renderLorasSection(node, arrayElement, configArray, arrayIdx, availableLoras, loraFolders); -``` - -The `renderExtraModelSamplingSection` call needs to move inside `renderModelsSection()` instead. Remove line 3979: - -```javascript - renderExtraModelSamplingSection(node, arrayElement, configArray, arrayIdx); -``` - -So it becomes: - -```javascript - renderConfigPromptsSection(node, arrayElement, configArray, arrayIdx); - renderModelsSection(node, arrayElement, configArray, arrayIdx, modelLists); - renderVAEsSection(node, arrayElement, configArray, arrayIdx, modelLists); - renderLorasSection(node, arrayElement, configArray, arrayIdx, availableLoras, loraFolders); -``` - -Now add the call inside `renderModelsSection()`. Find the end of `renderModelsSection()` — look for where the function appends its final elements and returns. Find the line that appends `modelGrid` to `div` (around line ~2060): - -```javascript - div.appendChild(modelGrid); -} -``` - -Just before the closing `}` of `renderModelsSection`, add: - -```javascript - // Extra Model & Sampling Options (sub-section of Models) - renderExtraModelSamplingSection(node, div, configArray, arrayIdx); -``` - -So it becomes: - -```javascript - div.appendChild(modelGrid); - - // Extra Model & Sampling Options (sub-section of Models) - renderExtraModelSamplingSection(node, div, configArray, arrayIdx); -} -``` - -**Step 3: Fix Text Encoders header to be bigger and match other sections** - -In `renderTextEncodersSection()` at line ~2068-2070, find: - -```javascript - const header = document.createElement("div"); - header.style.cssText = "font-size: 11px; font-weight: bold; color: #44aaff; margin-bottom: 6px;"; - header.textContent = "TEXT ENCODERS (CLIP)"; -``` - -Change the style to match the other section toggle headers (bigger font, same background pattern): - -```javascript - const header = document.createElement("div"); - header.className = "cb-section-toggle"; - header.style.cssText = "padding: 8px; background: #3a3a3a; border-radius: 4px; margin-bottom: 8px; font-weight: bold; color: #66bbff;"; - header.textContent = "Text Encoders (CLIP)"; -``` - -**Step 4: Fix VAE header — lighter background, brighter text** - -In `renderVAEsSection()` at line ~2274-2276, find: - -```javascript - const vaeHeader = document.createElement("div"); - vaeHeader.className = "cb-section-toggle"; - vaeHeader.style.cssText = "padding: 8px; background: #3a3a3a; border-radius: 4px; margin-bottom: 8px; font-weight: bold; color: #9900cc;"; -``` - -Change to lighter background and brighter text: - -```javascript - const vaeHeader = document.createElement("div"); - vaeHeader.className = "cb-section-toggle"; - vaeHeader.style.cssText = "padding: 8px; background: #444; border-radius: 4px; margin-bottom: 8px; font-weight: bold; color: #cc66ff;"; -``` - -**Step 5: Fix LoRAs header — brighter text** - -In `renderLorasSection()` at line ~2898-2900, find: - -```javascript - const loraHeader = document.createElement("div"); - loraHeader.className = "cb-section-toggle"; - loraHeader.style.cssText = "padding: 8px; background: #3a3a3a; border-radius: 4px; margin-bottom: 8px; font-weight: bold; color: #0066cc;"; -``` - -Change to brighter text: - -```javascript - const loraHeader = document.createElement("div"); - loraHeader.className = "cb-section-toggle"; - loraHeader.style.cssText = "padding: 8px; background: #3a3a3a; border-radius: 4px; margin-bottom: 8px; font-weight: bold; color: #4499ff;"; -``` - -**Step 6: Update sidebar icon order** - -In `createSidebar()` in `conf-builder-ui-components.js`, find the nav icon order at lines ~723-731: - -```javascript - addNavIcon("📝", "Global Prompts", "cb-sec-prompts"); - addNavIcon("⚙️", "Config Arrays", "cb-sec-configs"); - - // Distribution icon (only when enabled) - if (node.state.distribution_enabled) { - addNavIcon("🌐", "Distribution", "cb-sec-distribution"); - } - - addNavIcon("📄", "JSON Preview", "cb-sec-preview"); -``` - -Change to put Distribution first: - -```javascript - // Distribution icon (only when enabled, appears first) - if (node.state.distribution_enabled) { - addNavIcon("🌐", "Distribution", "cb-sec-distribution"); - } - - addNavIcon("📝", "Global Prompts", "cb-sec-prompts"); - addNavIcon("⚙️", "Config Arrays", "cb-sec-configs"); - - addNavIcon("📄", "JSON Preview", "cb-sec-preview"); -``` - -**Step 7: Commit** - -```bash -git add web/conf_builder/conf-builder-config-management.js web/conf_builder/conf-builder-ui-components.js -git commit -m "Reorganize sections and improve header contrast - -Move Distribution Settings to top of layout. Move Extra Model & -Sampling Options inside Models section as sub-section. Make TE header -bigger with matching toggle style. Lighten VAE header background and -brighten text. Brighten LoRAs header text color." -``` - ---- - -## Verification Checklist - -After all tasks are complete, verify each item: - -1. **Bypass toggles:** Toggle off a VAE/TE/model/LoRA and confirm it vanishes from JSON Preview immediately (no page reload needed) -2. **Tag diff:** Open a LoRA metadata modal, click re-fetch, verify the diff banner appears if tags changed, and the Update button writes to `loras_tags.json` -3. **Remote VAE presets:** Set a VAE to Remote URL type, verify the presets dropdown appears with HF-SD/SDXL/Flux/HunyuanVideo options, selecting one fills the URL input -4. **Sidebar nav:** With 2+ config arrays, verify numbered cog icons appear in sidebar, hovering shows sub-icons, clicking scrolls to the right section -5. **Section order:** Distribution is at top, Extra Model Sampling is inside Models, headers have improved contrast diff --git a/docs/plans/2026-03-01-builder-ui-navigation-design.md b/docs/plans/2026-03-01-builder-ui-navigation-design.md deleted file mode 100644 index 0163dfb..0000000 --- a/docs/plans/2026-03-01-builder-ui-navigation-design.md +++ /dev/null @@ -1,134 +0,0 @@ -# Builder UI Navigation, Layout & Styles Design - -**Date:** 2026-03-01 -**Status:** Approved - -## Summary - -Overhaul the Builder UI with a sticky top bar (session name + settings dropdown), -left sidebar with navigation icons and quick actions, uniform section headers with -accent color stripes, and improved dark mode colors for better readability. - -## Requirements - -1. **Sticky Top Bar:** Session name text input (far left), Settings button with - dropdown (far right). Settings contains: Load Session, Load Config, Save Config - (inline-expandable), Auto-Save toggle, Enable Distribution toggle, Refresh - Models action, Label Mode toggle, Include None toggle, Include Default toggle. - -2. **Left Sidebar:** ~40px wide icon column, sticky alongside top bar. Navigation - icons scroll to sections (Global Prompts, Config Arrays, Distribution Settings, - JSON Preview). Quick action icons (Add Config, Refresh Models) below divider. - Active section highlighted via scroll spy. - -3. **Remove Top Row:** The 3-section top row (Session Management, Config Management, - Distribution) is eliminated. Controls move to top bar, settings dropdown, and - distribution section. - -4. **Distribution Section:** When enabled via Settings dropdown, appears as its own - full-width section below Config Arrays (above Preview). Sidebar gets nav icon. - -5. **Uniform Section Headers:** White/light text (16px bold) on subtle dark gradient - bar with left accent color stripe. Consistent across all sections. - -6. **Color/Contrast Fixes:** Brighter text colors, better button contrast, improved - header visibility for dark mode. - -## Layout - -``` -+------------------------------------------------------+ -| TOP BAR (sticky, z-index: 100) | -| [Session Name input________] [Settings gear] | -+----+-------------------------------------------------+ -| S | MAIN CONTENT (scrollable) | -| I | | -| D | [Global Prompts section] | -| E | [Config Arrays section] | -| B | [Distribution Settings] (when enabled) | -| A | [JSON Preview section] | -| R | | -+----+-------------------------------------------------+ -``` - -## Top Bar - -- Height: ~40px, background: `#1a1a1a`, border-bottom: `1px solid #3a3a3a` -- Session name: text input, `flex: 1`, transparent bg, border on focus -- Settings button: `gear` icon, opens dropdown below on click - -### Settings Dropdown - -Dropdown opens below settings button, closes on click-outside. Items: - -| Item | Type | Behavior | -|------|------|----------| -| Load Session | Expandable | Click expands inline searchable select | -| Load Config | Expandable | Click expands inline searchable select | -| Save Config | Expandable | Click expands config name input + save button | -| --- | Divider | | -| Auto-Save (2s) | Toggle | Checkbox, immediate effect | -| --- | Divider | | -| Enable Distribution | Toggle | Checkbox, shows/hides distribution section | -| --- | Divider | | -| Refresh Models | Action | Click triggers refresh, shows feedback | -| Label Mode | Toggle | Checkbox | -| Include None | Toggle | Checkbox | -| Include Default | Toggle | Checkbox | - -## Sidebar - -- Width: ~40px, background: `#1e1e1e`, border-right: `1px solid #333` -- Position: sticky left column, full height below top bar -- Icons: emoji in ~36px square buttons, tooltip on hover - -### Icons (top to bottom) - -| Icon | Label | Type | Action | -|------|-------|------|--------| -| 📝 | Global Prompts | Nav | Scroll to section | -| ⚙️ | Config Arrays | Nav | Scroll to section | -| 🌐 | Distribution | Nav | Scroll to section (only when enabled) | -| 📄 | JSON Preview | Nav | Scroll to section | -| --- | Divider | | | -| + | Add Config | Action | Adds new config array | -| 🔄 | Refresh | Action | Refresh models/LoRAs | - -Active section highlighted via scroll spy (brighter background). - -## Section Headers - -Uniform style replacing varied `.cb-section-title`: - -- Font: 16px bold, color: `#e0e0e0` (bright white-gray) -- Background: subtle gradient from accent color at 15% opacity to transparent -- Left border: 3px solid accent color -- Padding: 10px 12px -- Each section has a unique accent color - -| Section | Accent Color | Emoji | -|---------|-------------|-------| -| Global Prompts | `#00cc55` (green) | 📝 | -| Config Arrays | `#0088ff` (blue) | ⚙️ | -| Distribution | `#ff8800` (orange) | 🌐 | -| JSON Preview | `#00cccc` (teal) | 📄 | - -## Color/Contrast Improvements - -| Element | Old | New | -|---------|-----|-----| -| `.cb-label` | `#aaa` | `#ccc` | -| `.cb-section-title` | `#0066cc` (14px) | `#e0e0e0` (16px) with accent stripe | -| `.cb-button` | `#4a4a4a` bg | `#4a4a4a` bg, `#e0e0e0` text | -| `.cb-button.primary` | `#0066cc` | `#0077dd` (slightly brighter) | -| `.cb-controls-bar` | `#252525` | `#2a2a2a` | -| `.cb-input, .cb-select` | `#1a1a1a` bg, `#4a4a4a` border | `#1a1a1a` bg, `#555` border | -| Info text | `#888` | `#999` | - -## Files Changed - -| File | Change | -|------|--------| -| `conf-builder-ui-components.js` | New CSS classes, `createTopBar()`, `createSidebar()`, `createSettingsDropdown()`, `createSectionHeader()`, color updates | -| `conf-builder-config-management.js` | `renderUI()` restructure, section ID attributes, remove top row rendering | -| `conf-builder-distribution.js` | New `renderDistributionSettingsSection()` for standalone full-width section | diff --git a/docs/plans/2026-03-01-builder-ui-navigation.md b/docs/plans/2026-03-01-builder-ui-navigation.md deleted file mode 100644 index 3325032..0000000 --- a/docs/plans/2026-03-01-builder-ui-navigation.md +++ /dev/null @@ -1,784 +0,0 @@ -# Builder UI Navigation, Layout & Styles Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Overhaul the Builder UI with sticky top bar, left sidebar navigation, uniform section headers, and improved dark mode colors. - -**Architecture:** Replace the 3-section top row with a compact top bar (session name + settings dropdown). Add a left sidebar with navigation icons and quick actions. Restructure the main container as a flex layout: top bar + (sidebar | scrollable content). All section headers get uniform styling with accent color stripes. - -**Tech Stack:** Vanilla JS (ComfyUI widget), embedded CSS via getStyles() - -**Critical constraint:** DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY. - ---- - -### Task 1: Add New CSS Classes to getStyles() - -**Files:** -- Modify: `web/conf_builder/conf-builder-ui-components.js:184-394` (getStyles function) - -**Step 1: Add top bar CSS** - -After the `.cb-container` rule (line 187), add: - -```css -.cb-top-bar { - display: flex; - align-items: center; - gap: 12px; - padding: 6px 12px; - background: #1a1a1a; - border-bottom: 1px solid #3a3a3a; - position: sticky; - top: 0; - z-index: 100; - min-height: 36px; - flex-shrink: 0; -} -.cb-top-bar-input { - flex: 1; - background: transparent; - border: 1px solid transparent; - color: #e0e0e0; - font-size: 14px; - font-weight: bold; - padding: 6px 10px; - border-radius: 4px; - font-family: monospace; - min-width: 100px; -} -.cb-top-bar-input:hover { border-color: #444; } -.cb-top-bar-input:focus { outline: none; border-color: #0077dd; background: #1a1a1a; } -.cb-settings-btn { - background: #333; - border: 1px solid #555; - color: #ccc; - padding: 6px 14px; - border-radius: 4px; - cursor: pointer; - font-size: 13px; - white-space: nowrap; - display: flex; - align-items: center; - gap: 6px; -} -.cb-settings-btn:hover { background: #444; color: #fff; } -``` - -**Step 2: Add settings dropdown CSS** - -```css -.cb-settings-dropdown { - position: absolute; - top: 100%; - right: 0; - min-width: 280px; - background: #1e1e1e; - border: 1px solid #444; - border-radius: 6px; - box-shadow: 0 8px 24px rgba(0,0,0,0.5); - z-index: 200; - overflow: hidden; - display: none; -} -.cb-settings-dropdown.open { display: block; } -.cb-settings-item { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 14px; - cursor: pointer; - color: #ccc; - font-size: 12px; - border: none; - background: none; - width: 100%; - text-align: left; -} -.cb-settings-item:hover { background: #2a2a2a; color: #fff; } -.cb-settings-divider { - height: 1px; - background: #333; - margin: 4px 0; -} -.cb-settings-expand { - padding: 8px 14px; - background: #252525; - border-top: 1px solid #333; - display: none; -} -.cb-settings-expand.open { display: block; } -``` - -**Step 3: Add sidebar CSS** - -```css -.cb-layout-wrapper { - display: flex; - flex: 1; - overflow: hidden; -} -.cb-sidebar { - width: 42px; - min-width: 42px; - background: #1e1e1e; - border-right: 1px solid #333; - display: flex; - flex-direction: column; - align-items: center; - padding-top: 8px; - gap: 2px; - flex-shrink: 0; - overflow-y: auto; -} -.cb-sidebar-icon { - width: 34px; - height: 34px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - cursor: pointer; - font-size: 16px; - background: transparent; - border: none; - color: #888; - position: relative; - transition: background 0.15s, color 0.15s; -} -.cb-sidebar-icon:hover { background: #333; color: #fff; } -.cb-sidebar-icon.active { background: #0077dd33; color: #4aa8ff; } -.cb-sidebar-icon[title]:hover::after { - content: attr(title); - position: absolute; - left: 42px; - top: 50%; - transform: translateY(-50%); - background: #222; - color: #eee; - padding: 4px 10px; - border-radius: 4px; - font-size: 11px; - white-space: nowrap; - z-index: 300; - border: 1px solid #444; - pointer-events: none; -} -.cb-sidebar-divider { - width: 24px; - height: 1px; - background: #444; - margin: 6px 0; -} -.cb-main-content { - flex: 1; - overflow-y: auto; - padding: 12px; -} -``` - -**Step 4: Add uniform section header CSS** - -```css -.cb-section-header { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 14px; - margin: -12px -12px 12px -12px; - border-radius: 4px 4px 0 0; - font-size: 16px; - font-weight: bold; - color: #e0e0e0; - user-select: none; -} -``` - -**Step 5: Update existing colors for better contrast** - -Update these existing rules: -- `.cb-label`: change `color: #aaa` to `color: #ccc` -- `.cb-button`: ensure text is `color: #e0e0e0` -- `.cb-button.primary`: change `background: #0066cc` to `background: #0077dd` -- `.cb-button.primary:hover`: change `background: #0077ee` to `background: #0088ee` -- `.cb-input, .cb-select`: change `border: 1px solid #4a4a4a` to `border: 1px solid #555` -- `.cb-controls-bar`: change `background: #252525` to `background: #2a2a2a` - -**Step 6: Update `.cb-container` for new layout** - -Change `.cb-container` from: -```css -.cb-container { padding: 12px; height: 100%; overflow-y: auto; box-sizing: border-box; } -``` -to: -```css -.cb-container { display: flex; flex-direction: column; height: 100%; overflow: hidden; box-sizing: border-box; } -``` - -The scrolling now happens in `.cb-main-content` instead. - -**Step 7: Commit** -``` -feat: add CSS classes for top bar, sidebar, settings dropdown, section headers -``` - ---- - -### Task 2: Create Top Bar and Settings Dropdown Components - -**Files:** -- Modify: `web/conf_builder/conf-builder-ui-components.js` (add new exports after getStyles) - -**Step 1: Add `createTopBar()` function** - -Add after `getStyles()` function (after line ~395): - -```javascript -// --- TOP BAR COMPONENT --- - -export function createTopBar(node, { - availableSessions, availableConfigs, - refreshAllConfigBuilders, - onLoadSession, onLoadConfig, onSaveConfig -}) { - const bar = document.createElement("div"); - bar.className = "cb-top-bar"; - - // Session name input - const sessionInput = document.createElement("input"); - sessionInput.className = "cb-top-bar-input"; - sessionInput.type = "text"; - sessionInput.value = node.state.session_name || "my_test_session"; - sessionInput.placeholder = "Session name..."; - sessionInput.title = "Session Name"; - sessionInput.onchange = () => { - node.state.session_name = sessionInput.value; - node.saveState(); - }; - bar.appendChild(sessionInput); - - // Settings button + dropdown container - const settingsWrapper = document.createElement("div"); - settingsWrapper.style.position = "relative"; - - const settingsBtn = document.createElement("button"); - settingsBtn.className = "cb-settings-btn"; - settingsBtn.innerHTML = "⚙ Settings ▾"; - - const dropdown = createSettingsDropdown(node, { - availableSessions, availableConfigs, - refreshAllConfigBuilders, - onLoadSession, onLoadConfig, onSaveConfig - }); - - settingsBtn.onclick = (e) => { - e.stopPropagation(); - dropdown.classList.toggle("open"); - }; - - // Close on click outside - document.addEventListener("click", (e) => { - if (!settingsWrapper.contains(e.target)) { - dropdown.classList.remove("open"); - } - }); - - settingsWrapper.appendChild(settingsBtn); - settingsWrapper.appendChild(dropdown); - bar.appendChild(settingsWrapper); - - return bar; -} -``` - -**Step 2: Add `createSettingsDropdown()` function** - -Add after `createTopBar()`: - -```javascript -function createSettingsDropdown(node, { - availableSessions, availableConfigs, - refreshAllConfigBuilders, - onLoadSession, onLoadConfig, onSaveConfig -}) { - const dropdown = document.createElement("div"); - dropdown.className = "cb-settings-dropdown"; - - // Helper: create expandable item - function addExpandableItem(emoji, label, expandContent) { - const item = document.createElement("div"); - item.className = "cb-settings-item"; - item.innerHTML = `${emoji}${label}▶`; - - const expand = document.createElement("div"); - expand.className = "cb-settings-expand"; - expand.appendChild(expandContent); - - item.onclick = (e) => { - e.stopPropagation(); - // Close other expands - dropdown.querySelectorAll(".cb-settings-expand.open").forEach(el => { - if (el !== expand) el.classList.remove("open"); - }); - expand.classList.toggle("open"); - const arrow = item.querySelector("span:last-child"); - arrow.textContent = expand.classList.contains("open") ? "▼" : "▶"; - }; - - dropdown.appendChild(item); - dropdown.appendChild(expand); - } - - // Helper: create toggle item - function addToggleItem(emoji, label, checked, onChange) { - const item = document.createElement("label"); - item.className = "cb-settings-item"; - item.style.cursor = "pointer"; - const cb = document.createElement("input"); - cb.type = "checkbox"; - cb.checked = checked; - cb.onchange = () => onChange(cb.checked); - item.innerHTML = `${emoji}`; - item.appendChild(cb); - const lbl = document.createElement("span"); - lbl.textContent = label; - item.appendChild(lbl); - dropdown.appendChild(item); - } - - // Helper: divider - function addDivider() { - const d = document.createElement("div"); - d.className = "cb-settings-divider"; - dropdown.appendChild(d); - } - - // === Load Session (expandable) === - const sessionSelectContainer = document.createElement("div"); - const sessionSelect = createSearchableSelect( - availableSessions || [], - "", - (value) => { - if (value && value !== "None") { - onLoadSession(value); - dropdown.classList.remove("open"); - } - }, - "Search sessions..." - ); - sessionSelectContainer.appendChild(sessionSelect); - addExpandableItem("📂", "Load Session", sessionSelectContainer); - - // === Load Config (expandable) === - const configSelectContainer = document.createElement("div"); - const configSelect = createSearchableSelect( - availableConfigs || [], - "", - (value) => { - if (value && value !== "None") { - onLoadConfig(value); - dropdown.classList.remove("open"); - } - }, - "Search configs..." - ); - configSelectContainer.appendChild(configSelect); - addExpandableItem("📂", "Load Config", configSelectContainer); - - // === Save Config (expandable) === - const saveContainer = document.createElement("div"); - saveContainer.style.cssText = "display: flex; gap: 6px; align-items: center;"; - const saveNameInput = document.createElement("input"); - saveNameInput.className = "cb-input"; - saveNameInput.style.cssText = "flex: 1; font-size: 12px; padding: 6px 8px;"; - saveNameInput.value = node.state.config_name || ""; - saveNameInput.placeholder = "config_name"; - saveNameInput.onchange = () => { - node.state.config_name = saveNameInput.value; - node.saveState(); - }; - const saveBtn = document.createElement("button"); - saveBtn.className = "cb-button primary"; - saveBtn.textContent = "💾 Save"; - saveBtn.style.cssText = "font-size: 12px; padding: 6px 12px;"; - saveBtn.onclick = async (e) => { - e.stopPropagation(); - node.state.config_name = saveNameInput.value; - await onSaveConfig(); - saveBtn.textContent = "✅ Saved!"; - setTimeout(() => { saveBtn.textContent = "💾 Save"; }, 2000); - }; - saveContainer.appendChild(saveNameInput); - saveContainer.appendChild(saveBtn); - addExpandableItem("💾", "Save Config", saveContainer); - - addDivider(); - - // === Auto-Save toggle === - addToggleItem("💾", "Auto-Save (2s)", node.state.auto_save || false, (val) => { - node.state.auto_save = val; - node.saveState(); - }); - - addDivider(); - - // === Enable Distribution toggle === - addToggleItem("🌐", "Enable Distribution", node.state.distribution_enabled || false, (val) => { - node.state.distribution_enabled = val; - node.saveState(); - node.renderUI(); - }); - - addDivider(); - - // === Refresh Models action === - const refreshItem = document.createElement("div"); - refreshItem.className = "cb-settings-item"; - refreshItem.innerHTML = "🔄Refresh Models & LoRAs"; - refreshItem.onclick = async () => { - refreshItem.querySelector("span:last-child").textContent = "Refreshing..."; - await refreshAllConfigBuilders(); - refreshItem.querySelector("span:last-child").textContent = "✅ Refreshed!"; - setTimeout(() => { - refreshItem.querySelector("span:last-child").textContent = "Refresh Models & LoRAs"; - }, 2000); - }; - dropdown.appendChild(refreshItem); - - // === Label Mode toggle === - addToggleItem("🏷️", "Label Mode", node.state.label_mode || false, (val) => { - node.state.label_mode = val; - node.saveState(); - node.renderUI(); - }); - - // === Include None toggle === - addToggleItem("⬜", "Include None", node.state.include_none || false, (val) => { - node.state.include_none = val; - node.saveState(); - }); - - // === Include Default toggle === - addToggleItem("⬜", "Include Default", node.state.include_default || false, (val) => { - node.state.include_default = val; - node.saveState(); - }); - - return dropdown; -} -``` - -**Step 3: Commit** -``` -feat: add top bar and settings dropdown components -``` - ---- - -### Task 3: Create Sidebar Component - -**Files:** -- Modify: `web/conf_builder/conf-builder-ui-components.js` (add after settings dropdown) - -**Step 1: Add `createSidebar()` function** - -```javascript -// --- SIDEBAR COMPONENT --- - -export function createSidebar(node, mainContent) { - const sidebar = document.createElement("div"); - sidebar.className = "cb-sidebar"; - - const icons = []; - - // Navigation icons - function addNavIcon(emoji, title, targetId) { - const btn = document.createElement("button"); - btn.className = "cb-sidebar-icon"; - btn.textContent = emoji; - btn.title = title; - btn.dataset.targetId = targetId; - btn.onclick = () => { - const target = mainContent.querySelector(`#${targetId}`); - if (target) target.scrollIntoView({ behavior: "smooth", block: "start" }); - }; - sidebar.appendChild(btn); - icons.push(btn); - return btn; - } - - addNavIcon("📝", "Global Prompts", "cb-sec-prompts"); - addNavIcon("⚙️", "Config Arrays", "cb-sec-configs"); - - // Distribution icon (only when enabled) - if (node.state.distribution_enabled) { - addNavIcon("🌐", "Distribution", "cb-sec-distribution"); - } - - addNavIcon("📄", "JSON Preview", "cb-sec-preview"); - - // Divider - const divider = document.createElement("div"); - divider.className = "cb-sidebar-divider"; - sidebar.appendChild(divider); - - // Quick action: Add Config - const addBtn = document.createElement("button"); - addBtn.className = "cb-sidebar-icon"; - addBtn.textContent = "+"; - addBtn.title = "Add Config"; - addBtn.style.cssText = "font-size: 20px; font-weight: bold;"; - addBtn.onclick = () => { - // Dispatch to same handler as the Add Config button - node.state.config_arrays.push({ - name: `Config ${node.state.config_arrays.length + 1}`, - samplers: ["euler"], - schedulers: ["normal"], - steps: "20", - cfg: "7.0", - seed_behavior: "fixed", - models: ["None"], - vaes: ["None"], - text_encoders: [], - clip_type: "stable_diffusion", - gguf_options: {}, - loras: ["None"], - lora_omit_triggers: [], - lora_triggerwords_append_settings: {}, - lora_bypass_states: {}, - lora_strength_lock: {}, - model_bypass_states: {}, - vae_bypass_states: {}, - te_bypass_states: {}, - combine: false, - positive_prompt_groups: [], - negative_prompt: "", - use_custom_prompts: false, - model_prompt_prefix: "", - model_prompt_suffix: "", - attention_modes: ["default"], - resolutions: [], - model_sampling_override: "none", - model_sampling_shift: "1.73", - model_sampling_flux_max_shift: "1.15", - model_sampling_flux_base_shift: "0.5", - use_advanced_sampling: false, - advanced_guider: "cfg_guider", - advanced_scheduler: "basic", - use_flux_guidance: false, - flux_guidance_value: "3.5" - }); - node.saveState(); - node.renderUI(); - }; - sidebar.appendChild(addBtn); - - // Quick action: Refresh Models - const refreshBtn = document.createElement("button"); - refreshBtn.className = "cb-sidebar-icon"; - refreshBtn.textContent = "🔄"; - refreshBtn.title = "Refresh Models & LoRAs"; - sidebar.appendChild(refreshBtn); - - // Scroll spy: highlight active section - const sectionIds = ["cb-sec-prompts", "cb-sec-configs", "cb-sec-distribution", "cb-sec-preview"]; - const updateActiveIcon = () => { - const scrollTop = mainContent.scrollTop; - let activeId = sectionIds[0]; - for (const id of sectionIds) { - const el = mainContent.querySelector(`#${id}`); - if (el && el.offsetTop - 20 <= scrollTop) { - activeId = id; - } - } - icons.forEach(btn => { - btn.classList.toggle("active", btn.dataset.targetId === activeId); - }); - }; - mainContent.addEventListener("scroll", updateActiveIcon); - // Initial highlight - setTimeout(updateActiveIcon, 50); - - return { sidebar, refreshBtn }; -} -``` - -**Step 2: Commit** -``` -feat: add sidebar navigation component with scroll spy -``` - ---- - -### Task 4: Create Uniform Section Header Helper - -**Files:** -- Modify: `web/conf_builder/conf-builder-ui-components.js` (add after sidebar) - -**Step 1: Add `createSectionHeader()` function** - -```javascript -// --- UNIFORM SECTION HEADER --- - -const SECTION_ACCENTS = { - prompts: "#00cc55", - configs: "#0088ff", - distribution: "#ff8800", - preview: "#00cccc" -}; - -export function createSectionHeader(emoji, title, accentKey, { collapsible = false, collapsed = false, onToggle = null } = {}) { - const header = document.createElement("div"); - header.className = "cb-section-header"; - header.style.cssText = ` - background: linear-gradient(90deg, ${SECTION_ACCENTS[accentKey] || "#0088ff"}22, transparent); - border-left: 3px solid ${SECTION_ACCENTS[accentKey] || "#0088ff"}; - `; - - const titleSpan = document.createElement("span"); - titleSpan.textContent = `${emoji} ${title}`; - header.appendChild(titleSpan); - - if (collapsible) { - header.style.cursor = "pointer"; - const arrow = document.createElement("span"); - arrow.textContent = collapsed ? "▶" : "▼"; - arrow.style.cssText = "margin-left: auto; font-size: 12px; color: #888;"; - header.appendChild(arrow); - - header.onclick = () => { - if (onToggle) onToggle(); - }; - } - - return header; -} -``` - -**Step 2: Commit** -``` -feat: add uniform section header component with accent colors -``` - ---- - -### Task 5: Restructure renderUI() — New Layout with Top Bar + Sidebar - -**Files:** -- Modify: `web/conf_builder/conf-builder-config-management.js:3837-3963` (renderUI function) - -This is the main layout restructure. The renderUI function changes from: -1. `cb-container` > topRow (3 sections) > globalPrompts > configArrays > preview - -To: -1. `cb-container` > topBar (sticky) > layoutWrapper (sidebar | mainContent) -2. In mainContent: globalPrompts > configArrays > distributionSettings > preview - -**Step 1: Update imports** - -At the top of the file (line ~19-24), update the import from `conf-builder-ui-components.js` to include new exports: - -```javascript -import { - createSearchableSelect, - createSlider, - createInputGroup, - getStyles, - createTopBar, - createSidebar, - createSectionHeader -} from './conf-builder-ui-components.js'; -``` - -**Step 2: Replace the renderUI function body** - -The new renderUI function should: - -1. Save scroll from `.cb-main-content` (not `.cb-container`) -2. Inject styles + `cb-container` root -3. Create top bar with `createTopBar()` -4. Create layout wrapper (flex row) -5. Create main content area -6. Create sidebar with `createSidebar()` — pass mainContent for scroll spy -7. In main content: render sections with IDs and uniform headers -8. Wire up the sidebar refresh button -9. Restore scroll on `.cb-main-content` - -Replace lines 3837-3963 with the new version. Key changes: - -- Remove the `topRow` creation and the 3 `render*Section` calls -- Replace `root.appendChild(topRow)` with top bar -- Add sidebar + layout wrapper -- Add `id="cb-sec-prompts"` to Global Prompts section -- Add `id="cb-sec-configs"` to Config Arrays section -- Add `id="cb-sec-distribution"` to Distribution section (conditionally) -- Add `id="cb-sec-preview"` to Preview section -- Use `createSectionHeader()` for each section title instead of inline `.cb-section-title` -- Remove the old sticky nav bar (replaced by sidebar) -- Keep config nav bar inside config section (it supplements the sidebar for config-specific navigation) - -**Step 3: Update renderGlobalPromptsSection** - -Change the section header from inline-styled `.cb-section-title` to use `createSectionHeader()`: - -Replace the header creation in `renderGlobalPromptsSection` (~line 3571-3593) to: -- Add `id="cb-sec-prompts"` to the section element -- Use `createSectionHeader("📝", "Global Prompts", "prompts", { collapsible: true, ... })` instead of the manual header - -**Step 4: Update renderPreviewSection** - -Change the section header (~line 3819-3828): -- Add `id="cb-sec-preview"` to the section element -- Use `createSectionHeader("📄", "JSON Preview", "preview")` instead of inline title - -**Step 5: Commit** -``` -feat: restructure renderUI with top bar, sidebar, and uniform section headers -``` - ---- - -### Task 6: Refactor Distribution Section as Standalone Full-Width Section - -**Files:** -- Modify: `web/conf_builder/conf-builder-distribution.js` - -**Step 1: Add new export for standalone distribution section** - -Add a new function `renderDistributionSettingsSection()` that renders the distribution controls as a full-width section (not inside the top row). This function: - -- Creates a `cb-section full-width` div with `id="cb-sec-distribution"` -- Uses `createSectionHeader("🌐", "Distribution Settings", "distribution")` -- Contains all the existing distribution details (worker URLs, claim timeout, master encoding) but NOT the enable/disable toggle (that's now in Settings dropdown) -- Only rendered when `node.state.distribution_enabled === true` - -Keep the original `renderDistributionSection()` function intact (DO NOT REMOVE) but it will no longer be called from renderUI. - -**Step 2: Import `createSectionHeader` in distribution module** - -```javascript -import { createInputGroup, createSlider, createSectionHeader } from './conf-builder-ui-components.js'; -``` - -**Step 3: Commit** -``` -feat: add standalone distribution settings section for new layout -``` - ---- - -### Task 7: Verify & Commit - -**Step 1: JS brace balance check** - -Verify all modified JS files have balanced braces/parens. - -**Step 2: Visual review** - -Check that all section IDs are consistent between sidebar nav targets and section elements. - -**Step 3: Final commit** - -``` -feat: builder UI overhaul — top bar, sidebar nav, uniform headers, dark mode contrast -``` diff --git a/docs/plans/2026-03-01-per-config-resolutions-design.md b/docs/plans/2026-03-01-per-config-resolutions-design.md deleted file mode 100644 index 18d12cc..0000000 --- a/docs/plans/2026-03-01-per-config-resolutions-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Per-Config Resolutions in Builder UI - -**Date:** 2026-03-01 -**Status:** Approved - -## Summary - -Add per-config resolution support to the Builder UI. Each config array gets its own -resolution list displayed as chips (same pattern as samplers/schedulers). Resolutions -flow through `configs_json` and override the sampler node's `resolutions_json` input -on a per-config basis. - -## Requirements - -1. **Per-config scope:** Each config array has its own resolution list, independent of - other config arrays. -2. **Override behavior:** If a config includes resolutions, they replace the sampler - node's `resolutions_json` for that config. If a config has NO resolutions, the - sampler's `resolutions_json` is used as fallback. -3. **Categorized preset dropdown:** Quick-add dropdown organized by model type - (SD 1.5, SDXL, Flux) with sub-categories for orientation (Square, Portrait, - Landscape), sizes listed largest to smallest within each sub-category. -4. **Custom resolution input:** W x H number inputs for arbitrary resolutions. -5. **Chip display:** Selected resolutions shown as removable chips with x button, - matching the visual pattern of samplers/schedulers. - -## UI Design - -### Preset Dropdown Structure (using native ``) - -``` -── SD 1.5 — Square ── - 512 x 512 -── SD 1.5 — Portrait ── - 512 x 896 - 512 x 768 -── SD 1.5 — Landscape ── - 896 x 512 - 768 x 512 -── SDXL — Square ── - 1024 x 1024 -── SDXL — Portrait ── - 768 x 1344 - 832 x 1216 - 896 x 1152 -── SDXL — Landscape ── - 1344 x 768 - 1216 x 832 - 1152 x 896 -── Flux — Square ── - 1024 x 1024 -── Flux — Portrait ── - 768 x 1344 - 832 x 1216 -── Flux — Landscape ── - 1344 x 768 - 1216 x 832 -``` - -### Custom Input - -Small inline W x H number inputs with an Add button for arbitrary resolutions. - -### Chip Display - -Amber-accented (#cc8800) rounded chips showing "WxH" with x remove button. -Clear button to remove all. Same visual pattern as samplers/schedulers. - -## Data Flow - -### State Storage - -```javascript -configArray.resolutions = ["1024x1024", "832x1216", "1344x768"] -``` - -Array of "WxH" strings, same pattern as `configArray.samplers`. - -### Config JSON Output - -In `convertStateToConfigs()`, resolutions are converted to nested arrays: - -```json -{ - "sampler": ["euler"], - "scheduler": ["normal"], - "resolutions": [[1024, 1024], [832, 1216], [1344, 768]] -} -``` - -Only included when the config has resolutions specified (omitted = use sampler fallback). - -### Config Expansion - -In `expand_configs()`, resolutions become another axis of the Cartesian product. -Each expanded config gets a single `"resolution": [w, h]` field. - -### Generation Loop Override - -In the orchestrator, if an expanded config has a `resolution` field, it provides -its own width/height instead of using the global `input_jobs` from `resolutions_json`. - -Configs WITHOUT resolutions continue to iterate over the sampler's `input_jobs` -as before. - -## Files Changed - -| File | Change | -|------|--------| -| `conf-builder-config-management.js` | New resolution chip builder with categorized optgroup dropdown + custom input | -| `conf-builder-utilities.js` | Add `resolutions` to `convertStateToConfigs()` output | -| `config_utils.py` | Expand `resolutions` array in `expand_configs()` Cartesian product | -| `generation_orchestrator.py` | Per-config resolution override in generation loop | -| `conf-builder-main.js` | State migration: add empty `resolutions: []` to existing config arrays | diff --git a/docs/plans/2026-03-01-per-config-resolutions.md b/docs/plans/2026-03-01-per-config-resolutions.md deleted file mode 100644 index 4ee30cc..0000000 --- a/docs/plans/2026-03-01-per-config-resolutions.md +++ /dev/null @@ -1,455 +0,0 @@ -# Per-Config Resolutions Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add per-config resolution support to the Builder UI with a categorized preset dropdown, flowing through configs_json to override the sampler's resolutions_json on a per-config basis. - -**Architecture:** Each config array gains a `resolutions` array (e.g., `["1024x1024", "832x1216"]`). The UI uses an ``-based dropdown categorized by model type and orientation plus a custom W×H input. Resolutions flow through `configs_json` as `[[w,h],...]`, get expanded into the Cartesian product in `expand_configs()`, and override the sampler's global `resolutions_json` in the generation loop. - -**Tech Stack:** Vanilla JS (ComfyUI widget), Python (config expansion + generation loop) - -**Critical constraint:** DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY. - ---- - -### Task 1: State Migration — Add `resolutions` to Config Arrays - -**Files:** -- Modify: `web/conf_builder/conf-builder-main.js:392-397` (migration block) - -**Step 1: Add migration for resolutions field** - -In the per-config-array migration block (around line 397, after the `attention_modes` migration), add: - -```javascript -if (!arr.resolutions) arr.resolutions = []; -``` - -This follows the exact pattern of `if (!arr.attention_modes) arr.attention_modes = ["default"];` at line 397. - -**Step 2: Verify by checking the file** - -Confirm the new line sits inside the `this.state.config_arrays.forEach(arr => { ... })` block, after the existing migrations. - -**Step 3: Commit** - -``` -feat: add resolutions field migration for config arrays -``` - ---- - -### Task 2: Resolution Chip Builder UI - -**Files:** -- Modify: `web/conf_builder/conf-builder-config-management.js` - - New function `createResolutionBuilder()` (after `createChipListBuilder` ~line 270) - - Render call after Attention builder (~line 362) - -**Step 1: Define the preset resolution data** - -Add this constant near the top of the file (after imports, ~line 20): - -```javascript -const RESOLUTION_PRESETS = [ - { group: "SD 1.5 — Square", items: ["512x512"] }, - { group: "SD 1.5 — Portrait", items: ["512x896", "512x768"] }, - { group: "SD 1.5 — Landscape", items: ["896x512", "768x512"] }, - { group: "SDXL — Square", items: ["1024x1024"] }, - { group: "SDXL — Portrait", items: ["768x1344", "832x1216", "896x1152"] }, - { group: "SDXL — Landscape", items: ["1344x768", "1216x832", "1152x896"] }, - { group: "Flux — Square", items: ["1024x1024"] }, - { group: "Flux — Portrait", items: ["768x1344", "832x1216"] }, - { group: "Flux — Landscape", items: ["1344x768", "1216x832"] }, -]; -``` - -**Step 2: Create the `createResolutionBuilder()` function** - -Add after `createChipListBuilder` (~line 270). This function creates: -- Chip display area (same pattern as `createChipListBuilder`) -- `` dropdown in the SESSION section of the cog menu populated from the list_sessions endpoint. Selecting a session loads it. - -## Item 5: Distribution Settings Refactor - -### 5a: Always-Visible Toggle -Distribution Settings section currently only renders when `node.state.distribution_enabled` is true. Change to: -1. Always render the section at the top -2. Show a simple On/Off toggle at the top -3. When Off, collapse/hide all settings below the toggle -4. When On, show full settings as before - -### 5b: Remove Separate distribution_config Output/Input (Clean Break) -**Config Builder (`config_builder_node.py`):** -- Change `RETURN_TYPES` from `("STRING", "STRING", "STRING")` to `("STRING", "STRING")` -- Change `RETURN_NAMES` from `("configs_json", "session_name", "distribution_config")` to `("configs_json", "session_name")` -- Embed distribution_config data inside the configs_json output as a top-level `_distribution` key -- Return format: `{"configs": [...], "_distribution": {...}}` instead of just `[...]` - -**Sampler Node (`sampler_node.py`):** -- Remove `distribution_config` from optional INPUT_TYPES -- Remove `distribution_config` parameter from `run_tests()` -- Parse distribution config from within `configs_json` (look for `_distribution` key) - -**Generation Orchestrator (`generation_orchestrator.py`):** -- Update `run_generation_loop()` signature to remove `distribution_config` parameter -- Extract `_distribution` from the parsed configs_json data instead diff --git a/docs/plans/2026-03-05-builder-dashboard-improvements.md b/docs/plans/2026-03-05-builder-dashboard-improvements.md deleted file mode 100644 index e1b2cf0..0000000 --- a/docs/plans/2026-03-05-builder-dashboard-improvements.md +++ /dev/null @@ -1,35 +0,0 @@ -# Builder UI & Dashboard Improvements Implementation Plan - -**Critical Constraint:** DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY. - ---- - -## Task 1: Double Prompt JSON Editor Size (Easy) -**File:** `web/conf_builder/conf-builder-ui-components.js` -- Change `.cb-prompt-raw-editor` `min-height: 80px` → `min-height: 160px` - -## Task 2: Sidebar Always-Open + Icon Changes -**File:** `web/conf_builder/conf-builder-ui-components.js` -- CSS: Change `.cb-sidebar-sub-icons` `display: none` → `display: flex` -- CSS: Remove `.cb-sidebar-config-group:hover .cb-sidebar-sub-icons` rule -- CSS: Add `overflow-y: auto` to sidebar container -- JS: Change config icon content from `idx+1` to `⚙️${idx+1}` -- JS: Change TE icon from "📎" to "🔣" -- JS: Change LoRA icon from "🔗" to "🔮" - -## Task 3: Dashboard Shift+N Column Hotkeys -**File:** `resources/logic_virtual.js` -- Add cases in `setupKeyboardShortcuts()` for digits 0-9 with shiftKey - -## Task 4: Dashboard Session Landing & Reload -**Files:** `resources/template.html`, `resources/logic_utils.js`, `resources/logic_init.js`, `resources/report.css`, `__init__.py` -- Rename topbar load button -- Add list_sessions endpoint -- Add landing page rendering -- Add session dropdown in cog menu - -## Task 5: Distribution Settings Refactor -**Files:** `web/conf_builder/conf-builder-distribution.js`, `web/conf_builder/conf-builder-config-management.js`, `config_builder_node.py`, `sampler_node.py`, `generation_orchestrator.py` -- Always show distribution toggle -- Embed distribution_config in configs_json -- Remove separate output/input diff --git a/docs/plans/2026-03-08-seed-flicker-upscale-cooldown.md b/docs/plans/2026-03-08-seed-flicker-upscale-cooldown.md deleted file mode 100644 index b5b4d84..0000000 --- a/docs/plans/2026-03-08-seed-flicker-upscale-cooldown.md +++ /dev/null @@ -1,955 +0,0 @@ -# Seed Settings, Canvas Flicker Fix, Upscaling & GPU Cooldown Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add 4 features: expanded seed behavior options per-config, fix canvas flicker during simultaneous pan+zoom, add 3 upscaling modes as session-level settings, and add GPU cooldown breaks between generations. - -**Architecture:** Seed settings extend existing per-config `seed_behavior` dropdown with new options (random/increment/decrement before/after full run). Canvas flicker fix batches pan+zoom transforms via requestAnimationFrame. Upscaling and cooldown are session-level settings stored in `node.state` and output alongside `configs_json`, consumed by `generation_orchestrator.py` and `distribution_worker.py`. - -**Tech Stack:** Python (ComfyUI custom nodes), JavaScript (Builder UI widgets), ComfyUI model management APIs, requestAnimationFrame - ---- - -## CRITICAL RULE - -**DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY.** - ---- - -## Task 1: Expand Seed Behavior Options (Builder UI) - -**Files:** -- Modify: `web/conf_builder/conf-builder-config-management.js:1018-1029` (seed select dropdown) -- Modify: `web/conf_builder/conf-builder-utilities.js:637-639` (convertStateToConfigs seed output) -- Modify: `web/conf_builder/conf-builder-ui-components.js:867` (default state) - -**Step 1: Update the seed behavior dropdown** - -In `web/conf_builder/conf-builder-config-management.js`, find the seed select at line 1018-1029. Replace the ` - - - -
- - -
- -
- - -
-``` - -Then after the `
` line (line 228), add the delete button: - -```html - -
-``` - -**Step 2: Commit** - -```bash -git add resources/template.html -git commit -m "feat: add workflow packing checkboxes and delete non-favorites button to template" -``` - ---- - -## Task 2: Extract buildComfyNodesWorkflow from copyConfigsAsComfyNodes - -**Files:** -- Modify: `resources/logic_ui.js:692-1001` - -**Step 1: Create reusable buildComfyNodesWorkflow function** - -In `resources/logic_ui.js`, add a new function BEFORE `copyConfigsAsComfyNodes` (before line 692). This function takes an item data object and returns a workflow JSON object. It contains the workflow-building logic extracted from `copyConfigsAsComfyNodes`: - -```javascript -// Build a pure ComfyUI nodes workflow JSON from image config data -// Reusable version of the clipboard copy logic — returns the workflow object -function buildComfyNodesWorkflow(d) { - // Parse LoRA string into array - const loras = []; - if (d.lora && d.lora !== "None") { - const loraEntries = d.lora.split(' + ').filter(e => e.trim().length > 0); - loraEntries.forEach(entry => { - const parts = entry.split(':'); - const name = parts[0]; - const strength_model = parseFloat(parts[1] || 1.0); - const strength_clip = parseFloat(parts[2] || strength_model); - loras.push({ name, strength_model, strength_clip }); - }); - } - - // Generate node IDs - let nodeId = 1; - const checkpointNode = nodeId++; - const loraNodes = loras.map(() => nodeId++); - const positiveClipNode = nodeId++; - const negativeClipNode = nodeId++; - const emptyLatentNode = nodeId++; - const ksamplerNode = nodeId++; - const vaeDecodeNode = nodeId++; - const previewNode = nodeId++; - - if (!crypto.randomUUID) { - crypto.randomUUID = function () { - return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) - ); - }; - } - - const workflow = { - id: crypto.randomUUID(), - revision: 0, - last_node_id: nodeId - 1, - last_link_id: 100, - nodes: [], - links: [], - groups: [], - config: {}, - extra: { - workflowRendererVersion: "LG", - ds: { scale: 0.573, offset: [488, 377] } - }, - version: 0.4 - }; - - // Checkpoint - workflow.nodes.push({ - id: checkpointNode, - type: "CheckpointLoaderSimple", - pos: [-200, 60], - size: [315, 98], - flags: {}, order: 0, mode: 0, - inputs: [], - outputs: [ - { name: "MODEL", type: "MODEL", links: [] }, - { name: "CLIP", type: "CLIP", links: [] }, - { name: "VAE", type: "VAE", links: [] } - ], - properties: { "Node name for S&R": "CheckpointLoaderSimple" }, - widgets_values: [d.model || ""] - }); - - // LoRAs - loras.forEach((lora, index) => { - workflow.nodes.push({ - id: loraNodes[index], - type: "LoraLoader", - pos: [170 + (index * 312), 60], - size: [270, 126], - flags: {}, order: index + 1, mode: 0, - inputs: [ - { name: "model", type: "MODEL", link: null }, - { name: "clip", type: "CLIP", link: null } - ], - outputs: [ - { name: "MODEL", type: "MODEL", links: [] }, - { name: "CLIP", type: "CLIP", links: [] } - ], - properties: { "Node name for S&R": "LoraLoader" }, - widgets_values: [String(lora.name).replace(/\//g, "\\"), lora.strength_model, lora.strength_clip] - }); - }); - - const posX = 910 + (loras.length * 312); - - // Positive Clip - workflow.nodes.push({ - id: positiveClipNode, - type: "CLIPTextEncode", - pos: [posX, 3.52], - size: [460, 190], - flags: {}, order: loras.length + 1, mode: 0, - inputs: [{ name: "clip", type: "CLIP", link: null }], - outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [] }], - properties: { "Node name for S&R": "CLIPTextEncode" }, - widgets_values: [d.positive || ""] - }); - - // Negative Clip - workflow.nodes.push({ - id: negativeClipNode, - type: "CLIPTextEncode", - pos: [posX, 240], - size: [470, 200], - flags: {}, order: loras.length + 2, mode: 0, - inputs: [{ name: "clip", type: "CLIP", link: null }], - outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [] }], - properties: { "Node name for S&R": "CLIPTextEncode" }, - widgets_values: [d.negative || ""] - }); - - // Empty Latent - workflow.nodes.push({ - id: emptyLatentNode, - type: "EmptyLatentImage", - pos: [posX + 130, 510], - size: [270, 106], - flags: {}, order: 1, mode: 0, - inputs: [], - outputs: [{ name: "LATENT", type: "LATENT", links: [] }], - properties: { "Node name for S&R": "EmptyLatentImage" }, - widgets_values: [d.width || 1080, d.height || 1584, 1] - }); - - // KSampler - const sampX = posX + 510; - workflow.nodes.push({ - id: ksamplerNode, - type: "KSampler", - pos: [sampX, 23.52], - size: [315, 708], - flags: {}, order: loras.length + 3, mode: 0, - inputs: [ - { name: "model", type: "MODEL", link: null }, - { name: "positive", type: "CONDITIONING", link: null }, - { name: "negative", type: "CONDITIONING", link: null }, - { name: "latent_image", type: "LATENT", link: null } - ], - outputs: [{ name: "LATENT", type: "LATENT", links: [] }], - properties: { "Node name for S&R": "KSampler" }, - widgets_values: [ - d.seed || 0, "fixed", d.steps || 25, d.cfg || 7, - d.sampler || "dpmpp_2m", d.scheduler || "karras", d.denoise || 1 - ] - }); - - // VAE Decode - workflow.nodes.push({ - id: vaeDecodeNode, - type: "VAEDecode", - pos: [sampX + 390, 33], - size: [210, 46], - flags: {}, order: loras.length + 4, mode: 0, - inputs: [ - { name: "samples", type: "LATENT", link: null }, - { name: "vae", type: "VAE", link: null } - ], - outputs: [{ name: "IMAGE", type: "IMAGE", links: [] }], - properties: { "Node name for S&R": "VAEDecode" }, - widgets_values: [] - }); - - // Preview - workflow.nodes.push({ - id: previewNode, - type: "PreviewImage", - pos: [sampX + 370, 173], - size: [418, 556], - flags: {}, order: loras.length + 5, mode: 0, - inputs: [{ name: "images", type: "IMAGE", link: null }], - outputs: [], - properties: { "Node name for S&R": "PreviewImage" }, - widgets_values: [] - }); - - // Wire nodes - const getNode = (id) => workflow.nodes.find(n => n.id === id); - let linkId = 1; - - let currentModelSource = { id: checkpointNode, slot: 0 }; - let currentClipSource = { id: checkpointNode, slot: 1 }; - - // Daisy chain LoRAs - for (let i = 0; i < loras.length; i++) { - const thisLoraId = loraNodes[i]; - workflow.links.push([linkId, currentModelSource.id, currentModelSource.slot, thisLoraId, 0, "MODEL"]); - getNode(currentModelSource.id).outputs[currentModelSource.slot].links.push(linkId); - getNode(thisLoraId).inputs[0].link = linkId; - linkId++; - workflow.links.push([linkId, currentClipSource.id, currentClipSource.slot, thisLoraId, 1, "CLIP"]); - getNode(currentClipSource.id).outputs[currentClipSource.slot].links.push(linkId); - getNode(thisLoraId).inputs[1].link = linkId; - linkId++; - currentModelSource = { id: thisLoraId, slot: 0 }; - currentClipSource = { id: thisLoraId, slot: 1 }; - } - - // Final Model -> KSampler - workflow.links.push([linkId, currentModelSource.id, currentModelSource.slot, ksamplerNode, 0, "MODEL"]); - getNode(currentModelSource.id).outputs[currentModelSource.slot].links.push(linkId); - getNode(ksamplerNode).inputs[0].link = linkId; - linkId++; - - // Final CLIP -> Positive Prompt - workflow.links.push([linkId, currentClipSource.id, currentClipSource.slot, positiveClipNode, 0, "CLIP"]); - getNode(currentClipSource.id).outputs[currentClipSource.slot].links.push(linkId); - getNode(positiveClipNode).inputs[0].link = linkId; - linkId++; - - // Final CLIP -> Negative Prompt - workflow.links.push([linkId, currentClipSource.id, currentClipSource.slot, negativeClipNode, 0, "CLIP"]); - getNode(currentClipSource.id).outputs[currentClipSource.slot].links.push(linkId); - getNode(negativeClipNode).inputs[0].link = linkId; - linkId++; - - // VAE: Checkpoint -> VAE Decode - workflow.links.push([linkId, checkpointNode, 2, vaeDecodeNode, 1, "VAE"]); - getNode(checkpointNode).outputs[2].links.push(linkId); - getNode(vaeDecodeNode).inputs[1].link = linkId; - linkId++; - - // Conditioning: Positive -> KSampler - workflow.links.push([linkId, positiveClipNode, 0, ksamplerNode, 1, "CONDITIONING"]); - getNode(positiveClipNode).outputs[0].links.push(linkId); - getNode(ksamplerNode).inputs[1].link = linkId; - linkId++; - - // Conditioning: Negative -> KSampler - workflow.links.push([linkId, negativeClipNode, 0, ksamplerNode, 2, "CONDITIONING"]); - getNode(negativeClipNode).outputs[0].links.push(linkId); - getNode(ksamplerNode).inputs[2].link = linkId; - linkId++; - - // Latent: Empty Latent -> KSampler - workflow.links.push([linkId, emptyLatentNode, 0, ksamplerNode, 3, "LATENT"]); - getNode(emptyLatentNode).outputs[0].links.push(linkId); - getNode(ksamplerNode).inputs[3].link = linkId; - linkId++; - - // Latent: KSampler -> VAE Decode - workflow.links.push([linkId, ksamplerNode, 0, vaeDecodeNode, 0, "LATENT"]); - getNode(ksamplerNode).outputs[0].links.push(linkId); - getNode(vaeDecodeNode).inputs[0].link = linkId; - linkId++; - - // Image: VAE Decode -> Preview - workflow.links.push([linkId, vaeDecodeNode, 0, previewNode, 0, "IMAGE"]); - getNode(vaeDecodeNode).outputs[0].links.push(linkId); - getNode(previewNode).inputs[0].link = linkId; - linkId++; - - workflow.last_link_id = linkId; - return workflow; -} -``` - -**Step 2: Refactor copyConfigsAsComfyNodes to use the new function** - -In `copyConfigsAsComfyNodes` (line 692), replace lines 702-979 (everything inside the `try` block before the clipboard copy logic at line 980) with a call to the new function: - -Replace from `// Parse LoRA string into array` through `workflow.last_link_id = linkId;` (lines 703-978) with: - -```javascript - const workflow = buildComfyNodesWorkflow(d); -``` - -Keep the existing clipboard copy logic (lines 980-1001+) unchanged. - -**Step 3: Commit** - -```bash -git add resources/logic_ui.js -git commit -m "refactor: extract buildComfyNodesWorkflow from copyConfigsAsComfyNodes" -``` - ---- - -## Task 3: Update exportFavorites JS to Send New Parameters - -**Files:** -- Modify: `resources/logic_utils.js:252-340` - -**Step 1: Add checkbox reads and workflow capture** - -In `resources/logic_utils.js`, in the `exportFavorites()` function, after line 279 (the `exportPromptTxt` variable), add: - -```javascript - // Get copy manifest checkbox state - const copyManifestCheckbox = document.getElementById('copy-manifest-checkbox'); - const copyManifest = copyManifestCheckbox ? copyManifestCheckbox.checked : true; - - // Get pack workflow checkbox state - const packWorkflowCheckbox = document.getElementById('pack-workflow-checkbox'); - const packWorkflow = packWorkflowCheckbox ? packWorkflowCheckbox.checked : false; - - // Get pack nodes workflow checkbox state - const packNodesWorkflowCheckbox = document.getElementById('pack-nodes-workflow-checkbox'); - const packNodesWorkflow = packNodesWorkflowCheckbox ? packNodesWorkflowCheckbox.checked : false; - - // Capture current ComfyUI workflow if pack_workflow is checked - let workflowData = null; - if (packWorkflow) { - try { - const app = window.app || window.parent?.app; - if (app && app.graph) { - workflowData = app.graph.serialize(); - console.log('[Export] Captured current ComfyUI workflow'); - } else { - console.warn('[Export] Could not access ComfyUI app.graph — workflow will not be packed'); - } - } catch (e) { - console.warn('[Export] Error capturing workflow:', e); - } - } - - // Generate per-image nodes workflows if pack_nodes_workflow is checked - let nodesWorkflows = null; - if (packNodesWorkflow && typeof buildComfyNodesWorkflow === 'function') { - try { - nodesWorkflows = {}; - // activeData is the global array of all manifest items in the dashboard - const favoritedItems = activeData.filter(item => item.favorited); - for (const item of favoritedItems) { - const wf = buildComfyNodesWorkflow(item); - // Key by the item's file path so backend can match - nodesWorkflows[item.file || item.id] = wf; - } - console.log(`[Export] Generated ${Object.keys(nodesWorkflows).length} per-image node workflows`); - } catch (e) { - console.warn('[Export] Error generating node workflows:', e); - } - } -``` - -**Step 2: Add new parameters to fetch body** - -In the same function, update the `JSON.stringify` body (around line 296-302) to include the new parameters. Replace: - -```javascript - body: JSON.stringify({ - session_name: sessionName, - pack_metadata: packMetadata, - organize_by_prompt: organizeByPrompt, - organize_by_lora: organizeByLora, - export_prompt_txt: exportPromptTxt - }) -``` - -With: - -```javascript - body: JSON.stringify({ - session_name: sessionName, - pack_metadata: packMetadata, - organize_by_prompt: organizeByPrompt, - organize_by_lora: organizeByLora, - export_prompt_txt: exportPromptTxt, - copy_manifest: copyManifest, - pack_workflow: packWorkflow, - pack_nodes_workflow: packNodesWorkflow, - workflow_data: workflowData, - nodes_workflows: nodesWorkflows - }) -``` - -**Step 3: Add deleteNonFavorites function** - -After the `exportFavorites()` function (after line 340), add: - -```javascript -// Delete all non-favorited items from the session -async function deleteNonFavorites() { - if (!window.confirm("Are you sure you want to delete all non-favorited items? This cannot be undone.")) { - return; - } - - const statusEl = document.getElementById('delete-status'); - const sessInput = document.getElementById('session-input'); - if (!sessInput) { - if (statusEl) statusEl.innerText = '❌ Error: Session input not found'; - return; - } - const sessionName = sessInput.value; - - if (statusEl) { - statusEl.innerText = '⏳ Deleting non-favorited items...'; - statusEl.style.color = '#ffaa00'; - } - - try { - const response = await fetch('/config_tester/delete_non_favorites', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ session_name: sessionName }) - }); - - const resultText = await response.text(); - - if (response.ok) { - if (statusEl) { - statusEl.innerText = '✅ ' + resultText; - statusEl.style.color = '#4caf50'; - } - // Reload the page to refresh the dashboard with updated manifest - setTimeout(() => { location.reload(); }, 2000); - } else { - if (statusEl) { - statusEl.innerText = '❌ Error: ' + resultText; - statusEl.style.color = '#ff3860'; - } - } - } catch (error) { - console.error('[Delete] Error:', error); - if (statusEl) { - statusEl.innerText = '❌ Network error: ' + error.message; - statusEl.style.color = '#ff3860'; - } - } -} -``` - -**Step 4: Commit** - -```bash -git add resources/logic_utils.js -git commit -m "feat: add workflow capture, nodes workflow generation, and delete non-favorites to export" -``` - ---- - -## Task 4: Fix metadata_packer.py Bug + Accept Workflow Data - -**Files:** -- Modify: `metadata_packer.py:429-430` - -**Step 1: Fix the hardcoded workflowExample bug** - -In `metadata_packer.py`, find line 429-430: - -```python - if True: - workflow_data_full = workflowExample -``` - -Replace with: - -```python - if not workflow_data_full: - workflow_data_full = workflowExample -``` - -This ensures the `workflowExample` is only used as a fallback when no actual workflow data was provided, which was the original intent. - -**Step 2: Commit** - -```bash -git add metadata_packer.py -git commit -m "fix: use workflowExample as fallback only when no workflow data provided" -``` - ---- - -## Task 5: Backend — Handle New Export Parameters + Delete Endpoint - -**Files:** -- Modify: `__init__.py:434-621` - -**Step 1: Add new parameters to export_favorites endpoint** - -In `__init__.py`, in the `export_favorites` function (line 434), after line 447 (`export_prompt_txt = data.get(...)`), add: - -```python - copy_manifest = data.get("copy_manifest", True) - pack_workflow = data.get("pack_workflow", False) - pack_nodes_workflow = data.get("pack_nodes_workflow", False) - workflow_data = data.get("workflow_data", None) - nodes_workflows = data.get("nodes_workflows", None) -``` - -**Step 2: Pass workflow data to pack_metadata_into_image** - -In the same function, find line 579 where `pack_metadata_into_image` is called: - -```python - pack_metadata_into_image(source_path, dest_path, item, manifest.get("meta", {})) -``` - -Replace with: - -```python - # Determine which workflow to embed - item_workflow = None - if pack_nodes_workflow and nodes_workflows: - # Per-image nodes workflow takes priority - item_workflow = nodes_workflows.get(item.get("file", "")) - elif pack_workflow and workflow_data: - # Full ComfyUI graph workflow - item_workflow = workflow_data - pack_metadata_into_image(source_path, dest_path, item, manifest.get("meta", {}), workflow_data=item_workflow) -``` - -**Step 3: Add cleaned manifest copy** - -After the `for item in favorited:` loop (after line 612, before the `result_msg` line), add: - -```python - # Copy cleaned favorites-only manifest if requested - if copy_manifest: - try: - cleaned_manifest = { - "items": favorited, - "meta": manifest.get("meta", {}), - "session_name": session_name - } - manifest_dest = os.path.join(export_base, "manifest.json") - with open(manifest_dest, "w", encoding="utf-8") as f: - json.dump(cleaned_manifest, f, indent=2, ensure_ascii=False) - print(f"[Export] Saved cleaned favorites manifest to {manifest_dest}") - except Exception as e: - print(f"[Export] Error saving cleaned manifest: {e}") -``` - -**Step 4: Update result message** - -After the existing result message conditionals (after line 612 `if export_prompt_txt:`), add: - -```python - if copy_manifest: - result_msg += " (with favorites manifest)" - if pack_workflow: - result_msg += " (with full workflow)" - if pack_nodes_workflow: - result_msg += " (with nodes workflows)" -``` - -**Step 5: Add delete_non_favorites endpoint** - -After the `export_favorites` function's closing `except` block (after line 621), add: - -```python -# ============================================================================= -# API: DELETE NON-FAVORITED ITEMS -# ============================================================================= - -@server.PromptServer.instance.routes.post("/config_tester/delete_non_favorites") -async def delete_non_favorites(request): - """ - Delete all non-favorited images from a session and update the manifest. - """ - try: - data = await request.json() - session_name = data.get("session_name") - - # Sanitize - if session_name: - session_name = re.sub(r'[^\w\-]', '', session_name) - - if not session_name: - return web.Response(status=400, text="Missing session_name") - - # Paths - base_dir = os.path.join(folder_paths.get_output_directory(), "benchmarks", session_name) - - if not _is_path_within(base_dir, _get_benchmarks_base()): - return web.Response(status=403, text="Forbidden: path outside benchmarks directory") - - manifest_path = os.path.join(base_dir, "manifest.json") - images_dir = os.path.join(base_dir, "images") - - # Load manifest - if not os.path.exists(manifest_path): - return web.Response(status=404, text=f"Session '{session_name}' not found") - - with open(manifest_path, "r") as f: - manifest = json.load(f) - - items = manifest.get("items", []) - favorited = [item for item in items if item.get("favorited", False)] - non_favorited = [item for item in items if not item.get("favorited", False)] - - if not non_favorited: - return web.Response(status=200, text="No non-favorited items to delete") - - # Delete non-favorited image files - deleted_count = 0 - for item in non_favorited: - file_path = item.get("file", "") - - # Parse filename from various formats - if file_path.startswith("/view?"): - parsed_url = urllib.parse.urlparse(file_path) - url_params = urllib.parse.parse_qs(parsed_url.query) - filename = url_params.get("filename", [""])[0] - elif file_path.startswith("./images/"): - filename = file_path[9:] - elif "filename=" in file_path: - filename = file_path.split("filename=")[-1].split("&")[0] - else: - filename = os.path.basename(file_path) - - if filename: - image_path = os.path.join(images_dir, filename) - if os.path.exists(image_path): - try: - os.remove(image_path) - deleted_count += 1 - except Exception as e: - print(f"[Delete] Error deleting {filename}: {e}") - - # Update manifest to only contain favorited items - manifest["items"] = favorited - with open(manifest_path, "w", encoding="utf-8") as f: - json.dump(manifest, f, indent=2, ensure_ascii=False) - - result_msg = f"Deleted {deleted_count} non-favorited images. {len(favorited)} favorited items remain." - print(f"[ConfigTester] 🗑️ {result_msg}") - return web.Response(status=200, text=result_msg) - - except Exception as e: - print(f"[ConfigTester] Error deleting non-favorites: {e}") - import traceback - traceback.print_exc() - return web.Response(status=500, text=str(e)) -``` - -**Step 6: Commit** - -```bash -git add __init__.py -git commit -m "feat: add workflow packing to export, cleaned manifest copy, and delete non-favorites endpoint" -``` - ---- - -## Task 6: Integration Verification - -**Step 1: Verify template renders correctly** - -Open ComfyUI, load a session in the Dashboard. Verify: -- 3 new checkboxes appear below the existing 4 -- "Copy cleaned favorites-only manifest" is checked by default -- The other two are unchecked by default -- Red "DELETE ALL NON FAVORITED ITEMS" button appears below the status line - -**Step 2: Verify export favorites with new options** - -1. Favorite a few images in the dashboard -2. Check "Pack full ComfyUI workflow" checkbox -3. Click "COPY FAVORITES TO BENCHMARK FAVORITES FOLDER" -4. Verify favorites folder contains images + `manifest.json` -5. Drag a PNG from favorites into ComfyUI — should load the full workflow - -**Step 3: Verify pack nodes workflow** - -1. Uncheck "Pack full ComfyUI workflow" -2. Check "Pack config data as pure nodes workflow" -3. Export favorites again -4. Drag a PNG into ComfyUI — should load a clean Checkpoint→LoRA→CLIP→KSampler workflow with that image's specific settings - -**Step 4: Verify delete non-favorites** - -1. Click "DELETE ALL NON FAVORITED ITEMS" -2. Confirm the dialog appears -3. Click Cancel — verify nothing happens -4. Click the button again, confirm Yes -5. Verify non-favorited images are deleted, manifest is updated, dashboard refreshes - -**Step 5: Commit (if any fixes needed)** - -```bash -git add -A -git commit -m "fix: integration fixes for favorites workflow packing" -```