ceroomedia commited on
Commit
42ec332
·
verified ·
1 Parent(s): ab7ca41

Release 0.2.0: Slate redesign and Manual sampling defaults

Browse files
CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
  # Changelog
2
 
 
 
 
 
 
 
 
 
 
 
3
  ## 0.1.3 — 2026-09-29
4
 
5
  - Give the new Prompter the unique internal ID `DMAINodesPrompter`, keeping its visible name **DMAI Prompter**. Older DMAI Suite and standalone Prompter installations can keep their original node and frontend hooks.
 
1
  # Changelog
2
 
3
+ ## 0.2.0 - 2026-09-29
4
+
5
+ - Refresh the Slate interface with larger Inter text, roomier controls, a larger prompt field and clearer section spacing.
6
+ - Use two-row LoRA cards with full filenames, visible order, enable/edit actions and independently editable Model and CLIP strengths. Keep drag, arrow and keyboard reordering.
7
+ - Add readable captions outside the native Canvas socket dots and enforce minimum node dimensions so controls and ports remain usable.
8
+ - Start new Engines and all starter workflows in Manual, with enhancement disabled. Keep saved workflow settings and existing model adapters.
9
+ - Show JSON upload only in DMAI Enhanced. New preset choices come from imported settings; historical built-in presets remain resolvable for saved workflows. Remove preset export from the interface.
10
+ - Preserve manual drafts when switching modes. An Enhanced Engine without a selected preset asks for a DMAI JSON upload or a switch back to Manual.
11
+ - Keep the conflict-free `DMAINodesPrompter` ID and legacy workflow migration introduced in 0.1.3.
12
+
13
  ## 0.1.3 — 2026-09-29
14
 
15
  - Give the new Prompter the unique internal ID `DMAINodesPrompter`, keeping its visible name **DMAI Prompter**. Older DMAI Suite and standalone Prompter installations can keep their original node and frontend hooks.
DMAI-NODES-v0.2.0.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:81eca7e53dc58d56c6dac8575be431924fed78b90779155bfc7ca309fcf189a2
3
+ size 157471
DMAI-NODES-v0.2.0.zip.sha256 ADDED
@@ -0,0 +1 @@
 
 
1
+ 81eca7e53dc58d56c6dac8575be431924fed78b90779155bfc7ca309fcf189a2 DMAI-NODES-v0.2.0.zip
README.md CHANGED
@@ -12,9 +12,9 @@ tags:
12
 
13
  # DMAI NODES
14
 
15
- **A compact image workflow for ComfyUI.** Slate surfaces, clean Inter typography and English controls.
16
 
