Skip to content

Commit e38f20e

Browse files
committed
Merge branch 'dev/ptrottier/presets' into 'main'
REMIX-4985: Added Presets + Various Other Improvements See merge request lightspeedrtx/lss-ai-tools/comfyui-rtx_remix!45
2 parents a377c7e + cabedcd commit e38f20e

74 files changed

Lines changed: 9492 additions & 796 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
### Removed
1717

18+
## [3.0.0] - 2026-02-10
19+
20+
### Added
21+
22+
- **Preset Panel**: New sidebar panel to save, manage, and apply preset configurations for tagged inputs
23+
- Save current input values as named presets
24+
- Apply presets to restore saved values
25+
- Set default presets that auto-apply on workflow load
26+
- Edit and delete presets via context menu
27+
- Group presets for organization
28+
- **Grouped List Component**: Reusable collapsible grouped list UI component
29+
- **Popover Component**: Reusable popover UI component for contextual actions
30+
- **Form Field Components**: Standardized input components (text, number, checkbox, select, textarea)
31+
- **Graph Store**: Centralized store for managing graph state and tagged inputs
32+
- **Preset Store**: Centralized store for managing presets with localStorage persistence
33+
- **Frontend Documentation**: Added web/README.md with architecture and contribution guidelines
34+
- **Download Node**: Auto-reset hash and archive fields when URL changes to prevent stale data
35+
- **Download Node**: Progress bar in UI during file downloads
36+
- **Download Node**: Cancellation support - stops download and cleans up partial files when prompt is cancelled
37+
38+
### Changed
39+
40+
- **Export Dialog**: Improved layout, better details panel, pending changes detection
41+
- **UI Architecture**: Refactored to controller/core/store pattern for better separation of concerns
42+
- **Keyboard Shortcuts**: Improved keybind handling and consistency
43+
- **Styling**: Enhanced CSS with new utility classes, variables, and animations
44+
1845
## [2.2.1] - 2026-02-23
1946

2047
### Fixed

README.md

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ Mixing approaches is also _possible_ but **not recommended**.
4545
**Key benefits:**
4646

