Compare commits

...

97 Commits

Author SHA1 Message Date
Riccardo Giovanetti
7adac4581a translationBot(ui): update translation (Italian)
Currently translated at 98.7% (1800 of 1822 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.7% (1798 of 1820 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.7% (1796 of 1818 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-03-17 10:49:22 +11:00
Hosted Weblate
962db86cac translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2025-03-17 10:49:22 +11:00
psychedelicious
d65ec0e250 feat(ui): configurable form field constraints (WIP3) 2025-03-17 10:47:01 +11:00
psychedelicious
7fdde5e84a tests(ui): fix constrainNumber 2025-03-17 10:47:01 +11:00
psychedelicious
895956bcfe chore(ui): lint 2025-03-17 10:47:01 +11:00
psychedelicious
f27d26cfa2 feat(ui): configurable form field constraints (WIP2) 2025-03-17 10:47:01 +11:00
psychedelicious
965bcba6c2 feat(ui): configurable form field constraints (WIP) 2025-03-17 10:47:01 +11:00
psychedelicious
c9f2460ff2 fix(ui): generator widget should stretch to fill when added to builder 2025-03-17 10:41:59 +11:00
psychedelicious
5abbbf4b5b feat(ui): allow pasting images on workflows tab when workflows not focused 2025-03-17 10:37:27 +11:00
psychedelicious
e66688edbf feat(ui): only paste into canvas when canvas is focused 2025-03-17 10:37:27 +11:00
joshistoast
a519483f95 refactor(ui): ♻️ memoize merged styles, simplify data attribute conditional 2025-03-17 10:34:49 +11:00
joshistoast
75c91604bb fix: 🐛 export the region wrapper
am silly
2025-03-17 10:34:49 +11:00
joshistoast
53bdaba7b6 style: 🚨 linting 2025-03-17 10:34:49 +11:00
joshistoast
f3f405ca77 refactor(ui): ♻️ remove forward ref usage 2025-03-17 10:34:49 +11:00
joshistoast
dda69950a7 refactor(ui): ♻️ apply memoization, system style objects, and data attribute to region highlight wrapper 2025-03-17 10:34:49 +11:00
joshistoast
b2198b9fa7 feat: 🔧 region highlighting disabled by default
some users may not like this
2025-03-17 10:34:49 +11:00
joshistoast
02b91e8e7b feat: highlight focused regions
adds a region wrapper with a highlight effect when that region is focused, this behavior can be toggled as a setting
2025-03-17 10:34:49 +11:00
psychedelicious
09bf7c35eb chore(ui): typegen 2025-03-17 10:32:19 +11:00
psychedelicious
deb9a65b3d chore(ui): update whats new 2025-03-17 10:32:19 +11:00
psychedelicious
5be9a7227c chore: remove all explicit image references in default workflows 2025-03-17 10:32:19 +11:00
psychedelicious
bb9f886bd4 docs: update default workflows dev docs 2025-03-17 10:32:19 +11:00
psychedelicious
46520946f8 chore: remove all explicit model references in default workflows 2025-03-17 10:32:19 +11:00
psychedelicious
830880a6fc chore(nodes): update titles of all model-specific nodes to reference their models
Also bump versions on all of them.
2025-03-17 10:32:19 +11:00
psychedelicious
63b94a8ff3 feat(ui): add sd3.5 default workflows tag 2025-03-17 10:32:19 +11:00
psychedelicious
f12924a1e1 chore: update default workflow tags & names 2025-03-17 10:32:19 +11:00
psychedelicious
f8e51c86f5 chore: bump version to v5.8.0 2025-03-17 10:32:19 +11:00
psychedelicious
c84a646735 ci: pin tj-actions/changed-files
Closes #7793
2025-03-17 08:36:17 +11:00
psychedelicious
b52f8121af fix(ui): duplicate edges on reconnect
Closes #7127
2025-03-15 10:12:50 +11:00
psychedelicious
05bed3fddd fix(ui): do not mark workflow as touched when setting form field initial values 2025-03-15 10:10:21 +11:00
psychedelicious
87ea20192f chore(ui): knip 2025-03-14 20:54:58 +11:00
psychedelicious
2f9c95c462 fix(ui): return early in error-selecting hooks
Prevent an error when a node is deleted and the hook is being called
2025-03-14 20:54:58 +11:00
psychedelicious
47cadbb48e feat(ui): show field errors in tooltips 2025-03-14 20:54:58 +11:00
psychedelicious
23518b9830 feat(ui): useDebouncedAppSelector
Hook that replicates `useSelector`, but debounces calling the selector.
2025-03-14 20:54:58 +11:00
psychedelicious
94dcf391a6 tweak(ui): styling for image collection fields 2025-03-14 20:50:35 +11:00
psychedelicious
e7a60c01ed fix(ui): prevent vertical scrolling on row containers 2025-03-14 07:15:58 +11:00
Mary Hipp
4b54ccc29c getting started copy for workflows 2025-03-13 12:25:14 -04:00
Mary Hipp
c4183ec98c add with_hash to prevent rerenders on default 2025-03-13 10:29:22 -04:00
Mary Hipp
5a9cbe35e0 typegen fix 2025-03-13 10:29:22 -04:00
Mary Hipp
df18fe0298 make sure that recent view always sorts by opened_at even if not available as sort option in UI 2025-03-13 10:29:22 -04:00
Mary Hipp
e5591d145f allow workflow sort options to be passed in 2025-03-13 08:27:51 -04:00
psychedelicious
371c187fc3 chore: bump version to v5.8.0rc1 2025-03-13 23:00:01 +11:00
psychedelicious
e982c95687 fix(ui): respect line breaks in builder text and heading elements 2025-03-13 09:39:41 +11:00
psychedelicious
0eeb0dd67b feat(ui): use invoke logo for thumbnail fallback for default workflows 2025-03-13 08:45:12 +11:00
psychedelicious
28c74cbe38 revert(app): remove test image from default workflow thumbnails 2025-03-13 08:45:12 +11:00
psychedelicious
7414f68acc fix(ui): save as marks workflow as not touched 2025-03-13 08:45:12 +11:00
psychedelicious
a984462b80 tweak(ui): workflow library card layout to fit 2 lines of title and 3 lines of desc 2025-03-13 08:45:12 +11:00
psychedelicious
c6c2567203 tweak(ui): workflow description shows 1 line w/ tooltip for full content 2025-03-13 08:45:12 +11:00
psychedelicious
f05c8b909f fix(ui): mark workflow touched on form builder state changes 2025-03-13 07:10:59 +11:00
psychedelicious
73330a1308 chore(ui): lint 2025-03-13 07:10:59 +11:00
psychedelicious
6f568d48ed fix(ui): studio init action workflow loading 2025-03-13 07:10:59 +11:00
psychedelicious
81a97f3796 fix(ui): load workflow from object 2025-03-13 07:10:59 +11:00
psychedelicious
3f9535d2f9 fix(ui): load workflow from graph 2025-03-13 07:10:59 +11:00
psychedelicious
83bfbdcad4 feat(ui): more workflow loading standardization
There is now a single entrypoint for loading a workflow - `useLoadWorkflowWithDialog`.

The hook:
Handles loading workflows from various sources. If there are unsaved changes, the user will be prompted to confirm before loading the workflow.

It returns  a function that:
Loads a workflow from various sources. If there are unsaved changes, the user will be prompted to confirm before loading the workflow. The workflow will be loaded immediately if there are no unsaved changes. On success, error or completion, the corresponding callback will be called.

WHEW
2025-03-13 07:10:59 +11:00
psychedelicious
729428084c feat(ui): prompt when loading workflow from file if unsaved changes 2025-03-13 07:10:59 +11:00
psychedelicious
523a932ecc feat(ui): accept button on workflow load dialog is "Load" 2025-03-13 07:10:59 +11:00
psychedelicious
21be7d7157 feat(ui): allow load workflow confirm dialog to load workflows from object instead of only id 2025-03-13 07:10:59 +11:00
psychedelicious
a29fb18c0b feat(ui): standardize and clean up workflow loading hooks and logic 2025-03-13 07:10:59 +11:00
psychedelicious
aed446f013 fix(ui): make the workflow load from file menu item work the same as the button in library
Upload and save as instead of just upload as draft.
2025-03-13 07:10:59 +11:00
Mary Hipp
e81c9b0d6e add default for opened_at 2025-03-12 14:35:34 -04:00
psychedelicious
89f457c486 fix(ui): mark workflow as opened when creating a new workflow 2025-03-12 12:11:00 +11:00
psychedelicious
30ed09a36e fix(ui): default categories for oss 2025-03-12 12:11:00 +11:00
psychedelicious
3334652acc feat(db): drop the opened_at column instead of marking deprecated 2025-03-12 12:11:00 +11:00
psychedelicious
e83536f396 chore(ui): lint 2025-03-12 12:11:00 +11:00
psychedelicious
97593f95f6 feat(ui): on first load, if the selected library view has no workflows, switch to the first view that has workflows 2025-03-12 12:11:00 +11:00
psychedelicious
7f14cee17e chore(ui): typegen 2025-03-12 12:11:00 +11:00
psychedelicious
0a836d6fc1 feat(app): add method and route to get workflow library counts by category 2025-03-12 12:11:00 +11:00
psychedelicious
54e781d5bb tidy(app): remove unused method in workflow records service 2025-03-12 12:11:00 +11:00
psychedelicious
aa71d0c817 tweak(ui): 'is_recent' -> 'has_been_opened' 2025-03-12 12:11:00 +11:00
psychedelicious
07313e429d chore(ui): typegen 2025-03-12 12:11:00 +11:00
psychedelicious
bad5023238 tweak(app): 'is_recent' -> 'has_been_opened' 2025-03-12 12:11:00 +11:00
psychedelicious
73a0d2c06c fix(ui): memo WorkflowLibraryModal 2025-03-12 12:11:00 +11:00
psychedelicious
918e9c8ccc feat(app): drop and recreate index on opened_at
Not sure if this is strictly required but doing it anyways.
2025-03-12 12:11:00 +11:00
psychedelicious
1e388e9ca4 tweak(ui): align new and upload workflow buttons 2025-03-12 12:11:00 +11:00
psychedelicious
5b84d45932 perf(ui): memoize workflow library components 2025-03-12 12:11:00 +11:00
psychedelicious
dc3f1184b2 fix(ui): other stuff borked by rebase 2025-03-12 12:11:00 +11:00
psychedelicious
87438bcad7 fix(ui): rebase broke things 2025-03-12 12:11:00 +11:00
Mary Hipp
afd894fd04 update recent workflows UI 2025-03-12 12:11:00 +11:00
Mary Hipp
df305c0b99 allow opened_at to be nullable for workflows that the user has never opened 2025-03-12 12:11:00 +11:00
psychedelicious
deecb7f3c3 feat(ui): "Reset Filters" -> "Deselect All" 2025-03-12 08:00:18 +11:00
psychedelicious
dd5f353465 revert(ui): use reverted API for workflow library 2025-03-12 08:00:18 +11:00
psychedelicious
a8759ea0a6 chore(ui): typegen 2025-03-12 08:00:18 +11:00
psychedelicious
3ff529c718 revert(app): use OR logic for workflow library filtering 2025-03-12 08:00:18 +11:00
psychedelicious
3b0fecafb0 fix(ui): URL mismatch for tag_counts_with_filter 2025-03-12 08:00:18 +11:00
psychedelicious
099011000f chore(ui): lint 2025-03-12 08:00:18 +11:00
psychedelicious
155daa3137 feat(ui): hide filters with no workflows 2025-03-12 08:00:18 +11:00
psychedelicious
c493e223cf feat(ui): "Reset Tags" -> "Reset Filters" 2025-03-12 08:00:18 +11:00
psychedelicious
124ca23f8b feat(ui): use new tag filtering for workflow library 2025-03-12 08:00:18 +11:00
psychedelicious
a8023cbcb6 chore(ui): typegen 2025-03-12 08:00:18 +11:00
psychedelicious
b733d3897e feat(app): revised workflow library filtering by tag
- Replace `get_counts` method with `get_tag_counts_with_filter` which gets the counts for a list of tags, filtering by a list of selected tags
- Update `get_many` logic to apply tag filtering with AND logic, to match the new `get_tag_counts_with_filter` method
- Update workflow library router
2025-03-12 08:00:18 +11:00
psychedelicious
ef95b37ace fix(ui): workflow library infinite query providesTags 2025-03-12 08:00:18 +11:00
psychedelicious
4feff5a185 chore(ui): bump @reduxjs/toolkit from 1.6.0 to 1.6.1
This brings in some fixes for the new infinite query support.
2025-03-12 08:00:18 +11:00
psychedelicious
6c8dc32d5c docs(ui): add comments to workflow library cache invalidation 2025-03-12 08:00:18 +11:00
psychedelicious
e5da808b2f fix(ui): updating workflow content should not invalidate the infinite query cache 2025-03-12 08:00:18 +11:00
psychedelicious
7d3434da62 fix(ui): updating workflow opened at invalidates infinite query cache 2025-03-12 08:00:18 +11:00
psychedelicious
4cc70d9f16 feat(ui): add cache tags for workflow library's infinite query 2025-03-12 08:00:18 +11:00
psychedelicious
7988bc1a59 chore(ui): remove unused WorkflowsRecent RTKQ tag
This didn't actually do anything. Will be implementing the actual functionality that you'd _think_ this tag would do in a future change.
2025-03-12 08:00:18 +11:00
psychedelicious
1756d885f6 refactor(ui): split workflow library state into separate slice
Has no business being in the workflow state slice.
2025-03-12 08:00:18 +11:00
143 changed files with 2897 additions and 1575 deletions

View File

@@ -44,7 +44,12 @@ jobs:
- name: check for changed frontend files
if: ${{ inputs.always_run != true }}
id: changed-files
uses: tj-actions/changed-files@v42
# Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
# See:
# - CVE-2025-30066
# - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
# - https://github.com/tj-actions/changed-files/issues/2463
uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8
with:
files_yaml: |
frontend:

View File

@@ -44,7 +44,12 @@ jobs:
- name: check for changed frontend files
if: ${{ inputs.always_run != true }}
id: changed-files
uses: tj-actions/changed-files@v42
# Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
# See:
# - CVE-2025-30066
# - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
# - https://github.com/tj-actions/changed-files/issues/2463
uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8
with:
files_yaml: |
frontend:

View File

@@ -43,7 +43,12 @@ jobs:
- name: check for changed python files
if: ${{ inputs.always_run != true }}
id: changed-files
uses: tj-actions/changed-files@v42
# Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
# See:
# - CVE-2025-30066
# - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
# - https://github.com/tj-actions/changed-files/issues/2463
uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8
with:
files_yaml: |
python:

View File

@@ -77,7 +77,12 @@ jobs:
- name: check for changed python files
if: ${{ inputs.always_run != true }}
id: changed-files
uses: tj-actions/changed-files@v42
# Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
# See:
# - CVE-2025-30066
# - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
# - https://github.com/tj-actions/changed-files/issues/2463
uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8
with:
files_yaml: |
python:

View File

@@ -42,7 +42,12 @@ jobs:
- name: check for changed files
if: ${{ inputs.always_run != true }}
id: changed-files
uses: tj-actions/changed-files@v42
# Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
# See:
# - CVE-2025-30066
# - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
# - https://github.com/tj-actions/changed-files/issues/2463
uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8
with:
files_yaml: |
src:

View File

@@ -105,6 +105,7 @@ async def list_workflows(
categories: Optional[list[WorkflowCategory]] = Query(default=None, description="The categories of workflow to get"),
tags: Optional[list[str]] = Query(default=None, description="The tags of workflow to get"),
query: Optional[str] = Query(default=None, description="The text to query by (matches name and description)"),
has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
) -> PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]:
"""Gets a page of workflows"""
workflows_with_thumbnails: list[WorkflowRecordListItemWithThumbnailDTO] = []
@@ -116,6 +117,7 @@ async def list_workflows(
query=query,
categories=categories,
tags=tags,
has_been_opened=has_been_opened,
)
for workflow in workflows.items:
workflows_with_thumbnails.append(
@@ -221,14 +223,29 @@ async def get_workflow_thumbnail(
raise HTTPException(status_code=404)
@workflows_router.get("/counts", operation_id="get_counts")
async def get_counts(
tags: Optional[list[str]] = Query(default=None, description="The tags to include"),
@workflows_router.get("/counts_by_tag", operation_id="get_counts_by_tag")
async def get_counts_by_tag(
tags: list[str] = Query(description="The tags to get counts for"),
categories: Optional[list[WorkflowCategory]] = Query(default=None, description="The categories to include"),
) -> int:
"""Gets a the count of workflows that include the specified tags and categories"""
has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
) -> dict[str, int]:
"""Counts workflows by tag"""
return ApiDependencies.invoker.services.workflow_records.get_counts(tags=tags, categories=categories)
return ApiDependencies.invoker.services.workflow_records.counts_by_tag(
tags=tags, categories=categories, has_been_opened=has_been_opened
)
@workflows_router.get("/counts_by_category", operation_id="counts_by_category")
async def counts_by_category(
categories: list[WorkflowCategory] = Query(description="The categories to include"),
has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
) -> dict[str, int]:
"""Counts workflows by category"""
return ApiDependencies.invoker.services.workflow_records.counts_by_category(
categories=categories, has_been_opened=has_been_opened
)
@workflows_router.put(

View File

@@ -40,10 +40,10 @@ from invokeai.backend.util.devices import TorchDevice
@invocation(
"compel",
title="Prompt",
title="Prompt - SD1.5",
tags=["prompt", "compel"],
category="conditioning",
version="1.2.0",
version="1.2.1",
)
class CompelInvocation(BaseInvocation):
"""Parse prompt using compel package to conditioning."""
@@ -233,10 +233,10 @@ class SDXLPromptInvocationBase:
@invocation(
"sdxl_compel_prompt",
title="SDXL Prompt",
title="Prompt - SDXL",
tags=["sdxl", "compel", "prompt"],
category="conditioning",
version="1.2.0",
version="1.2.1",
)
class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
"""Parse prompt using compel package to conditioning."""
@@ -327,10 +327,10 @@ class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
@invocation(
"sdxl_refiner_compel_prompt",
title="SDXL Refiner Prompt",
title="Prompt - SDXL Refiner",
tags=["sdxl", "compel", "prompt"],
category="conditioning",
version="1.1.1",
version="1.1.2",
)
class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
"""Parse prompt using compel package to conditioning."""
@@ -376,10 +376,10 @@ class CLIPSkipInvocationOutput(BaseInvocationOutput):
@invocation(
"clip_skip",
title="CLIP Skip",
title="Apply CLIP Skip - SD1.5, SDXL",
tags=["clipskip", "clip", "skip"],
category="conditioning",
version="1.1.0",
version="1.1.1",
)
class CLIPSkipInvocation(BaseInvocation):
"""Skip layers in clip text_encoder model."""

View File

@@ -87,7 +87,7 @@ class ControlOutput(BaseInvocationOutput):
control: ControlField = OutputField(description=FieldDescriptions.control)
@invocation("controlnet", title="ControlNet", tags=["controlnet"], category="controlnet", version="1.1.2")
@invocation("controlnet", title="ControlNet - SD1.5, SDXL", tags=["controlnet"], category="controlnet", version="1.1.3")
class ControlNetInvocation(BaseInvocation):
"""Collects ControlNet info to pass to other nodes"""

View File

@@ -127,10 +127,10 @@ def get_scheduler(
@invocation(
"denoise_latents",
title="Denoise Latents",
title="Denoise - SD1.5, SDXL",
tags=["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
category="latents",
version="1.5.3",
version="1.5.4",
)
class DenoiseLatentsInvocation(BaseInvocation):
"""Denoises noisy latents to decodable images"""

View File

@@ -21,10 +21,10 @@ class FluxControlLoRALoaderOutput(BaseInvocationOutput):
@invocation(
"flux_control_lora_loader",
title="Flux Control LoRA",
title="Control LoRA - FLUX",
tags=["lora", "model", "flux"],
category="model",
version="1.1.0",
version="1.1.1",
classification=Classification.Prototype,
)
class FluxControlLoRALoaderInvocation(BaseInvocation):

View File

@@ -37,10 +37,10 @@ class FluxModelLoaderOutput(BaseInvocationOutput):
@invocation(
"flux_model_loader",
title="Flux Main Model",
title="Main Model - FLUX",
tags=["model", "flux"],
category="model",
version="1.0.5",
version="1.0.6",
classification=Classification.Prototype,
)
class FluxModelLoaderInvocation(BaseInvocation):

View File

@@ -26,10 +26,10 @@ from invokeai.backend.stable_diffusion.diffusion.conditioning_data import Condit
@invocation(
"flux_text_encoder",
title="FLUX Text Encoding",
title="Prompt - FLUX",
tags=["prompt", "conditioning", "flux"],
category="conditioning",
version="1.1.1",
version="1.1.2",
classification=Classification.Prototype,
)
class FluxTextEncoderInvocation(BaseInvocation):

View File

@@ -22,10 +22,10 @@ from invokeai.backend.util.devices import TorchDevice
@invocation(
"flux_vae_decode",
title="FLUX Latents to Image",
title="Latents to Image - FLUX",
tags=["latents", "image", "vae", "l2i", "flux"],
category="latents",
version="1.0.1",
version="1.0.2",
)
class FluxVaeDecodeInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Generates an image from latents."""

View File

@@ -19,10 +19,10 @@ from invokeai.backend.util.devices import TorchDevice
@invocation(
"flux_vae_encode",
title="FLUX Image to Latents",
title="Image to Latents - FLUX",
tags=["latents", "image", "vae", "i2l", "flux"],
category="latents",
version="1.0.0",
version="1.0.1",
)
class FluxVaeEncodeInvocation(BaseInvocation):
"""Encodes an image into latents."""

View File

@@ -19,9 +19,9 @@ class IdealSizeOutput(BaseInvocationOutput):
@invocation(
"ideal_size",
title="Ideal Size",
title="Ideal Size - SD1.5, SDXL",
tags=["latents", "math", "ideal_size"],
version="1.0.4",
version="1.0.5",
)
class IdealSizeInvocation(BaseInvocation):
"""Calculates the ideal size for generation to avoid duplication"""

View File

@@ -31,10 +31,10 @@ from invokeai.backend.util.devices import TorchDevice
@invocation(
"i2l",
title="Image to Latents",
title="Image to Latents - SD1.5, SDXL",
tags=["latents", "image", "vae", "i2l"],
category="latents",
version="1.1.0",
version="1.1.1",
)
class ImageToLatentsInvocation(BaseInvocation):
"""Encodes an image into latents."""

View File

@@ -69,7 +69,13 @@ CLIP_VISION_MODEL_MAP: dict[Literal["ViT-L", "ViT-H", "ViT-G"], StarterModel] =
}
@invocation("ip_adapter", title="IP-Adapter", tags=["ip_adapter", "control"], category="ip_adapter", version="1.5.0")
@invocation(
"ip_adapter",
title="IP-Adapter - SD1.5, SDXL",
tags=["ip_adapter", "control"],
category="ip_adapter",
version="1.5.1",
)
class IPAdapterInvocation(BaseInvocation):
"""Collects IP-Adapter info to pass to other nodes."""

View File

@@ -31,10 +31,10 @@ from invokeai.backend.util.devices import TorchDevice
@invocation(
"l2i",
title="Latents to Image",
title="Latents to Image - SD1.5, SDXL",
tags=["latents", "image", "vae", "l2i"],
category="latents",
version="1.3.1",
version="1.3.2",
)
class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Generates an image from latents."""

View File

@@ -610,10 +610,10 @@ class LatentsMetaOutput(LatentsOutput, MetadataOutput):
@invocation(
"denoise_latents_meta",
title="Denoise Latents + metadata",
title=f"{DenoiseLatentsInvocation.UIConfig.title} + Metadata",
tags=["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
category="latents",
version="1.1.0",
version="1.1.1",
)
class DenoiseLatentsMetaInvocation(DenoiseLatentsInvocation, WithMetadata):
def invoke(self, context: InvocationContext) -> LatentsMetaOutput:
@@ -675,10 +675,10 @@ class DenoiseLatentsMetaInvocation(DenoiseLatentsInvocation, WithMetadata):
@invocation(
"flux_denoise_meta",
title="Flux Denoise + metadata",
title=f"{FluxDenoiseInvocation.UIConfig.title} + Metadata",
tags=["flux", "latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
category="latents",
version="1.0.0",
version="1.0.1",
)
class FluxDenoiseLatentsMetaInvocation(FluxDenoiseInvocation, WithMetadata):
"""Run denoising process with a FLUX transformer model + metadata."""

View File

@@ -122,10 +122,10 @@ class ModelIdentifierOutput(BaseInvocationOutput):
@invocation(
"model_identifier",
title="Model identifier",
title="Any Model",
tags=["model"],
category="model",
version="1.0.0",
version="1.0.1",
classification=Classification.Prototype,
)
class ModelIdentifierInvocation(BaseInvocation):
@@ -144,10 +144,10 @@ class ModelIdentifierInvocation(BaseInvocation):
@invocation(
"main_model_loader",
title="Main Model",
title="Main Model - SD1.5",
tags=["model"],
category="model",
version="1.0.3",
version="1.0.4",
)
class MainModelLoaderInvocation(BaseInvocation):
"""Loads a main model, outputting its submodels."""
@@ -244,7 +244,7 @@ class LoRASelectorOutput(BaseInvocationOutput):
lora: LoRAField = OutputField(description="LoRA model and weight", title="LoRA")
@invocation("lora_selector", title="LoRA Selector", tags=["model"], category="model", version="1.0.1")
@invocation("lora_selector", title="LoRA Model - SD1.5", tags=["model"], category="model", version="1.0.2")
class LoRASelectorInvocation(BaseInvocation):
"""Selects a LoRA model and weight."""
@@ -257,7 +257,9 @@ class LoRASelectorInvocation(BaseInvocation):
return LoRASelectorOutput(lora=LoRAField(lora=self.lora, weight=self.weight))
@invocation("lora_collection_loader", title="LoRA Collection Loader", tags=["model"], category="model", version="1.1.0")
@invocation(
"lora_collection_loader", title="LoRA Collection - SD1.5", tags=["model"], category="model", version="1.1.1"
)
class LoRACollectionLoader(BaseInvocation):
"""Applies a collection of LoRAs to the provided UNet and CLIP models."""
@@ -320,10 +322,10 @@ class SDXLLoRALoaderOutput(BaseInvocationOutput):
@invocation(
"sdxl_lora_loader",
title="SDXL LoRA",
title="LoRA Model - SDXL",
tags=["lora", "model"],
category="model",
version="1.0.3",
version="1.0.4",
)
class SDXLLoRALoaderInvocation(BaseInvocation):
"""Apply selected lora to unet and text_encoder."""
@@ -400,10 +402,10 @@ class SDXLLoRALoaderInvocation(BaseInvocation):
@invocation(
"sdxl_lora_collection_loader",
title="SDXL LoRA Collection Loader",
title="LoRA Collection - SDXL",
tags=["model"],
category="model",
version="1.1.0",
version="1.1.1",
)
class SDXLLoRACollectionLoader(BaseInvocation):
"""Applies a collection of SDXL LoRAs to the provided UNet and CLIP models."""
@@ -469,7 +471,9 @@ class SDXLLoRACollectionLoader(BaseInvocation):
return output
@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.3")
@invocation(
"vae_loader", title="VAE Model - SD1.5, SDXL, SD3, FLUX", tags=["vae", "model"], category="model", version="1.0.4"
)
class VAELoaderInvocation(BaseInvocation):
"""Loads a VAE model, outputting a VaeLoaderOutput"""
@@ -496,10 +500,10 @@ class SeamlessModeOutput(BaseInvocationOutput):
@invocation(
"seamless",
title="Seamless",
title="Apply Seamless - SD1.5, SDXL",
tags=["seamless", "model"],
category="model",
version="1.0.1",
version="1.0.2",
)
class SeamlessModeInvocation(BaseInvocation):
"""Applies the seamless transformation to the Model UNet and VAE."""
@@ -539,7 +543,7 @@ class SeamlessModeInvocation(BaseInvocation):
return SeamlessModeOutput(unet=unet, vae=vae)
@invocation("freeu", title="FreeU", tags=["freeu"], category="unet", version="1.0.1")
@invocation("freeu", title="Apply FreeU - SD1.5, SDXL", tags=["freeu"], category="unet", version="1.0.2")
class FreeUInvocation(BaseInvocation):
"""
Applies FreeU to the UNet. Suggested values (b1/b2/s1/s2):

View File

@@ -72,10 +72,10 @@ class NoiseOutput(BaseInvocationOutput):
@invocation(
"noise",
title="Noise",
title="Create Latent Noise",
tags=["latents", "noise"],
category="latents",
version="1.0.2",
version="1.0.3",
)
class NoiseInvocation(BaseInvocation):
"""Generates latent noise."""

View File

@@ -32,10 +32,10 @@ from invokeai.backend.util.devices import TorchDevice
@invocation(
"sd3_denoise",
title="SD3 Denoise",
title="Denoise - SD3",
tags=["image", "sd3"],
category="image",
version="1.1.0",
version="1.1.1",
classification=Classification.Prototype,
)
class SD3DenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):

View File

@@ -21,10 +21,10 @@ from invokeai.backend.util.devices import TorchDevice
@invocation(
"sd3_i2l",
title="SD3 Image to Latents",
title="Image to Latents - SD3",
tags=["image", "latents", "vae", "i2l", "sd3"],
category="image",
version="1.0.0",
version="1.0.1",
classification=Classification.Prototype,
)
class SD3ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):

View File

@@ -24,10 +24,10 @@ from invokeai.backend.util.devices import TorchDevice
@invocation(
"sd3_l2i",
title="SD3 Latents to Image",
title="Latents to Image - SD3",
tags=["latents", "image", "vae", "l2i", "sd3"],
category="latents",
version="1.3.1",
version="1.3.2",
)
class SD3LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Generates an image from latents."""

View File

@@ -30,10 +30,10 @@ class Sd3ModelLoaderOutput(BaseInvocationOutput):
@invocation(
"sd3_model_loader",
title="SD3 Main Model",
title="Main Model - SD3",
tags=["model", "sd3"],
category="model",
version="1.0.0",
version="1.0.1",
classification=Classification.Prototype,
)
class Sd3ModelLoaderInvocation(BaseInvocation):

View File

@@ -29,10 +29,10 @@ SD3_T5_MAX_SEQ_LEN = 256
@invocation(
"sd3_text_encoder",
title="SD3 Text Encoding",
title="Prompt - SD3",
tags=["prompt", "conditioning", "sd3"],
category="conditioning",
version="1.0.0",
version="1.0.1",
classification=Classification.Prototype,
)
class Sd3TextEncoderInvocation(BaseInvocation):

View File

@@ -24,7 +24,7 @@ class SDXLRefinerModelLoaderOutput(BaseInvocationOutput):
vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
@invocation("sdxl_model_loader", title="SDXL Main Model", tags=["model", "sdxl"], category="model", version="1.0.3")
@invocation("sdxl_model_loader", title="Main Model - SDXL", tags=["model", "sdxl"], category="model", version="1.0.4")
class SDXLModelLoaderInvocation(BaseInvocation):
"""Loads an sdxl base model, outputting its submodels."""
@@ -58,10 +58,10 @@ class SDXLModelLoaderInvocation(BaseInvocation):
@invocation(
"sdxl_refiner_model_loader",
title="SDXL Refiner Model",
title="Refiner Model - SDXL",
tags=["model", "sdxl", "refiner"],
category="model",
version="1.0.3",
version="1.0.4",
)
class SDXLRefinerModelLoaderInvocation(BaseInvocation):
"""Loads an sdxl refiner model, outputting its submodels."""

View File

@@ -45,7 +45,11 @@ class T2IAdapterOutput(BaseInvocationOutput):
@invocation(
"t2i_adapter", title="T2I-Adapter", tags=["t2i_adapter", "control"], category="t2i_adapter", version="1.0.3"
"t2i_adapter",
title="T2I-Adapter - SD1.5, SDXL",
tags=["t2i_adapter", "control"],
category="t2i_adapter",
version="1.0.4",
)
class T2IAdapterInvocation(BaseInvocation):
"""Collects T2I-Adapter info to pass to other nodes."""

View File

@@ -53,11 +53,11 @@ def crop_controlnet_data(control_data: ControlNetData, latent_region: TBLR) -> C
@invocation(
"tiled_multi_diffusion_denoise_latents",
title="Tiled Multi-Diffusion Denoise Latents",
title="Tiled Multi-Diffusion Denoise - SD1.5, SDXL",
tags=["upscale", "denoise"],
category="latents",
classification=Classification.Beta,
version="1.0.0",
version="1.0.1",
)
class TiledMultiDiffusionDenoiseLatents(BaseInvocation):
"""Tiled Multi-Diffusion denoising.

View File

@@ -20,6 +20,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_14 import
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_15 import build_migration_15
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_16 import build_migration_16
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_17 import build_migration_17
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_18 import build_migration_18
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
@@ -57,6 +58,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator.register_migration(build_migration_15())
migrator.register_migration(build_migration_16())
migrator.register_migration(build_migration_17())
migrator.register_migration(build_migration_18())
migrator.run_migrations()
return db

View File

@@ -0,0 +1,47 @@
import sqlite3
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
class Migration18Callback:
def __call__(self, cursor: sqlite3.Cursor) -> None:
self._make_workflow_opened_at_nullable(cursor)
def _make_workflow_opened_at_nullable(self, cursor: sqlite3.Cursor) -> None:
"""
Make the `opened_at` column nullable in the `workflow_library` table. This is accomplished by:
- Dropping the existing `idx_workflow_library_opened_at` index (must be done before dropping the column)
- Dropping the existing `opened_at` column
- Adding a new nullable column `opened_at` (no data migration needed, all values will be NULL)
- Adding a new `idx_workflow_library_opened_at` index on the `opened_at` column
"""
# For index renaming in SQLite, we need to drop and recreate
cursor.execute("DROP INDEX IF EXISTS idx_workflow_library_opened_at;")
# Rename existing column to deprecated
cursor.execute("ALTER TABLE workflow_library DROP COLUMN opened_at;")
# Add new nullable column - all values will be NULL - no migration of data needed
cursor.execute("ALTER TABLE workflow_library ADD COLUMN opened_at DATETIME;")
# Create new index on the new column
cursor.execute(
"CREATE INDEX idx_workflow_library_opened_at ON workflow_library(opened_at);",
)
def build_migration_18() -> Migration:
"""
Build the migration from database version 17 to 18.
This migration does the following:
- Make the `opened_at` column nullable in the `workflow_library` table. This is accomplished by:
- Dropping the existing `idx_workflow_library_opened_at` index (must be done before dropping the column)
- Dropping the existing `opened_at` column
- Adding a new nullable column `opened_at` (no data migration needed, all values will be NULL)
- Adding a new `idx_workflow_library_opened_at` index on the `opened_at` column
"""
migration_18 = Migration(
from_version=17,
to_version=18,
callback=Migration18Callback(),
)
return migration_18

View File

@@ -1,11 +1,11 @@
{
"id": "default_686bb1d0-d086-4c70-9fa3-2f600b922023",
"name": "ESRGAN Upscaling with Canny ControlNet",
"name": "Upscaler - SD1.5, ESRGAN",
"author": "InvokeAI",
"description": "Sample workflow for using Upscaling with ControlNet with SD1.5",
"description": "Sample workflow for using ESRGAN to upscale with ControlNet with SD1.5",
"version": "2.1.0",
"contact": "invoke@invoke.ai",
"tags": "upscaling, controlnet, default",
"tags": "sd1.5, upscaling, control",
"notes": "",
"exposedFields": [
{
@@ -185,14 +185,7 @@
},
"control_model": {
"name": "control_model",
"label": "Control Model (select Canny)",
"value": {
"key": "a7b9c76f-4bc5-42aa-b918-c1c458a5bb24",
"hash": "blake3:260c7f8e10aefea9868cfc68d89970e91033bd37132b14b903e70ee05ebf530e",
"name": "sd-controlnet-canny",
"base": "sd-1",
"type": "controlnet"
}
"label": "Control Model (select Canny)"
},
"control_weight": {
"name": "control_weight",
@@ -295,14 +288,7 @@
"inputs": {
"model": {
"name": "model",
"label": "",
"value": {
"key": "5cd43ca0-dd0a-418d-9f7e-35b2b9d5e106",
"hash": "blake3:6987f323017f597213cc3264250edf57056d21a40a0a85d83a1a33a7d44dc41a",
"name": "Deliberate_v5",
"base": "sd-1",
"type": "main"
}
"label": ""
}
},
"isOpen": true,
@@ -849,4 +835,4 @@
"targetHandle": "image_resolution"
}
]
}
}

View File

@@ -1,11 +1,11 @@
{
"id": "default_cbf0e034-7b54-4b2c-b670-3b1e2e4b4a88",
"name": "FLUX Image to Image",
"name": "Image to Image - FLUX",
"author": "InvokeAI",
"description": "A simple image-to-image workflow using a FLUX dev model. ",
"version": "1.1.0",
"contact": "",
"tags": "image2image, flux, image-to-image, image to image",
"tags": "flux, image to image",
"notes": "Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend using FLUX dev models for image-to-image workflows. The image-to-image performance with FLUX schnell models is poor.",
"exposedFields": [
{
@@ -201,36 +201,15 @@
},
"t5_encoder_model": {
"name": "t5_encoder_model",
"label": "",
"value": {
"key": "d18d5575-96b6-4da3-b3d8-eb58308d6705",
"hash": "random:f2f9ed74acdfb4bf6fec200e780f6c25f8dd8764a35e65d425d606912fdf573a",
"name": "t5_bnb_int8_quantized_encoder",
"base": "any",
"type": "t5_encoder"
}
"label": ""
},
"clip_embed_model": {
"name": "clip_embed_model",
"label": "",
"value": {
"key": "5a19d7e5-8d98-43cd-8a81-87515e4b3b4e",
"hash": "random:4bd08514c08fb6ff04088db9aeb45def3c488e8b5fd09a35f2cc4f2dc346f99f",
"name": "clip-vit-large-patch14",
"base": "any",
"type": "clip_embed"
}
"label": ""
},
"vae_model": {
"name": "vae_model",
"label": "",
"value": {
"key": "9172beab-5c1d-43f0-b2f0-6e0b956710d9",
"hash": "random:c54dde288e5fa2e6137f1c92e9d611f598049e6f16e360207b6d96c9f5a67ba0",
"name": "FLUX.1-schnell_ae",
"base": "flux",
"type": "vae"
}
"label": ""
}
}
},

View File

@@ -1,11 +1,11 @@
{
"id": "default_dec5a2e9-f59c-40d9-8869-a056751d79b8",
"name": "Face Detailer with IP-Adapter & Canny (See Note in Details)",
"name": "Face Detailer - SD1.5",
"author": "kosmoskatten",
"description": "A workflow to add detail to and improve faces. This workflow is most effective when used with a model that creates realistic outputs. ",
"version": "2.1.0",
"contact": "invoke@invoke.ai",
"tags": "face detailer, IP-Adapter, Canny",
"tags": "sd1.5, reference image, control",
"notes": "Set this image as the blur mask: https://i.imgur.com/Gxi61zP.png",
"exposedFields": [
{
@@ -136,14 +136,7 @@
},
"control_model": {
"name": "control_model",
"label": "Control Model (select canny)",
"value": {
"key": "5bdaacf7-a7a3-4fb8-b394-cc0ffbb8941d",
"hash": "blake3:260c7f8e10aefea9868cfc68d89970e91033bd37132b14b903e70ee05ebf530e",
"name": "sd-controlnet-canny",
"base": "sd-1",
"type": "controlnet"
}
"label": "Control Model (select canny)"
},
"control_weight": {
"name": "control_weight",
@@ -197,14 +190,7 @@
},
"ip_adapter_model": {
"name": "ip_adapter_model",
"label": "IP-Adapter Model (select IP Adapter Face)",
"value": {
"key": "1cc210bb-4d0a-4312-b36c-b5d46c43768e",
"hash": "blake3:3d669dffa7471b357b4df088b99ffb6bf4d4383d5e0ef1de5ec1c89728a3d5a5",
"name": "ip_adapter_sd15",
"base": "sd-1",
"type": "ip_adapter"
}
"label": "IP-Adapter Model (select IP Adapter Face)"
},
"clip_vision_model": {
"name": "clip_vision_model",

View File

@@ -1,11 +1,11 @@
{
"id": "default_444fe292-896b-44fd-bfc6-c0b5d220fffc",
"name": "FLUX Text to Image",
"name": "Text to Image - FLUX",
"author": "InvokeAI",
"description": "A simple text-to-image workflow using FLUX dev or schnell models.",
"version": "1.1.0",
"contact": "",
"tags": "text2image, flux, text to image",
"tags": "flux, text to image",
"notes": "Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend 4 steps for FLUX schnell models and 30 steps for FLUX dev models.",
"exposedFields": [
{
@@ -169,36 +169,15 @@
},
"t5_encoder_model": {
"name": "t5_encoder_model",
"label": "",
"value": {
"key": "d18d5575-96b6-4da3-b3d8-eb58308d6705",
"hash": "random:f2f9ed74acdfb4bf6fec200e780f6c25f8dd8764a35e65d425d606912fdf573a",
"name": "t5_bnb_int8_quantized_encoder",
"base": "any",
"type": "t5_encoder"
}
"label": ""
},
"clip_embed_model": {
"name": "clip_embed_model",
"label": "",
"value": {
"key": "5a19d7e5-8d98-43cd-8a81-87515e4b3b4e",
"hash": "random:4bd08514c08fb6ff04088db9aeb45def3c488e8b5fd09a35f2cc4f2dc346f99f",
"name": "clip-vit-large-patch14",
"base": "any",
"type": "clip_embed"
}
"label": ""
},
"vae_model": {
"name": "vae_model",
"label": "",
"value": {
"key": "9172beab-5c1d-43f0-b2f0-6e0b956710d9",
"hash": "random:c54dde288e5fa2e6137f1c92e9d611f598049e6f16e360207b6d96c9f5a67ba0",
"name": "FLUX.1-schnell_ae",
"base": "flux",
"type": "vae"
}
"label": ""
}
}
},

View File

@@ -1,11 +1,11 @@
{
"id": "default_2d05e719-a6b9-4e64-9310-b875d3b2f9d2",
"name": "Multi ControlNet (Canny & Depth)",
"name": "Text to Image - SD1.5, Control",
"author": "InvokeAI",
"description": "A sample workflow using canny & depth ControlNets to guide the generation process. ",
"version": "2.1.0",
"contact": "invoke@invoke.ai",
"tags": "ControlNet, canny, depth",
"tags": "sd1.5, control, text to image",
"notes": "",
"exposedFields": [
{
@@ -217,14 +217,7 @@
},
"control_model": {
"name": "control_model",
"label": "Control Model (select canny)",
"value": {
"key": "5bdaacf7-a7a3-4fb8-b394-cc0ffbb8941d",
"hash": "blake3:260c7f8e10aefea9868cfc68d89970e91033bd37132b14b903e70ee05ebf530e",
"name": "sd-controlnet-canny",
"base": "sd-1",
"type": "controlnet"
}
"label": "Control Model (select canny)"
},
"control_weight": {
"name": "control_weight",
@@ -371,14 +364,7 @@
},
"control_model": {
"name": "control_model",
"label": "Control Model (select depth)",
"value": {
"key": "87e8855c-671f-4c9e-bbbb-8ed47ccb4aac",
"hash": "blake3:2550bf22a53942dfa28ab2fed9d10d80851112531f44d977168992edf9d0534c",
"name": "control_v11f1p_sd15_depth",
"base": "sd-1",
"type": "controlnet"
}
"label": "Control Model (select depth)"
},
"control_weight": {
"name": "control_weight",

View File

@@ -1,11 +1,11 @@
{
"id": "default_f96e794f-eb3e-4d01-a960-9b4e43402bcf",
"name": "MultiDiffusion SD1.5",
"name": "Upscaler - SD1.5, MultiDiffusion",
"author": "Invoke",
"description": "A workflow to upscale an input image with tiled upscaling, using SD1.5 based models.",
"version": "1.0.0",
"contact": "invoke@invoke.ai",
"tags": "tiled, upscaling, sdxl",
"tags": "sd1.5, upscaling",
"notes": "",
"exposedFields": [
{
@@ -135,14 +135,7 @@
"inputs": {
"model": {
"name": "model",
"label": "",
"value": {
"key": "e7b402e5-62e5-4acb-8c39-bee6bdb758ab",
"hash": "c8659e796168d076368256b57edbc1b48d6dafc1712f1bb37cc57c7c06889a6b",
"name": "526mix",
"base": "sd-1",
"type": "main"
}
"label": ""
}
}
},
@@ -384,21 +377,11 @@
},
"image": {
"name": "image",
"label": "Image to Upscale",
"value": {
"image_name": "ee7009f7-a35d-488b-a2a6-21237ef5ae05.png"
}
"label": "Image to Upscale"
},
"image_to_image_model": {
"name": "image_to_image_model",
"label": "",
"value": {
"key": "38bb1a29-8ede-42ba-b77f-64b3478896eb",
"hash": "blake3:e52fdbee46a484ebe9b3b20ea0aac0a35a453ab6d0d353da00acfd35ce7a91ed",
"name": "4xNomosWebPhoto_esrgan",
"base": "sdxl",
"type": "spandrel_image_to_image"
}
"label": ""
},
"tile_size": {
"name": "tile_size",
@@ -437,14 +420,7 @@
"inputs": {
"model": {
"name": "model",
"label": "ControlNet Model - Choose a Tile ControlNet",
"value": {
"key": "20645e4d-ef97-4c5a-9243-b834a3483925",
"hash": "f0812e13758f91baf4e54b7dbb707b70642937d3b2098cd2b94cc36d3eba308e",
"name": "tile",
"base": "sd-1",
"type": "controlnet"
}
"label": "ControlNet Model - Choose a Tile ControlNet"
}
}
},

View File

@@ -1,11 +1,11 @@
{
"id": "default_35658541-6d41-4a20-8ec5-4bf2561faed0",
"name": "MultiDiffusion SDXL",
"name": "Upscaler - SDXL, MultiDiffusion",
"author": "Invoke",
"description": "A workflow to upscale an input image with tiled upscaling, using SDXL based models.",
"version": "1.1.0",
"contact": "invoke@invoke.ai",
"tags": "tiled, upscaling, sdxl",
"tags": "sdxl, upscaling",
"notes": "",
"exposedFields": [
{
@@ -341,14 +341,7 @@
"inputs": {
"model": {
"name": "model",
"label": "ControlNet Model - Choose a Tile ControlNet",
"value": {
"key": "74f4651f-0ace-4b7b-b616-e98360257797",
"hash": "blake3:167a5b84583aaed3e5c8d660b45830e82e1c602743c689d3c27773c6c8b85b4a",
"name": "controlnet-tile-sdxl-1.0",
"base": "sdxl",
"type": "controlnet"
}
"label": "ControlNet Model - Choose a Tile ControlNet"
}
}
},
@@ -801,14 +794,7 @@
"inputs": {
"vae_model": {
"name": "vae_model",
"label": "",
"value": {
"key": "ff926845-090e-4d46-b81e-30289ee47474",
"hash": "9705ab1c31fa96b308734214fb7571a958621c7a9247eed82b7d277145f8d9fa",
"name": "VAEFix",
"base": "sdxl",
"type": "vae"
}
"label": ""
}
}
},
@@ -832,14 +818,7 @@
"inputs": {
"model": {
"name": "model",
"label": "SDXL Model",
"value": {
"key": "ab191f73-68d2-492c-8aec-b438a8cf0f45",
"hash": "blake3:2d50e940627e3bf555f015280ec0976d5c1fa100f7bc94e95ffbfc770e98b6fe",
"name": "CustomXLv7",
"base": "sdxl",
"type": "main"
}
"label": "SDXL Model"
}
}
},

View File

@@ -1,11 +1,11 @@
{
"id": "default_d7a1c60f-ca2f-4f90-9e33-75a826ca6d8f",
"name": "Prompt from File",
"name": "Text to Image - SD1.5, Prompt from File",
"author": "InvokeAI",
"description": "Sample workflow using Prompt from File node",
"version": "2.1.0",
"contact": "invoke@invoke.ai",
"tags": "text2image, prompt from file, default, text to image",
"tags": "sd1.5, text to image",
"notes": "",
"exposedFields": [
{
@@ -513,4 +513,4 @@
"targetHandle": "vae"
}
]
}
}

View File

@@ -10,6 +10,7 @@ _default workflows_ on app startup.
An exception will be raised during sync if this is not set correctly.
- Default workflows appear on the "Default Workflows" tab of the Workflow
Library.
- Default workflows should not reference any resources that are user-created or installed. That includes images and models. For example, if a default workflow references Juggernaut as an SDXL model, when a user loads the workflow, even if they have a version of Juggernaut installed, it will have a different UUID. They may see a warning. So, it's best to ship default workflows without any references to these types of resources.
After adding or updating default workflows, you **must** start the app up and
load them to ensure:

View File

@@ -1,11 +1,11 @@
{
"id": "default_dbe46d95-22aa-43fb-9c16-94400d0ce2fd",
"name": "SD3.5 Text to Image",
"name": "Text to Image - SD3.5",
"author": "InvokeAI",
"description": "Sample text to image workflow for Stable Diffusion 3.5",
"version": "1.0.0",
"contact": "invoke@invoke.ai",
"tags": "text2image, SD3.5, text to image",
"tags": "SD3.5, text to image",
"notes": "",
"exposedFields": [
{
@@ -38,14 +38,7 @@
"inputs": {
"model": {
"name": "model",
"label": "",
"value": {
"key": "f7b20be9-92a8-4cfb-bca4-6c3b5535c10b",
"hash": "placeholder",
"name": "stable-diffusion-3.5-medium",
"base": "sd-3",
"type": "main"
}
"label": ""
},
"t5_encoder_model": {
"name": "t5_encoder_model",

View File

@@ -5,7 +5,7 @@
"description": "Sample text to image workflow for Stable Diffusion 1.5/2",
"version": "2.1.0",
"contact": "invoke@invoke.ai",
"tags": "text2image, SD1.5, SD2, text to image",
"tags": "SD1.5, text to image",
"notes": "",
"exposedFields": [
{
@@ -417,4 +417,4 @@
"targetHandle": "vae"
}
]
}
}

View File

@@ -5,7 +5,7 @@
"description": "Sample text to image workflow for SDXL",
"version": "2.1.0",
"contact": "invoke@invoke.ai",
"tags": "text2image, SDXL, text to image",
"tags": "SDXL, text to image",
"notes": "",
"exposedFields": [
{
@@ -46,14 +46,7 @@
"inputs": {
"vae_model": {
"name": "vae_model",
"label": "VAE (use the FP16 model)",
"value": {
"key": "f20f9e5c-1bce-4c46-a84d-34ebfa7df069",
"hash": "blake3:9705ab1c31fa96b308734214fb7571a958621c7a9247eed82b7d277145f8d9fa",
"name": "sdxl-vae-fp16-fix",
"base": "sdxl",
"type": "vae"
}
"label": "VAE (use the FP16 model)"
}
},
"isOpen": true,
@@ -203,14 +196,7 @@
"inputs": {
"model": {
"name": "model",
"label": "",
"value": {
"key": "4a63b226-e8ff-4da4-854e-0b9f04b562ba",
"hash": "blake3:d279309ea6e5ee6e8fd52504275865cc280dac71cbf528c5b07c98b888bddaba",
"name": "dreamshaper-xl-v2-turbo",
"base": "sdxl",
"type": "main"
}
"label": ""
}
},
"isOpen": true,
@@ -715,4 +701,4 @@
"targetHandle": "style"
}
]
}
}

View File

@@ -1,11 +1,11 @@
{
"id": "default_e71d153c-2089-43c7-bd2c-f61f37d4c1c1",
"name": "Text to Image with LoRA",
"name": "Text to Image - SD1.5, LoRA",
"author": "InvokeAI",
"description": "Simple text to image workflow with a LoRA",
"version": "2.1.0",
"contact": "invoke@invoke.ai",
"tags": "text to image, lora, text to image",
"tags": "sd1.5, text to image, lora",
"notes": "",
"exposedFields": [
{

View File

@@ -1,11 +1,11 @@
{
"id": "default_43b0d7f7-6a12-4dcf-a5a4-50c940cbee29",
"name": "Tiled Upscaling (Beta)",
"name": "Upscaler - SD1.5, Tiled",
"author": "Invoke",
"description": "A workflow to upscale an input image with tiled upscaling. ",
"version": "2.1.0",
"contact": "invoke@invoke.ai",
"tags": "tiled, upscaling, sd1.5",
"tags": "sd1.5, upscaling",
"notes": "",
"exposedFields": [
{
@@ -86,14 +86,7 @@
},
"ip_adapter_model": {
"name": "ip_adapter_model",
"label": "IP-Adapter Model (select ip_adapter_sd15)",
"value": {
"key": "1cc210bb-4d0a-4312-b36c-b5d46c43768e",
"hash": "blake3:3d669dffa7471b357b4df088b99ffb6bf4d4383d5e0ef1de5ec1c89728a3d5a5",
"name": "ip_adapter_sd15",
"base": "sd-1",
"type": "ip_adapter"
}
"label": "IP-Adapter Model (select ip_adapter_sd15)"
},
"clip_vision_model": {
"name": "clip_vision_model",
@@ -201,14 +194,7 @@
},
"control_model": {
"name": "control_model",
"label": "Control Model (select contro_v11f1e_sd15_tile)",
"value": {
"key": "773843c8-db1f-4502-8f65-59782efa7960",
"hash": "blake3:f0812e13758f91baf4e54b7dbb707b70642937d3b2098cd2b94cc36d3eba308e",
"name": "control_v11f1e_sd15_tile",
"base": "sd-1",
"type": "controlnet"
}
"label": "Control Model (select control_v11f1e_sd15_tile)"
},
"control_weight": {
"name": "control_weight",
@@ -1816,4 +1802,4 @@
"targetHandle": "unet"
}
]
}
}

View File

@@ -46,17 +46,28 @@ class WorkflowRecordsStorageBase(ABC):
per_page: Optional[int],
query: Optional[str],
tags: Optional[list[str]],
has_been_opened: Optional[bool],
) -> PaginatedResults[WorkflowRecordListItemDTO]:
"""Gets many workflows."""
pass
@abstractmethod
def get_counts(
def counts_by_category(
self,
tags: Optional[list[str]],
categories: Optional[list[WorkflowCategory]],
) -> int:
"""Gets the count of workflows for the given tags and categories."""
categories: list[WorkflowCategory],
has_been_opened: Optional[bool] = None,
) -> dict[str, int]:
"""Gets a dictionary of counts for each of the provided categories."""
pass
@abstractmethod
def counts_by_tag(
self,
tags: list[str],
categories: Optional[list[WorkflowCategory]] = None,
has_been_opened: Optional[bool] = None,
) -> dict[str, int]:
"""Gets a dictionary of counts for each of the provided tags."""
pass
@abstractmethod

View File

@@ -1,6 +1,6 @@
import datetime
from enum import Enum
from typing import Any, Union
from typing import Any, Optional, Union
import semver
from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, field_validator
@@ -98,7 +98,9 @@ class WorkflowRecordDTOBase(BaseModel):
name: str = Field(description="The name of the workflow.")
created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the workflow.")
updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the workflow.")
opened_at: Union[datetime.datetime, str] = Field(description="The opened timestamp of the workflow.")
opened_at: Optional[Union[datetime.datetime, str]] = Field(
default=None, description="The opened timestamp of the workflow."
)
class WorkflowRecordDTO(WorkflowRecordDTOBase):

View File

@@ -118,6 +118,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
per_page: Optional[int] = None,
query: Optional[str] = None,
tags: Optional[list[str]] = None,
has_been_opened: Optional[bool] = None,
) -> PaginatedResults[WorkflowRecordListItemDTO]:
# sanitize!
assert order_by in WorkflowRecordOrderBy
@@ -175,6 +176,11 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
conditions.append(tags_condition)
params.extend(tags_params)
if has_been_opened:
conditions.append("opened_at IS NOT NULL")
elif has_been_opened is False:
conditions.append("opened_at IS NULL")
# Ignore whitespace in the query
stripped_query = query.strip() if query else None
if stripped_query:
@@ -230,54 +236,105 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
total=total,
)
def get_counts(
def counts_by_tag(
self,
tags: Optional[list[str]],
categories: Optional[list[WorkflowCategory]],
) -> int:
tags: list[str],
categories: Optional[list[WorkflowCategory]] = None,
has_been_opened: Optional[bool] = None,
) -> dict[str, int]:
if not tags:
return {}
cursor = self._conn.cursor()
result: dict[str, int] = {}
# Base conditions for categories and selected tags
base_conditions: list[str] = []
base_params: list[str | int] = []
# Start with an empty list of conditions and params
conditions: list[str] = []
params: list[str | int] = []
if tags:
# Construct a list of conditions for each tag
tags_conditions = ["tags LIKE ?" for _ in tags]
tags_conditions_joined = " OR ".join(tags_conditions)
tags_condition = f"({tags_conditions_joined})"
# And the params for the tags, case-insensitive
tags_params = [f"%{t.strip()}%" for t in tags]
conditions.append(tags_condition)
params.extend(tags_params)
# Add category conditions
if categories:
# Ensure all categories are valid (is this necessary?)
assert all(c in WorkflowCategory for c in categories)
# Construct a placeholder string for the number of categories
placeholders = ", ".join("?" for _ in categories)
base_conditions.append(f"category IN ({placeholders})")
base_params.extend([category.value for category in categories])
# Construct the condition string & params
conditions.append(f"category IN ({placeholders})")
params.extend([category.value for category in categories])
if has_been_opened:
base_conditions.append("opened_at IS NOT NULL")
elif has_been_opened is False:
base_conditions.append("opened_at IS NULL")
stmt = """--sql
SELECT COUNT(*)
FROM workflow_library
"""
# For each tag to count, run a separate query
for tag in tags:
# Start with the base conditions
conditions = base_conditions.copy()
params = base_params.copy()
if conditions:
# If there are conditions, add a WHERE clause and then join the conditions
stmt += " WHERE "
# Add this specific tag condition
conditions.append("tags LIKE ?")
params.append(f"%{tag.strip()}%")
all_conditions = " AND ".join(conditions)
stmt += all_conditions
# Construct the full query
stmt = """--sql
SELECT COUNT(*)
FROM workflow_library
"""
cursor.execute(stmt, tuple(params))
return cursor.fetchone()[0]
if conditions:
stmt += " WHERE " + " AND ".join(conditions)
cursor.execute(stmt, params)
count = cursor.fetchone()[0]
result[tag] = count
return result
def counts_by_category(
self,
categories: list[WorkflowCategory],
has_been_opened: Optional[bool] = None,
) -> dict[str, int]:
cursor = self._conn.cursor()
result: dict[str, int] = {}
# Base conditions for categories
base_conditions: list[str] = []
base_params: list[str | int] = []
# Add category conditions
if categories:
assert all(c in WorkflowCategory for c in categories)
placeholders = ", ".join("?" for _ in categories)
base_conditions.append(f"category IN ({placeholders})")
base_params.extend([category.value for category in categories])
if has_been_opened:
base_conditions.append("opened_at IS NOT NULL")
elif has_been_opened is False:
base_conditions.append("opened_at IS NULL")
# For each category to count, run a separate query
for category in categories:
# Start with the base conditions
conditions = base_conditions.copy()
params = base_params.copy()
# Add this specific category condition
conditions.append("category = ?")
params.append(category.value)
# Construct the full query
stmt = """--sql
SELECT COUNT(*)
FROM workflow_library
"""
if conditions:
stmt += " WHERE " + " AND ".join(conditions)
cursor.execute(stmt, params)
count = cursor.fetchone()[0]
result[category.value] = count
return result
def update_opened_at(self, workflow_id: str) -> None:
try:

View File

@@ -8,12 +8,12 @@ class WorkflowThumbnailServiceBase(ABC):
"""Base class for workflow thumbnail services"""
@abstractmethod
def get_path(self, workflow_id: str) -> Path:
def get_path(self, workflow_id: str, with_hash: bool = True) -> Path:
"""Gets the path to a workflow thumbnail"""
pass
@abstractmethod
def get_url(self, workflow_id: str) -> str | None:
def get_url(self, workflow_id: str, with_hash: bool = True) -> str | None:
"""Gets the URL of a workflow thumbnail"""
pass

View File

@@ -41,7 +41,7 @@ class WorkflowThumbnailFileStorageDisk(WorkflowThumbnailServiceBase):
except Exception as e:
raise WorkflowThumbnailFileSaveException from e
def get_path(self, workflow_id: str) -> Path:
def get_path(self, workflow_id: str, with_hash: bool = True) -> Path:
workflow = self._invoker.services.workflow_records.get(workflow_id).workflow
if workflow.meta.category is WorkflowCategory.Default:
default_thumbnails_dir = Path(__file__).parent / Path("default_workflow_thumbnails")
@@ -51,7 +51,7 @@ class WorkflowThumbnailFileStorageDisk(WorkflowThumbnailServiceBase):
return path
def get_url(self, workflow_id: str) -> str | None:
def get_url(self, workflow_id: str, with_hash: bool = True) -> str | None:
path = self.get_path(workflow_id)
if not self._validate_path(path):
return
@@ -59,7 +59,8 @@ class WorkflowThumbnailFileStorageDisk(WorkflowThumbnailServiceBase):
url = self._invoker.services.urls.get_workflow_thumbnail_url(workflow_id)
# The image URL never changes, so we must add random query string to it to prevent caching
url += f"?{uuid_string()}"
if with_hash:
url += f"?{uuid_string()}"
return url

View File

@@ -60,7 +60,7 @@
"@fontsource-variable/inter": "^5.1.0",
"@invoke-ai/ui-library": "^0.0.46",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.6.0",
"@reduxjs/toolkit": "2.6.1",
"@roarr/browser-log-writer": "^1.3.0",
"@xyflow/react": "^12.4.2",
"async-mutex": "^0.5.0",

View File

@@ -30,8 +30,8 @@ dependencies:
specifier: ^0.7.3
version: 0.7.3(nanostores@0.11.3)(react@18.3.1)
'@reduxjs/toolkit':
specifier: 2.6.0
version: 2.6.0(react-redux@9.1.2)(react@18.3.1)
specifier: 2.6.1
version: 2.6.1(react-redux@9.1.2)(react@18.3.1)
'@roarr/browser-log-writer':
specifier: ^1.3.0
version: 1.3.0
@@ -2311,8 +2311,8 @@ packages:
- supports-color
dev: true
/@reduxjs/toolkit@2.6.0(react-redux@9.1.2)(react@18.3.1):
resolution: {integrity: sha512-mWJCYpewLRyTuuzRSEC/IwIBBkYg2dKtQas8mty5MaV2iXzcmicS3gW554FDeOvLnY3x13NIk8MB1e8wHO7rqQ==}
/@reduxjs/toolkit@2.6.1(react-redux@9.1.2)(react@18.3.1):
resolution: {integrity: sha512-SSlIqZNYhqm/oMkXbtofwZSt9lrncblzo6YcZ9zoX+zLngRBrCOjK4lNLdkNucJF58RHOWrD9txT3bT3piH7Zw==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0

View File

@@ -116,6 +116,8 @@
"dontShowMeThese": "Don't show me these",
"editor": "Editor",
"error": "Error",
"error_withCount_one": "{{count}} error",
"error_withCount_other": "{{count}} errors",
"file": "File",
"folder": "Folder",
"format": "format",
@@ -1205,6 +1207,7 @@
"informationalPopoversDisabled": "Informational Popovers Disabled",
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
"enableModelDescriptions": "Enable Model Descriptions in Dropdowns",
"enableHighlightFocusedRegions": "Highlight Focused Regions",
"modelDescriptionsDisabled": "Model Descriptions in Dropdowns Disabled",
"modelDescriptionsDisabledDesc": "Model descriptions in dropdowns have been disabled. Enable them in Settings.",
"enableInvisibleWatermark": "Enable Invisible Watermark",
@@ -1692,10 +1695,11 @@
"filterByTags": "Filter by Tags",
"yourWorkflows": "Your Workflows",
"recentlyOpened": "Recently Opened",
"noRecentWorkflows": "No Recent Workflows",
"private": "Private",
"shared": "Shared",
"browseWorkflows": "Browse Workflows",
"resetTags": "Reset Tags",
"deselectAll": "Deselect All",
"opened": "Opened",
"openWorkflow": "Open Workflow",
"updated": "Updated",
@@ -1726,6 +1730,7 @@
"loadWorkflow": "$t(common.load) Workflow",
"autoLayout": "Auto Layout",
"edit": "Edit",
"view": "View",
"download": "Download",
"copyShareLink": "Copy Share Link",
"copyShareLinkForWorkflow": "Copy Share Link for Workflow",
@@ -1763,7 +1768,9 @@
"containerPlaceholder": "Empty Container",
"headingPlaceholder": "Empty Heading",
"textPlaceholder": "Empty Text",
"workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release."
"workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release.",
"minimum": "Minimum",
"maximum": "Maximum"
}
},
"controlLayers": {
@@ -2315,6 +2322,7 @@
"newUserExperience": {
"toGetStartedLocal": "To get started, make sure to download or import models needed to run Invoke. Then, enter a prompt in the box and click <StrongComponent>Invoke</StrongComponent> to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the <StrongComponent>Gallery</StrongComponent> or edit them to the <StrongComponent>Canvas</StrongComponent>.",
"toGetStarted": "To get started, enter a prompt in the box and click <StrongComponent>Invoke</StrongComponent> to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the <StrongComponent>Gallery</StrongComponent> or edit them to the <StrongComponent>Canvas</StrongComponent>.",
"toGetStartedWorkflow": "To get started, fill in the fields on the left and press <StrongComponent>Invoke</StrongComponent> to generate your image. Want to explore more workflows? Click the <StrongComponent>folder icon</StrongComponent> next to the workflow title to see a list of other templates you can try.",
"gettingStartedSeries": "Want more guidance? Check out our <LinkComponent>Getting Started Series</LinkComponent> for tips on unlocking the full potential of the Invoke Studio.",
"lowVRAMMode": "For best performance, follow our <LinkComponent>Low VRAM guide</LinkComponent>.",
"noModelsInstalled": "It looks like you don't have any models installed! You can <DownloadStarterModelsButton>download a starter model bundle</DownloadStarterModelsButton> or <ImportModelsButton>import models</ImportModelsButton>."
@@ -2322,8 +2330,8 @@
"whatsNew": {
"whatsNewInInvoke": "What's New in Invoke",
"items": [
"Memory Management: New setting for users with Nvidia GPUs to reduce VRAM usage.",
"Performance: Continued improvements to overall application performance and responsiveness."
"Workflows: New and improved Workflow Library.",
"FLUX: Support for FLUX Redux in Workflows and Canvas."
],
"readReleaseNotes": "Read Release Notes",
"watchRecentReleaseVideos": "Watch Recent Release Videos",

View File

@@ -110,7 +110,10 @@
"layout": "Schema",
"row": "Riga",
"column": "Colonna",
"saveChanges": "Salva modifiche"
"saveChanges": "Salva modifiche",
"error_withCount_one": "{{count}} errore",
"error_withCount_many": "{{count}} errori",
"error_withCount_other": "{{count}} errori"
},
"gallery": {
"galleryImageSize": "Dimensione dell'immagine",
@@ -1771,20 +1774,24 @@
"divider": "Divisore",
"container": "Contenitore",
"text": "Testo",
"numberInput": "Ingresso numerico"
"numberInput": "Ingresso numerico",
"containerRowLayout": "Contenitore (disposizione riga)",
"containerColumnLayout": "Contenitore (disposizione colonna)"
},
"loadMore": "Carica altro",
"searchPlaceholder": "Cerca per nome, descrizione o etichetta",
"filterByTags": "Filtra per etichetta",
"shared": "Condiviso",
"browseWorkflows": "Sfoglia i flussi di lavoro",
"resetTags": "Reimposta le etichette",
"allLoaded": "Tutti i flussi di lavoro caricati",
"saveChanges": "Salva modifiche",
"yourWorkflows": "I tuoi flussi di lavoro",
"recentlyOpened": "Aperto di recente",
"workflowThumbnail": "Miniatura del flusso di lavoro",
"private": "Privato"
"private": "Privato",
"deselectAll": "Deseleziona tutto",
"noRecentWorkflows": "Nessun flusso di lavoro recente",
"view": "Visualizza"
},
"accordions": {
"compositing": {
@@ -2334,7 +2341,8 @@
"toGetStarted": "Per iniziare, inserisci un prompt nella casella e fai clic su <StrongComponent>Invoke</StrongComponent> per generare la tua prima immagine. Seleziona un modello di prompt per migliorare i risultati. Puoi scegliere di salvare le tue immagini direttamente nella <StrongComponent>Galleria</StrongComponent> o modificarle nella <StrongComponent>Tela</StrongComponent>.",
"noModelsInstalled": "Sembra che non hai installato alcun modello! Puoi <DownloadStarterModelsButton>scaricare un pacchetto di modelli di avvio</DownloadStarterModelsButton> o <ImportModelsButton>importare modelli</ImportModelsButton>.",
"toGetStartedLocal": "Per iniziare, assicurati di scaricare o importare i modelli necessari per eseguire Invoke. Quindi, inserisci un prompt nella casella e fai clic su <StrongComponent>Invoke</StrongComponent> per generare la tua prima immagine. Seleziona un modello di prompt per migliorare i risultati. Puoi scegliere di salvare le tue immagini direttamente nella <StrongComponent>Galleria</StrongComponent> o modificarle nella <StrongComponent>Tela</StrongComponent>.",
"lowVRAMMode": "Per prestazioni ottimali, segui la nostra <LinkComponent>guida per bassa VRAM</LinkComponent>."
"lowVRAMMode": "Per prestazioni ottimali, segui la nostra <LinkComponent>guida per bassa VRAM</LinkComponent>.",
"toGetStartedWorkflow": "Per iniziare, compila i campi a sinistra e premi <StrongComponent>Invoke</StrongComponent> per generare la tua immagine. Vuoi esplorare altri flussi di lavoro? Fai clic sull'<StrongComponent>icona della cartella</StrongComponent> accanto al titolo del flusso di lavoro per visualizzare un elenco di altri modelli che puoi provare."
},
"whatsNew": {
"whatsNewInInvoke": "Novità in Invoke",

View File

@@ -2300,7 +2300,6 @@
"filterByTags": "Lọc Theo Nhãn",
"recentlyOpened": "Mở Gần Đây",
"private": "Cá Nhân",
"resetTags": "Khởi Động Lại Nhãn",
"loadMore": "Tải Thêm"
},
"upscaling": {

View File

@@ -16,10 +16,18 @@ import { $openAPISchemaUrl } from 'app/store/nanostores/openAPISchemaUrl';
import { $projectId, $projectName, $projectUrl } from 'app/store/nanostores/projectId';
import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId';
import { $store } from 'app/store/nanostores/store';
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
import { createStore } from 'app/store/store';
import type { PartialAppConfig } from 'app/types/invokeai';
import Loading from 'common/components/Loading/Loading';
import type { WorkflowSortOption, WorkflowTagCategory } from 'features/nodes/store/workflowLibrarySlice';
import {
$workflowLibraryCategoriesOptions,
$workflowLibrarySortOptions,
$workflowLibraryTagCategoriesOptions,
DEFAULT_WORKFLOW_LIBRARY_CATEGORIES,
DEFAULT_WORKFLOW_LIBRARY_SORT_OPTIONS,
DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES,
} from 'features/nodes/store/workflowLibrarySlice';
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import type { PropsWithChildren, ReactNode } from 'react';
import React, { lazy, memo, useEffect, useLayoutEffect, useMemo } from 'react';
@@ -48,6 +56,8 @@ interface Props extends PropsWithChildren {
isDebugging?: boolean;
logo?: ReactNode;
workflowCategories?: WorkflowCategory[];
workflowTagCategories?: WorkflowTagCategory[];
workflowSortOptions?: WorkflowSortOption[];
loggingOverrides?: LoggingOverrides;
}
@@ -68,6 +78,8 @@ const InvokeAIUI = ({
isDebugging = false,
logo,
workflowCategories,
workflowTagCategories,
workflowSortOptions,
loggingOverrides,
}: Props) => {
useLayoutEffect(() => {
@@ -195,14 +207,34 @@ const InvokeAIUI = ({
useEffect(() => {
if (workflowCategories) {
$workflowCategories.set(workflowCategories);
$workflowLibraryCategoriesOptions.set(workflowCategories);
}
return () => {
$workflowCategories.set([]);
$workflowLibraryCategoriesOptions.set(DEFAULT_WORKFLOW_LIBRARY_CATEGORIES);
};
}, [workflowCategories]);
useEffect(() => {
if (workflowTagCategories) {
$workflowLibraryTagCategoriesOptions.set(workflowTagCategories);
}
return () => {
$workflowLibraryTagCategoriesOptions.set(DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES);
};
}, [workflowTagCategories]);
useEffect(() => {
if (workflowSortOptions) {
$workflowLibrarySortOptions.set(workflowSortOptions);
}
return () => {
$workflowLibrarySortOptions.set(DEFAULT_WORKFLOW_LIBRARY_SORT_OPTIONS);
};
}, [workflowSortOptions]);
useEffect(() => {
if (socketOptions) {
$socketOptions.set(socketOptions);

View File

@@ -15,7 +15,7 @@ import { $isWorkflowLibraryModalOpen } from 'features/nodes/store/workflowLibrar
import { $isStylePresetsMenuOpen, activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { toast } from 'features/toast/toast';
import { activeTabCanvasRightPanelChanged, setActiveTab } from 'features/ui/store/uiSlice';
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { atom } from 'nanostores';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
@@ -57,7 +57,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
const { t } = useTranslation();
const didParseOpenAPISchema = useStore($hasTemplates);
const store = useAppStore();
const { getAndLoadWorkflow } = useGetAndLoadLibraryWorkflow();
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const handleSendToCanvas = useCallback(
async (imageName: string) => {
@@ -113,10 +113,15 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
const handleLoadWorkflow = useCallback(
async (workflowId: string) => {
// This shows a toast
await getAndLoadWorkflow(workflowId);
store.dispatch(setActiveTab('workflows'));
await loadWorkflowWithDialog({
type: 'library',
data: workflowId,
onSuccess: () => {
store.dispatch(setActiveTab('workflows'));
},
});
},
[getAndLoadWorkflow, store]
[loadWorkflowWithDialog, store]
);
const handleSelectStylePreset = useCallback(

View File

@@ -1,4 +0,0 @@
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import { atom } from 'nanostores';
export const $workflowCategories = atom<WorkflowCategory[]>(['user', 'default']);

View File

@@ -19,6 +19,7 @@ import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/galle
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice';
import { workflowLibraryPersistConfig, workflowLibrarySlice } from 'features/nodes/store/workflowLibrarySlice';
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
@@ -68,6 +69,7 @@ const allReducers = {
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
[canvasStagingAreaSlice.name]: canvasStagingAreaSlice.reducer,
[lorasSlice.name]: lorasSlice.reducer,
[workflowLibrarySlice.name]: workflowLibrarySlice.reducer,
};
const rootReducer = combineReducers(allReducers);
@@ -113,6 +115,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig,
[canvasStagingAreaPersistConfig.name]: canvasStagingAreaPersistConfig,
[lorasPersistConfig.name]: lorasPersistConfig,
[workflowLibraryPersistConfig.name]: workflowLibraryPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {

View File

@@ -0,0 +1,43 @@
import type { Selector } from '@reduxjs/toolkit';
import { useAppStore } from 'app/store/nanostores/store';
import type { RootState } from 'app/store/store';
import { useEffect, useState } from 'react';
/**
* A hook that returns a debounced value from the app state.
*
* @param selector The redux selector
* @param debounceMs The debounce time in milliseconds
* @returns The debounced value
*/
export const useDebouncedAppSelector = <T>(selector: Selector<RootState, T>, debounceMs: number = 300) => {
const store = useAppStore();
const [value, setValue] = useState<T>(() => selector(store.getState()));
useEffect(() => {
let prevValue = selector(store.getState());
let timeout: number | null = null;
const unsubscribe = store.subscribe(() => {
const value = selector(store.getState());
if (value !== prevValue) {
if (timeout !== null) {
window.clearTimeout(timeout);
}
timeout = window.setTimeout(() => {
setValue(value);
prevValue = value;
}, debounceMs);
}
});
return () => {
unsubscribe();
if (timeout !== null) {
window.clearTimeout(timeout);
}
};
}, [debounceMs, selector, store]);
return value;
};

View File

@@ -0,0 +1,56 @@
import { Box, type BoxProps, type SystemStyleObject } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { type FocusRegionName, useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
import { selectSystemShouldEnableHighlightFocusedRegions } from 'features/system/store/systemSlice';
import { memo, useMemo, useRef } from 'react';
interface FocusRegionWrapperProps extends BoxProps {
region: FocusRegionName;
focusOnMount?: boolean;
}
const FOCUS_REGION_STYLES: SystemStyleObject = {
position: 'relative',
'&[data-highlighted="true"]::after': {
borderColor: 'blue.700',
},
'&::after': {
content: '""',
position: 'absolute',
inset: 0,
zIndex: 1,
borderRadius: 'base',
border: '2px solid',
borderColor: 'transparent',
pointerEvents: 'none',
transition: 'border-color 0.1s ease-in-out',
},
};
export const FocusRegionWrapper = memo(
({ region, focusOnMount = false, sx, children, ...boxProps }: FocusRegionWrapperProps) => {
const shouldHighlightFocusedRegions = useAppSelector(selectSystemShouldEnableHighlightFocusedRegions);
const ref = useRef<HTMLDivElement>(null);
const options = useMemo(() => ({ focusOnMount }), [focusOnMount]);
useFocusRegion(region, ref, options);
const isFocused = useIsRegionFocused(region);
const isHighlighted = isFocused && shouldHighlightFocusedRegions;
return (
<Box
ref={ref}
tabIndex={-1}
sx={useMemo(() => ({ ...FOCUS_REGION_STYLES, ...sx }), [sx])}
data-highlighted={isHighlighted}
{...boxProps}
>
{children}
</Box>
);
}
);
FocusRegionWrapper.displayName = 'FocusRegionWrapper';

View File

@@ -30,7 +30,7 @@ const log = logger('system');
/**
* The names of the focus regions.
*/
type FocusRegionName = 'gallery' | 'layers' | 'canvas' | 'workflows' | 'viewer';
export type FocusRegionName = 'gallery' | 'layers' | 'canvas' | 'workflows' | 'viewer';
/**
* A map of focus regions to the elements that are part of that region.

View File

@@ -1,28 +1,33 @@
import { Divider, Flex } from '@invoke-ai/ui-library';
import { Divider, Flex, type SystemStyleObject } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion } from 'common/hooks/focus';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo, useRef } from 'react';
import { memo } from 'react';
import { ParamDenoisingStrength } from './ParamDenoisingStrength';
const FOCUS_REGION_STYLES: SystemStyleObject = {
width: 'full',
height: 'full',
};
export const CanvasLayersPanelContent = memo(() => {
const hasEntities = useAppSelector(selectHasEntities);
const layersPanelFocusRef = useRef<HTMLDivElement>(null);
useFocusRegion('layers', layersPanelFocusRef);
return (
<Flex ref={layersPanelFocusRef} flexDir="column" gap={2} w="full" h="full">
<EntityListSelectedEntityActionBar />
<Divider py={0} />
<ParamDenoisingStrength />
<Divider py={0} />
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
</Flex>
<FocusRegionWrapper region="layers" sx={FOCUS_REGION_STYLES}>
<Flex flexDir="column" gap={2} w="full" h="full">
<EntityListSelectedEntityActionBar />
<Divider py={0} />
<ParamDenoisingStrength />
<Divider py={0} />
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
</Flex>
</FocusRegionWrapper>
);
});

View File

@@ -1,6 +1,14 @@
import { ContextMenu, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import {
ContextMenu,
Flex,
IconButton,
Menu,
MenuButton,
MenuList,
type SystemStyleObject,
} from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion } from 'common/hooks/focus';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
@@ -18,11 +26,16 @@ import { Transform } from 'features/controlLayers/components/Transform/Transform
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import { GatedImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import { memo, useCallback, useRef } from 'react';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
const FOCUS_REGION_STYLES: SystemStyleObject = {
width: 'full',
height: 'full',
};
const MenuContent = () => {
return (
<CanvasManagerProviderGate>
@@ -35,7 +48,6 @@ const MenuContent = () => {
};
export const CanvasMainPanelContent = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
@@ -43,82 +55,81 @@ export const CanvasMainPanelContent = memo(() => {
return <MenuContent />;
}, []);
useFocusRegion('canvas', ref);
return (
<Flex
tabIndex={-1}
ref={ref}
borderRadius="base"
position="relative"
flexDirection="column"
height="full"
width="full"
gap={2}
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<CanvasManagerProviderGate>
<CanvasToolbar />
</CanvasManagerProviderGate>
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
<Flex
ref={ref}
position="relative"
w="full"
h="full"
bg={dynamicGrid ? 'base.850' : 'base.900'}
borderRadius="base"
overflow="hidden"
>
<InvokeCanvasComponent />
<CanvasManagerProviderGate>
<Flex
position="absolute"
flexDir="column"
top={1}
insetInlineStart={1}
pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask />
<CanvasAlertsSendingToGallery />
<CanvasAlertsInvocationProgress />
</Flex>
<Flex position="absolute" top={1} insetInlineEnd={1}>
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
<MenuContent />
</Menu>
</Flex>
</CanvasManagerProviderGate>
</Flex>
)}
</ContextMenu>
<Flex position="absolute" bottom={4} gap={2} align="center" justify="center">
<FocusRegionWrapper region="canvas" sx={FOCUS_REGION_STYLES}>
<Flex
tabIndex={-1}
borderRadius="base"
position="relative"
flexDirection="column"
height="full"
width="full"
gap={2}
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<CanvasManagerProviderGate>
<StagingAreaIsStagingGate>
<StagingAreaToolbar />
</StagingAreaIsStagingGate>
<CanvasToolbar />
</CanvasManagerProviderGate>
</Flex>
<Flex position="absolute" bottom={4}>
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
<Flex
ref={ref}
position="relative"
w="full"
h="full"
bg={dynamicGrid ? 'base.850' : 'base.900'}
borderRadius="base"
overflow="hidden"
>
<InvokeCanvasComponent />
<CanvasManagerProviderGate>
<Flex
position="absolute"
flexDir="column"
top={1}
insetInlineStart={1}
pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask />
<CanvasAlertsSendingToGallery />
<CanvasAlertsInvocationProgress />
</Flex>
<Flex position="absolute" top={1} insetInlineEnd={1}>
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
<MenuContent />
</Menu>
</Flex>
</CanvasManagerProviderGate>
</Flex>
)}
</ContextMenu>
<Flex position="absolute" bottom={4} gap={2} align="center" justify="center">
<CanvasManagerProviderGate>
<StagingAreaIsStagingGate>
<StagingAreaToolbar />
</StagingAreaIsStagingGate>
</CanvasManagerProviderGate>
</Flex>
<Flex position="absolute" bottom={4}>
<CanvasManagerProviderGate>
<Filter />
<Transform />
<SelectObject />
</CanvasManagerProviderGate>
</Flex>
<CanvasManagerProviderGate>
<Filter />
<Transform />
<SelectObject />
<CanvasDropArea />
</CanvasManagerProviderGate>
<GatedImageViewer />
</Flex>
<CanvasManagerProviderGate>
<CanvasDropArea />
</CanvasManagerProviderGate>
<GatedImageViewer />
</Flex>
</FocusRegionWrapper>
);
});

View File

@@ -7,6 +7,7 @@ import { Box, Flex, Heading } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { getStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
import { $focusedRegion } from 'common/hooks/focus';
import { setFileToPaste } from 'features/controlLayers/components/CanvasPasteModal';
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
import type { DndTargetState } from 'features/dnd/types';
@@ -99,10 +100,18 @@ export const FullscreenDropzone = memo(() => {
return;
}
const focusedRegion = $focusedRegion.get();
// While on the canvas tab and when pasting a single image, canvas may want to create a new layer. Let it handle
// the paste event.
const [firstImageFile] = files;
if (!isImageViewerOpen && activeTab === 'canvas' && files.length === 1 && firstImageFile) {
if (
focusedRegion === 'canvas' &&
!isImageViewerOpen &&
activeTab === 'canvas' &&
files.length === 1 &&
firstImageFile
) {
setFileToPaste(firstImageFile);
return;
}

View File

@@ -1,6 +1,15 @@
import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
import {
Box,
Button,
Collapse,
Divider,
Flex,
IconButton,
type SystemStyleObject,
useDisclosure,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion } from 'common/hooks/focus';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
@@ -20,14 +29,20 @@ import { Gallery } from './Gallery';
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
const FOCUS_REGION_STYLES: SystemStyleObject = {
width: 'full',
height: 'full',
position: 'relative',
flexDirection: 'column',
display: 'flex',
};
const GalleryPanelContent = () => {
const { t } = useTranslation();
const boardSearchText = useAppSelector(selectBoardSearchText);
const dispatch = useAppDispatch();
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const galleryPanelFocusRef = useRef<HTMLDivElement>(null);
useFocusRegion('gallery', galleryPanelFocusRef);
const boardsListPanelOptions = useMemo<UsePanelOptions>(
() => ({
@@ -50,7 +65,7 @@ const GalleryPanelContent = () => {
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
return (
<Flex ref={galleryPanelFocusRef} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
<FocusRegionWrapper region="gallery" sx={FOCUS_REGION_STYLES}>
<Flex alignItems="center" justifyContent="space-between" w="full">
<Flex flexGrow={1} flexBasis={0}>
<Button
@@ -99,7 +114,7 @@ const GalleryPanelContent = () => {
<Gallery />
</Panel>
</PanelGroup>
</Flex>
</FocusRegionWrapper>
);
};

View File

@@ -1,9 +1,8 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { SpinnerIcon } from 'features/gallery/components/ImageContextMenu/SpinnerIcon';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFlowArrowBold } from 'react-icons/pi';
@@ -11,19 +10,15 @@ import { PiFlowArrowBold } from 'react-icons/pi';
export const ImageMenuItemLoadWorkflow = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const [getAndLoadEmbeddedWorkflow, { isLoading }] = useGetAndLoadEmbeddedWorkflow();
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const hasTemplates = useStore($hasTemplates);
const onClick = useCallback(() => {
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
}, [getAndLoadEmbeddedWorkflow, imageDTO.image_name]);
loadWorkflowWithDialog({ type: 'image', data: imageDTO.image_name });
}, [loadWorkflowWithDialog, imageDTO.image_name]);
return (
<MenuItem
icon={isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
onClickCapture={onClick}
isDisabled={!imageDTO.has_workflow || !hasTemplates}
>
<MenuItem icon={<PiFlowArrowBold />} onClickCapture={onClick} isDisabled={!imageDTO.has_workflow || !hasTemplates}>
{t('nodes.loadWorkflow')}
</MenuItem>
);

View File

@@ -1,7 +0,0 @@
import { Flex, Spinner } from '@invoke-ai/ui-library';
export const SpinnerIcon = () => (
<Flex w="14px" alignItems="center" justifyContent="center">
<Spinner size="xs" />
</Flex>
);

View File

@@ -1,6 +1,6 @@
import { Box, Flex, IconButton } from '@invoke-ai/ui-library';
import { Box, IconButton, type SystemStyleObject } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion } from 'common/hooks/focus';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar';
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
@@ -9,7 +9,7 @@ import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewe
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
import { selectHasImageToCompare } from 'features/gallery/store/gallerySelectors';
import type { ReactNode } from 'react';
import { memo, useRef } from 'react';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
@@ -25,29 +25,24 @@ const useFocusRegionOptions = {
focusOnMount: true,
};
const FOCUS_REGION_STYLES: SystemStyleObject = {
width: 'full',
height: 'full',
position: 'absolute',
flexDirection: 'column',
inset: 0,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
};
export const ImageViewer = memo(({ closeButton }: Props) => {
useAssertSingleton('ImageViewer');
const hasImageToCompare = useAppSelector(selectHasImageToCompare);
const [containerRef, containerDims] = useMeasure<HTMLDivElement>();
const ref = useRef<HTMLDivElement>(null);
useFocusRegion('viewer', ref, useFocusRegionOptions);
return (
<Flex
ref={ref}
tabIndex={-1}
layerStyle="first"
borderRadius="base"
position="absolute"
flexDirection="column"
top={0}
right={0}
bottom={0}
left={0}
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<FocusRegionWrapper region="viewer" sx={FOCUS_REGION_STYLES} layerStyle="first" {...useFocusRegionOptions}>
{hasImageToCompare && <CompareToolbar />}
{!hasImageToCompare && <ViewerToolbar closeButton={closeButton} />}
<Box ref={containerRef} w="full" h="full" p={2}>
@@ -55,7 +50,7 @@ export const ImageViewer = memo(({ closeButton }: Props) => {
{hasImageToCompare && <ImageComparison containerDims={containerDims} />}
</Box>
<ImageComparisonDroppable />
</Flex>
</FocusRegionWrapper>
);
});

View File

@@ -7,6 +7,7 @@ import { LOADING_SYMBOL, useHasImages } from 'features/gallery/hooks/useHasImage
import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectIsLocal } from 'features/system/store/configSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { setActiveTab } from 'features/ui/store/uiSlice';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useMemo } from 'react';
@@ -19,6 +20,7 @@ export const NoContentForViewer = memo(() => {
const [mainModels, { data }] = useMainModels();
const isLocal = useAppSelector(selectIsLocal);
const isEnabled = useFeatureStatus('starterModels');
const activeTab = useAppSelector(selectActiveTab);
const { t } = useTranslation();
const showStarterBundles = useMemo(() => {
@@ -38,10 +40,10 @@ export const NoContentForViewer = memo(() => {
}
return (
<Flex flexDir="column" gap={8} alignItems="center" textAlign="center" maxW="600px">
<Flex flexDir="column" gap={8} alignItems="center" textAlign="center" maxW="400px">
<InvokeLogoIcon w={32} h={32} />
<Flex flexDir="column" gap={4} alignItems="center" textAlign="center">
{isLocal ? <GetStartedLocal /> : <GetStartedCommercial />}
{isLocal ? <GetStartedLocal /> : activeTab === 'workflows' ? <GetStartedWorkflows /> : <GetStartedCommercial />}
{showStarterBundles && <StarterBundlesCallout />}
<Divider />
<GettingStartedVideosCallout />
@@ -103,6 +105,14 @@ const GetStartedCommercial = () => {
);
};
const GetStartedWorkflows = () => {
return (
<Text fontSize="md" color="base.200">
<Trans i18nKey="newUserExperience.toGetStartedWorkflow" components={{ StrongComponent }} />
</Text>
);
};
const GettingStartedVideosCallout = () => {
return (
<Text fontSize="md" color="base.200">

View File

@@ -17,7 +17,7 @@ import {
} from 'features/stylePresets/store/stylePresetSlice';
import { toast } from 'features/toast/toast';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
@@ -147,14 +147,15 @@ export const useImageActions = (imageDTO: ImageDTO) => {
});
}, [metadata, imageDTO]);
const [getAndLoadEmbeddedWorkflow] = useGetAndLoadEmbeddedWorkflow();
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const loadWorkflow = useCallback(() => {
const loadWorkflowFromImage = useCallback(() => {
if (!imageDTO.has_workflow || !hasTemplates) {
return;
}
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
}, [getAndLoadEmbeddedWorkflow, hasTemplates, imageDTO.has_workflow, imageDTO.image_name]);
loadWorkflowWithDialog({ type: 'image', data: imageDTO.image_name });
}, [hasTemplates, imageDTO.has_workflow, imageDTO.image_name, loadWorkflowWithDialog]);
const recallSize = useCallback(() => {
if (isStaging) {
@@ -180,7 +181,7 @@ export const useImageActions = (imageDTO: ImageDTO) => {
recallSeed,
recallPrompts,
createAsPreset,
loadWorkflow,
loadWorkflow: loadWorkflowFromImage,
hasWorkflow: imageDTO.has_workflow,
recallSize,
upscale,

View File

@@ -1,10 +1,10 @@
import { Flex } from '@invoke-ai/ui-library';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { useFocusRegion } from 'common/hooks/focus';
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
import { memo, useRef } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFlowArrowBold } from 'react-icons/pi';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
@@ -13,24 +13,20 @@ import { Flow } from './flow/Flow';
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
const FOCUS_REGION_STYLES: SystemStyleObject = {
position: 'relative',
width: 'full',
height: 'full',
alignItems: 'center',
justifyContent: 'center',
};
const NodeEditor = () => {
const { data, isLoading } = useGetOpenAPISchemaQuery();
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
useFocusRegion('workflows', ref);
return (
<Flex
tabIndex={-1}
ref={ref}
layerStyle="first"
position="relative"
width="full"
height="full"
borderRadius="base"
alignItems="center"
justifyContent="center"
>
<FocusRegionWrapper region="workflows" layerStyle="first" sx={FOCUS_REGION_STYLES}>
{data && (
<>
<Flow />
@@ -42,7 +38,7 @@ const NodeEditor = () => {
)}
<WorkflowEditorSettings />
{isLoading && <IAINoContentFallback label={t('nodes.loadingNodes')} icon={PiFlowArrowBold} />}
</Flex>
</FocusRegionWrapper>
);
};

View File

@@ -252,7 +252,7 @@ export const Flow = memo(() => {
id: 'pasteSelection',
category: 'workflows',
callback: pasteSelection,
options: { preventDefault: true },
options: { enabled: isWorkflowsFocused, preventDefault: true },
dependencies: [pasteSelection],
});
@@ -260,7 +260,7 @@ export const Flow = memo(() => {
id: 'pasteSelectionWithEdges',
category: 'workflows',
callback: pasteSelectionWithEdges,
options: { preventDefault: true },
options: { enabled: isWorkflowsFocused, preventDefault: true },
dependencies: [pasteSelectionWithEdges],
});
@@ -270,7 +270,7 @@ export const Flow = memo(() => {
callback: () => {
dispatch(undo());
},
options: { enabled: mayUndo, preventDefault: true },
options: { enabled: isWorkflowsFocused && mayUndo, preventDefault: true },
dependencies: [mayUndo],
});
@@ -280,7 +280,7 @@ export const Flow = memo(() => {
callback: () => {
dispatch(redo());
},
options: { enabled: mayRedo, preventDefault: true },
options: { enabled: isWorkflowsFocused && mayRedo, preventDefault: true },
dependencies: [mayRedo],
});

View File

@@ -3,24 +3,35 @@ import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/f
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
export const FloatFieldInput = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
export const FloatFieldInput = memo(
(
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
nodeId,
field.name,
fieldTemplate,
settings
);
return (
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
flex="1 1 0"
/>
);
});
return (
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
flex="1 1 0"
/>
);
}
);
FloatFieldInput.displayName = 'FloatFieldInput ';

View File

@@ -3,18 +3,27 @@ import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/f
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
export const FloatFieldInputAndSlider = memo(
(props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
(
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
nodeId,
field.name,
fieldTemplate,
settings
);
return (
<>
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={value}
value={field.value}
min={min}
max={max}
step={step}
@@ -27,7 +36,7 @@ export const FloatFieldInputAndSlider = memo(
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={value}
value={field.value}
min={min}
max={max}
step={step}

View File

@@ -3,26 +3,37 @@ import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/f
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
export const FloatFieldSlider = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
export const FloatFieldSlider = memo(
(
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
nodeId,
field.name,
fieldTemplate,
settings
);
return (
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
marks
withThumbTooltip
flex="1 1 0"
/>
);
});
return (
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
marks
withThumbTooltip
flex="1 1 0"
/>
);
}
);
FloatFieldSlider.displayName = 'FloatFieldSlider ';

View File

@@ -1,65 +1,89 @@
import { NUMPY_RAND_MAX } from 'app/constants';
import { useAppDispatch } from 'app/store/storeHooks';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import type { FloatFieldInputTemplate } from 'features/nodes/types/field';
import { constrainNumber } from 'features/nodes/util/constrainNumber';
import { isNil } from 'lodash-es';
import { useCallback, useMemo } from 'react';
export const useFloatField = (props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
const { nodeId, field, fieldTemplate } = props;
export const useFloatField = (
nodeId: string,
fieldName: string,
fieldTemplate: FloatFieldInputTemplate,
overrides: { min?: number; max?: number; step?: number } = {}
) => {
const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
const dispatch = useAppDispatch();
const onChange = useCallback(
(value: number) => {
dispatch(fieldFloatValueChanged({ nodeId, fieldName: field.name, value }));
},
[dispatch, field.name, nodeId]
);
const min = useMemo(() => {
let min = -NUMPY_RAND_MAX;
if (!isNil(fieldTemplate.minimum)) {
min = fieldTemplate.minimum;
}
if (!isNil(fieldTemplate.exclusiveMinimum)) {
min = fieldTemplate.exclusiveMinimum + 0.01;
}
return min;
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
const max = useMemo(() => {
let max = NUMPY_RAND_MAX;
if (!isNil(fieldTemplate.maximum)) {
max = fieldTemplate.maximum;
}
if (!isNil(fieldTemplate.exclusiveMaximum)) {
max = fieldTemplate.exclusiveMaximum - 0.01;
}
return max;
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
const step = useMemo(() => {
if (overrideStep !== undefined) {
return overrideStep;
}
if (isNil(fieldTemplate.multipleOf)) {
return 0.1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf]);
}, [fieldTemplate.multipleOf, overrideStep]);
const fineStep = useMemo(() => {
if (overrideStep !== undefined) {
return overrideStep;
}
if (isNil(fieldTemplate.multipleOf)) {
return 0.01;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf]);
}, [fieldTemplate.multipleOf, overrideStep]);
const min = useMemo(() => {
let min = -NUMPY_RAND_MAX;
if (overrideMin !== undefined) {
min = overrideMin;
} else if (!isNil(fieldTemplate.minimum)) {
min = fieldTemplate.minimum;
} else if (!isNil(fieldTemplate.exclusiveMinimum)) {
min = fieldTemplate.exclusiveMinimum + 0.01;
}
return roundUpToMultiple(min, step);
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum, overrideMin, step]);
const max = useMemo(() => {
let max = NUMPY_RAND_MAX;
if (overrideMax !== undefined) {
max = overrideMax;
} else if (!isNil(fieldTemplate.maximum)) {
max = fieldTemplate.maximum;
} else if (!isNil(fieldTemplate.exclusiveMaximum)) {
max = fieldTemplate.exclusiveMaximum - 0.01;
}
return roundDownToMultiple(max, step);
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum, overrideMax, step]);
const constrainValue = useCallback(
(v: number) =>
constrainNumber(v, { min, max, step: step }, { min: overrideMin, max: overrideMax, step: overrideStep }),
[max, min, overrideMax, overrideMin, overrideStep, step]
);
const onChange = useCallback(
(value: number) => {
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value }));
},
[dispatch, fieldName, nodeId]
);
return {
defaultValue: fieldTemplate.default,
onChange,
value: field.value,
min,
max,
step,
fineStep,
constrainValue,
};
};

View File

@@ -95,6 +95,8 @@ import {
} from 'features/nodes/types/field';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
import BoardFieldInputComponent from './inputs/BoardFieldInputComponent';
import BooleanFieldInputComponent from './inputs/BooleanFieldInputComponent';
@@ -157,6 +159,8 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
return <StringFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
} else if (settings.component === 'textarea') {
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
} else {
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
}
}
@@ -171,32 +175,47 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
if (!isIntegerFieldInputInstance(field)) {
return null;
}
if (settings?.type !== 'integer-field-config') {
if (!settings || settings.type !== 'integer-field-config') {
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (settings.component === 'number-input') {
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
} else if (settings.component === 'slider') {
return <IntegerFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
} else if (settings.component === 'number-input-and-slider') {
return <IntegerFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
}
if (settings.component === 'slider') {
return <IntegerFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
}
if (settings.component === 'number-input-and-slider') {
return <IntegerFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
}
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
}
if (isFloatFieldInputTemplate(template)) {
if (!isFloatFieldInputInstance(field)) {
return null;
}
if (settings?.type !== 'float-field-config') {
if (!settings || settings.type !== 'float-field-config') {
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (settings.component === 'number-input') {
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
} else if (settings.component === 'slider') {
return <FloatFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
} else if (settings.component === 'number-input-and-slider') {
return <FloatFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
}
if (settings.component === 'slider') {
return <FloatFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
}
if (settings.component === 'number-input-and-slider') {
return <FloatFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
}
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
}
if (isIntegerFieldCollectionInputTemplate(template)) {

View File

@@ -1,4 +1,5 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library';
import { useInputFieldErrors } from 'features/nodes/hooks/useInputFieldErrors';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
@@ -17,6 +18,7 @@ export const InputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => {
const fieldInstance = useInputFieldInstance(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
const fieldErrors = useInputFieldErrors(nodeId, fieldName);
const fieldTitle = useMemo(() => {
if (fieldInstance.label && fieldTemplate.title) {
@@ -42,6 +44,16 @@ export const InputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => {
<Text>
{t('common.input')}: {startCase(fieldTemplate.input)}
</Text>
{fieldErrors.length > 0 && (
<>
<Text color="error.500">{t('common.error_withCount', { count: fieldErrors.length })}:</Text>
<UnorderedList>
{fieldErrors.map(({ issue }) => (
<ListItem key={issue}>{issue}</ListItem>
))}
</UnorderedList>
</>
)}
</Flex>
);
});

View File

@@ -3,23 +3,37 @@ import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/I
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
export const IntegerFieldInput = memo(
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
(
props: FieldComponentProps<
IntegerFieldInputInstance,
IntegerFieldInputTemplate,
{ settings?: NodeFieldIntegerSettings }
>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep, constrainValue } = useIntegerField(
nodeId,
field.name,
fieldTemplate,
settings
);
return (
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={value}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
flex="1 1 0"
constrainValue={constrainValue}
/>
);
}

View File

@@ -3,18 +3,31 @@ import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/I
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
export const IntegerFieldInputAndSlider = memo(
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
(
props: FieldComponentProps<
IntegerFieldInputInstance,
IntegerFieldInputTemplate,
{ settings?: NodeFieldIntegerSettings }
>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField(
nodeId,
field.name,
fieldTemplate,
settings
);
return (
<>
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={value}
value={field.value}
min={min}
max={max}
step={step}
@@ -27,7 +40,7 @@ export const IntegerFieldInputAndSlider = memo(
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={value}
value={field.value}
min={min}
max={max}
step={step}

View File

@@ -3,17 +3,30 @@ import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/I
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
export const IntegerFieldSlider = memo(
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
(
props: FieldComponentProps<
IntegerFieldInputInstance,
IntegerFieldInputTemplate,
{ settings?: NodeFieldIntegerSettings }
>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField(
nodeId,
field.name,
fieldTemplate,
settings
);
return (
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={value}
value={field.value}
min={min}
max={max}
step={step}

View File

@@ -1,65 +1,89 @@
import { NUMPY_RAND_MAX } from 'app/constants';
import { useAppDispatch } from 'app/store/storeHooks';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
import { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import type { IntegerFieldInputTemplate } from 'features/nodes/types/field';
import { constrainNumber } from 'features/nodes/util/constrainNumber';
import { isNil } from 'lodash-es';
import { useCallback, useMemo } from 'react';
export const useIntegerField = (props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
const { nodeId, field, fieldTemplate } = props;
export const useIntegerField = (
nodeId: string,
fieldName: string,
fieldTemplate: IntegerFieldInputTemplate,
overrides: { min?: number; max?: number; step?: number } = {}
) => {
const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
const dispatch = useAppDispatch();
const onChange = useCallback(
(value: number) => {
dispatch(fieldIntegerValueChanged({ nodeId, fieldName: field.name, value: Math.floor(Number(value)) }));
},
[dispatch, field.name, nodeId]
);
const step = useMemo(() => {
if (overrideStep !== undefined) {
return overrideStep;
}
if (isNil(fieldTemplate.multipleOf)) {
return 1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf, overrideStep]);
const fineStep = useMemo(() => {
if (overrideStep !== undefined) {
return overrideStep;
}
if (isNil(fieldTemplate.multipleOf)) {
return 1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf, overrideStep]);
const min = useMemo(() => {
let min = -NUMPY_RAND_MAX;
if (!isNil(fieldTemplate.minimum)) {
if (overrideMin !== undefined) {
min = overrideMin;
} else if (!isNil(fieldTemplate.minimum)) {
min = fieldTemplate.minimum;
}
if (!isNil(fieldTemplate.exclusiveMinimum)) {
} else if (!isNil(fieldTemplate.exclusiveMinimum)) {
min = fieldTemplate.exclusiveMinimum + 1;
}
return min;
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
return roundUpToMultiple(min, step);
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum, overrideMin, step]);
const max = useMemo(() => {
let max = NUMPY_RAND_MAX;
if (!isNil(fieldTemplate.maximum)) {
if (overrideMax !== undefined) {
max = overrideMax;
} else if (!isNil(fieldTemplate.maximum)) {
max = fieldTemplate.maximum;
}
if (!isNil(fieldTemplate.exclusiveMaximum)) {
} else if (!isNil(fieldTemplate.exclusiveMaximum)) {
max = fieldTemplate.exclusiveMaximum - 1;
}
return max;
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
const step = useMemo(() => {
if (isNil(fieldTemplate.multipleOf)) {
return 1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf]);
return roundDownToMultiple(max, step);
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum, overrideMax, step]);
const fineStep = useMemo(() => {
if (isNil(fieldTemplate.multipleOf)) {
return 1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf]);
const constrainValue = useCallback(
(v: number) =>
constrainNumber(v, { min, max, step: step }, { min: overrideMin, max: overrideMax, step: overrideStep }),
[max, min, overrideMax, overrideMin, overrideStep, step]
);
const onChange = useCallback(
(value: number) => {
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value }));
},
[dispatch, fieldName, nodeId]
);
return {
defaultValue: fieldTemplate.default,
onChange,
value: field.value,
min,
max,
step,
fineStep,
constrainValue,
};
};

View File

@@ -80,7 +80,7 @@ export const FloatGeneratorFieldInputComponent = memo(
}, [debouncedField, t]);
return (
<Flex flexDir="column" gap={2}>
<Flex flexDir="column" gap={2} flexGrow={1}>
<Select
className={`${NO_WHEEL_CLASS} ${NO_DRAG_CLASS}`}
onChange={onChangeGeneratorType}

View File

@@ -82,14 +82,7 @@ export const ImageFieldCollectionInputComponent = memo(
justifyContent="center"
>
{(!field.value || field.value.length === 0) && (
<UploadMultipleImageButton
w="full"
h="auto"
isError={isInvalid}
onUpload={onUpload}
fontSize={24}
variant="ghost"
/>
<UploadMultipleImageButton w="full" h="auto" isError={isInvalid} onUpload={onUpload} fontSize={24} />
)}
{field.value && field.value.length > 0 && (
<Box w="full" h="auto" p={1} sx={sx} data-error={isInvalid} borderRadius="base">

View File

@@ -70,7 +70,7 @@ export const ImageGeneratorFieldInputComponent = memo(
}, [field, resolveAndSetValuesAsString]);
return (
<Flex flexDir="column" gap={2}>
<Flex flexDir="column" gap={2} flexGrow={1}>
<Select
className={`${NO_WHEEL_CLASS} ${NO_DRAG_CLASS}`}
onChange={onChangeGeneratorType}

View File

@@ -82,7 +82,7 @@ export const IntegerGeneratorFieldInputComponent = memo(
}, [debouncedField, t]);
return (
<Flex flexDir="column" gap={2}>
<Flex flexDir="column" gap={2} flexGrow={1}>
<Select
className={`${NO_WHEEL_CLASS} ${NO_DRAG_CLASS}`}
onChange={onChangeGeneratorType}

View File

@@ -74,7 +74,7 @@ export const StringGeneratorFieldInputComponent = memo(
}, [field, resolveAndSetValuesAsString]);
return (
<Flex flexDir="column" gap={2}>
<Flex flexDir="column" gap={2} flexGrow={1}>
<Select
className={`${NO_WHEEL_CLASS} ${NO_DRAG_CLASS}`}
onChange={onChangeGeneratorType}

View File

@@ -1,7 +1,11 @@
import type { FieldInputInstance, FieldInputTemplate } from 'features/nodes/types/field';
export type FieldComponentProps<V extends FieldInputInstance, T extends FieldInputTemplate, C = void> = {
export type FieldComponentProps<
TFieldInstance extends FieldInputInstance,
TFieldTemplate extends FieldInputTemplate,
FieldSettings = void,
> = {
nodeId: string;
field: V;
fieldTemplate: T;
} & Omit<C, 'nodeId' | 'field' | 'fieldTemplate'>;
field: TFieldInstance;
fieldTemplate: TFieldTemplate;
} & Omit<FieldSettings, 'nodeId' | 'field' | 'fieldTemplate'>;

View File

@@ -1,4 +1,4 @@
import { Text } from '@invoke-ai/ui-library';
import { Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
import { selectWorkflowDescription } from 'features/nodes/store/workflowSlice';
@@ -13,9 +13,11 @@ export const ActiveWorkflowDescription = memo(() => {
}
return (
<Text color="base.300" fontStyle="italic" pb={2} sx={linkifySx}>
<Linkify options={linkifyOptions}>{description}</Linkify>
</Text>
<Tooltip label={description}>
<Text color="base.300" fontStyle="italic" sx={linkifySx} noOfLines={1}>
<Linkify options={linkifyOptions}>{description}</Linkify>
</Text>
</Tooltip>
);
});

View File

@@ -61,6 +61,7 @@ const containerViewModeSx: SystemStyleObject = {
overflowX: 'auto',
overflowY: 'visible',
h: 'min-content',
flexShrink: 0,
},
'&[data-parent-layout="column"]': {
w: 'full',

View File

@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
const headingSx: SystemStyleObject = {
fontWeight: 'bold',
fontSize: '2xl',
whiteSpace: 'pre-wrap',
'&[data-is-empty="true"]': {
opacity: 0.3,
},

View File

@@ -1,12 +1,37 @@
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
import { CompositeNumberInput, Flex, FormControl, FormLabel, Select, Switch } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import { type NodeFieldFloatSettings, zNumberComponent } from 'features/nodes/types/workflow';
import { constrainNumber } from 'features/nodes/util/constrainNumber';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const NodeFieldElementFloatSettings = memo(({ id, config }: { id: string; config: NodeFieldFloatSettings }) => {
type Props = {
id: string;
config: NodeFieldFloatSettings;
nodeId: string;
fieldName: string;
fieldTemplate: FloatFieldInputTemplate;
};
export const NodeFieldElementFloatSettings = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
return (
<>
<SettingComponent id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMin id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMax id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
</>
);
});
NodeFieldElementFloatSettings.displayName = 'NodeFieldElementFloatSettings';
const SettingComponent = memo(({ id, config }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -22,7 +47,7 @@ export const NodeFieldElementFloatSettings = memo(({ id, config }: { id: string;
);
return (
<FormControl>
<FormControl orientation="vertical">
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
<Select value={config.component} onChange={onChangeComponent} size="sm">
<option value="number-input">{t('workflows.builder.numberInput')}</option>
@@ -32,4 +57,128 @@ export const NodeFieldElementFloatSettings = memo(({ id, config }: { id: string;
</FormControl>
);
});
NodeFieldElementFloatSettings.displayName = 'NodeFieldElementFloatSettings';
SettingComponent.displayName = 'SettingComponent';
const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
const onToggleSetting = useCallback(() => {
const newConfig: NodeFieldFloatSettings = {
...config,
min: config.min !== undefined ? undefined : floatField.min,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
}, [config, dispatch, floatField, id]);
const onChange = useCallback(
(min: number) => {
const newConfig: NodeFieldFloatSettings = {
...config,
min,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
// We may need to update the value if it is outside the new min/max range
const constrained = constrainNumber(field.value, floatField, newConfig);
if (field.value !== constrained) {
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: constrained }));
}
},
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
);
const constraintMin = useMemo(
() => roundUpToMultiple(floatField.min, floatField.step),
[floatField.min, floatField.step]
);
const constraintMax = useMemo(
() => (config.max ?? floatField.max) - floatField.step,
[config.max, floatField.max, floatField.step]
);
return (
<FormControl orientation="vertical">
<Flex justifyContent="space-between" w="full" alignItems="center">
<FormLabel m={0}>{t('workflows.builder.minimum')}</FormLabel>
<Switch isChecked={config.min !== undefined} onChange={onToggleSetting} size="sm" />
</Flex>
<CompositeNumberInput
w="full"
isDisabled={config.min === undefined}
value={config.min === undefined ? (`${floatField.min} (inherited)` as unknown as number) : config.min}
onChange={onChange}
min={constraintMin}
max={constraintMax}
step={floatField.step}
/>
</FormControl>
);
});
SettingMin.displayName = 'SettingMin';
const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
const onToggleSetting = useCallback(() => {
const newConfig: NodeFieldFloatSettings = {
...config,
max: config.max !== undefined ? undefined : floatField.max,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
}, [config, dispatch, floatField, id]);
const onChange = useCallback(
(max: number) => {
const newConfig: NodeFieldFloatSettings = {
...config,
max,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
// We may need to update the value if it is outside the new min/max range
const constrained = constrainNumber(field.value, floatField, newConfig);
if (field.value !== constrained) {
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: constrained }));
}
},
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
);
const constraintMin = useMemo(
() => (config.min ?? floatField.min) + floatField.step,
[config.min, floatField.min, floatField.step]
);
const constraintMax = useMemo(
() => roundDownToMultiple(floatField.max, floatField.step),
[floatField.max, floatField.step]
);
return (
<FormControl orientation="vertical">
<Flex justifyContent="space-between" w="full" alignItems="center">
<FormLabel m={0}>{t('workflows.builder.maximum')}</FormLabel>
<Switch isChecked={config.max !== undefined} onChange={onToggleSetting} size="sm" />
</Flex>
<CompositeNumberInput
w="full"
isDisabled={config.max === undefined}
value={config.max === undefined ? (`${floatField.max} (inherited)` as unknown as number) : config.max}
onChange={onChange}
min={constraintMin}
max={constraintMax}
step={floatField.step}
/>
</FormControl>
);
});
SettingMax.displayName = 'SettingMax';

View File

@@ -1,37 +1,187 @@
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
import { CompositeNumberInput, Flex, FormControl, FormLabel, Select, Switch } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice';
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
import { type NodeFieldIntegerSettings, zNumberComponent } from 'features/nodes/types/workflow';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
import { zNumberComponent } from 'features/nodes/types/workflow';
import { constrainNumber } from 'features/nodes/util/constrainNumber';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const NodeFieldElementIntegerConfig = memo(
({ id, config }: { id: string; config: NodeFieldIntegerSettings }) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
type Props = {
id: string;
config: NodeFieldIntegerSettings;
nodeId: string;
fieldName: string;
fieldTemplate: IntegerFieldInputTemplate;
};
const onChangeComponent = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const newConfig: NodeFieldIntegerSettings = {
...config,
component: zNumberComponent.parse(e.target.value),
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
},
[config, dispatch, id]
);
export const NodeFieldElementIntegerSettings = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
return (
<>
<SettingComponent id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMin id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMax id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
</>
);
});
NodeFieldElementIntegerSettings.displayName = 'NodeFieldElementIntegerSettings';
return (
<FormControl>
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
<Select value={config.component} onChange={onChangeComponent} size="sm">
<option value="number-input">{t('workflows.builder.numberInput')}</option>
<option value="slider">{t('workflows.builder.slider')}</option>
<option value="number-input-and-slider">{t('workflows.builder.both')}</option>
</Select>
</FormControl>
);
}
);
NodeFieldElementIntegerConfig.displayName = 'NodeFieldElementIntegerConfig';
const SettingComponent = memo(({ id, config }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onChangeComponent = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const newConfig: NodeFieldIntegerSettings = {
...config,
component: zNumberComponent.parse(e.target.value),
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
},
[config, dispatch, id]
);
return (
<FormControl orientation="vertical">
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
<Select value={config.component} onChange={onChangeComponent} size="sm">
<option value="number-input">{t('workflows.builder.numberInput')}</option>
<option value="slider">{t('workflows.builder.slider')}</option>
<option value="number-input-and-slider">{t('workflows.builder.both')}</option>
</Select>
</FormControl>
);
});
SettingComponent.displayName = 'SettingComponent';
const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
const integerField = useIntegerField(nodeId, fieldName, fieldTemplate);
const onToggleSetting = useCallback(() => {
const newConfig: NodeFieldIntegerSettings = {
...config,
min: config.min !== undefined ? undefined : integerField.min,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
}, [config, dispatch, integerField.min, id]);
const onChange = useCallback(
(min: number) => {
const newConfig: NodeFieldIntegerSettings = {
...config,
min,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
// We may need to update the value if it is outside the new min/max range
const constrained = constrainNumber(field.value, integerField, newConfig);
if (field.value !== constrained) {
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: constrained }));
}
},
[config, dispatch, id, field, integerField, nodeId, fieldName]
);
const constraintMin = useMemo(
() => roundUpToMultiple(integerField.min, integerField.step),
[integerField.min, integerField.step]
);
const constraintMax = useMemo(
() => (config.max ?? integerField.max) - integerField.step,
[config.max, integerField.max, integerField.step]
);
return (
<FormControl orientation="vertical">
<Flex justifyContent="space-between" w="full" alignItems="center">
<FormLabel m={0}>{t('workflows.builder.minimum')}</FormLabel>
<Switch isChecked={config.min !== undefined} onChange={onToggleSetting} size="sm" />
</Flex>
<CompositeNumberInput
w="full"
isDisabled={config.min === undefined}
value={config.min ?? (`${integerField.min} (inherited)` as unknown as number)}
onChange={onChange}
min={constraintMin}
max={constraintMax}
step={integerField.step}
/>
</FormControl>
);
});
SettingMin.displayName = 'SettingMin';
const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
const integerField = useIntegerField(nodeId, fieldName, fieldTemplate);
const onToggleSetting = useCallback(() => {
const newConfig: NodeFieldIntegerSettings = {
...config,
max: config.max !== undefined ? undefined : integerField.max,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
}, [config, dispatch, integerField.max, id]);
const onChange = useCallback(
(max: number) => {
const newConfig: NodeFieldIntegerSettings = {
...config,
max,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
// We may need to update the value if it is outside the new min/max range
const constrained = constrainNumber(field.value, integerField, newConfig);
if (field.value !== constrained) {
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: constrained }));
}
},
[config, dispatch, field.value, fieldName, integerField, id, nodeId]
);
const constraintMin = useMemo(
() => (config.min ?? integerField.min) + integerField.step,
[config.min, integerField.min, integerField.step]
);
const constraintMax = useMemo(
() => roundDownToMultiple(integerField.max, integerField.step),
[integerField.max, integerField.step]
);
return (
<FormControl orientation="vertical">
<Flex justifyContent="space-between" w="full" alignItems="center">
<FormLabel m={0}>{t('workflows.builder.maximum')}</FormLabel>
<Switch isChecked={config.max !== undefined} onChange={onToggleSetting} size="sm" />
</Flex>
<CompositeNumberInput
w="full"
isDisabled={config.max === undefined}
value={config.max ?? (`${integerField.max} (inherited)` as unknown as number)}
onChange={onChange}
min={constraintMin}
max={constraintMax}
step={integerField.step}
/>
</FormControl>
);
});
SettingMax.displayName = 'SettingMax';

View File

@@ -1,4 +1,5 @@
import {
Flex,
FormControl,
FormLabel,
IconButton,
@@ -12,7 +13,7 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { NodeFieldElementFloatSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings';
import { NodeFieldElementIntegerConfig } from 'features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings';
import { NodeFieldElementIntegerSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings';
import { NodeFieldElementStringSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementStringSettings';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
@@ -70,13 +71,31 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
<PopoverContent>
<PopoverArrow />
<PopoverBody minW={48}>
<FormControl>
<FormLabel flex={1}>{t('workflows.builder.showDescription')}</FormLabel>
<Switch size="sm" isChecked={showDescription} onChange={toggleShowDescription} />
</FormControl>
{settings?.type === 'integer-field-config' && <NodeFieldElementIntegerConfig id={id} config={settings} />}
{settings?.type === 'float-field-config' && <NodeFieldElementFloatSettings id={id} config={settings} />}
{settings?.type === 'string-field-config' && <NodeFieldElementStringSettings id={id} config={settings} />}
<Flex w="full" h="full" gap={2} flexDir="column">
<FormControl>
<FormLabel flex={1}>{t('workflows.builder.showDescription')}</FormLabel>
<Switch size="sm" isChecked={showDescription} onChange={toggleShowDescription} />
</FormControl>
{settings?.type === 'integer-field-config' && isIntegerFieldInputTemplate(fieldTemplate) && (
<NodeFieldElementIntegerSettings
id={id}
config={settings}
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
/>
)}
{settings?.type === 'float-field-config' && isFloatFieldInputTemplate(fieldTemplate) && (
<NodeFieldElementFloatSettings
id={id}
config={settings}
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
/>
)}
{settings?.type === 'string-field-config' && <NodeFieldElementStringSettings id={id} config={settings} />}
</Flex>
</PopoverBody>
</PopoverContent>
</Portal>

View File

@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
const textSx: SystemStyleObject = {
fontSize: 'md',
whiteSpace: 'pre-wrap',
overflowWrap: 'anywhere',
'&[data-is-empty="true"]': {
opacity: 0.3,

View File

@@ -20,7 +20,8 @@ export const DeleteWorkflow = ({ workflowId }: { workflowId: string }) => {
<Tooltip label={t('workflows.delete')} closeOnScroll>
<IconButton
size="sm"
variant="ghost"
variant="link"
alignSelf="stretch"
aria-label={t('workflows.delete')}
onClick={handleClickDelete}
colorScheme="error"

View File

@@ -21,7 +21,8 @@ export const DownloadWorkflow = ({ workflowId }: { workflowId: string }) => {
<Tooltip label={t('workflows.download')} closeOnScroll>
<IconButton
size="sm"
variant="ghost"
variant="link"
alignSelf="stretch"
aria-label={t('workflows.download')}
onClick={handleClickDownload}
icon={<PiDownloadSimpleBold />}

View File

@@ -1,27 +1,37 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { useAppDispatch } from 'app/store/storeHooks';
import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import type { MouseEvent } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPencilBold } from 'react-icons/pi';
export const EditWorkflow = ({ workflowId }: { workflowId: string }) => {
const loadWorkflow = useLoadWorkflow();
const dispatch = useAppDispatch();
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const { t } = useTranslation();
const handleClickEdit = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
loadWorkflow.loadWithDialog(workflowId, 'edit');
loadWorkflowWithDialog({
type: 'library',
data: workflowId,
onSuccess: () => {
dispatch(workflowModeChanged('edit'));
},
});
},
[loadWorkflow, workflowId]
[dispatch, loadWorkflowWithDialog, workflowId]
);
return (
<Tooltip label={t('workflows.edit')} closeOnScroll>
<IconButton
size="sm"
variant="ghost"
variant="link"
alignSelf="stretch"
aria-label={t('workflows.edit')}
onClick={handleClickEdit}
icon={<PiPencilBold />}

Some files were not shown because too many files have changed in this diff Show More