17
- **0.1.3 preview** · [GitHub releases](https://github.com/ceroomedia/DMAI-NODES/releases) · [Hugging Face download mirror](https://huggingface.co/ceroomedia/DMAI-NODES)
18
 
19
  Prompter → Generation Engine → Gallery, with an ordered LoRA Loader feeding the engine.
20
 
@@ -23,8 +23,8 @@ Prompter → Generation Engine → Gallery, with an ordered LoRA Loader feeding
23
  | Node | Purpose |
24
  | --- | --- |
25
  | **DMAI Prompter** | Prompt, 19 aspect ratios, 1K / 2K / Custom dimensions, image count, Generate button and generation progress outline. |
26
- | **DMAI LoRA Loader** | Reorderable stack, enabled state, separate model/CLIP strengths and strict file checks. |
27
- | **DMAI Generation Engine** | Installed checkpoint and diffusion model picker, DMAI Enhanced or Manual sampling, imported settings presets, seed sequence and execution report. |
28
  | **DMAI Gallery** | Persistent PNG originals, multiple selection, per-image download and selection ZIP. |
29
  | **DMAI Krea Enhancer** | A standalone Krea-only model node, using the optional upstream Krea2T Enhancer. |
30
  | **DMAI VAE Encode / Decode** | Standard Comfy VAE operations, usable separately and with the engine's advanced inputs. |
@@ -33,6 +33,8 @@ Sampler and scheduler choices come from the running ComfyUI installation. Option
33
 
34
  The simple path loads models inside the engine. Advanced workflows can supply MODEL, CLIP and VAE together, and an encoded LATENT. Krea enhancement is only available for Krea profiles. A settings preset never installs a new model adapter.
35
 
 
 
36
  ## Install
37
 
38
  From your ComfyUI directory, using **the same Python environment as ComfyUI**:
@@ -44,11 +46,12 @@ python -m pip install -r custom_nodes/DMAI-NODES/requirements.txt
44
 
45
  Restart ComfyUI and refresh the browser. Search for **DMAI** in the node library.
46
 
47
- **Using an older DMAI package too?** Version 0.1.3 gives this package's Prompter its own internal ID, `DMAINodesPrompter`. Choose **DMAI Prompter** from the **DMAI-NODES** package in category **DMAI NODES**. The older Suite and standalone Prompter keep their existing IDs. See [updating from 0.1.2](docs/INSTALLATION.md#updating-from-012-or-earlier) for saved workflows and API clients.
48
 
49
  - [Windows Portable, local environments and Runpod](docs/INSTALLATION.md)
50
  - [Model files, presets and enhancement](docs/MODELS.md)
51
  - [API workflow and future website integration](docs/API.md)
 
52
  - [Validation results and limits](docs/VALIDATION.md)
53
 
54
  ZIP installation is also supported: extract the release so `ComfyUI/custom_nodes/DMAI-NODES/__init__.py` exists. A Hugging Face mirror includes the package and checksum; this repository contains **code and documentation, no model weights**.
@@ -56,13 +59,14 @@ ZIP installation is also supported: extract the release so `ComfyUI/custom_nodes
56
  ## First workflow
57
 
58
  1. Import a starter from `workflows`: Krea, SDXL, Qwen or FLUX. Use the `.json` file for the UI and `.api.json` for API calls.
59
- 2. Select an installed file at the top of Engine under **Checkpoints** or **Diffusion models**. Open **Model files** to set its **Architecture** and any separate text encoders and VAE. Checkpoint loading currently supports SDXL; diffusion loading supports Krea 2 Turbo, Qwen Image and FLUX.1 Dev. Install any optional enhancement dependency needed by the selected preset.
60
- 3. Enter the prompt and image count. Add LoRAs if needed.
61
- 4. Choose **Generate** in the Prompter. Its softly glowing outline follows generation across all requested images and reaches 100% when the workflow finishes. Preview or download saved results from Gallery.
 
62
 
63
- For Krea without the optional enhancer, choose **Krea / Clean Base** or Manual with Enhancer set to `none`. Models and LoRAs remain subject to their own licenses.
64
 
65
- To wire nodes yourself, drag the native output port to a matching input port: Prompter **request** → Engine **request**, LoRA Loader **loras** → Engine **loras**, and Engine **images** → Gallery **images**. Engine **report** → Gallery **report** adds generation metadata. Use ComfyUI's native node controls to reveal optional advanced inputs; see [advanced connections](docs/MODELS.md#advanced-connections).
66
 
67
  ## Behavior that matters
68
 
@@ -72,11 +76,12 @@ To wire nodes yourself, drag the native output port to a matching input port: Pr
72
  - Gallery IDs are serialized in workflows. The new package uses separate storage under `output/DMAI-NODES`; existing DMAI Studio history is not deleted or automatically imported.
73
  - ZIP exports are limited to **100 images / 2 GiB**. Full-history selection uses a snapshot so later images are not silently added.
74
  - Invalid saved JSON remains visible for correction. It is not replaced by defaults.
 
75
  - This is a new package with new node IDs. Existing Studio, Carousel and Krea Edit workflows remain in their original package.
76
 
77
  ## Compatibility
78
 
79
- Built against ComfyUI **v0.37.0**, with the frontend supplied by that tag. Renderer and inference coverage are recorded in [VALIDATION.md](docs/VALIDATION.md). Native connection ports remain available; custom port positioning may differ between Canvas and Nodes 2.0.
80
 
81
  No cloud service or paid generation is enabled by installation. The HTTP routes use ComfyUI's existing access boundary; put authentication and TLS in front of a remote deployment.
82
 
 
12
 
13
  # DMAI NODES
14
 
15
+ **A clear image workflow for ComfyUI.** Slate surfaces, readable Inter typography and English controls.
16
 
17
+ **0.2.0 preview** · [GitHub releases](https://github.com/ceroomedia/DMAI-NODES/releases) · [Hugging Face download mirror](https://huggingface.co/ceroomedia/DMAI-NODES)
18
 
19
  Prompter → Generation Engine → Gallery, with an ordered LoRA Loader feeding the engine.
20
 
 
23
  | Node | Purpose |
24
  | --- | --- |
25
  | **DMAI Prompter** | Prompt, 19 aspect ratios, 1K / 2K / Custom dimensions, image count, Generate button and generation progress outline. |
26
+ | **DMAI LoRA Loader** | Roomy two-row cards with full filenames, enable controls, independent Model / CLIP strengths, reordering and strict file checks. |
27
+ | **DMAI Generation Engine** | Installed checkpoint and diffusion model picker, Manual sampling by default, uploaded DMAI Enhanced settings, seed sequence and execution report. |
28
  | **DMAI Gallery** | Persistent PNG originals, multiple selection, per-image download and selection ZIP. |
29
  | **DMAI Krea Enhancer** | A standalone Krea-only model node, using the optional upstream Krea2T Enhancer. |
30
  | **DMAI VAE Encode / Decode** | Standard Comfy VAE operations, usable separately and with the engine's advanced inputs. |
 
33
 
34
  The simple path loads models inside the engine. Advanced workflows can supply MODEL, CLIP and VAE together, and an encoded LATENT. Krea enhancement is only available for Krea profiles. A settings preset never installs a new model adapter.
35
 
36
+ The larger Prompter puts the prompt, image format, count and Generate control together. Engine exposes its sampling settings in Manual; switch to **DMAI Enhanced** to upload a settings JSON. Readable captions sit outside the native Canvas connection dots, and minimum node sizes keep controls and ports usable when resizing.
37
+
38
  ## Install
39
 
40
  From your ComfyUI directory, using **the same Python environment as ComfyUI**:
 
46
 
47
  Restart ComfyUI and refresh the browser. Search for **DMAI** in the node library.
48
 
49
+ **Using an older DMAI package too?** Since version 0.1.3, this package's Prompter has its own internal ID, `DMAINodesPrompter`. Choose **DMAI Prompter** from the **DMAI-NODES** package in category **DMAI NODES**. The older Suite and standalone Prompter keep their existing IDs. See [updating from 0.1.2](docs/INSTALLATION.md#updating-from-012-or-earlier) for saved workflows and API clients.
50
 
51
  - [Windows Portable, local environments and Runpod](docs/INSTALLATION.md)
52
  - [Model files, presets and enhancement](docs/MODELS.md)
53
  - [API workflow and future website integration](docs/API.md)
54
+ - [Interface design](docs/DESIGN.md)
55
  - [Validation results and limits](docs/VALIDATION.md)
56
 
57
  ZIP installation is also supported: extract the release so `ComfyUI/custom_nodes/DMAI-NODES/__init__.py` exists. A Hugging Face mirror includes the package and checksum; this repository contains **code and documentation, no model weights**.
 
59
  ## First workflow
60
 
61
  1. Import a starter from `workflows`: Krea, SDXL, Qwen or FLUX. Use the `.json` file for the UI and `.api.json` for API calls.
62
+ 2. Select an installed file at the top of Engine under **Checkpoints** or **Diffusion models**. Open **Model files** to set its **Architecture** and any separate text encoders and VAE. Checkpoint loading currently supports SDXL; diffusion loading supports Krea 2 Turbo, Qwen Image and FLUX.1 Dev.
63
+ 3. Keep **Manual** to adjust Steps, CFG, Sampler, Scheduler and Denoise. For a prepared settings file, switch to **DMAI Enhanced** and choose **Upload JSON**. Enhanced requires a matching preset before generation; returning to Manual restores your manual settings.
64
+ 4. Enter the prompt and image count. Add LoRAs if needed, setting Model and CLIP strengths independently.
65
+ 5. Choose **Generate** in the Prompter. Its softly glowing outline follows generation across all requested images and reaches 100% when the workflow finishes. Preview or download saved results from Gallery.
66
 
67
+ New nodes and starter workflows use Manual with enhancement `none`. Enable the optional Krea enhancer only after installing its dependency. Existing workflows retain their saved sampling settings and selected legacy preset. Models and LoRAs remain subject to their own licenses.
68
 
69
+ To wire nodes yourself, drag the native output dot to a matching input dot: Prompter **Prompt** → Engine **Prompt** (`request`), LoRA Loader **LoRAs** → Engine **LoRAs**, and Engine **Images** → Gallery **Images**. Engine **Report** → Gallery **Report** adds generation metadata. The captions identify the existing ports; dragging still uses ComfyUI's dots. Use ComfyUI's native node controls to reveal optional advanced inputs; see [advanced connections](docs/MODELS.md#advanced-connections).
70
 
71
  ## Behavior that matters
72
 
 
76
  - Gallery IDs are serialized in workflows. The new package uses separate storage under `output/DMAI-NODES`; existing DMAI Studio history is not deleted or automatically imported.
77
  - ZIP exports are limited to **100 images / 2 GiB**. Full-history selection uses a snapshot so later images are not silently added.
78
  - Invalid saved JSON remains visible for correction. It is not replaced by defaults.
79
+ - Preset upload is available in DMAI Enhanced. The preset export button has been removed; keep the original JSON file for reuse. Historical built-in presets remain available to saved workflows, with imported settings shown for new selections.
80
  - This is a new package with new node IDs. Existing Studio, Carousel and Krea Edit workflows remain in their original package.
81
 
82
  ## Compatibility
83
 
84
+ Built against ComfyUI **v0.37.0**, with the frontend supplied by that tag. Renderer and inference coverage are recorded in [VALIDATION.md](docs/VALIDATION.md). Native connection ports remain available. External socket captions target Canvas; custom positioning and captions may differ in Nodes 2.0.
85
 
86
  No cloud service or paid generation is enabled by installation. The HTTP routes use ComfyUI's existing access boundary; put authentication and TLS in front of a remote deployment.
87
 
dmai_nodes/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
  """DMAI NODES domain modules; importing these does not initialize a GPU."""
2
- __version__ = "0.1.3"
 
1
  """DMAI NODES domain modules; importing these does not initialize a GPU."""
2
+ __version__ = "0.2.0"
dmai_nodes/engine.py CHANGED
@@ -94,6 +94,8 @@ def parse_engine_config(value):
94
  if any(entry["id"] in builtin_ids for entry in presets):
95
  raise ValueError("Imported presets cannot replace built-in preset IDs. Use a new ID.")
96
  if value["mode"] == "enhanced":
 
 
97
  preset, _ = resolve_preset(value["preset_id"], profile["id"], presets)
98
  settings = preset["settings"]
99
  return {"schema_version": 1, "profile_id": profile["id"], "mode": value["mode"],
 
94
  if any(entry["id"] in builtin_ids for entry in presets):
95
  raise ValueError("Imported presets cannot replace built-in preset IDs. Use a new ID.")
96
  if value["mode"] == "enhanced":
97
+ if not value["preset_id"]:
98
+ raise ValueError("Upload a DMAI JSON preset in DMAI Enhanced, or switch the Engine to Manual.")
99
  preset, _ = resolve_preset(value["preset_id"], profile["id"], presets)
100
  settings = preset["settings"]
101
  return {"schema_version": 1, "profile_id": profile["id"], "mode": value["mode"],
dmai_nodes/nodes.py CHANGED
@@ -10,8 +10,8 @@ LoraPort = io.Custom("DMAI_LORA_STACK")
10
  ReportPort = io.Custom("DMAI_REPORT")
11
  DEFAULT_PROMPT = {"schema_version":1,"prompt":"","negative_prompt":"","width":832,"height":1024,"count":4}
12
  DEFAULT_LORAS = {"schema_version":1,"entries":[]}
13
- DEFAULT_ENGINE = {"schema_version":1,"profile_id":"krea2-turbo","mode":"enhanced","preset_id":"dmai-krea2-original-v1",
14
- "settings":{"steps":8,"cfg":1.1,"sampler":"euler","scheduler":"beta","denoise":1,"enhancer":"krea2t"},
15
  "seed":481516,"models":{"diffusion_model":"krea2_turbo_fp8.safetensors","text_encoder":"qwen3vl_4b_fp8_scaled.safetensors","vae":"wan_2.1_vae.safetensors","checkpoint":""},"presets":[]}
16
 
17
 
 
10
  ReportPort = io.Custom("DMAI_REPORT")
11
  DEFAULT_PROMPT = {"schema_version":1,"prompt":"","negative_prompt":"","width":832,"height":1024,"count":4}
12
  DEFAULT_LORAS = {"schema_version":1,"entries":[]}
13
+ DEFAULT_ENGINE = {"schema_version":1,"profile_id":"krea2-turbo","mode":"manual","preset_id":"",
14
+ "settings":{"steps":8,"cfg":1.1,"sampler":"euler","scheduler":"beta","denoise":1,"enhancer":"none"},
15
  "seed":481516,"models":{"diffusion_model":"krea2_turbo_fp8.safetensors","text_encoder":"qwen3vl_4b_fp8_scaled.safetensors","vae":"wan_2.1_vae.safetensors","checkpoint":""},"presets":[]}
16
 
17
 
dmai_nodes/presets.py CHANGED
@@ -150,4 +150,4 @@ def resolve_preset(preset_id, profile_id, imports):
150
  if entry["model"]["id"] != profile_id:
151
  raise ValueError("Preset belongs to a different model profile.")
152
  return deepcopy(entry), source
153
- raise ValueError("Selected preset was not found. Import it or choose a built-in preset.")
 
150
  if entry["model"]["id"] != profile_id:
151
  raise ValueError("Preset belongs to a different model profile.")
152
  return deepcopy(entry), source
153
+ raise ValueError("Selected preset was not found. Upload its JSON in DMAI Enhanced, or switch to Manual.")
docs/API.md CHANGED
@@ -13,7 +13,7 @@ python custom_nodes/DMAI-NODES/tools/run_workflow.py \
13
  --count 2 --seed 481516 --gallery website-demo --wait
14
  ```
15
 
16
- Run the command from your ComfyUI directory using its Python environment. The default example uses Krea Original and its optional enhancer. Use `--workflow` for a different included API example. The helper expects starter node IDs `1` (Prompter), `3` (Engine) and `4` (Gallery); adapt these lookups before using a workflow with different IDs. For the SDXL example, `--checkpoint` can set the installed checkpoint filename. Edit model selections in other API examples to match your catalog. This command queues real generation on your own server.
17
 
18
  1. `POST /prompt` with `{"prompt": <API workflow>, "client_id": "<unique ID>"}`.
19
  2. Keep the returned `prompt_id`.
@@ -24,9 +24,17 @@ In a completed starter job, Gallery metadata is at `history[prompt_id]["outputs"
24
 
25
  Use seed strings for the full unsigned 64-bit range; JavaScript numbers cannot represent all of it exactly. Each image's exact seed is recorded in the Engine report. The CLI timeout only stops waiting; it does not cancel the job.
26
 
 
 
 
 
 
 
 
 
27
  ## Prompter ID migration
28
 
29
- Version 0.1.3 uses `DMAINodesPrompter` as the Prompter's API `class_type`. The request configuration, `DMAI_PROMPT` output and Engine connections keep the same contracts. Older DMAI packages still own `DMAIPrompter`; the server intentionally has no alias that could overwrite them.
30
 
31
  Use the updated starter API files for new integrations. The included `run_workflow.py` converts identifiable 0.1.0-0.1.2 DMAI NODES Prompters in memory before submitting and leaves the source file intact. Direct `/prompt` callers should migrate their template first:
32
 
@@ -80,7 +88,7 @@ Routes are relative to ComfyUI's base URL. JSON responses use `{"ok": true, "dat
80
  | Method | Route | Purpose |
81
  | --- | --- | --- |
82
  | GET | `/dmai-nodes/v1/health` | Package version and capability flags |
83
- | GET | `/dmai-nodes/v1/bootstrap` | Model profiles, installed files, LoRAs, presets, runtime samplers and schedulers |
84
  | POST | `/dmai-nodes/v1/presets/validate` | Validate a single JSON preset or pack |
85
  | GET | `/dmai-nodes/v1/galleries/{gallery_id}?offset=0&limit=60` | Saved images, total and snapshot watermark |
86
  | GET | `/dmai-nodes/v1/images/{image_id}` | Original PNG |
 
13
  --count 2 --seed 481516 --gallery website-demo --wait
14
  ```
15
 
16
+ Run the command from your ComfyUI directory using its Python environment. The default example uses Krea 2 Turbo in Manual with enhancement `none`; it does not require the optional Krea enhancer. Use `--workflow` for a different included API example. The helper expects starter node IDs `1` (Prompter), `3` (Engine) and `4` (Gallery); adapt these lookups before using a workflow with different IDs. For the SDXL example, `--checkpoint` can set the installed checkpoint filename. Edit model selections in other API examples to match your catalog. This command queues real generation on your own server.
17
 
18
  1. `POST /prompt` with `{"prompt": <API workflow>, "client_id": "<unique ID>"}`.
19
  2. Keep the returned `prompt_id`.
 
24
 
25
  Use seed strings for the full unsigned 64-bit range; JavaScript numbers cannot represent all of it exactly. Each image's exact seed is recorded in the Engine report. The CLI timeout only stops waiting; it does not cancel the job.
26
 
27
+ ## Engine sampling modes
28
+
29
+ Version 0.2.0 starts new Engines and all starter templates in `"mode": "manual"`, with `"preset_id": ""` and `"enhancer": "none"` inside `settings`. Manual executes the validated `settings` values. Existing saved configs retain their mode and settings.
30
+
31
+ For DMAI Enhanced, use `"mode": "enhanced"`, a selected `preset_id`, and the matching validated preset in the Engine's `presets` array. Presets are data, not uploaded executable code. The frontend exposes **Upload JSON** only in Enhanced and has no preset export button; headless clients supply the same configuration directly. See [preset format and limits](MODELS.md#manual-or-dmai-enhanced).
32
+
33
+ An Enhanced config with an empty `preset_id` fails with an actionable error: upload a DMAI JSON preset or switch to Manual. The server continues resolving historical built-in preset IDs for saved workflows and API templates. The `bootstrap` response retains that catalog for compatibility; its presence does not mean the frontend offers those presets as new choices. Current node IDs, request types, API routes and JSON schema versions are unchanged from 0.1.3.
34
+
35
  ## Prompter ID migration
36
 
37
+ Since version 0.1.3, the Prompter's API `class_type` is `DMAINodesPrompter`. The request configuration, `DMAI_PROMPT` output and Engine connections keep the same contracts. Older DMAI packages still own `DMAIPrompter`; the server intentionally has no alias that could overwrite them.
38
 
39
  Use the updated starter API files for new integrations. The included `run_workflow.py` converts identifiable 0.1.0-0.1.2 DMAI NODES Prompters in memory before submitting and leaves the source file intact. Direct `/prompt` callers should migrate their template first:
40
 
 
88
  | Method | Route | Purpose |
89
  | --- | --- | --- |
90
  | GET | `/dmai-nodes/v1/health` | Package version and capability flags |
91
+ | GET | `/dmai-nodes/v1/bootstrap` | Model profiles, installed files, LoRAs, historical preset catalog, runtime samplers and schedulers |
92
  | POST | `/dmai-nodes/v1/presets/validate` | Validate a single JSON preset or pack |
93
  | GET | `/dmai-nodes/v1/galleries/{gallery_id}?offset=0&limit=60` | Saved images, total and snapshot watermark |
94
  | GET | `/dmai-nodes/v1/images/{image_id}` | Original PNG |
docs/DESIGN.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Interface design — 0.2.0
2
+
3
+ The interface uses Slate charcoal, a lime action color and locally bundled Inter. All controls are in English. It follows the hierarchy and alignment principles in Apple's [Layout](https://developer.apple.com/design/human-interface-guidelines/layout) and [Typography](https://developer.apple.com/design/human-interface-guidelines/typography) guidance, with its own colors and components.
4
+
5
+ ## Working sizes
6
+
7
+ - Body: 15 px. Prompt text: 16 px. Node headings: 21 px bold. Secondary labels: 13 px.
8
+ - Form controls: at least 44 px high. Generate: 52 px high.
9
+ - Panel padding: 22 px. Related controls use consistent 8–16 px gaps.
10
+ - Lime identifies the primary action, selected formats and active connections. Text and shape also communicate state.
11
+ - Reduced-motion preferences disable decorative animation; numeric progress remains available.
12
+
13
+ ComfyUI scales every node with the canvas. Around 70–100% zoom is a useful working range; fitting a large graph can make every extension's controls smaller. Minimum dimensions prevent the saved narrow layouts from squeezing the new controls. Larger user-chosen dimensions remain available.
14
+
15
+ ## Structure
16
+
17
+ **Prompter:** description first, then visual aspect ratios, resolution, image count and Generate. Negative prompt is a disclosure. Real execution progress follows the perimeter.
18
+
19
+ **LoRA Loader:** the first row identifies a file, its position and enabled state. The second row holds independent Model and CLIP strengths plus ordering arrows. Names wrap rather than collapsing into a tiny column. The list scrolls when needed.
20
+
21
+ **Generation Engine:** the installed-file selector stays first. Supporting model files are under a disclosure. Manual exposes sampling controls directly. DMAI Enhanced is the only place to upload JSON settings. The old preset catalog is retained internally for saved workflows.
22
+
23
+ **Gallery:** images take the available space. Selection and ZIP stay in a visible footer while the grid scrolls. Each image retains its preview and download buttons.
24
+
25
+ **Connections:** Canvas captions sit just above the wire next to the real native socket. There is no second connector toolbar. Leave roughly 200 px between node edges when both sides have captions. Serialized input names, types and link behavior remain unchanged. Nodes 2.0 uses its own renderer and is not the visual reference for this release.
docs/INSTALLATION.md CHANGED
@@ -30,7 +30,7 @@ For ComfyUI Desktop, use the installation directory shown in Desktop's settings
30
  ## Runpod
31
 
32
  1. Start your chosen ComfyUI Pod using your normal template and persistent volume. GPU availability, model licenses and Pod cost are separate from this package.
33
- 2. Open the Pod terminal and find the actual ComfyUI folder. `/workspace/ComfyUI` is common; templates differ.
34
  3. Activate the same environment as the running ComfyUI server. Check its launcher if unsure.
35
  4. Run the commands below, adjusting the path when needed.
36
 
@@ -48,7 +48,7 @@ The instructions do not start a Pod or buy compute. No Runpod template installat
48
 
49
  ## Optional Krea enhancement
50
 
51
- The **DMAI Krea Enhancer** and **Krea / Original** preset require the upstream extension. From the ComfyUI directory:
52
 
53
  ```bash
54
  git clone https://github.com/capitan01R/ComfyUI-Krea2T-Enhancer.git custom_nodes/ComfyUI-Krea2T-Enhancer
@@ -61,31 +61,43 @@ RES4LYF is optional and installed separately. Follow its upstream instructions a
61
 
62
  ## Update and remove
63
 
64
- Update only when no job is running. From the ComfyUI directory with its Python environment active:
65
 
66
  ```bash
67
- git -C custom_nodes/DMAI-NODES pull --ff-only
 
68
  python -m pip install -r custom_nodes/DMAI-NODES/requirements.txt
 
69
  ```
70
 
71
  For Windows Portable, run from the portable folder instead:
72
 
73
  ```powershell
74
- git -C .\ComfyUI\custom_nodes\DMAI-NODES pull --ff-only
 
75
  .\python_embeded\python.exe -m pip install -r .\ComfyUI\custom_nodes\DMAI-NODES\requirements.txt
 
76
  ```
77
 
78
- Restart ComfyUI afterward. For ZIP installations, replace package files from the new release while preserving your workflows and output. To uninstall, remove only the custom-node folder after stopping ComfyUI. Saved originals and the gallery index are in `output/DMAI-NODES` and are not removed by package updates.
 
 
 
 
 
 
 
 
79
 
80
  ### Updating from 0.1.2 or earlier
81
 
82
- The new Prompter now registers as `DMAINodesPrompter`. Its visible name remains **DMAI Prompter**, under **DMAI NODES**, with the **DMAI-NODES** package badge. Older DMAI Suite and standalone Prompter installations use `DMAIPrompter`; they can remain enabled alongside this package.
83
 
84
  To install exactly this release from an existing Git checkout, including a checkout pinned to an older tag, run inside `custom_nodes/DMAI-NODES`:
85
 
86
  ```bash
87
- git fetch origin tag v0.1.3
88
- git checkout v0.1.3
89
  python -m pip install -r requirements.txt
90
  git describe --tags --exact-match
91
  ```
@@ -99,11 +111,12 @@ For API JSON sent directly to `/prompt`, use the new `DMAINodesPrompter` class t
99
  ## Troubleshooting
100
 
101
  - **No DMAI nodes:** inspect the ComfyUI console for import errors; check the Python environment and one-level ZIP extraction.
102
- - **Prompter missing when older DMAI nodes are installed:** update to 0.1.3 or newer, restart ComfyUI and hard-refresh. Search `DMAINodesPrompter` or select **DMAI Prompter** with the **DMAI-NODES** badge. The new node has a `request` output of type `DMAI_PROMPT`.
103
  - **Missing model:** select the exact catalog entry, including subfolder. Refresh the model list after adding files.
104
- - **Enhancer unavailable:** install the pinned dependency or choose Clean Base/Manual with `none`.
 
105
  - **LoRA has no matching patches:** verify the LoRA family and selected base model; this is deliberately an error.
106
  - **Gallery missing original:** restore the file from your backup. An export will not silently omit a selected missing file.
107
- - **Frontend layout issue:** refresh first, then test Comfy's Canvas renderer. Keep the exact Core and Frontend versions in your issue report.
108
 
109
  Official references: [Portable](https://docs.comfy.org/installation/comfyui_portable_windows), [manual installation](https://docs.comfy.org/installation/manual_install), [Runpod ComfyUI guide](https://docs.runpod.io/tutorials/pods/comfyui).
 
30
  ## Runpod
31
 
32
  1. Start your chosen ComfyUI Pod using your normal template and persistent volume. GPU availability, model licenses and Pod cost are separate from this package.
33
+ 2. Open the Pod terminal and find the actual ComfyUI folder. Common examples are `/workspace/ComfyUI` and `/workspace/runpod-slim/ComfyUI`; templates differ.
34
  3. Activate the same environment as the running ComfyUI server. Check its launcher if unsure.
35
  4. Run the commands below, adjusting the path when needed.
36
 
 
48
 
49
  ## Optional Krea enhancement
50
 
51
+ The **DMAI Krea Enhancer**, Manual sampling with Enhancer set to `krea2t`, and any preset enabling that enhancer require the upstream extension. The historical **Krea / Original** recipe also enables it. New nodes and starter workflows leave enhancement at `none`. From the ComfyUI directory:
52
 
53
  ```bash
54
  git clone https://github.com/capitan01R/ComfyUI-Krea2T-Enhancer.git custom_nodes/ComfyUI-Krea2T-Enhancer
 
61
 
62
  ## Update and remove
63
 
64
+ Update only when no job is running. To install **0.2.0 preview**, including from a checkout pinned to an older release, run from the ComfyUI directory with its Python environment active:
65
 
66
  ```bash
67
+ git -C custom_nodes/DMAI-NODES fetch origin tag v0.2.0
68
+ git -C custom_nodes/DMAI-NODES checkout v0.2.0
69
  python -m pip install -r custom_nodes/DMAI-NODES/requirements.txt
70
+ git -C custom_nodes/DMAI-NODES describe --tags --exact-match
71
  ```
72
 
73
  For Windows Portable, run from the portable folder instead:
74
 
75
  ```powershell
76
+ git -C .\ComfyUI\custom_nodes\DMAI-NODES fetch origin tag v0.2.0
77
+ git -C .\ComfyUI\custom_nodes\DMAI-NODES checkout v0.2.0
78
  .\python_embeded\python.exe -m pip install -r .\ComfyUI\custom_nodes\DMAI-NODES\requirements.txt
79
+ git -C .\ComfyUI\custom_nodes\DMAI-NODES describe --tags --exact-match
80
  ```
81
 
82
+ The final command should print `v0.2.0`. Git's detached HEAD message is expected when selecting a fixed release tag. If Git reports local changes, preserve those changes before retrying; the commands above do not discard them.
83
+
84
+ Restart ComfyUI afterward, then hard-refresh with Ctrl+F5. For ZIP installations, replace package files from the new release while preserving your workflows and output. To uninstall, remove only the custom-node folder after stopping ComfyUI. Saved originals and the gallery index are in `output/DMAI-NODES` and are not removed by package updates.
85
+
86
+ ### What changes in 0.2.0
87
+
88
+ The larger interface adds readable Canvas socket captions and minimum node sizes. Saved nodes may grow to the new minimum dimensions on load; their IDs and connections remain unchanged. Manual is the default for new Engines and starter workflows. Existing workflows retain their saved sampling settings and any selected historical preset.
89
+
90
+ For new prepared settings, switch to **DMAI Enhanced** and choose **Upload JSON**. Upload is available only in that mode. The old preset export control is removed. Keep your original preset files; existing imported presets remain saved in the workflow. See [sampling modes and legacy preset compatibility](MODELS.md#manual-or-dmai-enhanced).
91
 
92
  ### Updating from 0.1.2 or earlier
93
 
94
+ Since 0.1.3, the new Prompter registers as `DMAINodesPrompter`. Its visible name remains **DMAI Prompter**, under **DMAI NODES**, with the **DMAI-NODES** package badge. Older DMAI Suite and standalone Prompter installations use `DMAIPrompter`; they can remain enabled alongside this package. Version 0.2.0 keeps that separation.
95
 
96
  To install exactly this release from an existing Git checkout, including a checkout pinned to an older tag, run inside `custom_nodes/DMAI-NODES`:
97
 
98
  ```bash
99
+ git fetch origin tag v0.2.0
100
+ git checkout v0.2.0
101
  python -m pip install -r requirements.txt
102
  git describe --tags --exact-match
103
  ```
 
111
  ## Troubleshooting
112
 
113
  - **No DMAI nodes:** inspect the ComfyUI console for import errors; check the Python environment and one-level ZIP extraction.
114
+ - **Prompter missing when older DMAI nodes are installed:** update to 0.2.0 using the commands above, restart ComfyUI and hard-refresh. The coexistence fix was introduced in 0.1.3. Search `DMAINodesPrompter` or select **DMAI Prompter** with the **DMAI-NODES** badge. The new node has a `request` output of type `DMAI_PROMPT`, captioned **Prompt** in Canvas.
115
  - **Missing model:** select the exact catalog entry, including subfolder. Refresh the model list after adding files.
116
+ - **Enhancer unavailable:** install the pinned dependency or use Manual with Enhancer set to `none`.
117
+ - **Enhanced asks for a preset:** choose **Upload JSON** in DMAI Enhanced and upload matching settings, or switch to Manual.
118
  - **LoRA has no matching patches:** verify the LoRA family and selected base model; this is deliberately an error.
119
  - **Gallery missing original:** restore the file from your backup. An export will not silently omit a selected missing file.
120
+ - **Frontend layout issue:** hard-refresh first, then test Comfy's Canvas renderer. Socket captions are drawn outside the node; leave room between neighboring nodes. Keep the exact Core and Frontend versions in your issue report.
121
 
122
  Official references: [Portable](https://docs.comfy.org/installation/comfyui_portable_windows), [manual installation](https://docs.comfy.org/installation/manual_install), [Runpod ComfyUI guide](https://docs.runpod.io/tutorials/pods/comfyui).
docs/MODELS.md CHANGED
@@ -19,13 +19,17 @@ These are example filenames, not included downloads. Select the compatible files
19
 
20
  ## Manual or DMAI Enhanced
21
 
22
- **Manual** exposes Steps, CFG, Sampler, Scheduler and Denoise. Choices come from the active ComfyUI sampler registry, including names registered by extensions. A name appearing in the registry does not prove it works with every model.
23
 
24
- **DMAI Enhanced** selects a validated settings preset. The built-in Krea Original recipe uses 8 steps, CFG 1.1, Euler, Beta, denoise 1 and the Krea enhancer at strength 1 / text scale 1. Clean Base disables enhancement. SDXL, Qwen and FLUX base presets provide editable starting points, not quality guarantees.
 
 
 
 
25
 
26
  FLUX.1 Dev uses embedded guidance 3.5; CFG is a separate parameter. Advanced users can supply both conditioning inputs, using Comfy's FluxGuidance upstream to change embedded guidance. External conditioning bypasses Prompter text encoding; apply any CLIP LoRAs before that external encoding and set the Engine recipe's CLIP strengths to zero. The Engine rejects active nonzero CLIP strengths with supplied conditioning.
27
 
28
- The preset import button accepts a single preset or a pack of up to 20. Files are limited to 128 KiB. Unknown fields, model descriptors, non-finite values and unsupported sampling choices are rejected. Imported presets cannot replace built-in IDs. Use a new ID for your variation.
29
 
30
  ```json
31
  {
@@ -37,17 +41,17 @@ The preset import button accepts a single preset or a pack of up to 20. Files ar
37
  }
38
  ```
39
 
40
- Preset files contain data only. They cannot download models, execute Python or register adapters. See `resources/presets/dmai-presets-v1.json` for the built-in pack.
41
 
42
  ## Krea enhancer as a separate node
43
 
44
- Connect a Krea MODEL through **DMAI Krea Enhancer**, then to Engine's advanced MODEL input. Also connect matching CLIP and VAE. Select Krea Clean Base or Manual with enhancement `none` in Engine when the upstream node has already applied it. Double enhancement is rejected.
45
 
46
  The standalone node requires the separately installed, reviewed [Krea2T Enhancer](https://github.com/capitan01R/ComfyUI-Krea2T-Enhancer). It exposes Enabled, Strength and Text scale. It rejects non-Krea models when enabled. Qwen, FLUX and SDXL never receive this enhancement. See [installation](INSTALLATION.md#optional-krea-enhancement) for the pinned dependency.
47
 
48
  ## Advanced connections
49
 
50
- Drag ComfyUI's native output ports to matching input ports. The basic links are Prompter **request** → Engine **request**, LoRA Loader **loras** → Engine **loras**, Engine **images** → Gallery **images**, and optionally Engine **report** → Gallery **report**. These are workflow connections, separate from buttons inside the node panels.
51
 
52
  Reveal optional advanced inputs using ComfyUI's native node controls.
53
 
@@ -71,7 +75,9 @@ Engine already decodes its normal IMAGE output. The separate nodes expose the sa
71
 
72
  ## LoRA behavior
73
 
74
- The LoRA Loader produces an ordered recipe. Engine applies it before encoding both prompts. Use the row arrows to change the order. Model and CLIP strengths are separate; disabled rows and rows with both strengths at zero are skipped. Active nonzero rows fail on missing or ambiguous paths or zero effective matches. Partial matches appear in the execution report.
 
 
75
 
76
  File names preserve case and subfolders. Active file content hashes participate in cache invalidation. Registered patches are compared to Comfy's standard loader in CPU tests; visual compatibility with every LoRA is not guaranteed.
77
 
 
19
 
20
  ## Manual or DMAI Enhanced
21
 
22
+ **Manual** is the default for new Engines and all included starter workflows. It exposes Steps, CFG, Sampler, Scheduler and Denoise, with Enhancer available for Krea. Choices come from the active ComfyUI sampler registry, including names registered by extensions. A name appearing in the registry does not prove it works with every model. Changing a model file keeps the current mode and settings; a newly selected architecture starts in Manual unless that workflow already holds a saved draft for it.
23
 
24
+ **DMAI Enhanced** uses a validated JSON settings file. Switch to this mode, choose **Upload JSON**, and select a file for the intended architecture. If several matching presets have been imported, choose one under **DMAI settings**. Switching back to Manual restores your manual draft. The upload control appears only in Enhanced; there is no preset export button, so keep the source JSON for reuse.
25
+
26
+ Enhanced can be opened before a file has been uploaded. Generation requires a selected preset: an empty Enhanced configuration reports **Upload a DMAI JSON preset in DMAI Enhanced, or switch the Engine to Manual.** Upload matching settings or choose Manual before trying again.
27
+
28
+ Historical built-in presets are retained for existing workflows and API templates. A previously selected preset remains visible as saved workflow settings; the interface does not offer the full built-in catalog for new selections. The historical Krea Original recipe uses 8 steps, CFG 1.1, Euler, Beta, denoise 1 and the Krea enhancer at strength 1 / text scale 1. New Krea nodes use Manual with the same sampling values and enhancement `none`. These settings are starting points, not quality guarantees.
29
 
30
  FLUX.1 Dev uses embedded guidance 3.5; CFG is a separate parameter. Advanced users can supply both conditioning inputs, using Comfy's FluxGuidance upstream to change embedded guidance. External conditioning bypasses Prompter text encoding; apply any CLIP LoRAs before that external encoding and set the Engine recipe's CLIP strengths to zero. The Engine rejects active nonzero CLIP strengths with supplied conditioning.
31
 
32
+ The Enhanced upload accepts a single preset or a pack of up to 20. Files are limited to 128 KiB. Unknown fields, model descriptors, non-finite values and unsupported sampling choices are rejected. Imported presets cannot replace the reserved historical built-in IDs. Use a new ID for your variation. A pack may contain different supported architectures; the interface prefers a preset matching the current architecture, or switches to the first imported preset's architecture when none matches. Confirm the model files after an architecture change.
33
 
34
  ```json
35
  {
 
41
  }
42
  ```
43
 
44
+ Preset files contain data only. They cannot download models, execute Python or register adapters. `resources/presets/dmai-presets-v1.json` is the historical compatibility catalog, retained as a reference for saved workflows. Its IDs are reserved; copy the settings into a new preset with a unique ID for upload.
45
 
46
  ## Krea enhancer as a separate node
47
 
48
+ Connect a Krea MODEL through **DMAI Krea Enhancer**, then to Engine's advanced MODEL input. Also connect matching CLIP and VAE. Use Manual with enhancement `none`, or an imported preset with `"enhancer": "none"`, when the upstream node has already applied it. Double enhancement is rejected.
49
 
50
  The standalone node requires the separately installed, reviewed [Krea2T Enhancer](https://github.com/capitan01R/ComfyUI-Krea2T-Enhancer). It exposes Enabled, Strength and Text scale. It rejects non-Krea models when enabled. Qwen, FLUX and SDXL never receive this enhancement. See [installation](INSTALLATION.md#optional-krea-enhancement) for the pinned dependency.
51
 
52
  ## Advanced connections
53
 
54
+ Drag ComfyUI's native output dots to matching input dots. In Canvas, readable captions sit outside the nodes: Prompter **Prompt** → Engine **Prompt**, LoRA Loader **LoRAs** → Engine **LoRAs**, Engine **Images** → Gallery **Images**, and optionally Engine **Report** → Gallery **Report**. The caption **Prompt** represents the existing `request` input/output; serialized names and types are unchanged. Captions do not add another connection bar or replace native drag targets. Minimum node dimensions keep the controls and socket spacing usable; Nodes 2.0 may present ports differently.
55
 
56
  Reveal optional advanced inputs using ComfyUI's native node controls.
57
 
 
75
 
76
  ## LoRA behavior
77
 
78
+ The LoRA Loader produces an ordered recipe. Engine applies it before encoding both prompts. Each card shows its order and full filename above separate **Model** and **CLIP** strength controls. Use the arrows, drag handle, or Alt+Up / Alt+Down on the handle to change the order. Both strengths accept values from -20 to 20. Invalid drafts block reordering and queue submission until corrected.
79
+
80
+ Disabled rows and rows with both strengths at zero are skipped. The active count updates as the strengths change. Active nonzero rows fail on missing or ambiguous paths or zero effective matches. Partial matches appear in the execution report.
81
 
82
  File names preserve case and subfolders. Active file content hashes participate in cache invalidation. Registered patches are compared to Comfy's standard loader in CPU tests; visual compatibility with every LoRA is not guaranteed.
83
 
docs/VALIDATION.md CHANGED
@@ -1,6 +1,20 @@
1
- # Validation — 0.1.3 preview
2
 
3
- Updated on 2026-09-29. This release has working ComfyUI registration, CPU contract tests and real local API checks. GPU image generation is still pending. The coexistence checks below were added for 0.1.3; earlier Gallery, model-picker and connector observations were made on 2026-09-28.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  ## 0.1.3 coexistence and migration
6
 
@@ -25,8 +39,8 @@ These checks establish registration, UI coexistence and workflow migration. They
25
 
26
  | Check | Evidence |
27
  | --- | --- |
28
- | Python suite | 99 tests passed with optional real-Comfy CPU tests enabled, including registration coexistence, workflow migration, scoped native progress registry updates and multi-image progress. |
29
- | JavaScript suite | 64 tests passed: dimensions, exact seeds, ordered manifests, strength edits, mode persistence, sampler overrides, ZIP selection, keyboard containment, Comfy change events, progress presentation, job lifecycle handling, native socket geometry, model-file selection and safe workflow migration. |
30
  | Node registration | All seven native V3 nodes appear in real `/object_info`. |
31
  | Runtime choices | Four model profiles, 45 native sampler names and 9 schedulers in the test installation. Extension installations can change these counts. |
32
  | LoRA loading | Four real CPU comparisons use tiny safetensors, Comfy's file loader, ModelPatcher and LoRA weight calculation. Results match the standard loader and expected low-rank math exactly. |
 
1
+ # Validation — 0.2.0 preview
2
 
3
+ Updated on 2026-09-29. This release has working ComfyUI registration, CPU contract tests and real local API checks. GPU image generation is still pending. The 0.2.0 interface checks below were made on 2026-09-29. Earlier Gallery and sampling observations retain their original scope.
4
+
5
+ ## 0.2.0 interface and Manual defaults
6
+
7
+ - New and starter Engines use Manual with enhancement `none`. CPU schema and engine regressions confirm the defaults and the actionable empty-Enhanced error. Existing selected legacy presets still resolve.
8
+ - In the real Canvas frontend with the older DMAI Suite also installed, Manual displayed no preset selector, upload or export. Switching to Enhanced exposed Upload JSON. A local QA JSON imported successfully through the actual file chooser and validation route, showing 28 steps. Returning to Manual restored the edited 24-step draft.
9
+ - DPM++ 2M and Karras selections, the imported settings and the manual draft survived workflow save/reload. A native Prompter-to-Engine drag recreated the link; the saved graph contained the new link ID with unchanged port types.
10
+ - Inline LoRA CLIP strength changed from 0.4 to 0.3, independently of Model strength 0.65. Moving that LoRA to position 2 retained both values. Regression tests also cover invalid CLIP drafts blocking reorder/queue serialization.
11
+ - Canvas captions identify native sockets at 14 px. Connector tests cover hit-target clearance, minimum dimensions after loading/resizing, caption geometry, connected metadata and collapsed nodes.
12
+ - The rendered interface was inspected at 70% canvas zoom. Prompter Generate and LoRA Add remain visible at the new default sizes. Gallery uses a fixed selection footer with an independently scrolling image grid; its panel has no outer overflow in the measured fixture.
13
+ - The compact Enhanced card measured 616 px client height and 616 px scroll height, with Upload and Seed visible. Returning to Manual grew it to 716 px without overflow, restored 24 steps and removed the upload control.
14
+ - Preset mode and adaptive-height tests preserve larger saved/user dimensions, including Comfy's extra native widget padding. Package-specific LoRA classes avoid the matching global selectors in the older Suite and standalone LoRA extension.
15
+ - Versioned frontend imports and stylesheet URLs refresh the changed assets. Existing frontend initialization/deprecation warnings from the reference host remain; this release does not claim a warning-free third-party installation.
16
+
17
+ The images in the QA Gallery are synthetic solid-color fixtures. These checks do not add GPU inference or image-quality coverage.
18
 
19
  ## 0.1.3 coexistence and migration
20
 
 
39
 
40
  | Check | Evidence |
41
  | --- | --- |
42
+ | Python suite | 102 tests passed with optional real-Comfy CPU tests enabled, including registration coexistence, workflow migration, scoped native progress registry updates and multi-image progress. |
43
+ | JavaScript suite | 80 tests passed: dimensions, exact seeds, ordered manifests, strength edits, mode persistence, sampler overrides, ZIP selection, keyboard containment, Comfy change events, progress presentation, job lifecycle handling, native socket geometry, model-file selection and safe workflow migration. |
44
  | Node registration | All seven native V3 nodes appear in real `/object_info`. |
45
  | Runtime choices | Four model profiles, 45 native sampler names and 9 schedulers in the test installation. Extension installations can change these counts. |
46
  | LoRA loading | Four real CPU comparisons use tiny safetensors, Comfy's file loader, ModelPatcher and LoRA weight calculation. Results match the standard loader and expected low-rank math exactly. |
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
  [project]
2
  name = "dmai-nodes"
3
- version = "0.1.3"
4
  description = "Compact image workflows for ComfyUI: Prompter, ordered LoRAs, Generation Engine and Gallery."
5
  authors = [{name = "DMAI / ceroomedia"}]
6
  readme = "README.md"
 
1
  [project]
2
  name = "dmai-nodes"
3
+ version = "0.2.0"
4
  description = "Compact image workflows for ComfyUI: Prompter, ordered LoRAs, Generation Engine and Gallery."
5
  authors = [{name = "DMAI / ceroomedia"}]
6
  readme = "README.md"
release.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "DMAI NODES",
3
- "version": "0.1.3",
4
  "channel": "preview",
5
  "license": "MIT",
6
  "comfyui_reference": "0.37.0",
 
1
  {
2
  "name": "DMAI NODES",
3
+ "version": "0.2.0",
4
  "channel": "preview",
5
  "license": "MIT",
6
  "comfyui_reference": "0.37.0",
tools/build_release.py CHANGED
@@ -7,7 +7,7 @@ import shutil
7
  import zipfile
8
 
9
  ROOT=Path(__file__).resolve().parents[1]
10
- VERSION="0.1.3"
11
  ROOT_FILES={"__init__.py","README.md","LICENSE","pyproject.toml","requirements.txt","THIRD_PARTY_NOTICES.md","CHANGELOG.md","release.json"}
12
  DIRECTORIES={"dmai_nodes","web","docs","resources","licenses","workflows"}
13
  FORBIDDEN={".git","__pycache__",".validation","node_modules","dist"}
 
7
  import zipfile
8
 
9
  ROOT=Path(__file__).resolve().parents[1]
10
+ VERSION="0.2.0"
11
  ROOT_FILES={"__init__.py","README.md","LICENSE","pyproject.toml","requirements.txt","THIRD_PARTY_NOTICES.md","CHANGELOG.md","release.json"}
12
  DIRECTORIES={"dmai_nodes","web","docs","resources","licenses","workflows"}
13
  FORBIDDEN={".git","__pycache__",".validation","node_modules","dist"}
web/dmai-nodes.js CHANGED
@@ -1,11 +1,11 @@
1
  import { app } from "../../scripts/app.js";
2
  import { api } from "../../scripts/api.js";
3
- import { envelope } from "./core.mjs";
4
- import { Prompter, LoRAStack } from "./inputs.mjs";
5
- import { Engine } from "./engine.mjs";
6
- import { Gallery } from "./gallery.mjs";
7
- import { GenerationProgress } from "./progress.mjs";
8
- import { PROMPTER_TYPE, migratePreviewPrompters } from "./workflow-migration.mjs";
9
 
10
  const controllers = new WeakMap();
11
  const types = {
@@ -74,7 +74,7 @@ app.registerExtension({
74
  name: "DMAI.Nodes.Slate",
75
  setup() {
76
  progress.start();
77
- const href = new URL("./nodes.css", import.meta.url).href;
78
  if (!document.querySelector("link[data-dmai-nodes]")) {
79
  const link = document.createElement("link");
80
  link.rel = "stylesheet";
 
1
  import { app } from "../../scripts/app.js";
2
  import { api } from "../../scripts/api.js";
3
+ import { envelope } from "./core.mjs?v=0.2.0";
4
+ import { Prompter, LoRAStack } from "./inputs.mjs?v=0.2.0";
5
+ import { Engine } from "./engine.mjs?v=0.2.0";
6
+ import { Gallery } from "./gallery.mjs?v=0.2.0";
7
+ import { GenerationProgress } from "./progress.mjs?v=0.2.0";
8
+ import { PROMPTER_TYPE, migratePreviewPrompters } from "./workflow-migration.mjs?v=0.2.0";
9
 
10
  const controllers = new WeakMap();
11
  const types = {
 
74
  name: "DMAI.Nodes.Slate",
75
  setup() {
76
  progress.start();
77
+ const href = new URL("./nodes.css?v=0.2.0", import.meta.url).href;
78
  if (!document.querySelector("link[data-dmai-nodes]")) {
79
  const link = document.createElement("link");
80
  link.rel = "stylesheet";
web/dom.mjs CHANGED
@@ -1,10 +1,35 @@
1
- import { icon } from "./icons.mjs";
2
 
3
  // Comfy's DOM wrapper receives pointer events across its whole rectangle. A
4
  // real widget margin leaves canvas space for native socket hit targets; CSS
5
  // padding on the panel alone would still leave the wrapper over the sockets.
6
  export const SOCKET_GUTTER = 22;
7
  export const SOCKET_INSET = 12;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  export function layoutNativeSockets(node) {
10
  const width = node.size?.[0] ?? 360;
@@ -15,7 +40,7 @@ export function layoutNativeSockets(node) {
15
  ]) {
16
  // Widget-backed inputs keep Comfy's own conversion, visibility and layout.
17
  // In particular, our hidden JSON widgets must not consume a socket row.
18
- const sockets = slots.filter((slot) => !slot.widget);
19
  const top = 75;
20
  const bottom = Math.max(top, height - 40);
21
  sockets.forEach((slot, index) => {
@@ -29,6 +54,40 @@ export function layoutNativeSockets(node) {
29
  }
30
  }
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  export const el = (tag, className = "", text) => {
33
  const element = document.createElement(tag);
34
  element.className = className;
@@ -179,8 +238,22 @@ export class Controller {
179
  },
180
  );
181
  this.widget.serialize = false;
182
- this.wrap("onConfigure", () => queueMicrotask(() => this.hydrate()));
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  this.wrap("onDrawBackground", () => this.positionSlots());
 
184
  this.wrap("onResize", () => this.positionSlots());
185
  this.wrap("onRemoved", () => this.dispose());
186
  const oldRemove = this.widget.onRemove?.bind(this.widget);
@@ -207,14 +280,29 @@ export class Controller {
207
  };
208
  }
209
  size(width, height) {
 
210
  this.minHeight = height;
 
211
  this.node.setSize?.([
212
- Math.max(width + SOCKET_GUTTER * 2, this.node.size?.[0] ?? 0),
213
- Math.max(height + SOCKET_GUTTER * 2 + 4, this.node.size?.[1] ?? 0),
214
  ]);
215
  this.positionSlots();
216
  }
 
 
 
 
 
 
217
  positionSlots() {
 
 
 
 
 
 
 
218
  layoutNativeSockets(this.node);
219
  }
220
  set(value) {
 
1
+ import { icon } from "./icons.mjs?v=0.2.0";
2
 
3
  // Comfy's DOM wrapper receives pointer events across its whole rectangle. A
4
  // real widget margin leaves canvas space for native socket hit targets; CSS
5
  // padding on the panel alone would still leave the wrapper over the sockets.
6
  export const SOCKET_GUTTER = 22;
7
  export const SOCKET_INSET = 12;
8
+ export const SOCKET_PITCH = 36;
9
+ export const SOCKET_LABEL_FONT = '500 14px "DMAI Inter", "Segoe UI", sans-serif';
10
+
11
+ const SOCKET_LABELS = {
12
+ request: "Prompt",
13
+ text: "Text",
14
+ loras: "LoRAs",
15
+ model: "Model",
16
+ clip: "CLIP",
17
+ vae: "VAE",
18
+ sampler: "Sampler",
19
+ sigmas: "Sigmas",
20
+ latent: "Latent",
21
+ positive: "Positive",
22
+ negative: "Negative",
23
+ images: "Images",
24
+ report: "Report",
25
+ };
26
+
27
+ const nativeSockets = (slots) => (slots ?? []).filter((slot) => !slot.widget);
28
+
29
+ export function minimumSocketHeight(node) {
30
+ const count = Math.max(nativeSockets(node.inputs).length, nativeSockets(node.outputs).length);
31
+ return count ? 75 + (count - 1) * SOCKET_PITCH + 40 : 0;
32
+ }
33
 
34
  export function layoutNativeSockets(node) {
35
  const width = node.size?.[0] ?? 360;
 
40
  ]) {
41
  // Widget-backed inputs keep Comfy's own conversion, visibility and layout.
42
  // In particular, our hidden JSON widgets must not consume a socket row.
43
+ const sockets = nativeSockets(slots);
44
  const top = 75;
45
  const bottom = Math.max(top, height - 40);
46
  sockets.forEach((slot, index) => {
 
54
  }
55
  }
56
 
57
+ // These captions are paint only. The dots, hit targets and links remain Comfy's
58
+ // native sockets. Drawing just outside the card leaves its controls full width;
59
+ // lifting the caption above the dot also leaves the wire and drag target clear.
60
+ export function drawNativeSocketLabels(node, ctx) {
61
+ if (node.flags?.collapsed || node.collapsed || !ctx) return;
62
+ const width = node.size?.[0] ?? 360;
63
+ ctx.save();
64
+ ctx.font = SOCKET_LABEL_FONT;
65
+ ctx.textBaseline = "middle";
66
+ ctx.textAlign = "left";
67
+ ctx.lineWidth = 1;
68
+ for (const [slots, output] of [[node.inputs, false], [node.outputs, true]]) {
69
+ for (const slot of nativeSockets(slots)) {
70
+ if (!slot.pos) continue;
71
+ const label = SOCKET_LABELS[slot.name] ?? slot.label ?? slot.name;
72
+ if (!label) continue;
73
+ const labelWidth = ctx.measureText(label).width;
74
+ const boxWidth = labelWidth + 16;
75
+ const x = output ? width + 8 : -8 - boxWidth;
76
+ const y = slot.pos[1] - 34;
77
+ const connected = output ? Boolean(slot.links?.length) : slot.link != null;
78
+ ctx.fillStyle = "#191d22";
79
+ ctx.strokeStyle = connected ? "#4e6543" : "#343b46";
80
+ ctx.beginPath();
81
+ ctx.roundRect(x, y, boxWidth, 24, 6);
82
+ ctx.fill();
83
+ ctx.stroke();
84
+ ctx.fillStyle = connected ? "#d9edc2" : "#d5dbe4";
85
+ ctx.fillText(label, x + 8, y + 12);
86
+ }
87
+ }
88
+ ctx.restore();
89
+ }
90
+
91
  export const el = (tag, className = "", text) => {
92
  const element = document.createElement(tag);
93
  element.className = className;
 
238
  },
239
  );
240
  this.widget.serialize = false;
241
+ const computeSize = this.node.computeSize?.bind(this.node);
242
+ if (computeSize) {
243
+ this.node.computeSize = (...args) => {
244
+ const size = computeSize(...args);
245
+ const minimum = this.minimumSize();
246
+ size[0] = Math.max(size[0], minimum[0]);
247
+ size[1] = Math.max(size[1], minimum[1]);
248
+ return size;
249
+ };
250
+ }
251
+ this.wrap("onConfigure", () => queueMicrotask(() => {
252
+ this.positionSlots();
253
+ this.hydrate();
254
+ }));
255
  this.wrap("onDrawBackground", () => this.positionSlots());
256
+ this.wrap("onDrawForeground", (ctx) => drawNativeSocketLabels(this.node, ctx));
257
  this.wrap("onResize", () => this.positionSlots());
258
  this.wrap("onRemoved", () => this.dispose());
259
  const oldRemove = this.widget.onRemove?.bind(this.widget);
 
280
  };
281
  }
282
  size(width, height) {
283
+ this.minWidth = width;
284
  this.minHeight = height;
285
+ const minimum = this.minimumSize();
286
  this.node.setSize?.([
287
+ Math.max(minimum[0], this.node.size?.[0] ?? 0),
288
+ Math.max(minimum[1], this.node.size?.[1] ?? 0),
289
  ]);
290
  this.positionSlots();
291
  }
292
+ minimumSize() {
293
+ return [
294
+ (this.minWidth ?? 0) + SOCKET_GUTTER * 2,
295
+ Math.max((this.minHeight ?? 0) + SOCKET_GUTTER * 2 + 4, minimumSocketHeight(this.node)),
296
+ ];
297
+ }
298
  positionSlots() {
299
+ const minimum = this.minimumSize();
300
+ const size = this.node.size ?? minimum;
301
+ if (size[0] < minimum[0] || size[1] < minimum[1]) {
302
+ // Assign through LiteGraph's size setter so its DOM layout store updates,
303
+ // without recursively dispatching onResize or adding a history edit.
304
+ this.node.size = [Math.max(size[0], minimum[0]), Math.max(size[1], minimum[1])];
305
+ }
306
  layoutNativeSockets(this.node);
307
  }
308
  set(value) {
web/engine.mjs CHANGED
@@ -5,17 +5,14 @@ import {
5
  input,
6
  select,
7
  details,
8
- saveBlob,
9
- statusMessage,
10
- } from "./dom.mjs";
11
  import {
12
  readEngine,
13
  validateSettings,
14
  clone,
15
  mergePresets,
16
  envelope,
17
- uid,
18
- } from "./core.mjs";
19
  import {
20
  modelSource,
21
  modelGroups,
@@ -23,12 +20,13 @@ import {
23
  parseModelToken,
24
  profileForSource,
25
  inventoryName,
26
- } from "./model-picker.mjs";
27
 
28
  export class Engine extends Controller {
29
  constructor(node, context) {
30
  super(node, "config_json", "Generation Engine", "bolt", context);
31
- this.size(365, 480);
 
32
  this.wrap("onConnectionsChange", () =>
33
  queueMicrotask(() => {
34
  if (!this.disposed && this.value && this.commitControls()) this.build();
@@ -45,6 +43,15 @@ export class Engine extends Controller {
45
  });
46
  this.hydrate();
47
  }
 
 
 
 
 
 
 
 
 
48
  hydrate() {
49
  if (this.disposed) return;
50
  const revision = (this.revision = (this.revision ?? 0) + 1);
@@ -78,6 +85,17 @@ export class Engine extends Controller {
78
  p.id === this.value.preset_id && p.model.id === this.value.profile_id,
79
  );
80
  }
 
 
 
 
 
 
 
 
 
 
 
81
  effective() {
82
  return this.value.mode === "enhanced"
83
  ? (this.chosen()?.settings ?? this.value.settings)
@@ -125,13 +143,15 @@ export class Engine extends Controller {
125
  if (mode === "enhanced") {
126
  const preset =
127
  this.chosen() ??
128
- this.presets().find((p) => p.model.id === this.value.profile_id);
129
- if (!preset) {
130
- this.error(new Error("Import a preset for this profile first."));
131
- return;
 
 
 
 
132
  }
133
- this.value.preset_id = preset.id;
134
- this.value.settings = clone(preset.settings);
135
  } else
136
  this.value.settings = clone(
137
  this.manualDrafts()[this.value.profile_id] ?? this.effective(),
@@ -150,7 +170,6 @@ export class Engine extends Controller {
150
  const profile = this.data.profiles.find((p) => p.id === id);
151
  if (!profile) return false;
152
  const saved = this.drafts()[id],
153
- preset = this.presets().find((p) => p.model.id === id),
154
  models = {
155
  diffusion_model: "",
156
  text_encoder: "",
@@ -167,9 +186,9 @@ export class Engine extends Controller {
167
  saved
168
  ? clone(saved)
169
  : {
170
- mode: preset ? "enhanced" : "manual",
171
- preset_id: preset?.id ?? "",
172
- settings: clone(preset?.settings ?? profile.default_settings),
173
  models,
174
  },
175
  );
@@ -241,6 +260,22 @@ export class Engine extends Controller {
241
  });
242
  return picker.field;
243
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  build() {
245
  this.modelFilesExpanded = this.modelFilesDetails?.open ?? this.modelFilesExpanded;
246
  this.body.replaceChildren();
@@ -248,14 +283,15 @@ export class Engine extends Controller {
248
  this.commitSeed = null;
249
  const profile = this.profile(),
250
  s = this.effective();
 
251
  this.body.append(this.modelPicker(profile));
252
  if (["model", "clip", "vae"].every((name) => this.linked(name)))
253
  this.body.append(el("div", "dmai-badge", "Connected model · CLIP · VAE"));
254
  this.modelFiles(profile);
255
  const mode = el("div", "dmai-segmented dmai-mode");
256
  for (const [value, label, glyph] of [
257
- ["enhanced", "DMAI Enhanced", "spark"],
258
  ["manual", "Manual", "sliders"],
 
259
  ]) {
260
  const b = button(label, glyph, () => this.applyMode(value), true);
261
  b.setAttribute("aria-pressed", String(this.value.mode === value));
@@ -263,86 +299,8 @@ export class Engine extends Controller {
263
  mode.append(b);
264
  }
265
  this.body.append(mode);
266
- const presetRow = el("div", "dmai-preset-row"),
267
- presetSelect = select(
268
- "Preset",
269
- [
270
- {
271
- value: "",
272
- label:
273
- this.value.mode === "manual"
274
- ? "Custom settings"
275
- : "Choose a preset",
276
- },
277
- ...this.presets()
278
- .filter((p) => p.model.id === this.value.profile_id)
279
- .map((p) => ({ value: p.id, label: p.name })),
280
- ],
281
- this.value.mode === "enhanced" ? this.value.preset_id : "",
282
- );
283
- presetSelect.control.disabled = !this.data;
284
- presetSelect.control.addEventListener("change", () => {
285
- const p = this.presets().find((p) => p.id === presetSelect.control.value);
286
- if (!p) return;
287
- if (!this.commitControls()) {
288
- presetSelect.control.value =
289
- this.value.mode === "enhanced" ? this.value.preset_id : "";
290
- return;
291
- }
292
- if (this.value.mode === "manual")
293
- this.manualDrafts()[this.value.profile_id] = clone(this.value.settings);
294
- this.value.mode = "enhanced";
295
- this.value.preset_id = p.id;
296
- this.value.settings = clone(p.settings);
297
- this.persist();
298
- this.build();
299
- });
300
- const file = el("input");
301
- file.type = "file";
302
- file.accept = ".json,application/json";
303
- file.hidden = true;
304
- file.addEventListener("change", () =>
305
- this.guard(async () => {
306
- const f = file.files?.[0];
307
- if (f) await this.importFile(f);
308
- file.value = "";
309
- }),
310
- );
311
- const importButton = button("Import preset JSON", "upload", () =>
312
- file.click(),
313
- ),
314
- exportButton = button("Export preset JSON", "download", () =>
315
- this.exportPreset(),
316
- );
317
- importButton.disabled = !this.data;
318
- exportButton.disabled = !profile;
319
- presetRow.append(presetSelect.field, importButton, exportButton, file);
320
- this.body.append(presetRow);
321
- const meta = el("div", "dmai-row dmai-preset-meta"),
322
- origin =
323
- this.value.mode === "manual"
324
- ? "Manual"
325
- : this.value.presets.some((p) => p.id === this.value.preset_id)
326
- ? "Imported"
327
- : "DMAI";
328
- meta.append(
329
- el("span", "dmai-badge", origin),
330
- el(
331
- "span",
332
- "dmai-muted",
333
- `${this.linked("sigmas") ? "External sigmas" : `${s.steps} steps`} · CFG ${s.cfg}`,
334
- ),
335
- );
336
- this.body.append(meta);
337
  if (this.value.mode === "manual") this.manual(s, profile);
338
- else
339
- this.body.append(
340
- el(
341
- "div",
342
- "dmai-engine-summary",
343
- `${this.samplingSummary(s)}${s.enhancer === "krea2t" ? " · Enhancer" : ""}`,
344
- ),
345
- );
346
  if (this.linked("sampler") || this.linked("sigmas"))
347
  this.body.append(
348
  el(
@@ -396,6 +354,72 @@ export class Engine extends Controller {
396
  ),
397
  );
398
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  modelFiles(profile) {
400
  const advanced = details("Model files");
401
  this.modelFilesDetails = advanced.root;
@@ -477,15 +501,6 @@ export class Engine extends Controller {
477
  true,
478
  ),
479
  );
480
- if (this.value.presets.length)
481
- advanced.body.append(
482
- button(
483
- "Manage imported presets",
484
- "layers",
485
- () => this.managePresets(),
486
- true,
487
- ),
488
- );
489
  if (profile?.default_guidance !== undefined && !this.linked("positive"))
490
  advanced.body.append(
491
  el(
@@ -498,7 +513,7 @@ export class Engine extends Controller {
498
  }
499
  manual(settings, profile) {
500
  const grid = el("div", "dmai-settings");
501
- const controls = {};
502
  for (const [name, label, type, min, max, step] of [
503
  ["steps", "Steps", "number", 1, 150, 1],
504
  ["cfg", "CFG", "number", 0, 30, 0.1],
@@ -508,7 +523,7 @@ export class Engine extends Controller {
508
  Object.assign(f.control, { min, max, step });
509
  f.field.hidden = this.linked("sigmas") && name !== "cfg";
510
  controls[name] = f.control;
511
- grid.append(f.field);
512
  }
513
  for (const [name, label, items] of [
514
  ["sampler", "Sampler", this.data?.samplers ?? [settings.sampler]],
@@ -520,12 +535,16 @@ export class Engine extends Controller {
520
  ],
521
  ]) {
522
  const f = select(label, items, settings[name]);
 
 
523
  f.field.hidden =
524
  (name === "sampler" && this.linked("sampler")) ||
525
- (name === "scheduler" && this.linked("sigmas"));
 
526
  controls[name] = f.control;
527
- grid.append(f.field);
528
  }
 
529
  this.commitManual = () => {
530
  try {
531
  const next = validateSettings({
@@ -555,7 +574,10 @@ export class Engine extends Controller {
555
  this.body.append(grid);
556
  }
557
  async importFile(file) {
 
 
558
  if (!this.commitControls()) return;
 
559
  if (file.size > 128 * 1024)
560
  throw new Error("Preset files must be 128 KiB or smaller.");
561
  let payload;
@@ -570,12 +592,14 @@ export class Engine extends Controller {
570
  body: JSON.stringify(payload),
571
  });
572
  if (this.disposed || !this.commitControls()) return;
 
 
573
  const presets = mergePresets(
574
  this.value.presets,
575
  result.presets,
576
  this.data.presets,
577
  );
578
- const first = result.presets[0];
579
  this.value.presets = presets;
580
  if (this.value.profile_id !== first.model.id)
581
  this.changeProfile(first.model.id);
@@ -626,48 +650,4 @@ export class Engine extends Controller {
626
  };
627
  draw();
628
  }
629
- exportPreset() {
630
- if (!this.commitControls()) return;
631
- const d = this.show("Export preset"),
632
- name = input("Preset name", "text", "My settings"),
633
- message = el("p", "dmai-status"),
634
- save = button(
635
- "Download JSON",
636
- "download",
637
- () =>
638
- this.guard(async () => {
639
- try {
640
- const p = this.profile(),
641
- payload = {
642
- schema_version: 1,
643
- id: `custom-${uid()}`,
644
- name: name.control.value,
645
- model: { id: p.id, label: p.label, family: p.family },
646
- settings: clone(this.effective()),
647
- };
648
- const result = await envelope(
649
- this.context.api,
650
- "/presets/validate",
651
- {
652
- method: "POST",
653
- headers: { "Content-Type": "application/json" },
654
- body: JSON.stringify(payload),
655
- },
656
- );
657
- saveBlob(
658
- new Blob([JSON.stringify(result.presets[0], null, 2) + "\n"], {
659
- type: "application/json",
660
- }),
661
- `${payload.id}.json`,
662
- );
663
- d.root.close();
664
- } catch (error) {
665
- statusMessage(message, error.message, true);
666
- }
667
- }),
668
- true,
669
- );
670
- save.classList.add("dmai-primary");
671
- d.body.append(name.field, message, save);
672
- }
673
  }
 
5
  input,
6
  select,
7
  details,
8
+ } from "./dom.mjs?v=0.2.0";
 
 
9
  import {
10
  readEngine,
11
  validateSettings,
12
  clone,
13
  mergePresets,
14
  envelope,
15
+ } from "./core.mjs?v=0.2.0";
 
16
  import {
17
  modelSource,
18
  modelGroups,
 
20
  parseModelToken,
21
  profileForSource,
22
  inventoryName,
23
+ } from "./model-picker.mjs?v=0.2.0";
24
 
25
  export class Engine extends Controller {
26
  constructor(node, context) {
27
  super(node, "config_json", "Generation Engine", "bolt", context);
28
+ this.root.classList.add("dmai-engine");
29
+ this.initializeSize();
30
  this.wrap("onConnectionsChange", () =>
31
  queueMicrotask(() => {
32
  if (!this.disposed && this.value && this.commitControls()) this.build();
 
43
  });
44
  this.hydrate();
45
  }
46
+ initializeSize() {
47
+ let height = 700;
48
+ try {
49
+ if (readEngine(this.native.value).mode === "enhanced") height = 600;
50
+ } catch {
51
+ // hydrate() reports invalid saved data without replacing its value.
52
+ }
53
+ this.size(440, height);
54
+ }
55
  hydrate() {
56
  if (this.disposed) return;
57
  const revision = (this.revision = (this.revision ?? 0) + 1);
 
85
  p.id === this.value.preset_id && p.model.id === this.value.profile_id,
86
  );
87
  }
88
+ availablePresets() {
89
+ // The built-in catalog is retained only for saved-workflow compatibility.
90
+ // New choices in the interface are the JSON settings the owner imported.
91
+ const presets = this.value.presets.filter(
92
+ (preset) => preset.model.id === this.value.profile_id,
93
+ );
94
+ const current = this.chosen();
95
+ if (current && !presets.some((preset) => preset.id === current.id))
96
+ presets.unshift(current);
97
+ return presets;
98
+ }
99
  effective() {
100
  return this.value.mode === "enhanced"
101
  ? (this.chosen()?.settings ?? this.value.settings)
 
143
  if (mode === "enhanced") {
144
  const preset =
145
  this.chosen() ??
146
+ this.value.presets.find((p) => p.model.id === this.value.profile_id);
147
+ if (preset) {
148
+ this.value.preset_id = preset.id;
149
+ this.value.settings = clone(preset.settings);
150
+ } else {
151
+ // Enhanced must be reachable before a JSON file exists. Execution
152
+ // validates the missing preset, while Manual keeps its separate draft.
153
+ this.value.preset_id = "";
154
  }
 
 
155
  } else
156
  this.value.settings = clone(
157
  this.manualDrafts()[this.value.profile_id] ?? this.effective(),
 
170
  const profile = this.data.profiles.find((p) => p.id === id);
171
  if (!profile) return false;
172
  const saved = this.drafts()[id],
 
173
  models = {
174
  diffusion_model: "",
175
  text_encoder: "",
 
186
  saved
187
  ? clone(saved)
188
  : {
189
+ mode: "manual",
190
+ preset_id: "",
191
+ settings: clone(profile.default_settings),
192
  models,
193
  },
194
  );
 
260
  });
261
  return picker.field;
262
  }
263
+ fitHeight(profile) {
264
+ const current = this.node.size?.[1] ?? 0,
265
+ previousMinimum = Math.max(this.minimumSize()[1], this.node.computeSize?.()?.[1] ?? 0);
266
+ // Remember an explicit larger size across automatic growth (for example,
267
+ // a Krea enhancer row), so changing modes cannot erase the user's height.
268
+ if (this.lastEngineHeight === undefined || Math.abs(current - this.lastEngineHeight) > 2)
269
+ this.preferredEngineHeight = current > previousMinimum + 2 ? current : undefined;
270
+ this.minHeight = this.value.mode === "enhanced" ? 600 : profile?.family === "krea2" ? 770 : 700;
271
+ const minimum = this.minimumSize(),
272
+ height = Math.max(minimum[1], this.node.computeSize?.()?.[1] ?? 0, this.preferredEngineHeight ?? 0),
273
+ width = Math.max(minimum[0], this.node.size?.[0] ?? 0);
274
+ if (height !== current || width !== this.node.size?.[0])
275
+ this.node.setSize?.([width, height]);
276
+ this.positionSlots();
277
+ this.lastEngineHeight = this.node.size?.[1] ?? height;
278
+ }
279
  build() {
280
  this.modelFilesExpanded = this.modelFilesDetails?.open ?? this.modelFilesExpanded;
281
  this.body.replaceChildren();
 
283
  this.commitSeed = null;
284
  const profile = this.profile(),
285
  s = this.effective();
286
+ this.fitHeight(profile);
287
  this.body.append(this.modelPicker(profile));
288
  if (["model", "clip", "vae"].every((name) => this.linked(name)))
289
  this.body.append(el("div", "dmai-badge", "Connected model · CLIP · VAE"));
290
  this.modelFiles(profile);
291
  const mode = el("div", "dmai-segmented dmai-mode");
292
  for (const [value, label, glyph] of [
 
293
  ["manual", "Manual", "sliders"],
294
+ ["enhanced", "DMAI Enhanced", "spark"],
295
  ]) {
296
  const b = button(label, glyph, () => this.applyMode(value), true);
297
  b.setAttribute("aria-pressed", String(this.value.mode === value));
 
299
  mode.append(b);
300
  }
301
  this.body.append(mode);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  if (this.value.mode === "manual") this.manual(s, profile);
303
+ else this.enhanced(s);
 
 
 
 
 
 
 
304
  if (this.linked("sampler") || this.linked("sigmas"))
305
  this.body.append(
306
  el(
 
354
  ),
355
  );
356
  }
357
+ enhanced(settings) {
358
+ const panel = el("section", "dmai-enhanced"),
359
+ preset = this.chosen(),
360
+ choices = this.availablePresets();
361
+ panel.setAttribute("aria-label", "DMAI Enhanced settings");
362
+ if (choices.length > 1) {
363
+ const picker = select(
364
+ "DMAI settings",
365
+ choices.map((entry) => ({ value: entry.id, label: entry.name })),
366
+ this.value.preset_id,
367
+ );
368
+ picker.control.addEventListener("change", () => {
369
+ if (!this.selectPreset(picker.control.value))
370
+ picker.control.value = this.value.preset_id;
371
+ });
372
+ panel.append(picker.field);
373
+ } else {
374
+ const title = el("div", "dmai-enhanced-title", preset?.name ?? "Your DMAI settings");
375
+ if (preset)
376
+ title.title = this.value.presets.some((entry) => entry.id === preset.id)
377
+ ? "Imported settings" : "Saved workflow settings";
378
+ panel.append(title);
379
+ if (!preset)
380
+ panel.append(el("p", "dmai-enhanced-copy", "Upload a DMAI JSON file for this model."));
381
+ }
382
+ if (preset) {
383
+ panel.append(
384
+ el("div", "dmai-enhanced-summary", `${this.linked("sigmas") ? "Connected sigmas" : `${settings.steps} steps`} · CFG ${settings.cfg}`),
385
+ el("div", "dmai-enhanced-sampling", this.samplingSummary(settings)),
386
+ );
387
+ if (settings.enhancer === "krea2t")
388
+ panel.append(el("div", "dmai-badge", "Krea enhancer enabled"));
389
+ }
390
+ const file = el("input");
391
+ file.type = "file";
392
+ file.accept = ".json,application/json";
393
+ file.hidden = true;
394
+ file.addEventListener("change", () =>
395
+ this.guard(async () => {
396
+ try {
397
+ const selected = file.files?.[0];
398
+ if (selected) await this.importFile(selected);
399
+ } finally {
400
+ file.value = "";
401
+ }
402
+ }),
403
+ );
404
+ const actions = el("div", "dmai-enhanced-actions"),
405
+ upload = button("Upload JSON", "upload", () => file.click(), true);
406
+ upload.disabled = !this.data;
407
+ actions.append(upload, file);
408
+ if (this.value.presets.length)
409
+ actions.append(button("Manage imported settings", "layers", () => this.managePresets()));
410
+ panel.append(actions);
411
+ this.body.append(panel);
412
+ }
413
+ selectPreset(id) {
414
+ const preset = this.availablePresets().find((entry) => entry.id === id);
415
+ if (!preset || !this.commitControls()) return false;
416
+ this.value.mode = "enhanced";
417
+ this.value.preset_id = preset.id;
418
+ this.value.settings = clone(preset.settings);
419
+ this.persist();
420
+ this.build();
421
+ return true;
422
+ }
423
  modelFiles(profile) {
424
  const advanced = details("Model files");
425
  this.modelFilesDetails = advanced.root;
 
501
  true,
502
  ),
503
  );
 
 
 
 
 
 
 
 
 
504
  if (profile?.default_guidance !== undefined && !this.linked("positive"))
505
  advanced.body.append(
506
  el(
 
513
  }
514
  manual(settings, profile) {
515
  const grid = el("div", "dmai-settings");
516
+ const controls = {}, fields = {};
517
  for (const [name, label, type, min, max, step] of [
518
  ["steps", "Steps", "number", 1, 150, 1],
519
  ["cfg", "CFG", "number", 0, 30, 0.1],
 
523
  Object.assign(f.control, { min, max, step });
524
  f.field.hidden = this.linked("sigmas") && name !== "cfg";
525
  controls[name] = f.control;
526
+ fields[name] = f.field;
527
  }
528
  for (const [name, label, items] of [
529
  ["sampler", "Sampler", this.data?.samplers ?? [settings.sampler]],
 
535
  ],
536
  ]) {
537
  const f = select(label, items, settings[name]);
538
+ if (name === "sampler" || name === "enhancer")
539
+ f.field.classList.add("dmai-field-wide");
540
  f.field.hidden =
541
  (name === "sampler" && this.linked("sampler")) ||
542
+ (name === "scheduler" && this.linked("sigmas")) ||
543
+ (name === "enhancer" && profile?.family !== "krea2");
544
  controls[name] = f.control;
545
+ fields[name] = f.field;
546
  }
547
+ grid.append(...["steps", "cfg", "sampler", "scheduler", "denoise", "enhancer"].map((name) => fields[name]));
548
  this.commitManual = () => {
549
  try {
550
  const next = validateSettings({
 
574
  this.body.append(grid);
575
  }
576
  async importFile(file) {
577
+ if (this.value.mode !== "enhanced")
578
+ throw new Error("Switch to DMAI Enhanced to upload JSON settings.");
579
  if (!this.commitControls()) return;
580
+ const profileId = this.value.profile_id;
581
  if (file.size > 128 * 1024)
582
  throw new Error("Preset files must be 128 KiB or smaller.");
583
  let payload;
 
592
  body: JSON.stringify(payload),
593
  });
594
  if (this.disposed || !this.commitControls()) return;
595
+ if (this.value.mode !== "enhanced" || this.value.profile_id !== profileId)
596
+ throw new Error("Engine mode or model changed during upload. Upload the JSON again in DMAI Enhanced.");
597
  const presets = mergePresets(
598
  this.value.presets,
599
  result.presets,
600
  this.data.presets,
601
  );
602
+ const first = result.presets.find((preset) => preset.model.id === profileId) ?? result.presets[0];
603
  this.value.presets = presets;
604
  if (this.value.profile_id !== first.model.id)
605
  this.changeProfile(first.model.id);
 
650
  };
651
  draw();
652
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  }
web/gallery.mjs CHANGED
@@ -1,10 +1,11 @@
1
- import { Controller, el, button, saveBlob } from "./dom.mjs";
2
- import { API, envelope, binary, selectionBody, uid } from "./core.mjs";
3
 
4
  export class Gallery extends Controller {
5
  constructor(node, context) {
6
  super(node, "gallery_id", "Gallery", "grid", context);
7
- this.size(465, 500);
 
8
  this.items = [];
9
  this.total = 0;
10
  this.watermark = 0;
 
1
+ import { Controller, el, button, saveBlob } from "./dom.mjs?v=0.2.0";
2
+ import { API, envelope, binary, selectionBody, uid } from "./core.mjs?v=0.2.0";
3
 
4
  export class Gallery extends Controller {
5
  constructor(node, context) {
6
  super(node, "gallery_id", "Gallery", "grid", context);
7
+ this.root.classList.add("dmai-gallery");
8
+ this.size(760, 570);
9
  this.items = [];
10
  this.total = 0;
11
  this.watermark = 0;
web/inputs.mjs CHANGED
@@ -5,7 +5,7 @@ import {
5
  input,
6
  details,
7
  statusMessage,
8
- } from "./dom.mjs";
9
  import {
10
  RATIOS,
11
  dimensions,
@@ -14,8 +14,8 @@ import {
14
  readManifest,
15
  moveEntry,
16
  uid,
17
- } from "./core.mjs";
18
- import { ProgressView, progressPresentation } from "./progress-view.mjs";
19
 
20
  export class Prompter extends Controller {
21
  constructor(node, context) {
@@ -26,7 +26,7 @@ export class Prompter extends Controller {
26
  this.root.append(scroll);
27
  this.progress = { phase: "idle", percent: 0 };
28
  this.progressView = new ProgressView(this.root);
29
- this.size(360, 400);
30
  this.hydrate();
31
  this.stopProgress = context.watchProgress?.(node, (state) => {
32
  if (!this.disposed) this.updateProgress(state);
@@ -55,14 +55,24 @@ export class Prompter extends Controller {
55
  this.value.prompt = prompt.value;
56
  this.persist();
57
  });
58
- this.body.append(prompt);
 
 
 
 
 
59
  this.ratioRow = el("div", "dmai-ratios");
 
 
60
  for (const ratio of ["1:1", "4:5", "9:16", "16:9"])
61
  this.ratioRow.append(this.ratioButton(ratio));
62
- this.ratioRow.append(
63
- button("All 19 aspect ratios", "more", () => this.showRatios()),
64
- );
65
- this.body.append(this.ratioRow);
 
 
 
66
  const resolutionRow = el("div", "dmai-resolution"),
67
  group = el("div", "dmai-segmented");
68
  this.resolutionButtons = {};
@@ -78,9 +88,12 @@ export class Prompter extends Controller {
78
  }
79
  this.dimensions = el("output", "dmai-dimensions");
80
  resolutionRow.append(group, this.dimensions);
81
- this.body.append(resolutionRow);
 
 
 
82
  const countRow = el("div", "dmai-row dmai-count");
83
- countRow.append(el("span", "dmai-muted", "Images"));
84
  this.less = button("One fewer image", "minus", () => this.count(-1));
85
  this.more = button("One more image", "plus", () => this.count(1));
86
  this.countOutput = el("output");
@@ -88,8 +101,9 @@ export class Prompter extends Controller {
88
  const stepper = el("div", "dmai-stepper");
89
  stepper.append(this.less, this.countOutput, this.more);
90
  countRow.append(stepper);
91
- this.body.append(countRow);
92
- const advanced = details();
 
93
  const negative = el("textarea", "dmai-negative");
94
  negative.value = this.value.negative_prompt;
95
  negative.placeholder = "Negative prompt";
@@ -279,6 +293,7 @@ export class Prompter extends Controller {
279
  export class LoRAStack extends Controller {
280
  constructor(node, context) {
281
  super(node, "manifest_json", "LoRA Loader", "layers", context);
 
282
  const serialize = this.native.serializeValue?.bind(this.native);
283
  this.native.serializeValue = (...args) => {
284
  if (!this.syncStrengths())
@@ -288,7 +303,7 @@ export class LoRAStack extends Controller {
288
  readManifest(this.native.value);
289
  return serialize ? serialize(...args) : this.native.value;
290
  };
291
- this.size(390, 275);
292
  this.hydrate();
293
  }
294
  hydrate() {
@@ -304,26 +319,28 @@ export class LoRAStack extends Controller {
304
  persist() {
305
  this.set(this.value);
306
  }
307
- updateStrength(id, text) {
 
308
  const value = text.trim() === "" ? NaN : Number(text);
309
  if (!Number.isFinite(value) || value < -20 || value > 20) {
310
  this.error(
311
  new Error(
312
- "Strength must be from −20 to 20. Correct it before queueing.",
313
  ),
314
  );
315
  return false;
316
  }
317
  const entry = this.value.entries.find((item) => item.id === id);
318
  if (!entry) return false;
319
- entry.strength_model = value;
320
  this.persist();
321
  this.note();
 
322
  return true;
323
  }
324
  syncStrengths() {
325
  for (const control of this.body.querySelectorAll(".dmai-strength")) {
326
- if (!this.updateStrength(control.dataset.entryId, control.value)) {
327
  control.setAttribute("aria-invalid", "true");
328
  control.focus();
329
  return false;
@@ -332,6 +349,34 @@ export class LoRAStack extends Controller {
332
  }
333
  return true;
334
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  move(id, to) {
336
  if (!this.syncStrengths()) return;
337
  this.value.entries = moveEntry(this.value.entries, id, to);
@@ -348,11 +393,18 @@ export class LoRAStack extends Controller {
348
  build() {
349
  this.body.replaceChildren();
350
  const list = el("div", "dmai-lora-list");
 
351
  list.setAttribute("aria-label", "Ordered LoRA stack");
352
  let dragged = null;
353
  this.value.entries.forEach((entry, index) => {
354
  const row = el("div", `dmai-nodes-lora-row${entry.enabled ? "" : " dmai-off"}`);
355
  row.dataset.entryId = entry.id;
 
 
 
 
 
 
356
  const grip = button(`Reorder ${entry.name}; Alt and arrow keys`, "grip");
357
  grip.classList.add("dmai-grip");
358
  grip.draggable = true;
@@ -390,31 +442,9 @@ export class LoRAStack extends Controller {
390
  dragged = null;
391
  });
392
  const copy = el("div", "dmai-lora-copy"),
393
- title = el("span", "dmai-nodes-lora-name", entry.name.split(/[\\/]/).pop());
394
  title.title = entry.name;
395
- copy.append(
396
- title,
397
- el(
398
- "span",
399
- "dmai-muted",
400
- `${String(index + 1).padStart(2, "0")} · CLIP ${entry.strength_clip}`,
401
- ),
402
- );
403
- const strength = el("input", "dmai-strength");
404
- strength.type = "number";
405
- strength.min = -20;
406
- strength.max = 20;
407
- strength.step = 0.05;
408
- strength.value = entry.strength_model;
409
- strength.dataset.entryId = entry.id;
410
- strength.setAttribute("aria-label", `Model strength for ${entry.name}`);
411
- const commitStrength = () => {
412
- if (this.updateStrength(entry.id, strength.value))
413
- strength.removeAttribute("aria-invalid");
414
- else strength.setAttribute("aria-invalid", "true");
415
- };
416
- strength.addEventListener("input", commitStrength);
417
- strength.addEventListener("change", commitStrength);
418
  const toggle = button(
419
  `${entry.enabled ? "Disable" : "Enable"} ${entry.name}`,
420
  "power",
@@ -427,6 +457,8 @@ export class LoRAStack extends Controller {
427
  );
428
  toggle.setAttribute("aria-pressed", String(entry.enabled));
429
  const arrows = el("div", "dmai-reorder");
 
 
430
  const up = button(`Move ${entry.name} up`, "up", () =>
431
  this.move(entry.id, index - 1),
432
  ),
@@ -439,45 +471,49 @@ export class LoRAStack extends Controller {
439
  const edit = button(`Edit ${entry.name}`, "sliders", () =>
440
  this.edit(entry),
441
  );
442
- row.append(grip, copy, strength, toggle, arrows, edit);
 
 
 
443
  list.append(row);
444
  });
445
  if (!this.value.entries.length)
446
  list.append(el("div", "dmai-empty", "Add a LoRA to build your stack."));
447
- this.body.append(
448
- list,
449
- button("Add LoRA", "plus", () => this.guard(() => this.library()), true),
450
- );
451
- const count = this.value.entries.filter(
452
- (e) => e.enabled && (e.strength_model !== 0 || e.strength_clip !== 0),
453
- ).length;
454
- this.body.append(
455
- el("div", "dmai-footnote", `${count} selected · top to bottom`),
456
- );
457
  }
458
  edit(entry) {
459
  if (!this.syncStrengths()) return;
460
  const d = this.show("LoRA settings"),
461
- field = input("CLIP strength", "number", entry.strength_clip);
462
- field.control.min = -20;
463
- field.control.max = 20;
464
- field.control.step = 0.05;
 
 
 
 
 
465
  const message = el("p", "dmai-status"),
466
  apply = button(
467
  "Apply",
468
  "check",
469
  () => {
470
- const n =
471
- field.control.value === "" ? NaN : Number(field.control.value);
472
- if (!Number.isFinite(n) || n < -20 || n > 20) {
 
473
  statusMessage(
474
  message,
475
- "CLIP strength must be from −20 to 20.",
476
  true,
477
  );
478
  return;
479
  }
480
- entry.strength_clip = n;
481
  this.persist();
482
  this.build();
483
  d.root.close();
@@ -499,7 +535,7 @@ export class LoRAStack extends Controller {
499
  );
500
  d.body.append(
501
  el("p", "dmai-filename", entry.name),
502
- field.field,
503
  message,
504
  apply,
505
  remove,
 
5
  input,
6
  details,
7
  statusMessage,
8
+ } from "./dom.mjs?v=0.2.0";
9
  import {
10
  RATIOS,
11
  dimensions,
 
14
  readManifest,
15
  moveEntry,
16
  uid,
17
+ } from "./core.mjs?v=0.2.0";
18
+ import { ProgressView, progressPresentation } from "./progress-view.mjs?v=0.2.0";
19
 
20
  export class Prompter extends Controller {
21
  constructor(node, context) {
 
26
  this.root.append(scroll);
27
  this.progress = { phase: "idle", percent: 0 };
28
  this.progressView = new ProgressView(this.root);
29
+ this.size(460, 690);
30
  this.hydrate();
31
  this.stopProgress = context.watchProgress?.(node, (state) => {
32
  if (!this.disposed) this.updateProgress(state);
 
55
  this.value.prompt = prompt.value;
56
  this.persist();
57
  });
58
+ const promptField = el("label", "dmai-prompt-field");
59
+ promptField.append(el("span", "dmai-section-label", "Prompt"), prompt);
60
+ this.body.append(promptField);
61
+ const settings = el("div", "dmai-prompt-settings"),
62
+ format = el("div", "dmai-format-section");
63
+ format.append(el("span", "dmai-section-label", "Aspect ratio"));
64
  this.ratioRow = el("div", "dmai-ratios");
65
+ this.ratioRow.setAttribute("role", "group");
66
+ this.ratioRow.setAttribute("aria-label", "Aspect ratio");
67
  for (const ratio of ["1:1", "4:5", "9:16", "16:9"])
68
  this.ratioRow.append(this.ratioButton(ratio));
69
+ const moreRatios = button("More", "more", () => this.showRatios(), true);
70
+ moreRatios.classList.add("dmai-ratio-more");
71
+ moreRatios.setAttribute("aria-label", "All 19 aspect ratios");
72
+ moreRatios.title = "All 19 aspect ratios";
73
+ this.ratioRow.append(moreRatios);
74
+ format.append(this.ratioRow);
75
+ settings.append(format);
76
  const resolutionRow = el("div", "dmai-resolution"),
77
  group = el("div", "dmai-segmented");
78
  this.resolutionButtons = {};
 
88
  }
89
  this.dimensions = el("output", "dmai-dimensions");
90
  resolutionRow.append(group, this.dimensions);
91
+ group.setAttribute("role", "group");
92
+ group.setAttribute("aria-label", "Resolution");
93
+ this.dimensions.setAttribute("aria-label", "Image dimensions");
94
+ settings.append(resolutionRow);
95
  const countRow = el("div", "dmai-row dmai-count");
96
+ countRow.append(el("span", "dmai-section-label", "Images"));
97
  this.less = button("One fewer image", "minus", () => this.count(-1));
98
  this.more = button("One more image", "plus", () => this.count(1));
99
  this.countOutput = el("output");
 
101
  const stepper = el("div", "dmai-stepper");
102
  stepper.append(this.less, this.countOutput, this.more);
103
  countRow.append(stepper);
104
+ settings.append(countRow);
105
+ this.body.append(settings);
106
+ const advanced = details("Negative prompt");
107
  const negative = el("textarea", "dmai-negative");
108
  negative.value = this.value.negative_prompt;
109
  negative.placeholder = "Negative prompt";
 
293
  export class LoRAStack extends Controller {
294
  constructor(node, context) {
295
  super(node, "manifest_json", "LoRA Loader", "layers", context);
296
+ this.root.classList.add("dmai-lora");
297
  const serialize = this.native.serializeValue?.bind(this.native);
298
  this.native.serializeValue = (...args) => {
299
  if (!this.syncStrengths())
 
303
  readManifest(this.native.value);
304
  return serialize ? serialize(...args) : this.native.value;
305
  };
306
+ this.size(480, 520);
307
  this.hydrate();
308
  }
309
  hydrate() {
 
319
  persist() {
320
  this.set(this.value);
321
  }
322
+ updateStrength(id, text, channel = "model") {
323
+ if (!["model", "clip"].includes(channel)) return false;
324
  const value = text.trim() === "" ? NaN : Number(text);
325
  if (!Number.isFinite(value) || value < -20 || value > 20) {
326
  this.error(
327
  new Error(
328
+ `${channel === "clip" ? "CLIP" : "Model"} strength must be from −20 to 20. Correct it before queueing.`,
329
  ),
330
  );
331
  return false;
332
  }
333
  const entry = this.value.entries.find((item) => item.id === id);
334
  if (!entry) return false;
335
+ entry[`strength_${channel}`] = value;
336
  this.persist();
337
  this.note();
338
+ this.updateSummary();
339
  return true;
340
  }
341
  syncStrengths() {
342
  for (const control of this.body.querySelectorAll(".dmai-strength")) {
343
+ if (!this.updateStrength(control.dataset.entryId, control.value, control.dataset.strength ?? "model")) {
344
  control.setAttribute("aria-invalid", "true");
345
  control.focus();
346
  return false;
 
349
  }
350
  return true;
351
  }
352
+ updateSummary() {
353
+ const count = this.value.entries.filter(
354
+ (entry) => entry.enabled && (entry.strength_model !== 0 || entry.strength_clip !== 0),
355
+ ).length;
356
+ if (this.summary) this.summary.textContent = `${count} active · top to bottom`;
357
+ }
358
+ strengthField(entry, channel) {
359
+ const label = channel === "clip" ? "CLIP" : "Model",
360
+ field = el("label", "dmai-field dmai-lora-strength-field"),
361
+ strength = el("input", "dmai-strength");
362
+ strength.type = "number";
363
+ strength.min = -20;
364
+ strength.max = 20;
365
+ strength.step = 0.05;
366
+ strength.value = entry[`strength_${channel}`];
367
+ strength.dataset.entryId = entry.id;
368
+ strength.dataset.strength = channel;
369
+ strength.setAttribute("aria-label", `${label} strength for ${entry.name}`);
370
+ const commitStrength = () => {
371
+ if (this.updateStrength(entry.id, strength.value, channel))
372
+ strength.removeAttribute("aria-invalid");
373
+ else strength.setAttribute("aria-invalid", "true");
374
+ };
375
+ strength.addEventListener("input", commitStrength);
376
+ strength.addEventListener("change", commitStrength);
377
+ field.append(el("span", "", label), strength);
378
+ return field;
379
+ }
380
  move(id, to) {
381
  if (!this.syncStrengths()) return;
382
  this.value.entries = moveEntry(this.value.entries, id, to);
 
393
  build() {
394
  this.body.replaceChildren();
395
  const list = el("div", "dmai-lora-list");
396
+ list.setAttribute("role", "list");
397
  list.setAttribute("aria-label", "Ordered LoRA stack");
398
  let dragged = null;
399
  this.value.entries.forEach((entry, index) => {
400
  const row = el("div", `dmai-nodes-lora-row${entry.enabled ? "" : " dmai-off"}`);
401
  row.dataset.entryId = entry.id;
402
+ row.setAttribute("role", "listitem");
403
+ const main = el("div", "dmai-lora-main"),
404
+ controls = el("div", "dmai-lora-controls"),
405
+ actions = el("div", "dmai-lora-actions"),
406
+ order = el("span", "dmai-nodes-lora-order", String(index + 1).padStart(2, "0"));
407
+ order.setAttribute("aria-label", `Position ${index + 1}`);
408
  const grip = button(`Reorder ${entry.name}; Alt and arrow keys`, "grip");
409
  grip.classList.add("dmai-grip");
410
  grip.draggable = true;
 
442
  dragged = null;
443
  });
444
  const copy = el("div", "dmai-lora-copy"),
445
+ title = el("span", "dmai-nodes-lora-name", entry.name);
446
  title.title = entry.name;
447
+ copy.append(title);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  const toggle = button(
449
  `${entry.enabled ? "Disable" : "Enable"} ${entry.name}`,
450
  "power",
 
457
  );
458
  toggle.setAttribute("aria-pressed", String(entry.enabled));
459
  const arrows = el("div", "dmai-reorder");
460
+ arrows.setAttribute("role", "group");
461
+ arrows.setAttribute("aria-label", `Order for ${entry.name}`);
462
  const up = button(`Move ${entry.name} up`, "up", () =>
463
  this.move(entry.id, index - 1),
464
  ),
 
471
  const edit = button(`Edit ${entry.name}`, "sliders", () =>
472
  this.edit(entry),
473
  );
474
+ actions.append(toggle, edit);
475
+ main.append(grip, order, copy, actions);
476
+ controls.append(this.strengthField(entry, "model"), this.strengthField(entry, "clip"), arrows);
477
+ row.append(main, controls);
478
  list.append(row);
479
  });
480
  if (!this.value.entries.length)
481
  list.append(el("div", "dmai-empty", "Add a LoRA to build your stack."));
482
+ const add = button("Add LoRA", "plus", () => this.guard(() => this.library()), true);
483
+ add.classList.add("dmai-nodes-lora-add");
484
+ this.summary = el("div", "dmai-footnote");
485
+ this.body.append(list, add, this.summary);
486
+ this.updateSummary();
 
 
 
 
 
487
  }
488
  edit(entry) {
489
  if (!this.syncStrengths()) return;
490
  const d = this.show("LoRA settings"),
491
+ strengths = el("div", "dmai-lora-dialog-strengths"),
492
+ model = input("Model strength", "number", entry.strength_model),
493
+ clip = input("CLIP strength", "number", entry.strength_clip);
494
+ for (const field of [model, clip]) {
495
+ field.control.min = -20;
496
+ field.control.max = 20;
497
+ field.control.step = 0.05;
498
+ }
499
+ strengths.append(model.field, clip.field);
500
  const message = el("p", "dmai-status"),
501
  apply = button(
502
  "Apply",
503
  "check",
504
  () => {
505
+ const values = [model, clip].map((field) =>
506
+ field.control.value.trim() === "" ? NaN : Number(field.control.value),
507
+ );
508
+ if (values.some((value) => !Number.isFinite(value) || value < -20 || value > 20)) {
509
  statusMessage(
510
  message,
511
+ "Model and CLIP strength must each be from −20 to 20.",
512
  true,
513
  );
514
  return;
515
  }
516
+ [entry.strength_model, entry.strength_clip] = values;
517
  this.persist();
518
  this.build();
519
  d.root.close();
 
535
  );
536
  d.body.append(
537
  el("p", "dmai-filename", entry.name),
538
+ strengths,
539
  message,
540
  apply,
541
  remove,
web/nodes.css CHANGED
@@ -6,33 +6,23 @@
6
  font-display: swap;
7
  }
8
  .dmai-nodes {
9
- --dmai-panel: #202522;
10
- --dmai-inset: #191e1b;
11
- --dmai-text: #ecf0e9;
12
- --dmai-muted: #a3ada5;
13
- --dmai-line: #333d35;
14
- --dmai-accent: #c8ee99;
15
- --dmai-hover: #2a332d;
16
  color-scheme: dark;
17
  color: var(--dmai-text);
18
- font:
19
- 400 12px/1.5 "DMAI Inter",
20
- "Segoe UI",
21
- sans-serif;
22
- font-style: normal;
23
  box-sizing: border-box;
24
  text-align: left;
 
25
  }
26
- .dmai-nodes * {
27
- box-sizing: border-box;
28
- }
29
- .dmai-nodes [hidden] {
30
- display: none !important;
31
- }
32
- .dmai-nodes button,
33
- .dmai-nodes input,
34
- .dmai-nodes textarea,
35
- .dmai-nodes select {
36
  font: inherit;
37
  color: inherit;
38
  }
@@ -40,676 +30,198 @@
40
  cursor: pointer;
41
  border: 0;
42
  background: none;
 
43
  }
44
- .dmai-nodes button:disabled {
45
- opacity: 0.4;
46
- cursor: default;
47
- }
48
- .dmai-nodes button:hover:not(:disabled) {
49
- background: var(--dmai-hover);
50
- }
51
- .dmai-nodes button:focus-visible,
52
- .dmai-nodes input:focus-visible,
53
- .dmai-nodes select:focus-visible,
54
- .dmai-nodes textarea:focus-visible,
55
- .dmai-nodes summary:focus-visible {
56
  outline: 2px solid var(--dmai-accent);
57
- outline-offset: 2px;
58
  }
59
- .dmai-nodes input,
60
- .dmai-nodes textarea,
61
- .dmai-nodes select {
62
  background: var(--dmai-inset);
63
  border: 1px solid var(--dmai-line);
64
- border-radius: 7px;
65
- padding: 8px;
66
  min-width: 0;
67
  max-width: 100%;
 
68
  }
69
- .dmai-nodes input[aria-invalid="true"] {
70
- border-color: #edb89e;
71
- }
72
- .dmai-nodes input[type="number"] {
73
- appearance: textfield;
74
- }
75
- .dmai-nodes input::-webkit-inner-spin-button {
76
- appearance: none;
77
- }
78
  .dmai-nodes .icon {
79
- width: 17px;
80
- height: 17px;
81
- stroke: currentColor;
82
- stroke-width: 1.6;
83
- fill: none;
84
- stroke-linecap: round;
85
- stroke-linejoin: round;
86
- flex: none;
87
- vertical-align: middle;
88
  }
89
  .dmai-panel {
90
  background: var(--dmai-panel);
91
  border: 1px solid var(--dmai-line);
92
- border-radius: 12px;
93
- width: 100%;
94
- height: 100%;
95
- min-width: 0;
96
- overflow: auto;
97
- position: relative;
98
- scrollbar-width: thin;
99
- scrollbar-color: #4b584e transparent;
100
  }
101
  .dmai-nodes .dmai-head {
102
- height: 49px;
103
- display: flex;
104
- align-items: center;
105
- gap: 10px;
106
- padding: 0 15px;
107
- border-bottom: 1px solid var(--dmai-line);
108
- }
109
- .dmai-nodes .dmai-head h2 {
110
- font:
111
- 700 13px "DMAI Inter",
112
- sans-serif;
113
- letter-spacing: -0.1px;
114
- margin: 0;
115
- white-space: nowrap;
116
- }
117
- .dmai-nodes .dmai-glyph {
118
- color: var(--dmai-muted);
119
- }
120
- .dmai-nodes .dmai-brand {
121
- margin-left: auto;
122
- font-size: 8px;
123
- font-weight: 700;
124
- letter-spacing: 1.5px;
125
- color: var(--dmai-muted);
126
- }
127
- .dmai-nodes .dmai-body {
128
- padding: 14px;
129
- min-width: 0;
130
- }
131
  .dmai-nodes .dmai-icon {
132
- display: inline-flex;
133
- align-items: center;
134
- justify-content: center;
135
- width: 30px;
136
- height: 31px;
137
- border-radius: 6px;
138
- padding: 0;
139
- color: var(--dmai-muted);
140
- flex: none;
141
- }
142
- .dmai-nodes .dmai-icon:hover {
143
- color: var(--dmai-accent);
144
  }
 
145
  .dmai-nodes .dmai-button {
146
- display: inline-flex;
147
- align-items: center;
148
- justify-content: center;
149
- gap: 7px;
150
- padding: 8px 11px;
151
- border-radius: 7px;
152
- color: var(--dmai-muted);
153
- font-size: 11px;
154
  }
155
  .dmai-nodes .dmai-primary {
156
- background: var(--dmai-accent);
157
- color: #1d2c12;
158
- font-weight: 700;
159
- padding: 9px 14px;
160
- border-radius: 7px;
161
- display: inline-flex;
162
- align-items: center;
163
- justify-content: center;
164
- gap: 8px;
165
- }
166
- .dmai-nodes .dmai-primary:hover:not(:disabled) {
167
- background: #d6f7b0;
168
- color: #1d2c12;
169
- }
170
- .dmai-nodes .dmai-muted {
171
- color: var(--dmai-muted);
172
- font-size: 10px;
173
- }
174
- .dmai-nodes .dmai-row {
175
- display: flex;
176
- align-items: center;
177
- justify-content: space-between;
178
- gap: 8px;
179
- }
180
- .dmai-nodes .dmai-segmented {
181
- display: flex;
182
- align-items: center;
183
- background: var(--dmai-inset);
184
- border-radius: 7px;
185
- padding: 3px;
186
- gap: 3px;
187
- }
188
- .dmai-nodes .dmai-segmented button {
189
- padding: 6px 9px;
190
- border-radius: 5px;
191
- font-size: 10px;
192
- white-space: nowrap;
193
- }
194
- .dmai-nodes button[aria-pressed="true"] {
195
- color: var(--dmai-accent);
196
- }
197
- .dmai-nodes .dmai-segmented button[aria-pressed="true"] {
198
- background: var(--dmai-hover);
199
- }
200
- .dmai-nodes .dmai-segmented .icon {
201
- width: 12px;
202
- height: 12px;
203
- }
204
  .dmai-nodes .dmai-prompt {
205
- display: block;
206
- width: 100%;
207
- background: transparent;
208
- border: 0;
209
- resize: vertical;
210
- min-height: 100px;
211
- padding: 0;
212
- font-size: 12px;
213
- line-height: 1.7;
214
- }
215
- .dmai-nodes .dmai-ratios {
216
- display: flex;
217
- gap: 5px;
218
- margin: 12px 0;
219
  }
 
 
220
  .dmai-nodes .dmai-ratios .dmai-button {
221
- flex: 1;
222
- flex-direction: column;
223
- gap: 5px;
224
- padding: 7px 4px;
225
- font-size: 9px;
226
- border: 1px solid transparent;
227
- }
228
- .dmai-nodes .dmai-ratios .dmai-button[aria-pressed="true"] {
229
- border-color: var(--dmai-accent);
230
- background: #c8ee9909;
231
- }
232
- .dmai-nodes .dmai-ratio-shape {
233
- display: block;
234
- height: 15px;
235
- border: 1px solid currentColor;
236
- border-radius: 2px;
237
- max-width: 50px;
238
- }
239
- .dmai-nodes .dmai-resolution {
240
- display: flex;
241
- align-items: center;
242
- justify-content: space-between;
243
- gap: 7px;
244
- margin: 12px 0;
245
- }
246
- .dmai-nodes .dmai-dimensions {
247
- font-size: 9px;
248
- color: var(--dmai-muted);
249
- white-space: nowrap;
250
- }
251
- .dmai-nodes .dmai-count {
252
- border-top: 1px solid var(--dmai-line);
253
- padding-top: 8px;
254
- }
255
- .dmai-nodes .dmai-stepper {
256
- display: flex;
257
- align-items: center;
258
- gap: 6px;
259
- }
260
- .dmai-nodes .dmai-stepper output {
261
- min-width: 22px;
262
- text-align: center;
263
- font-weight: 700;
264
- }
265
- .dmai-nodes .dmai-details {
266
- border-top: 1px solid var(--dmai-line);
267
- padding-top: 10px;
268
- margin-top: 12px;
269
- }
270
- .dmai-nodes .dmai-details summary {
271
- cursor: pointer;
272
- color: var(--dmai-muted);
273
- font-size: 10px;
274
- }
275
- .dmai-nodes .dmai-details-body {
276
- padding-top: 10px;
277
- display: grid;
278
- gap: 10px;
279
- }
280
- .dmai-nodes .dmai-negative {
281
- width: 100%;
282
- min-height: 65px;
283
- resize: vertical;
284
- }
285
- .dmai-nodes .dmai-field {
286
- display: flex;
287
- flex-direction: column;
288
- gap: 6px;
289
- min-width: 0;
290
- }
291
- .dmai-nodes .dmai-field > span {
292
- font-size: 10px;
293
- color: var(--dmai-muted);
294
- }
295
- .dmai-nodes .dmai-field select,
296
- .dmai-nodes .dmai-field input {
297
- width: 100%;
298
- font-size: 11px;
299
- }
300
- .dmai-nodes .dmai-model select {
301
- font-size: 13px;
302
- font-weight: 700;
303
- padding: 12px;
304
- }
305
- .dmai-nodes .dmai-mode {
306
- margin: 15px 0;
307
- }
308
- .dmai-nodes .dmai-mode button {
309
- flex: 1;
310
- }
311
- .dmai-nodes .dmai-preset-row {
312
- display: flex;
313
- align-items: flex-end;
314
- gap: 5px;
315
- }
316
- .dmai-nodes .dmai-preset-row .dmai-field {
317
- flex: 1;
318
- }
319
- .dmai-nodes .dmai-preset-row > .dmai-icon {
320
- background: var(--dmai-inset);
321
- border: 1px solid var(--dmai-line);
322
- height: 34px;
323
- }
324
- .dmai-nodes .dmai-preset-meta {
325
- margin: 10px 0;
326
- }
327
- .dmai-nodes .dmai-badge {
328
- font-size: 9px;
329
- background: #c8ee9910;
330
- color: var(--dmai-accent);
331
- border-radius: 4px;
332
- padding: 3px 5px;
333
- }
334
- .dmai-nodes .dmai-engine-summary {
335
- font-size: 10px;
336
- color: var(--dmai-muted);
337
- margin: 12px 0;
338
- }
339
- .dmai-nodes .dmai-settings {
340
- display: grid;
341
- grid-template-columns: 1fr 1fr;
342
- gap: 10px;
343
- margin: 12px 0;
344
- }
345
- .dmai-nodes .dmai-seed {
346
- display: flex;
347
- gap: 7px;
348
- align-items: flex-end;
349
- border-top: 1px solid var(--dmai-line);
350
- padding-top: 12px;
351
- }
352
- .dmai-nodes .dmai-seed .dmai-field {
353
- flex: 1;
354
- }
355
- .dmai-nodes .dmai-generate {
356
- width: 100%;
357
- margin-top: 14px;
358
- }
359
- .dmai-nodes .dmai-lora-list {
360
- display: grid;
361
- gap: 3px;
362
- margin-bottom: 8px;
363
- }
364
- .dmai-nodes .dmai-nodes-lora-row {
365
- display: grid;
366
- grid-template-columns: 14px minmax(70px, 1fr) 46px 25px 18px 25px;
367
- align-items: center;
368
- gap: 5px;
369
- min-height: 65px;
370
- border-bottom: 1px solid var(--dmai-line);
371
- }
372
- .dmai-nodes .dmai-nodes-lora-row:last-child {
373
- border: 0;
374
- }
375
- .dmai-nodes .dmai-lora-copy {
376
- overflow: hidden;
377
- }
378
- .dmai-nodes .dmai-nodes-lora-name {
379
- display: block;
380
- white-space: nowrap;
381
- text-overflow: ellipsis;
382
- overflow: hidden;
383
- font-size: 11px;
384
- }
385
- .dmai-nodes .dmai-lora-copy .dmai-muted {
386
- display: block;
387
- font-size: 8px;
388
- margin-top: 5px;
389
- }
390
- .dmai-nodes .dmai-strength {
391
- width: 46px;
392
- font-size: 10px;
393
- padding: 7px 3px;
394
- text-align: center;
395
- }
396
- .dmai-nodes .dmai-nodes-lora-row > .dmai-icon {
397
- width: 25px;
398
- }
399
- .dmai-nodes .dmai-nodes-lora-row > .dmai-grip {
400
- width: 14px;
401
- color: #7d8c81;
402
- cursor: grab;
403
- }
404
- .dmai-nodes .dmai-grip .icon {
405
- width: 12px;
406
- }
407
- .dmai-nodes .dmai-reorder {
408
- display: flex;
409
- flex-direction: column;
410
- }
411
- .dmai-nodes .dmai-reorder .dmai-icon {
412
- height: 22px;
413
- width: 18px;
414
- }
415
- .dmai-nodes .dmai-reorder .icon {
416
- width: 13px;
417
- height: 13px;
418
- }
419
- .dmai-nodes .dmai-off {
420
- opacity: 0.52;
421
- }
422
- .dmai-nodes .dmai-dragging {
423
- opacity: 0.45;
424
- }
425
- .dmai-nodes .dmai-dragover {
426
- box-shadow: inset 0 2px var(--dmai-accent);
427
- }
428
- .dmai-nodes .dmai-footnote {
429
- font-size: 9px;
430
- color: var(--dmai-muted);
431
- border-top: 1px solid var(--dmai-line);
432
- margin-top: 12px;
433
- padding-top: 10px;
434
- }
435
- .dmai-nodes .dmai-empty {
436
- padding: 22px 8px;
437
- color: var(--dmai-muted);
438
- font-size: 11px;
439
- text-align: center;
440
- }
441
- .dmai-nodes .dmai-gallery-toolbar {
442
- margin: -4px 0 8px;
443
- }
444
- .dmai-nodes .dmai-gallery-scroll {
445
- max-height: 355px;
446
- overflow: auto;
447
- scrollbar-width: thin;
448
- scrollbar-color: #4b584e transparent;
449
- min-height: 150px;
450
- }
451
- .dmai-nodes .dmai-gallery-grid {
452
- display: grid;
453
- grid-template-columns: repeat(3, minmax(0, 1fr));
454
- gap: 7px;
455
- }
456
- .dmai-nodes .dmai-gallery-grid.dmai-dense {
457
- grid-template-columns: repeat(4, minmax(0, 1fr));
458
- gap: 4px;
459
- }
460
- .dmai-nodes .dmai-image-card {
461
- position: relative;
462
- border-radius: 8px;
463
- overflow: hidden;
464
- isolation: isolate;
465
- aspect-ratio: 4/5;
466
- background: var(--dmai-inset);
467
- }
468
- .dmai-nodes .dmai-image-select {
469
- display: block;
470
- width: 100%;
471
- height: 100%;
472
- padding: 0;
473
- position: relative;
474
- border: 2px solid transparent;
475
- border-radius: 8px;
476
- overflow: hidden;
477
- }
478
- .dmai-nodes .dmai-image-select[aria-pressed="true"] {
479
- border-color: var(--dmai-accent);
480
- }
481
- .dmai-nodes .dmai-image-select img {
482
- display: block;
483
- width: 100%;
484
- height: 100%;
485
- object-fit: cover;
486
- }
487
- .dmai-nodes .dmai-check {
488
- position: absolute;
489
- left: 6px;
490
- top: 6px;
491
- width: 17px;
492
- height: 17px;
493
- border: 1px solid #ffffff88;
494
- background: #12181270;
495
- color: #c8ee99;
496
- border-radius: 5px;
497
- font-size: 12px;
498
- line-height: 15px;
499
- text-align: center;
500
- }
501
- .dmai-nodes .dmai-image-select[aria-pressed="true"] .dmai-check {
502
- background: var(--dmai-accent);
503
- border-color: var(--dmai-accent);
504
- color: #1d2c12;
505
- }
506
- .dmai-nodes .dmai-image-actions {
507
- position: absolute;
508
- bottom: 6px;
509
- right: 6px;
510
- display: flex;
511
- background: #101714d9;
512
- backdrop-filter: blur(8px);
513
- padding: 2px;
514
- border: 1px solid #ffffff24;
515
- border-radius: 7px;
516
- }
517
- .dmai-nodes .dmai-image-actions .dmai-icon {
518
- width: 24px;
519
- height: 26px;
520
- color: #ecf0e9;
521
- }
522
- .dmai-nodes .dmai-image-actions .dmai-icon:last-child {
523
- color: var(--dmai-accent);
524
- }
525
- .dmai-nodes .dmai-image-actions .icon {
526
- width: 13px;
527
- height: 13px;
528
- }
529
- .dmai-nodes .dmai-image-actions .dmai-icon:hover {
530
- background: var(--dmai-accent);
531
- color: #1d2c12;
532
- }
533
- .dmai-nodes .dmai-selection {
534
- border-top: 1px solid var(--dmai-line);
535
- margin-top: 12px;
536
- padding-top: 10px;
537
- font-size: 10px;
538
- justify-content: flex-start;
539
- }
540
- .dmai-nodes .dmai-selection .dmai-primary {
541
- margin-left: auto;
542
- width: auto;
543
- font-size: 10px;
544
- }
545
- .dmai-nodes .dmai-status {
546
- font-size: 10px;
547
- padding: 10px 14px;
548
- color: var(--dmai-muted);
549
- overflow-wrap: anywhere;
550
- border-top: 1px solid var(--dmai-line);
551
- }
552
- .dmai-nodes .dmai-status[data-error="true"],
553
- .dmai-nodes .dmai-warning {
554
- color: #edb89e;
555
- font-size: 10px;
556
- }
557
- .dmai-nodes .dmai-raw {
558
- width: 100%;
559
- height: 200px;
560
- resize: vertical;
561
- }
562
- .dmai-dialog {
563
- background: var(--dmai-panel);
564
- border: 1px solid var(--dmai-line);
565
- border-radius: 14px;
566
- padding: 0;
567
- width: min(520px, calc(100vw - 30px));
568
- max-height: calc(100vh - 40px);
569
- box-shadow: 0 25px 70px #0008;
570
- }
571
- .dmai-dialog::backdrop {
572
- background: #080e0bb0;
573
- backdrop-filter: blur(4px);
574
- }
575
- .dmai-nodes .dmai-dialog-head {
576
- display: flex;
577
- align-items: center;
578
- justify-content: space-between;
579
- padding: 16px 20px;
580
- border-bottom: 1px solid var(--dmai-line);
581
- gap: 10px;
582
- }
583
- .dmai-nodes .dmai-dialog-head h2 {
584
- font-size: 15px;
585
- font-weight: 700;
586
- margin: 0;
587
- }
588
- .dmai-nodes .dmai-dialog-body {
589
- padding: 20px;
590
- display: grid;
591
- gap: 15px;
592
- }
593
- .dmai-nodes .dmai-format-grid {
594
- display: grid;
595
- grid-template-columns: repeat(4, 1fr);
596
- gap: 8px;
597
- }
598
- .dmai-nodes .dmai-format-grid .dmai-button {
599
- border: 1px solid var(--dmai-line);
600
- padding: 14px 5px;
601
- display: flex;
602
- flex-direction: column;
603
- }
604
- .dmai-nodes .dmai-custom-size {
605
- display: grid;
606
- grid-template-columns: 1fr 30px 1fr;
607
- gap: 12px;
608
- align-items: end;
609
- }
610
- .dmai-nodes .dmai-size-output {
611
- text-align: center;
612
- background: var(--dmai-inset);
613
- padding: 25px;
614
- color: var(--dmai-accent);
615
- font-size: 18px;
616
- font-weight: 700;
617
- border: 1px solid var(--dmai-line);
618
- border-radius: 9px;
619
- }
620
- .dmai-nodes .dmai-library {
621
- display: grid;
622
- gap: 5px;
623
- max-height: 350px;
624
- overflow: auto;
625
- }
626
- .dmai-nodes .dmai-library .dmai-button {
627
- justify-content: flex-start;
628
- text-align: left;
629
- overflow-wrap: anywhere;
630
- }
631
- .dmai-nodes .dmai-filename {
632
- overflow-wrap: anywhere;
633
- font-size: 11px;
634
- }
635
- .dmai-nodes .dmai-lightbox {
636
- display: block;
637
- width: 100%;
638
- height: auto;
639
- max-height: 65vh;
640
- object-fit: contain;
641
- border-radius: 8px;
642
- }
643
- @media (prefers-reduced-motion: reduce) {
644
- .dmai-nodes * {
645
- transition: none !important;
646
- animation: none !important;
647
- }
648
- }
649
-
650
- /* The outline stays inside the node so real Comfy sockets remain unobstructed. */
651
- .dmai-panel.dmai-prompter {
652
- overflow: hidden;
653
- }
654
- .dmai-nodes .dmai-prompter-scroll {
655
- width: 100%;
656
- height: 100%;
657
- overflow: auto;
658
- scrollbar-width: thin;
659
- scrollbar-color: #4b584e transparent;
660
- border-radius: inherit;
661
- }
662
- .dmai-nodes .dmai-progress-outline {
663
- position: absolute;
664
- inset: 0;
665
- width: 100%;
666
- height: 100%;
667
- pointer-events: none;
668
- z-index: 2;
669
- overflow: visible;
670
- fill: none;
671
- }
672
- .dmai-nodes .dmai-progress-track {
673
- stroke: var(--dmai-accent);
674
- stroke-width: 1;
675
- opacity: 0.13;
676
- }
677
- .dmai-nodes .dmai-progress-stroke {
678
- stroke: var(--dmai-accent);
679
- stroke-width: 2;
680
- stroke-linecap: round;
681
- transition: stroke-dashoffset 350ms ease-out;
682
- filter: drop-shadow(0 0 3px #c8ee9966);
683
- }
684
- .dmai-nodes[data-generating="true"] .dmai-progress-stroke {
685
- animation: dmai-progress-glow 2.4s ease-in-out infinite;
686
- }
687
- .dmai-nodes[data-generation-phase="queued"] .dmai-progress-stroke {
688
- transition: none;
689
- }
690
- .dmai-nodes .dmai-progress-readout {
691
- display: flex;
692
- justify-content: space-between;
693
- align-items: center;
694
- gap: 12px;
695
- margin-top: 9px;
696
- color: var(--dmai-muted);
697
- font-size: 10px;
698
- }
699
- .dmai-nodes .dmai-progress-value {
700
- font-variant-numeric: tabular-nums;
701
- color: var(--dmai-accent);
702
- }
703
- .dmai-nodes[data-generation-phase="error"] .dmai-progress-stroke,
704
- .dmai-nodes[data-generation-phase="interrupted"] .dmai-progress-stroke {
705
- stroke: #edb89e;
706
- filter: none;
707
- }
708
- .dmai-nodes[data-generation-phase="error"] .dmai-progress-value,
709
- .dmai-nodes[data-generation-phase="interrupted"] .dmai-progress-value {
710
- color: #edb89e;
711
- }
712
- @keyframes dmai-progress-glow {
713
- 0%, 100% { opacity: 0.72; }
714
- 50% { opacity: 1; }
715
- }
 
6
  font-display: swap;
7
  }
8
  .dmai-nodes {
9
+ --dmai-panel: #191d22;
10
+ --dmai-inset: #242931;
11
+ --dmai-text: #f5f7fa;
12
+ --dmai-muted: #b1b9c6;
13
+ --dmai-line: #363e49;
14
+ --dmai-accent: #c6f18d;
15
+ --dmai-hover: #303842;
16
  color-scheme: dark;
17
  color: var(--dmai-text);
18
+ font: 400 15px/1.5 "DMAI Inter", "Segoe UI", sans-serif;
 
 
 
 
19
  box-sizing: border-box;
20
  text-align: left;
21
+ -webkit-font-smoothing: antialiased;
22
  }
23
+ .dmai-nodes * { box-sizing: border-box; }
24
+ .dmai-nodes [hidden] { display: none !important; }
25
+ .dmai-nodes button, .dmai-nodes input, .dmai-nodes textarea, .dmai-nodes select {
 
 
 
 
 
 
 
26
  font: inherit;
27
  color: inherit;
28
  }
 
30
  cursor: pointer;
31
  border: 0;
32
  background: none;
33
+ transition: background 120ms ease, color 120ms ease;
34
  }
35
+ .dmai-nodes button:disabled { opacity: .4; cursor: default; }
36
+ .dmai-nodes button:hover:not(:disabled) { background: var(--dmai-hover); }
37
+ .dmai-nodes :is(button, input, select, textarea, summary):focus-visible {
 
 
 
 
 
 
 
 
 
38
  outline: 2px solid var(--dmai-accent);
39
+ outline-offset: 3px;
40
  }
41
+ .dmai-nodes input, .dmai-nodes textarea, .dmai-nodes select {
 
 
42
  background: var(--dmai-inset);
43
  border: 1px solid var(--dmai-line);
44
+ border-radius: 10px;
45
+ padding: 11px 13px;
46
  min-width: 0;
47
  max-width: 100%;
48
+ min-height: 44px;
49
  }
50
+ .dmai-nodes input[aria-invalid="true"] { border-color: #ffc3a6; }
51
+ .dmai-nodes input[type="number"] { appearance: textfield; }
52
+ .dmai-nodes input::-webkit-inner-spin-button { appearance: none; }
53
+ .dmai-nodes input::placeholder, .dmai-nodes textarea::placeholder { color: #8f99a7; opacity: 1; }
 
 
 
 
 
54
  .dmai-nodes .icon {
55
+ width: 20px; height: 20px; stroke: currentColor; stroke-width: 1.65;
56
+ fill: none; stroke-linecap: round; stroke-linejoin: round;
57
+ flex: none; vertical-align: middle;
 
 
 
 
 
 
58
  }
59
  .dmai-panel {
60
  background: var(--dmai-panel);
61
  border: 1px solid var(--dmai-line);
62
+ border-radius: 18px;
63
+ width: 100%; height: 100%; min-width: 0;
64
+ overflow: auto; position: relative;
65
+ scrollbar-width: thin; scrollbar-color: #566172 transparent;
 
 
 
 
66
  }
67
  .dmai-nodes .dmai-head {
68
+ min-height: 72px; display: flex; align-items: center; gap: 12px;
69
+ padding: 18px 22px;
70
+ }
71
+ .dmai-nodes .dmai-head h2 { font-size: 21px; font-weight: 700; letter-spacing: -.65px; margin: 0; line-height: 1.2; }
72
+ .dmai-nodes .dmai-glyph { color: var(--dmai-accent); display: flex; }
73
+ .dmai-nodes .dmai-glyph .icon { width: 22px; height: 22px; }
74
+ .dmai-nodes .dmai-brand { margin-left: auto; font-size: 10px; font-weight: 700; letter-spacing: 1.6px; color: var(--dmai-muted); }
75
+ .dmai-nodes .dmai-body { padding: 0 22px 22px; min-width: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  .dmai-nodes .dmai-icon {
77
+ display: inline-flex; align-items: center; justify-content: center;
78
+ width: 40px; height: 40px; border-radius: 10px; padding: 0;
79
+ color: var(--dmai-muted); flex: none;
 
 
 
 
 
 
 
 
 
80
  }
81
+ .dmai-nodes .dmai-icon:hover { color: var(--dmai-text); }
82
  .dmai-nodes .dmai-button {
83
+ display: inline-flex; align-items: center; justify-content: center;
84
+ gap: 8px; padding: 11px 15px; border-radius: 10px;
85
+ color: var(--dmai-text); font-size: 14px; min-height: 44px; font-weight: 600;
 
 
 
 
 
86
  }
87
  .dmai-nodes .dmai-primary {
88
+ background: var(--dmai-accent); color: #17220f; font-weight: 700;
89
+ min-height: 46px; padding: 11px 18px; border-radius: 11px;
90
+ display: inline-flex; align-items: center; justify-content: center; gap: 9px;
91
+ }
92
+ .dmai-nodes .dmai-primary:hover:not(:disabled) { background: #d8f8ad; color: #17220f; }
93
+ .dmai-nodes .dmai-muted { color: var(--dmai-muted); font-size: 13px; }
94
+ .dmai-nodes .dmai-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
95
+ .dmai-nodes .dmai-segmented { display: flex; align-items: center; background: #101419; border-radius: 12px; padding: 4px; gap: 4px; }
96
+ .dmai-nodes .dmai-segmented button { min-height: 38px; padding: 8px 13px; border-radius: 8px; font-size: 13px; white-space: nowrap; color: var(--dmai-muted); }
97
+ .dmai-nodes button[aria-pressed="true"] { color: var(--dmai-accent); }
98
+ .dmai-nodes .dmai-segmented button[aria-pressed="true"] { background: #353e46; color: var(--dmai-text); box-shadow: 0 1px 4px #0004; }
99
+ .dmai-nodes .dmai-segmented .icon { width: 16px; height: 16px; }
100
+ .dmai-nodes .dmai-section-label { display: block; font-size: 13px; font-weight: 600; color: var(--dmai-muted); margin-bottom: 10px; }
101
+ .dmai-nodes .dmai-prompt-field { display: flex; flex-direction: column; gap: 10px; }
102
+ .dmai-nodes .dmai-prompt-field > span { font-size: 13px; font-weight: 600; color: var(--dmai-muted); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  .dmai-nodes .dmai-prompt {
104
+ display: block; width: 100%; min-height: 160px; resize: vertical;
105
+ padding: 16px; font-size: 16px; line-height: 1.65;
106
+ background: #11161c; border-color: #3b4553; border-radius: 12px;
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
+ .dmai-nodes .dmai-format-section { margin-top: 22px; }
109
+ .dmai-nodes .dmai-ratios { display: flex; align-items: stretch; gap: 8px; margin: 10px 0 16px; }
110
  .dmai-nodes .dmai-ratios .dmai-button {
111
+ flex: 1; flex-direction: column; gap: 9px; min-height: 68px;
112
+ padding: 10px 4px; font-size: 13px; border: 1px solid var(--dmai-line);
113
+ background: var(--dmai-inset); font-weight: 500;
114
+ }
115
+ .dmai-nodes .dmai-ratios .dmai-button[aria-pressed="true"] { border-color: var(--dmai-accent); background: #c6f18d0d; }
116
+ .dmai-nodes .dmai-ratios .dmai-ratio-more { flex: .7; }
117
+ .dmai-nodes .dmai-ratio-shape { display: block; height: 21px; border: 1.5px solid currentColor; border-radius: 3px; max-width: 42px; }
118
+ .dmai-nodes .dmai-resolution { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 16px 0 20px; flex-wrap: wrap; }
119
+ .dmai-nodes .dmai-dimensions { font-size: 13px; color: var(--dmai-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
120
+ .dmai-nodes .dmai-count { border-top: 1px solid var(--dmai-line); padding-top: 16px; }
121
+ .dmai-nodes .dmai-count > span { font-size: 15px; color: var(--dmai-text); font-weight: 600; }
122
+ .dmai-nodes .dmai-stepper { display: flex; align-items: center; gap: 8px; background: var(--dmai-inset); border-radius: 10px; }
123
+ .dmai-nodes .dmai-stepper output { min-width: 30px; text-align: center; font-weight: 700; font-size: 16px; font-variant-numeric: tabular-nums; }
124
+ .dmai-nodes .dmai-details { border-top: 1px solid var(--dmai-line); padding-top: 16px; margin-top: 18px; }
125
+ .dmai-nodes .dmai-details summary { cursor: pointer; color: var(--dmai-muted); font-size: 13px; font-weight: 500; min-height: 24px; }
126
+ .dmai-nodes .dmai-details-body { padding-top: 14px; display: grid; gap: 14px; }
127
+ .dmai-nodes .dmai-negative { width: 100%; min-height: 90px; resize: vertical; }
128
+ .dmai-nodes .dmai-field { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
129
+ .dmai-nodes .dmai-field > span { font-size: 13px; font-weight: 500; color: var(--dmai-muted); }
130
+ .dmai-nodes .dmai-field select, .dmai-nodes .dmai-field input { width: 100%; font-size: 15px; }
131
+ .dmai-nodes .dmai-model select { font-size: 16px; font-weight: 600; min-height: 52px; }
132
+ .dmai-nodes .dmai-mode { margin: 22px 0; }
133
+ .dmai-nodes .dmai-mode button { flex: 1; }
134
+ .dmai-nodes.dmai-engine .dmai-details { margin-top: 14px; padding-top: 12px; border-top: 0; }
135
+ .dmai-nodes .dmai-enhanced { background: #c6f18d07; border: 1px solid #c6f18d35; padding: 18px; border-radius: 12px; margin: 18px 0; }
136
+ .dmai-nodes .dmai-enhanced-title { font-size: 16px; font-weight: 600; margin: 0 0 6px; }
137
+ .dmai-nodes .dmai-enhanced-copy { color: var(--dmai-muted); font-size: 13px; line-height: 1.6; margin: 0 0 14px; }
138
+ .dmai-nodes .dmai-enhanced-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px; }
139
+ .dmai-nodes .dmai-enhanced-actions .dmai-button { background: var(--dmai-inset); border: 1px solid var(--dmai-line); }
140
+ .dmai-nodes .dmai-badge { font-size: 12px; background: #c6f18d12; color: var(--dmai-accent); border-radius: 6px; padding: 5px 8px; }
141
+ .dmai-nodes .dmai-engine-summary { font-size: 14px; color: var(--dmai-muted); margin: 16px 0; }
142
+ .dmai-nodes .dmai-settings { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 20px 0; }
143
+ .dmai-nodes .dmai-field-wide { grid-column: 1 / -1; }
144
+ .dmai-nodes .dmai-seed { display: flex; gap: 10px; align-items: flex-end; border-top: 1px solid var(--dmai-line); padding-top: 20px; margin-top: 20px; }
145
+ .dmai-nodes .dmai-seed .dmai-field { flex: 1; }
146
+ .dmai-nodes .dmai-seed > .dmai-icon { height: 46px; width: 46px; background: var(--dmai-inset); }
147
+ .dmai-nodes .dmai-generate { width: 100%; margin-top: 20px; min-height: 52px; font-size: 16px; }
148
+ .dmai-nodes .dmai-lora-list { display: grid; gap: 12px; margin-bottom: 16px; max-height: 490px; overflow-y: auto; scrollbar-width: thin; padding: 1px; }
149
+ .dmai-nodes .dmai-nodes-lora-row { display: flex; flex-direction: column; gap: 16px; padding: 15px; border: 1px solid var(--dmai-line); border-radius: 13px; background: #22272e; min-width: 0; }
150
+ .dmai-nodes .dmai-lora-main { display: flex; align-items: center; gap: 8px; min-width: 0; }
151
+ .dmai-nodes .dmai-lora-copy { min-width: 0; flex: 1; }
152
+ .dmai-nodes .dmai-nodes-lora-name { display: block; overflow-wrap: anywhere; font-size: 15px; font-weight: 600; line-height: 1.4; }
153
+ .dmai-nodes .dmai-nodes-lora-order { font-size: 12px; font-weight: 600; color: var(--dmai-muted); font-variant-numeric: tabular-nums; flex: none; }
154
+ .dmai-nodes .dmai-lora-main > .dmai-grip { width: 20px; height: 36px; cursor: grab; color: #8793a5; }
155
+ .dmai-nodes .dmai-grip .icon { width: 16px; }
156
+ .dmai-nodes .dmai-lora-actions { display: flex; align-items: center; flex: none; gap: 2px; }
157
+ .dmai-nodes .dmai-lora-actions .dmai-icon { width: 34px; height: 36px; }
158
+ .dmai-nodes .dmai-lora-controls { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr) auto; gap: 12px; align-items: end; }
159
+ .dmai-nodes .dmai-lora-strength-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
160
+ .dmai-nodes .dmai-lora-strength-field > span { font-size: 12px; color: var(--dmai-muted); }
161
+ .dmai-nodes .dmai-strength { width: 100%; font-size: 15px; font-weight: 600; padding: 9px 12px; font-variant-numeric: tabular-nums; background: #14191f; }
162
+ .dmai-nodes .dmai-reorder { display: flex; gap: 2px; }
163
+ .dmai-nodes .dmai-reorder .dmai-icon { width: 32px; height: 44px; }
164
+ .dmai-nodes .dmai-nodes-lora-add { width: 100%; border: 1px solid var(--dmai-line); background: var(--dmai-inset); }
165
+ .dmai-nodes .dmai-off { opacity: .55; }
166
+ .dmai-nodes .dmai-dragging { opacity: .45; }
167
+ .dmai-nodes .dmai-dragover { border-color: var(--dmai-accent); box-shadow: 0 -2px var(--dmai-accent); }
168
+ .dmai-nodes .dmai-footnote { font-size: 12px; color: var(--dmai-muted); margin-top: 14px; }
169
+ .dmai-nodes .dmai-empty { padding: 34px 12px; color: var(--dmai-muted); font-size: 14px; text-align: center; }
170
+ .dmai-nodes .dmai-gallery-toolbar { margin: -5px 0 16px; }
171
+ .dmai-nodes .dmai-gallery-toolbar > span { font-size: 14px; }
172
+ .dmai-nodes .dmai-gallery-scroll { max-height: 430px; overflow: auto; scrollbar-width: thin; scrollbar-color: #566172 transparent; min-height: 230px; }
173
+ .dmai-nodes .dmai-gallery-grid { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 12px; }
174
+ .dmai-nodes .dmai-gallery-grid.dmai-dense { grid-template-columns: repeat(4,minmax(0,1fr)); gap: 8px; }
175
+ .dmai-nodes .dmai-image-card { position: relative; border-radius: 12px; overflow: hidden; isolation: isolate; aspect-ratio: 4/5; background: var(--dmai-inset); }
176
+ .dmai-nodes .dmai-image-select { display: block; width: 100%; height: 100%; padding: 0; position: relative; border: 2px solid transparent; border-radius: 12px; overflow: hidden; }
177
+ .dmai-nodes .dmai-image-select[aria-pressed="true"] { border-color: var(--dmai-accent); }
178
+ .dmai-nodes .dmai-image-select img { display: block; width: 100%; height: 100%; object-fit: cover; }
179
+ .dmai-nodes .dmai-check { position: absolute; left: 10px; top: 10px; width: 25px; height: 25px; border: 1px solid #ffffffaa; background: #121820a0; color: var(--dmai-accent); border-radius: 7px; font-size: 16px; line-height: 23px; text-align: center; }
180
+ .dmai-nodes .dmai-image-select[aria-pressed="true"] .dmai-check { background: var(--dmai-accent); border-color: var(--dmai-accent); color: #17220f; }
181
+ .dmai-nodes .dmai-image-actions { position: absolute; bottom: 10px; right: 10px; display: flex; gap: 3px; background: #10151edf; backdrop-filter: blur(8px); padding: 4px; border: 1px solid #ffffff30; border-radius: 11px; }
182
+ .dmai-nodes .dmai-image-actions .dmai-icon { width: 36px; height: 36px; color: #f5f7fa; border-radius: 7px; }
183
+ .dmai-nodes .dmai-image-actions .dmai-icon:last-child { color: var(--dmai-accent); }
184
+ .dmai-nodes .dmai-image-actions .icon { width: 19px; height: 19px; }
185
+ .dmai-nodes .dmai-image-actions .dmai-icon:hover { background: var(--dmai-accent); color: #17220f; }
186
+ .dmai-nodes .dmai-selection { border-top: 1px solid var(--dmai-line); margin-top: 18px; padding-top: 16px; font-size: 14px; justify-content: flex-start; }
187
+ .dmai-nodes .dmai-selection .dmai-primary { margin-left: auto; width: auto; font-size: 14px; }
188
+ .dmai-nodes .dmai-status { font-size: 13px; padding: 14px 22px; color: var(--dmai-muted); overflow-wrap: anywhere; border-top: 1px solid var(--dmai-line); }
189
+ .dmai-nodes .dmai-status[data-error="true"], .dmai-nodes .dmai-warning { color: #ffc3a6; font-size: 13px; }
190
+ .dmai-nodes .dmai-raw { width: 100%; height: 240px; resize: vertical; }
191
+ .dmai-dialog { background: var(--dmai-panel); border: 1px solid var(--dmai-line); border-radius: 18px; padding: 0; width: min(600px,calc(100vw - 32px)); max-height: calc(100vh - 40px); box-shadow: 0 28px 80px #0009; }
192
+ .dmai-dialog::backdrop { background: #090d14b8; backdrop-filter: blur(4px); }
193
+ .dmai-nodes .dmai-dialog-head { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid var(--dmai-line); gap: 12px; }
194
+ .dmai-nodes .dmai-dialog-head h2 { font-size: 20px; font-weight: 700; margin: 0; letter-spacing: -.3px; }
195
+ .dmai-nodes .dmai-dialog-body { padding: 24px; display: grid; gap: 18px; }
196
+ .dmai-nodes .dmai-format-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 10px; }
197
+ .dmai-nodes .dmai-format-grid .dmai-button { border: 1px solid var(--dmai-line); padding: 16px 5px; display: flex; flex-direction: column; gap: 10px; }
198
+ .dmai-nodes .dmai-custom-size { display: grid; grid-template-columns: 1fr 40px 1fr; gap: 12px; align-items: end; }
199
+ .dmai-nodes .dmai-size-output { text-align: center; background: var(--dmai-inset); padding: 28px; color: var(--dmai-accent); font-size: 24px; font-weight: 700; border: 1px solid var(--dmai-line); border-radius: 12px; }
200
+ .dmai-nodes .dmai-library { display: grid; gap: 8px; max-height: 400px; overflow: auto; }
201
+ .dmai-nodes .dmai-library .dmai-button { justify-content: flex-start; text-align: left; overflow-wrap: anywhere; padding: 14px; background: var(--dmai-inset); }
202
+ .dmai-nodes .dmai-filename { overflow-wrap: anywhere; font-size: 14px; }
203
+ .dmai-nodes .dmai-lightbox { display: block; width: 100%; height: auto; max-height: 65vh; object-fit: contain; border-radius: 12px; }
204
+ /* Keep animation within the node; native socket hit targets remain unobstructed. */
205
+ .dmai-panel.dmai-prompter { overflow: hidden; }
206
+ .dmai-nodes .dmai-prompter-scroll { width: 100%; height: 100%; overflow: auto; scrollbar-width: thin; scrollbar-color: #566172 transparent; border-radius: inherit; }
207
+ .dmai-nodes .dmai-progress-outline { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; overflow: visible; fill: none; }
208
+ .dmai-nodes .dmai-progress-track { stroke: var(--dmai-accent); stroke-width: 1; opacity: .13; }
209
+ .dmai-nodes .dmai-progress-stroke { stroke: var(--dmai-accent); stroke-width: 2; stroke-linecap: round; transition: stroke-dashoffset 350ms ease-out; filter: drop-shadow(0 0 3px #c6f18d66); }
210
+ .dmai-nodes[data-generating="true"] .dmai-progress-stroke { animation: dmai-progress-glow 2.4s ease-in-out infinite; }
211
+ .dmai-nodes[data-generation-phase="queued"] .dmai-progress-stroke { transition: none; }
212
+ .dmai-nodes .dmai-progress-readout { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 12px; color: var(--dmai-muted); font-size: 13px; }
213
+ .dmai-nodes .dmai-progress-value { font-variant-numeric: tabular-nums; color: var(--dmai-accent); }
214
+ .dmai-nodes[data-generation-phase="error"] .dmai-progress-stroke, .dmai-nodes[data-generation-phase="interrupted"] .dmai-progress-stroke { stroke: #ffc3a6; filter: none; }
215
+ .dmai-nodes[data-generation-phase="error"] .dmai-progress-value, .dmai-nodes[data-generation-phase="interrupted"] .dmai-progress-value { color: #ffc3a6; }
216
+ @keyframes dmai-progress-glow { 0%,100% { opacity: .72; } 50% { opacity: 1; } }
217
+ @media (prefers-reduced-motion: reduce) { .dmai-nodes * { transition: none !important; animation: none !important; } }
218
+ /* The image grid takes the remaining height; downloads stay visible. */
219
+ .dmai-panel.dmai-gallery { display: flex; flex-direction: column; overflow: hidden; }
220
+ .dmai-nodes.dmai-gallery > .dmai-head { flex: none; }
221
+ .dmai-nodes.dmai-gallery > .dmai-body { flex: 1; display: flex; flex-direction: column; min-height: 0; }
222
+ .dmai-nodes.dmai-gallery .dmai-gallery-toolbar, .dmai-nodes.dmai-gallery .dmai-selection { flex: none; }
223
+ .dmai-nodes.dmai-gallery .dmai-gallery-scroll { flex: 1; min-height: 120px; max-height: none; }
224
+ .dmai-nodes.dmai-gallery > .dmai-status { flex: none; }
225
+ .dmai-nodes .dmai-lora-dialog-strengths { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
226
+ .dmai-nodes .dmai-enhanced-summary { margin: 10px 0 3px; font-size: 14px; font-weight: 600; }
227
+ .dmai-nodes .dmai-enhanced-sampling { margin: 0; font-size: 13px; color: var(--dmai-muted); overflow-wrap: anywhere; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/progress.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { PROMPTER_TYPE } from "./workflow-migration.mjs";
2
 
3
  // Job identity comes from the submitted API graph, never the current selection.
4
  const IDLE = Object.freeze({ phase: "idle", percent: 0 });
 
1
+ import { PROMPTER_TYPE } from "./workflow-migration.mjs?v=0.2.0";
2
 
3
  // Job identity comes from the submitted API graph, never the current selection.
4
  const IDLE = Object.freeze({ phase: "idle", percent: 0 });
web/workflow-migration.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { readPrompt } from "./core.mjs";
2
 
3
  export const PROMPTER_TYPE = "DMAINodesPrompter";
4
  const PREVIEW_PROMPTER_TYPE = "DMAIPrompter";
 
1
+ import { readPrompt } from "./core.mjs?v=0.2.0";
2
 
3
  export const PROMPTER_TYPE = "DMAINodesPrompter";
4
  const PREVIEW_PROMPTER_TYPE = "DMAIPrompter";
workflows/DMAI-NODES-FLUX.json CHANGED
@@ -6,12 +6,12 @@
6
  "id": 1,
7
  "type": "DMAINodesPrompter",
8
  "pos": [
9
- 40,
10
- 100
11
  ],
12
  "size": [
13
- 410,
14
- 475
15
  ],
16
  "flags": {},
17
  "order": 0,
@@ -44,12 +44,12 @@
44
  "id": 2,
45
  "type": "DMAILoRAStack",
46
  "pos": [
47
- 40,
48
- 660
49
  ],
50
  "size": [
51
- 410,
52
- 380
53
  ],
54
  "flags": {},
55
  "order": 1,
@@ -76,12 +76,12 @@
76
  "id": 3,
77
  "type": "DMAIGenerationEngine",
78
  "pos": [
79
- 570,
80
- 220
81
  ],
82
  "size": [
83
- 420,
84
- 700
85
  ],
86
  "flags": {},
87
  "order": 2,
@@ -167,12 +167,12 @@
167
  "id": 4,
168
  "type": "DMAIGallery",
169
  "pos": [
170
- 1100,
171
- 180
172
  ],
173
  "size": [
174
- 500,
175
- 720
176
  ],
177
  "flags": {},
178
  "order": 3,
@@ -245,8 +245,8 @@
245
  "ds": {
246
  "scale": 0.7,
247
  "offset": [
248
- 45,
249
- 20
250
  ]
251
  }
252
  },
 
6
  "id": 1,
7
  "type": "DMAINodesPrompter",
8
  "pos": [
9
+ 80,
10
+ 60
11
  ],
12
  "size": [
13
+ 504,
14
+ 738
15
  ],
16
  "flags": {},
17
  "order": 0,
 
44
  "id": 2,
45
  "type": "DMAILoRAStack",
46
  "pos": [
47
+ 80,
48
+ 880
49
  ],
50
  "size": [
51
+ 524,
52
+ 568
53
  ],
54
  "flags": {},
55
  "order": 1,
 
76
  "id": 3,
77
  "type": "DMAIGenerationEngine",
78
  "pos": [
79
+ 804,
80
+ 180
81
  ],
82
  "size": [
83
+ 484,
84
+ 748
85
  ],
86
  "flags": {},
87
  "order": 2,
 
167
  "id": 4,
168
  "type": "DMAIGallery",
169
  "pos": [
170
+ 1488,
171
+ 160
172
  ],
173
  "size": [
174
+ 804,
175
+ 618
176
  ],
177
  "flags": {},
178
  "order": 3,
 
245
  "ds": {
246
  "scale": 0.7,
247
  "offset": [
248
+ 25,
249
+ 25
250
  ]
251
  }
252
  },
workflows/DMAI-NODES-Krea.api.json CHANGED
@@ -22,7 +22,7 @@
22
  "2",
23
  0
24
  ],
25
- "config_json": "{\"schema_version\": 1, \"profile_id\": \"krea2-turbo\", \"mode\": \"enhanced\", \"preset_id\": \"dmai-krea2-original-v1\", \"settings\": {\"steps\": 8, \"cfg\": 1.1, \"sampler\": \"euler\", \"scheduler\": \"beta\", \"denoise\": 1, \"enhancer\": \"krea2t\"}, \"seed\": 481516, \"models\": {\"diffusion_model\": \"krea2_turbo_fp8.safetensors\", \"text_encoder\": \"qwen3vl_4b_fp8_scaled.safetensors\", \"vae\": \"wan_2.1_vae.safetensors\", \"checkpoint\": \"\"}, \"presets\": []}"
26
  }
27
  },
28
  "4": {
 
22
  "2",
23
  0
24
  ],
25
+ "config_json": "{\"schema_version\": 1, \"profile_id\": \"krea2-turbo\", \"mode\": \"manual\", \"preset_id\": \"\", \"settings\": {\"steps\": 8, \"cfg\": 1.1, \"sampler\": \"euler\", \"scheduler\": \"beta\", \"denoise\": 1, \"enhancer\": \"none\"}, \"seed\": 481516, \"models\": {\"diffusion_model\": \"krea2_turbo_fp8.safetensors\", \"text_encoder\": \"qwen3vl_4b_fp8_scaled.safetensors\", \"vae\": \"wan_2.1_vae.safetensors\", \"checkpoint\": \"\"}, \"presets\": []}"
26
  }
27
  },
28
  "4": {
workflows/DMAI-NODES-Krea.json CHANGED
@@ -6,12 +6,12 @@
6
  "id": 1,
7
  "type": "DMAINodesPrompter",
8
  "pos": [
9
- 40,
10
- 100
11
  ],
12
  "size": [
13
- 410,
14
- 475
15
  ],
16
  "flags": {},
17
  "order": 0,
@@ -44,12 +44,12 @@
44
  "id": 2,
45
  "type": "DMAILoRAStack",
46
  "pos": [
47
- 40,
48
- 660
49
  ],
50
  "size": [
51
- 410,
52
- 380
53
  ],
54
  "flags": {},
55
  "order": 1,
@@ -76,12 +76,12 @@
76
  "id": 3,
77
  "type": "DMAIGenerationEngine",
78
  "pos": [
79
- 570,
80
- 220
81
  ],
82
  "size": [
83
- 420,
84
- 700
85
  ],
86
  "flags": {},
87
  "order": 2,
@@ -160,19 +160,19 @@
160
  "Node name for S&R": "DMAIGenerationEngine"
161
  },
162
  "widgets_values": [
163
- "{\"schema_version\": 1, \"profile_id\": \"krea2-turbo\", \"mode\": \"enhanced\", \"preset_id\": \"dmai-krea2-original-v1\", \"settings\": {\"steps\": 8, \"cfg\": 1.1, \"sampler\": \"euler\", \"scheduler\": \"beta\", \"denoise\": 1, \"enhancer\": \"krea2t\"}, \"seed\": 481516, \"models\": {\"diffusion_model\": \"krea2_turbo_fp8.safetensors\", \"text_encoder\": \"qwen3vl_4b_fp8_scaled.safetensors\", \"vae\": \"wan_2.1_vae.safetensors\", \"checkpoint\": \"\"}, \"presets\": []}"
164
  ]
165
  },
166
  {
167
  "id": 4,
168
  "type": "DMAIGallery",
169
  "pos": [
170
- 1100,
171
- 180
172
  ],
173
  "size": [
174
- 500,
175
- 720
176
  ],
177
  "flags": {},
178
  "order": 3,
@@ -245,8 +245,8 @@
245
  "ds": {
246
  "scale": 0.7,
247
  "offset": [
248
- 45,
249
- 20
250
  ]
251
  }
252
  },
 
6
  "id": 1,
7
  "type": "DMAINodesPrompter",
8
  "pos": [
9
+ 80,
10
+ 60
11
  ],
12
  "size": [
13
+ 504,
14
+ 738
15
  ],
16
  "flags": {},
17
  "order": 0,
 
44
  "id": 2,
45
  "type": "DMAILoRAStack",
46
  "pos": [
47
+ 80,
48
+ 880
49
  ],
50
  "size": [
51
+ 524,
52
+ 568
53
  ],
54
  "flags": {},
55
  "order": 1,
 
76
  "id": 3,
77
  "type": "DMAIGenerationEngine",
78
  "pos": [
79
+ 804,
80
+ 180
81
  ],
82
  "size": [
83
+ 484,
84
+ 818
85
  ],
86
  "flags": {},
87
  "order": 2,
 
160
  "Node name for S&R": "DMAIGenerationEngine"
161
  },
162
  "widgets_values": [
163
+ "{\"schema_version\": 1, \"profile_id\": \"krea2-turbo\", \"mode\": \"manual\", \"preset_id\": \"\", \"settings\": {\"steps\": 8, \"cfg\": 1.1, \"sampler\": \"euler\", \"scheduler\": \"beta\", \"denoise\": 1, \"enhancer\": \"none\"}, \"seed\": 481516, \"models\": {\"diffusion_model\": \"krea2_turbo_fp8.safetensors\", \"text_encoder\": \"qwen3vl_4b_fp8_scaled.safetensors\", \"vae\": \"wan_2.1_vae.safetensors\", \"checkpoint\": \"\"}, \"presets\": []}"
164
  ]
165
  },
166
  {
167
  "id": 4,
168
  "type": "DMAIGallery",
169
  "pos": [
170
+ 1488,
171
+ 160
172
  ],
173
  "size": [
174
+ 804,
175
+ 618
176
  ],
177
  "flags": {},
178
  "order": 3,
 
245
  "ds": {
246
  "scale": 0.7,
247
  "offset": [
248
+ 25,
249
+ 25
250
  ]
251
  }
252
  },
workflows/DMAI-NODES-Qwen.json CHANGED
@@ -6,12 +6,12 @@
6
  "id": 1,
7
  "type": "DMAINodesPrompter",
8
  "pos": [
9
- 40,
10
- 100
11
  ],
12
  "size": [
13
- 410,
14
- 475
15
  ],
16
  "flags": {},
17
  "order": 0,
@@ -44,12 +44,12 @@
44
  "id": 2,
45
  "type": "DMAILoRAStack",
46
  "pos": [
47
- 40,
48
- 660
49
  ],
50
  "size": [
51
- 410,
52
- 380
53
  ],
54
  "flags": {},
55
  "order": 1,
@@ -76,12 +76,12 @@
76
  "id": 3,
77
  "type": "DMAIGenerationEngine",
78
  "pos": [
79
- 570,
80
- 220
81
  ],
82
  "size": [
83
- 420,
84
- 700
85
  ],
86
  "flags": {},
87
  "order": 2,
@@ -167,12 +167,12 @@
167
  "id": 4,
168
  "type": "DMAIGallery",
169
  "pos": [
170
- 1100,
171
- 180
172
  ],
173
  "size": [
174
- 500,
175
- 720
176
  ],
177
  "flags": {},
178
  "order": 3,
@@ -245,8 +245,8 @@
245
  "ds": {
246
  "scale": 0.7,
247
  "offset": [
248
- 45,
249
- 20
250
  ]
251
  }
252
  },
 
6
  "id": 1,
7
  "type": "DMAINodesPrompter",
8
  "pos": [
9
+ 80,
10
+ 60
11
  ],
12
  "size": [
13
+ 504,
14
+ 738
15
  ],
16
  "flags": {},
17
  "order": 0,
 
44
  "id": 2,
45
  "type": "DMAILoRAStack",
46
  "pos": [
47
+ 80,
48
+ 880
49
  ],
50
  "size": [
51
+ 524,
52
+ 568
53
  ],
54
  "flags": {},
55
  "order": 1,
 
76
  "id": 3,
77
  "type": "DMAIGenerationEngine",
78
  "pos": [
79
+ 804,
80
+ 180
81
  ],
82
  "size": [
83
+ 484,
84
+ 748
85
  ],
86
  "flags": {},
87
  "order": 2,
 
167
  "id": 4,
168
  "type": "DMAIGallery",
169
  "pos": [
170
+ 1488,
171
+ 160
172
  ],
173
  "size": [
174
+ 804,
175
+ 618
176
  ],
177
  "flags": {},
178
  "order": 3,
 
245
  "ds": {
246
  "scale": 0.7,
247
  "offset": [
248
+ 25,
249
+ 25
250
  ]
251
  }
252
  },
workflows/DMAI-NODES-SDXL.json CHANGED
@@ -6,12 +6,12 @@
6
  "id": 1,
7
  "type": "DMAINodesPrompter",
8
  "pos": [
9
- 40,
10
- 100
11
  ],
12
  "size": [
13
- 410,
14
- 475
15
  ],
16
  "flags": {},
17
  "order": 0,
@@ -44,12 +44,12 @@
44
  "id": 2,
45
  "type": "DMAILoRAStack",
46
  "pos": [
47
- 40,
48
- 660
49
  ],
50
  "size": [
51
- 410,
52
- 380
53
  ],
54
  "flags": {},
55
  "order": 1,
@@ -76,12 +76,12 @@
76
  "id": 3,
77
  "type": "DMAIGenerationEngine",
78
  "pos": [
79
- 570,
80
- 220
81
  ],
82
  "size": [
83
- 420,
84
- 700
85
  ],
86
  "flags": {},
87
  "order": 2,
@@ -167,12 +167,12 @@
167
  "id": 4,
168
  "type": "DMAIGallery",
169
  "pos": [
170
- 1100,
171
- 180
172
  ],
173
  "size": [
174
- 500,
175
- 720
176
  ],
177
  "flags": {},
178
  "order": 3,
@@ -245,8 +245,8 @@
245
  "ds": {
246
  "scale": 0.7,
247
  "offset": [
248
- 45,
249
- 20
250
  ]
251
  }
252
  },
 
6
  "id": 1,
7
  "type": "DMAINodesPrompter",
8
  "pos": [
9
+ 80,
10
+ 60
11
  ],
12
  "size": [
13
+ 504,
14
+ 738
15
  ],
16
  "flags": {},
17
  "order": 0,
 
44
  "id": 2,
45
  "type": "DMAILoRAStack",
46
  "pos": [
47
+ 80,
48
+ 880
49
  ],
50
  "size": [
51
+ 524,
52
+ 568
53
  ],
54
  "flags": {},
55
  "order": 1,
 
76
  "id": 3,
77
  "type": "DMAIGenerationEngine",
78
  "pos": [
79
+ 804,
80
+ 180
81
  ],
82
  "size": [
83
+ 484,
84
+ 748
85
  ],
86
  "flags": {},
87
  "order": 2,
 
167
  "id": 4,
168
  "type": "DMAIGallery",
169
  "pos": [
170
+ 1488,
171
+ 160
172
  ],
173
  "size": [
174
+ 804,
175
+ 618
176
  ],
177
  "flags": {},
178
  "order": 3,
 
245
  "ds": {
246
  "scale": 0.7,
247
  "offset": [
248
+ 25,
249
+ 25
250
  ]
251
  }
252
  },