4747
- Simple to set up and use: Tag your inputs and outputs, export the workflow, and the RTX Remix Toolkit will discover the workflow automatically.
48-
- Works directly from within the RTX RemixToolkit interface: Queue up jobs and set the workflow input values directly from the RTX Remix Toolkit interface.
48+
- Works directly from within the RTX Remix Toolkit interface: Queue up jobs and set the workflow input values directly from the RTX Remix Toolkit interface.
4949
- Easy workflow development and iteration: use standard ComfyUI nodes like `Load Image` to test your workflow without the RTX Remix Toolkit running
50+
- Built-in [Presets](#managing-presets) system for saving and switching between different parameter configurations during development
5051

5152
👉 **[Jump to Integration Workflow Guide](#integration-workflow-guide)**
5253

@@ -85,6 +86,8 @@ The integration workflow system consists of three key steps:
8586
2. **Tag inputs and outputs** - Mark which inputs/outputs the RTX Remix Toolkit should control
8687
3. **Export the workflow** - Save the workflow for the RTX Remix Toolkit to use
8788

89+
Optionally, use **[Presets](#managing-presets)** to save and compare different parameter configurations for your tagged inputs during workflow development.
90+
8891
Once exported, your workflow can be selected and run directly from within the RTX Remix Toolkit, which will:
8992

9093
- Provide input texture paths and parameters to tagged input slots
@@ -170,6 +173,8 @@ Output nodes are the final nodes in your workflow that produce the textures/mesh
170173
- **Green circles** on input slots = Tagged input
171174
- **Green outline** around entire node = Tagged output (must save files to disk)
172175

176+
**Tip:** Tagged inputs also appear in the [Presets](#managing-presets) sidebar panel, where you can save different value configurations and organize inputs into groups.
177+
173178
### Step 3: Export the Workflow
174179

175180
Once you've tagged your inputs and outputs, export the workflow for use in the RTX Remix Toolkit.
@@ -199,7 +204,8 @@ The exported workflow is saved to the `user/rtx-remix/workflows/` and `user/rtx-
199204
- **Set texture types for outputs** - Configure which texture map type each output represents (albedo, normal, roughness, etc.) - essential for proper shader input matching when using multiple outputs
200205
- **Add descriptions** - Provide user guidance for each parameter
201206
- **Set value ranges** - Define min/max constraints for numeric inputs
202-
- **Reorder inputs/outputs** - Drag and drop rows to change the order
207+
- **Reorder inputs/outputs** - Drag and drop rows to change order within a group
208+
- **Reorder groups** - Drag group headers to change the order of entire groups
203209
- **Expand metadata** - Click the chevron to see and edit additional fields
204210

205211
### Using Your Workflow in the RTX Remix Toolkit
@@ -220,6 +226,93 @@ After exporting, your workflow becomes available in the RTX Remix Toolkit's work
220226
- **Outputs:** Your workflow saves textures/meshes to disk (e.g., using `RTX Remix Save Texture` or `Save Image` nodes)
221227
- **Integration:** The RTX Remix Toolkit retrieves the output file paths from ComfyUI's output history and imports the processed assets into your project
222228

229+
### Managing Presets
230+
231+
Presets let you save and switch between different sets of parameter values for your tagged inputs. They're useful for:
232+
233+
- Testing your workflow with different parameter combinations (e.g., different upscale strengths, model choices, or prompt variations)
234+
- Shipping workflows with pre-configured settings for different use cases
235+
- Quickly comparing results across configurations without manually adjusting every input
236+
237+
Presets only work with [tagged inputs](#tagging-input-slots) — tag at least one input before creating presets.
238+
239+
#### The Preset Panel
240+
241+
Open the preset panel by clicking the **** (star) icon in the ComfyUI sidebar. The panel has two sections:
242+
243+
- **Preset list** (top) — All presets for the current workflow, with search, create, edit, and delete controls
244+
- **Tagged inputs** (bottom) — All tagged inputs for the active preset, with inline value editing and per-input actions
245+
246+
#### How Presets Work
247+
248+
Presets use a **default + override** model:
249+
250+
1. **Default Values** — A special preset that stores the baseline value for every tagged input. Created automatically when you make your first custom preset.
251+
2. **Custom presets** — Store only the values that **differ** from the default. Any value not explicitly overridden falls back to the Default Values.
252+
253+
**Value resolution order:** Custom preset override → Default Values → Current widget value
254+
255+
This keeps presets lightweight — you only configure what's different, and everything else inherits the baseline.
256+
257+
#### Creating and Managing Presets
258+
259+
| Action | How |
260+
|--------|-----|
261+
| **Create** | Click **Create Preset** in the panel header. Enter a name and optional description. The preset captures any values that currently differ from the Default Values. |
262+
| **Switch** | Click a preset in the list to activate it. Its values are applied to all tagged widgets. If you have unsaved changes, you'll be prompted to save or discard. |
263+
| **Rename** | Click the edit icon on a preset. The Default Values preset cannot be renamed. |
264+
| **Delete** | Click the delete icon. Default Values can only be cleared when no other presets exist. |
265+
266+
#### Editing Values and Overrides
267+
268+
Values can be edited in two ways — both stay in sync:
269+
270+
- **In the preset panel** — Edit inputs directly in the sidebar
271+
- **On the canvas** — Edit node widgets as usual; changes flow to the panel
272+
273+
Inputs whose values differ from the Default Values show a **colored override indicator** on the left side of the row, making it easy to see which values are customized in the current preset.
274+
275+
**Per-input actions** (click the **** menu on any input row):
276+
277+
- **Focus Node** — Center the canvas on the input's node
278+
- **Edit Metadata** — Change the input's export name, group, tooltip, or min/max/step values
279+
- **Apply to Default** — Promote this input's current value to the Default Values preset
280+
- **Reset to Default** — Revert this input back to the Default Values
281+
- **Untag Input** — Remove the RTX Remix tag from this input (removes it from all presets)
282+
283+
**Bulk actions** (buttons above the inputs list):
284+
285+
- **Apply All** — Apply all overrides in the current preset to the Default Values
286+
- **Reset All** — Revert all overrides in the current preset back to Default Values
287+
288+
Changes are held as **pending** until you click **Save**. You can also enable auto-save to skip the manual step.
289+
290+
#### Auto-Save
291+
292+
Click the **** dropdown arrow next to the Save button to access save options:
293+
294+
- **Discard** — Discard all pending changes and revert to the last saved state
295+
- **Auto-save** toggle — When enabled, changes save immediately as you edit. The manual Save button is disabled while auto-save is active.
296+
297+
#### Organizing Inputs with Groups
298+
299+
When a workflow has many tagged inputs, groups help keep them organized:
300+
301+
1. Click **** on any input → **Edit Metadata** → set a **Group** name
302+
2. Inputs sharing the same group name are displayed together in collapsible sections
303+
3. Drag group headers to reorder groups
304+
4. Drag rows within a group to reorder inputs
305+
5. Use the group header **** menu to untag all inputs in a group at once
306+
307+
Groups are shared between the preset panel and the [export dialog](#step-3-export-the-workflow) — organizing in one place updates the other.
308+
309+
#### Preset Storage
310+
311+
- Presets are **embedded in the workflow file** (stored in the graph's extra data). Loading a workflow restores all its presets automatically.
312+
- There are no external preset files — everything travels with the workflow.
313+
- Before exporting, the system checks for unsaved preset changes and prompts you to save or discard.
314+
- Presets manage input values for your development workflow. The exported workflow uses whatever values are currently set in the widgets at export time.
315+
223316
### Integration Workflow Nodes
224317

225318
#### RTX Remix Download Model

VERSION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.2.1
1+
3.0.0

api/configs/dynamic_ui.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
NodeUI,
2828
UrlHandler,
2929
UrlParser,
30-
WidgetVisibility,
30+
WidgetResetRule,
31+
WidgetVisibilityRule,
3132
)
3233
from ...nodes.constant import ModelSource
3334

@@ -61,12 +62,12 @@ class NodeUIConfig(str, Enum):
6162
),
6263
visibility_rules=[
6364
# Show model_source only after a URL is entered
64-
WidgetVisibility(
65+
WidgetVisibilityRule(
6566
source_field="url",
6667
show_when_filled=["model_source"],
6768
),
6869
# Show source-specific fields based on detected source
69-
WidgetVisibility(
70+
WidgetVisibilityRule(
7071
source_field="model_source",
7172
mapping={
7273
ModelSource.HUGGINGFACE: [
@@ -79,13 +80,20 @@ class NodeUIConfig(str, Enum):
7980
},
8081
),
8182
# Show archive fields when extract_archive is enabled
82-
WidgetVisibility(
83+
WidgetVisibilityRule(
8384
source_field="extract_archive",
8485
mapping={
8586
True: ["archive_model_filename", "extracted_model_hash"],
8687
},
8788
),
8889
],
90+
reset_rules=[
91+
# Reset hash and archive fields when URL changes
92+
WidgetResetRule(
93+
source_field="url",
94+
reset_fields=["file_hash", "archive_model_filename", "extracted_model_hash"],
95+
),
96+
],
8997
info_button=DynamicHelp(
9098
source_field="model_source",
9199
configs={

api/models/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
NodeUI,
2222
UrlHandler,
2323
UrlParser,
24-
WidgetVisibility,
24+
WidgetResetRule,
25+
WidgetVisibilityRule,
2526
)
2627

2728
__all__ = [
@@ -30,5 +31,6 @@
3031
"NodeUI",
3132
"UrlHandler",
3233
"UrlParser",
33-
"WidgetVisibility",
34+
"WidgetResetRule",
35+
"WidgetVisibilityRule",
3436
]

api/models/ui.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
__all__ = [
1919
"UrlParser",
2020
"UrlHandler",
21-
"WidgetVisibility",
21+
"WidgetVisibilityRule",
22+
"WidgetResetRule",
2223
"HelpContent",
2324
"DynamicHelp",
2425
"NodeUI",
@@ -51,7 +52,7 @@ class UrlHandler(BaseModel):
5152
"""URL parsers keyed by source value"""
5253

5354

54-
class WidgetVisibility(BaseModel):
55+
class WidgetVisibilityRule(BaseModel):
5556
"""Shows/hides widgets based on another widget's value."""
5657

5758
source_field: str
@@ -64,6 +65,16 @@ class WidgetVisibility(BaseModel):
6465
"""Widget names to show when source_field has any non-empty value"""
6566

6667

68+
class WidgetResetRule(BaseModel):
69+
"""Resets widget values when another widget's value changes."""
70+
71+
source_field: str
72+
"""Widget whose change triggers the reset (e.g., "url")"""
73+
74+
reset_fields: list[str] = []
75+
"""Widget names to clear when source_field changes"""
76+
77+
6778
class HelpContent(BaseModel):
6879
"""Content for a help dialog."""
6980

@@ -93,8 +104,11 @@ class NodeUI(BaseModel):
93104
url_handler: UrlHandler | None = None
94105
"""Auto-detect URLs and populate fields"""
95106

96-
visibility_rules: list[WidgetVisibility] = []
107+
visibility_rules: list[WidgetVisibilityRule] = []
97108
"""Conditional widget visibility rules"""
98109

110+
reset_rules: list[WidgetResetRule] = []
111+
"""Reset widget values when source fields change"""
112+
99113
info_button: DynamicHelp | None = None
100114
"""Context-sensitive help button"""

nodes/download.py

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333

3434
import folder_paths
3535
from comfy_execution.utils import get_executing_context
36+
from comfy_execution.progress import get_progress_state
3637
from comfy_api.latest import io
38+
from comfy.model_management import processing_interrupted, InterruptProcessingException
3739
from server import PromptServer
3840

3941
from .constant import (
@@ -310,7 +312,7 @@ def execute(
310312

311313
# === PHASE 3: Download the file ===
312314
logger.info(f"Downloading {download_info.filename} from {model_source}")
313-
cls._download_file(download_info.url, target_path, timeout, headers=download_info.headers)
315+
cls._download_file(download_info.url, target_path, timeout, headers=download_info.headers, node_id=unique_id)
314316
logger.info(f"Downloaded to {target_path}")
315317

316318
# === PHASE 4: Verify hash (if provided) ===
@@ -647,15 +649,18 @@ def _compute_file_hash(filepath: Path, hash_func=hashlib.sha256, prefix: bytes |
647649
return file_hash.hexdigest()
648650

649651
@staticmethod
650-
def _download_file(url: str, filepath: Path, timeout: int, headers: dict | None = None) -> None:
652+
def _download_file(
653+
url: str, filepath: Path, timeout: int, headers: dict | None = None, node_id: str | None = None
654+
) -> None:
651655
"""
652-
Download a file from URL to filepath with progress logging.
656+
Download a file from URL to filepath with progress logging and UI updates.
653657
654658
Args:
655659
url: The URL to download from
656660
filepath: Path where the file will be saved
657661
timeout: Download timeout in seconds
658662
headers: Optional HTTP headers to include in the request
663+
node_id: Optional node ID for progress updates in the UI
659664
660665
Raises:
661666
ValueError: If URL returns HTML content instead of a file
@@ -681,28 +686,57 @@ def _download_file(url: str, filepath: Path, timeout: int, headers: dict | None
681686
downloaded_size = 0
682687
last_logged_percent = -1
683688

689+
# Send initial progress
690+
if node_id and total_size > 0:
691+
get_progress_state().update_progress(node_id=node_id, value=0, max_value=total_size)
692+
684693
# Download and validate content
685694
first_chunk = None
686-
with filepath.open("wb") as f:
687-
for chunk in response.iter_content(chunk_size=CHUNK_SIZE_BYTES):
688-
if first_chunk is None:
689-
first_chunk = chunk
690-
f.write(chunk)
691-
downloaded_size += len(chunk)
692-
693-
# Log progress every 10%
694-
if total_size > 0:
695-
percent = int((downloaded_size / total_size) * 100)
696-
if percent >= last_logged_percent + 10:
697-
last_logged_percent = (percent // 10) * 10
698-
downloaded_mb = downloaded_size / (1024 * 1024)
699-
total_mb = total_size / (1024 * 1024)
700-
logger.info(f"Downloading: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({last_logged_percent}%)")
701-
702-
# Log completion
695+
try:
696+
with filepath.open("wb") as f:
697+
for chunk in response.iter_content(chunk_size=CHUNK_SIZE_BYTES):
698+
# Check if user cancelled the prompt
699+
if processing_interrupted():
700+
raise InterruptProcessingException()
701+
702+
if first_chunk is None:
703+
first_chunk = chunk
704+
f.write(chunk)
705+
downloaded_size += len(chunk)
706+
707+
# Update progress every 10%
708+
if total_size > 0:
709+
percent = int((downloaded_size / total_size) * 100)
710+
if percent >= last_logged_percent + 10:
711+
last_logged_percent = (percent // 10) * 10
712+
downloaded_mb = downloaded_size / (1024 * 1024)
713+
total_mb = total_size / (1024 * 1024)
714+
logger.info(f"Downloading: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({last_logged_percent}%)")
715+
716+
# Update UI progress bar
717+
if node_id:
718+
get_progress_state().update_progress(
719+
node_id=node_id,
720+
value=downloaded_size,
721+
max_value=total_size,
722+
)
723+
except InterruptProcessingException:
724+
# Clean up partial file on cancellation
725+
logger.info("Download cancelled by user")
726+
try:
727+
if filepath.exists():
728+
filepath.unlink()
729+
logger.info(f"Deleted partial download: {filepath.name}")
730+
except Exception as e:
731+
logger.warning(f"Failed to delete partial download {filepath.name}: {e}")
732+
raise
733+
734+
# Log completion and update UI
703735
if total_size > 0:
704736
total_mb = total_size / (1024 * 1024)
705737
logger.info(f"Download complete: {total_mb:.1f} MB (100%)")
738+
if node_id:
739+
get_progress_state().update_progress(node_id=node_id, value=total_size, max_value=total_size)
706740
else:
707741
downloaded_mb = downloaded_size / (1024 * 1024)
708742
logger.info(f"Download complete: {downloaded_mb:.1f} MB")

0 commit comments

Comments
 (0)