Compare commits

..

166 Commits

Author SHA1 Message Date
psychedelicious
f6c4682b99 fix(ui): builder alpha status alert not visible when many elements added 2025-02-14 15:33:02 +11:00
psychedelicious
b3288ed64e chore: bump version to v5.7.0a1 2025-02-14 15:33:02 +11:00
psychedelicious
f3dfb1b6ea chore(ui): knip 2025-02-14 14:50:56 +11:00
psychedelicious
65a37ca4ff feat(ui): give vertical dividers a min height 2025-02-14 14:50:56 +11:00
psychedelicious
9adbe31fec tweak(ui): form element edit mode styling 2025-02-14 14:50:56 +11:00
psychedelicious
0a2925f02b feat(ui): add warning about alpha status of builder 2025-02-14 14:50:56 +11:00
psychedelicious
877dcc73c3 feat(ui): check image access for image collections when loading workflows 2025-02-14 14:50:56 +11:00
psychedelicious
aec2136323 fix(ui): force refetch when checking image access to ensure stale RTK query cache isn't use 2025-02-14 14:50:56 +11:00
psychedelicious
8ef5c54ffe feat(ui): add delete button to missing image placeholder for image collection fields 2025-02-14 14:50:56 +11:00
psychedelicious
6faed4f1ec fix(ui): remove images from node image collections when deleted 2025-02-14 14:50:56 +11:00
psychedelicious
aa71db4d31 tidy(ui): remove nonfunctional conditionals 2025-02-14 14:50:56 +11:00
psychedelicious
6407ab4a2e tweak(ui): builder padding 2025-02-14 14:50:56 +11:00
psychedelicious
a91b0f25cb feat(ui): consolidate row/column dnd draggables into container 2025-02-14 14:50:56 +11:00
psychedelicious
ef664863b5 feat(ui): remove separate flag for form vs workflow edit mode 2025-02-14 14:50:56 +11:00
psychedelicious
bf8ba1bb37 feat(ui): text and heading element default content is empty string 2025-02-14 14:50:56 +11:00
psychedelicious
54747bd521 feat(ui): remove element id from edit mode header 2025-02-14 14:50:56 +11:00
psychedelicious
d040a6953f tweak(ui): styling for edit mode 2025-02-14 14:50:56 +11:00
psychedelicious
828497cf89 feat(ui): remove node field reset button from edit mode header 2025-02-14 14:50:56 +11:00
psychedelicious
28950a4891 fix(ui): ignore dropping on self 2025-02-14 14:50:56 +11:00
psychedelicious
1c92838bf9 tidy(ui): builder dnd monitor logic rearrange 2025-02-14 14:50:56 +11:00
psychedelicious
71f6737e19 feat(ui): remove the showLabel flag for node fields 2025-02-14 14:50:56 +11:00
psychedelicious
dcac65f46b feat(ui): add initial values for builder fields 2025-02-14 14:50:56 +11:00
psychedelicious
46f549a57a feat(ui): better placeholders for text/heading 2025-02-14 14:50:56 +11:00
psychedelicious
fb93101085 tweak(ui): layout of workflow builder field settings 2025-02-14 14:50:56 +11:00
psychedelicious
9aabcfa4b8 feat(ui): default form field settings 2025-02-14 14:50:56 +11:00
psychedelicious
64587b37db refactor(ui): remove confusing containerId from various builder actions 2025-02-14 14:50:56 +11:00
psychedelicious
c673b6e11d feat(ui): demote dnd logs to trace 2025-02-14 14:50:56 +11:00
psychedelicious
a3a49ddda0 tidy(ui): useNodeFieldDnd 2025-02-14 14:50:56 +11:00
psychedelicious
330a0f0028 tidy(ui): extract util in dnd 2025-02-14 14:50:56 +11:00
psychedelicious
1104d2a00f feat(ui): initial values for form fields (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
aed802fa74 feat(ui): rearrange builder buttons to be less annoying 2025-02-14 14:50:56 +11:00
psychedelicious
498d99c828 fix(ui): handle form fields not existing on node on workflow load 2025-02-14 14:50:56 +11:00
psychedelicious
3d19b98208 chore(ui): lint 2025-02-14 14:50:56 +11:00
psychedelicious
85f5bb4a02 fix(ui): incorrect node data used during update 2025-02-14 14:50:56 +11:00
psychedelicious
269f718d2c tidy(ui): node description components 2025-02-14 14:50:56 +11:00
psychedelicious
211bb8a204 feat(ui): auto-update nodes on loading workflow 2025-02-14 14:50:56 +11:00
psychedelicious
ef0ef875dd feat(ui): migrated linear view exposed fields to builder form on load 2025-02-14 14:50:56 +11:00
psychedelicious
9c62648283 fix(ui): do not error in node/field selectors are used outside field gate components 2025-02-14 14:50:56 +11:00
psychedelicious
4ca45f7651 feat(ui): be double extra sure migrated workflows are parsed before loading 2025-02-14 14:50:56 +11:00
psychedelicious
2abe2f52f7 feat(ui): workflow builder layout 2025-02-14 14:50:56 +11:00
psychedelicious
6f1c814af4 revert(ui): code lint that broke stuff 2025-02-14 14:50:56 +11:00
psychedelicious
1ad6ccc426 tidy(ui): dnd code lint 2025-02-14 14:50:56 +11:00
psychedelicious
aedee536a0 tidy(ui): rename builder dnd file 2025-02-14 14:50:56 +11:00
psychedelicious
d2b15fba12 tidy(ui): improve dnd hook names 2025-02-14 14:50:56 +11:00
psychedelicious
a674e781a1 tidy(ui): dnd logic formatting 2025-02-14 14:50:56 +11:00
psychedelicious
0db74f0cde refactor(ui): add vars in dnd logic for conciseness 2025-02-14 14:50:56 +11:00
psychedelicious
d66db67d1a refactor(ui): clean up dnd logic 2025-02-14 14:50:56 +11:00
psychedelicious
2507a7f674 tidy(ui): rename root utils in dnd 2025-02-14 14:50:56 +11:00
psychedelicious
145503a0a0 refactor(ui): add dispatchAndFlash util for dnd 2025-02-14 14:50:56 +11:00
psychedelicious
32e8dd5647 fix(ui): divider rendering 2025-02-14 14:50:56 +11:00
psychedelicious
fe87adcb52 feat(ui): builder edit/view buttons 2025-02-14 14:50:56 +11:00
psychedelicious
e95255f6e8 feat(ui): make drop targets that are in root sticky 2025-02-14 14:50:56 +11:00
psychedelicious
efec224523 fix(ui): remove node field from form correctly when node is deleted 2025-02-14 14:50:56 +11:00
psychedelicious
e948e236e7 feat(ui): iterate on builder data structure 2025-02-14 14:50:56 +11:00
psychedelicious
189eb85663 feat(ui): delete form elements when node is deleted from workflow 2025-02-14 14:50:56 +11:00
psychedelicious
94f90f4082 feat(ui): string field settings 2025-02-14 14:50:56 +11:00
psychedelicious
1eb491fdaa feat(ui): builder empty state (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
176248a023 feat(ui): empty state for drop containers 2025-02-14 14:50:56 +11:00
psychedelicious
3c676ed11a fix(ui): drop target jank 2025-02-14 14:50:56 +11:00
psychedelicious
7a9340b850 fix(ui): tsc issues 2025-02-14 14:50:56 +11:00
psychedelicious
2c0b474f55 feat(ui): editable node form field labels & descriptions 2025-02-14 14:50:56 +11:00
psychedelicious
74c76611a9 feat(ui): add float field display settings 2025-02-14 14:50:56 +11:00
psychedelicious
1c7176b3f4 feat(ui): use useEditable in builder 2025-02-14 14:50:56 +11:00
psychedelicious
30363a0018 feat(ui): builder field settings (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
b46dbcc76d fix(ui): divider layout 2025-02-14 14:50:56 +11:00
psychedelicious
09879f4e19 feat(ui): builder field settings (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
4daa82c912 feat(ui): builder field settings (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
1cb04d9a4a refactor(ui): updated component structure for input and output fields 2025-02-14 14:50:56 +11:00
psychedelicious
3e6969128c feat(ui): remove sizes from text & heading 2025-02-14 14:50:56 +11:00
psychedelicious
e14c490ac6 fix(ui): drop indicator getting greyed out when dragging over self 2025-02-14 14:50:56 +11:00
psychedelicious
3ef3b97c58 feat(ui): editable heading and text elements 2025-02-14 14:50:56 +11:00
psychedelicious
3baaefb0cc chore(ui): bump @invoke-ai/ui-library 2025-02-14 14:50:56 +11:00
psychedelicious
98b0a8ffb2 feat(ui): plumbing for editable form elements 2025-02-14 14:50:56 +11:00
psychedelicious
4f85bf078a tidy(ui): import reactflow css in main theme provider 2025-02-14 14:50:56 +11:00
psychedelicious
f0563d41db fix(ui): circular dep 2025-02-14 14:50:56 +11:00
psychedelicious
a7a71ca935 perf(ui): faster InputFieldRenderer
Use non-zod type guards for input field types and fail early when possible
2025-02-14 14:50:56 +11:00
psychedelicious
c04822054b chore(ui): lint 2025-02-14 14:50:56 +11:00
psychedelicious
132e9bebd7 chore(ui): lint 2025-02-14 14:50:56 +11:00
psychedelicious
0dc45ac903 fix(ui): node-autoconnect showing invalid connection options 2025-02-14 14:50:56 +11:00
psychedelicious
4f9d81917c fix(ui): do not render dashed edges unless animation is enabled 2025-02-14 14:50:56 +11:00
psychedelicious
d3c22eceaf tweak(ui): node selection colors 2025-02-14 14:50:56 +11:00
psychedelicious
fb77d271ab refactor(ui): edge rendering
- Fix issues with positioning of labels
- Optimize styling to be less reliant on JS
2025-02-14 14:50:56 +11:00
psychedelicious
0371881349 chore(ui): upgrade reactflow to v12 2025-02-14 14:50:56 +11:00
psychedelicious
4b178fdeca fix(ui): hide nonfunctional delete button on root form element 2025-02-14 14:50:56 +11:00
psychedelicious
b53e36aaaa tidy(ui): remove unused mock form builder data 2025-02-14 14:50:56 +11:00
psychedelicious
c061cd5e54 fix(ui): use redux store for form 2025-02-14 14:50:56 +11:00
psychedelicious
ddda915ebd fix(ui): start workflow w/ single column as root 2025-02-14 14:50:56 +11:00
psychedelicious
9a2d8844a2 fix(ui): allow root element to be drop target 2025-02-14 14:50:56 +11:00
psychedelicious
48583df02e feat(ui): support adding form elements and node fields with dnd 2025-02-14 14:50:56 +11:00
psychedelicious
f9432d10d2 feat(ui): improved drop target styling 2025-02-14 14:50:56 +11:00
psychedelicious
0d28cd7ebe fix(ui): do not allow reparenting to self 2025-02-14 14:50:56 +11:00
psychedelicious
c9f9a2f2d4 feat(ui): dnd drop target styling 2025-02-14 14:50:56 +11:00
psychedelicious
a05d10f648 feat(ui): improved dnd hitbox for edges when center drop is allowed 2025-02-14 14:50:56 +11:00
psychedelicious
14845932fb feat(ui): dnd almost fully working (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
2aa1fc9301 feat(ui): dnd mostly working (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
98139562f3 feat(ui): dim form element while dragging 2025-02-14 14:50:56 +11:00
psychedelicious
8365bba5ba feat(ui): hacking on dnd (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
9f07e83a23 feat(ui): iterate on builder (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
1f995d0257 feat(ui): iterate on builder (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
6ae2d5ef9d feat(ui): iterate on builder (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
55973b4c66 feat(ui): iterate on builder (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
d8c6531b70 feat(ui): getPrefixedId supports custom separator 2025-02-14 14:50:56 +11:00
psychedelicious
81e385a756 feat(ui): iterate on builder (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
f6cb1a455f feat(ui): iterate on builder (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
bf60be99dc feat(ui): iterate on builder (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
bee0e8248f feat(ui): iterate on builder (WIP) 2025-02-14 14:50:56 +11:00
psychedelicious
1e658cf9e7 chore(ui): lint 2025-02-14 14:50:56 +11:00
psychedelicious
f130fa4d66 feat(ui): rough out workflow builder data structure 2025-02-14 14:50:56 +11:00
psychedelicious
02a47a6806 refactor(ui): split integer, float and string field components in prep for builder 2025-02-14 14:50:56 +11:00
psychedelicious
1063498458 revert(ui): rip out linear view config stuff 2025-02-14 14:50:56 +11:00
psychedelicious
e9a13ec882 refactor(ui): split up float and integer field renderers 2025-02-14 14:50:56 +11:00
psychedelicious
bd0765b744 feat(ui): rough out workflow builder data structure & dummy data 2025-02-14 14:50:56 +11:00
psychedelicious
6e1388f4fc fix(ui): dynamic prompts infinite recursion (wip) 2025-02-14 14:50:56 +11:00
psychedelicious
2a9f2b2fe2 feat(ui): use workflows view context 2025-02-14 14:50:56 +11:00
psychedelicious
0a6b0dc3bf feat(ui): get configurable notes display working 2025-02-14 14:50:56 +11:00
psychedelicious
8753406a6c fix(ui): color field component layout 2025-02-14 14:50:56 +11:00
psychedelicious
e2b09bed62 refactor(ui): continued reorg of components & hooks 2025-02-14 14:50:56 +11:00
psychedelicious
011910a08c refactor(ui): continued reorg of components & hooks 2025-02-14 14:50:56 +11:00
psychedelicious
bfd70be50b fix(ui): remove accidental change to zFieldInput schema 2025-02-14 14:50:56 +11:00
psychedelicious
9c53bd6a3b refactor(ui): workflows left panel internal components structure 2025-02-14 14:50:56 +11:00
psychedelicious
e479cb5fe4 refactor(ui): workflows component structure (WIP)
- Simplify and de-insane-ify component structure, hooks, selectors, etc.
- Some perf improvements by using data attributes for styling instead of dynamic CSS-in-JS.
- Add field notes and start of linear view config, got blocked when I ran into deeper layout issues that made it very difficult to handle field configs. So those are WIP in this commit.
2025-02-14 14:50:56 +11:00
psychedelicious
52947f40c3 perf(ui): use data attribute for input field wrapper styles 2025-02-14 14:50:56 +11:00
psychedelicious
bce9a23b25 feat(ui): add ViewContext so components can know where they are being rendered (user-linear view, editor-linear view, or editor-nodes view) 2025-02-14 14:50:56 +11:00
psychedelicious
2d05579568 feat(ui): clean up user-linear view styling 2025-02-14 14:50:56 +11:00
psychedelicious
11aabb5693 feat(ui): show notes icon on user-linear view, replacing info icon 2025-02-14 14:50:56 +11:00
psychedelicious
1e1e31d5b7 feat(ui): show notes icon on editor linear view 2025-02-14 14:50:56 +11:00
psychedelicious
fe86cf6d99 feat(ui): add notes popover to field title bar 2025-02-14 14:50:56 +11:00
psychedelicious
cfb63c1b81 feat(ui): add notes state to fields 2025-02-14 14:50:56 +11:00
Ryan Dick
b44415415a Use a default tile size of 1024 for VAE encode/decode operations in upscaling workflows. Previously, the model default was used (512 for SD1, 1024 for SDXL). Larger tile sizes help to prevent tiling artifacts. 2025-02-14 14:23:42 +11:00
psychedelicious
9353298b4f chore: bump version to v5.6.2 2025-02-14 13:13:33 +11:00
Eugene Brodsky
cf22e09b28 chore(ui): upgrade vite, vitest, and related plugins to latest versions 2025-02-14 11:09:51 +11:00
Linos
6e5ca7ece8 translationBot(ui): update translation (Vietnamese)
Currently translated at 99.8% (1753 of 1755 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 99.8% (1751 of 1753 strings)

Co-authored-by: Linos <linos.coding@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2025-02-13 19:26:55 +11:00
Thomas Bolteau
b81209e751 translationBot(ui): update translation (French)
Currently translated at 91.7% (1609 of 1753 strings)

Co-authored-by: Thomas Bolteau <thomas.bolteau50@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fr/
Translation: InvokeAI/Web UI
2025-02-13 19:26:55 +11:00
Riccardo Giovanetti
c4040eb2f0 translationBot(ui): update translation (Italian)
Currently translated at 98.9% (1735 of 1753 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.9% (1731 of 1749 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.9% (1731 of 1749 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.6% (1726 of 1749 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-02-13 19:26:55 +11:00
Billy Lisner
046ea611f9 Remove files 2025-02-13 19:24:01 +11:00
Billy Lisner
1439da5e88 Make ruff gods happy 2025-02-13 19:24:01 +11:00
Billy Lisner
69a504710f More detailed error messages 2025-02-13 19:24:01 +11:00
Billy Lisner
842b770938 Update OpenAI Schema 2025-02-13 19:24:01 +11:00
Billy Lisner
ba39331594 Make ruff happy 2025-02-13 19:24:01 +11:00
Billy Lisner
8ee9509eec Add Metadata Field Extractor 2025-02-13 19:24:01 +11:00
psychedelicious
7b5dcffb3f fix(ui): prevent overflow on document root 2025-02-13 09:10:38 +11:00
Mary Hipp
6927e95444 update defaults 2025-02-12 15:49:15 -05:00
Mary Hipp
76618fee9c feat(ui): separate upscaling settings so that tab does not inherit from main generation settings 2025-02-12 15:49:15 -05:00
Maxim Evtush
b51312f1ba Update model_images_common.py 2025-02-11 20:03:11 +11:00
Maxim Evtush
c2b71854be Update useGalleryNavigation.ts 2025-02-11 20:03:11 +11:00
Maxim Evtush
df793c898f Update denoise_latents.py 2025-02-11 20:03:11 +11:00
Maxim Evtush
d6181e4d64 Update useImageViewer.ts 2025-02-11 20:03:11 +11:00
Maxim Evtush
0a4ea9ac6f Update validateWorkflow.ts 2025-02-11 20:03:11 +11:00
dunkeroni
9e6f3e9338 image channel multiply node loads as RGBA now 2025-02-11 18:32:56 +11:00
psychedelicious
d3a40d85b9 Update invokeai_version.py 2025-02-11 11:36:15 +11:00
psychedelicious
b224cc8158 fix(ui): canvas image error placeholder never disappears 2025-02-11 11:10:14 +11:00
psychedelicious
b75d08a2d0 fix(ui): ensure CanvasObjectImage's visibility is set correctly when updating it 2025-02-11 11:10:14 +11:00
psychedelicious
5f1a30ea82 fix(ui): flicker when transitioning from an output image to next generation's progress image 2025-02-11 11:10:14 +11:00
psychedelicious
d09e600802 feat(ui): add more to CanvasStagingAreaModule repr 2025-02-11 11:10:14 +11:00
psychedelicious
f4ee59b92a feat(ui): add more to CanvasObjectImage repr 2025-02-11 11:10:14 +11:00
psychedelicious
ad0b40b669 feat(ui): differentiate between failure modes for canvas image rendering 2025-02-11 11:10:14 +11:00
Mary Hipp
f3fbcf0014 fix [object object] OOM error 2025-02-11 07:04:11 +11:00
Mary Hipp
588e8a0195 fix oom toast title 2025-02-11 07:04:11 +11:00
psychedelicious
c194281f4d docs: install troubleshooting 2025-02-08 10:40:04 +11:00
psychedelicious
7daff465d3 docs: remove outdated info & update other items in FAQ 2025-02-07 12:14:23 +11:00
psychedelicious
0747a5f464 docs: add link to low vram in requirements 2025-02-07 12:14:23 +11:00
psychedelicious
e7aafdfdbf feat(ui): migrate all clipboard stuff to useClipboard 2025-02-07 11:08:03 +11:00
psychedelicious
ecb38c2bae feat(ui): disallow direct access to clipboard via eslint 2025-02-07 11:08:03 +11:00
psychedelicious
d3ef94cb3e feat(ui): add useClipboard hook to safely wrap clipboard access 2025-02-07 11:08:03 +11:00
psychedelicious
eb27b437ee docs: add firefox clipboard fix 2025-02-07 11:08:03 +11:00
Mary Hipp
25bb96ed66 restore missing translation 2025-02-06 14:10:28 -05:00
234 changed files with 7953 additions and 4058 deletions

View File

@@ -1,26 +1,18 @@
# FAQ
!!! info "How to Reinstall"
Many issues can be resolved by re-installing the application. You won't lose any data by re-installing. We suggest downloading the [latest release](https://github.com/invoke-ai/InvokeAI/releases/latest) and using it to re-install the application. Consult the [installer guide](./installation/installer.md) for more information.
When you run the installer, you'll have an option to select the version to install. If you aren't ready to upgrade, you choose the current version to fix a broken install.
If the troubleshooting steps on this page don't get you up and running, please either [create an issue] or hop on [discord] for help.
## How to Install
You can download the latest installers [here](https://github.com/invoke-ai/InvokeAI/releases).
Note that any releases marked as _pre-release_ are in a beta state. You may experience some issues, but we appreciate your help testing those! For stable/reliable installations, please install the [latest release].
Follow the [Quick Start guide](./installation/quick_start.md) to install Invoke.
## Downloading models and using existing models
The Model Manager tab in the UI provides a few ways to install models, including using your already-downloaded models. You'll see a popup directing you there on first startup. For more information, see the [model install docs].
## Missing models after updating to v4
## Missing models after updating from v3
If you find some models are missing after updating to v4, it's likely they weren't correctly registered before the update and didn't get picked up in the migration.
If you find some models are missing after updating from v3, it's likely they weren't correctly registered before the update and didn't get picked up in the migration.
You can use the `Scan Folder` tab in the Model Manager UI to fix this. The models will either be in the old, now-unused `autoimport` folder, or your `models` folder.
@@ -37,115 +29,27 @@ Follow the same steps to scan and import the missing models.
## Slow generation
- Check the [system requirements] to ensure that your system is capable of generating images.
- Check the `ram` setting in `invokeai.yaml`. This setting tells Invoke how much of your system RAM can be used to cache models. Having this too high or too low can slow things down. That said, it's generally safest to not set this at all and instead let Invoke manage it.
- Check the `vram` setting in `invokeai.yaml`. This setting tells Invoke how much of your GPU VRAM can be used to cache models. Counter-intuitively, if this setting is too high, Invoke will need to do a lot of shuffling of models as it juggles the VRAM cache and the currently-loaded model. The default value of 0.25 is generally works well for GPUs without 16GB or more VRAM. Even on a 24GB card, the default works well.
- Check that your generations are happening on your GPU (if you have one). InvokeAI will log what is being used for generation upon startup. If your GPU isn't used, re-install to ensure the correct versions of torch get installed.
- If you are on Windows, you may have exceeded your GPU's VRAM capacity and are using slower [shared GPU memory](#shared-gpu-memory-windows). There's a guide to opt out of this behaviour in the linked FAQ entry.
## Shared GPU Memory (Windows)
!!! tip "Nvidia GPUs with driver 536.40"
This only applies to current Nvidia cards with driver 536.40 or later, released in June 2023.
When the GPU doesn't have enough VRAM for a task, Windows is able to allocate some of its CPU RAM to the GPU. This is much slower than VRAM, but it does allow the system to generate when it otherwise might no have enough VRAM.
When shared GPU memory is used, generation slows down dramatically - but at least it doesn't crash.
If you'd like to opt out of this behavior and instead get an error when you exceed your GPU's VRAM, follow [this guide from Nvidia](https://nvidia.custhelp.com/app/answers/detail/a_id/5490).
Here's how to get the python path required in the linked guide:
- Run `invoke.bat`.
- Select option 2 for developer console.
- At least one python path will be printed. Copy the path that includes your invoke installation directory (typically the first).
## Installer cannot find python (Windows)
Ensure that you checked **Add python.exe to PATH** when installing Python. This can be found at the bottom of the Python Installer window. If you already have Python installed, you can re-run the python installer, choose the Modify option and check the box.
- Follow the [Low-VRAM mode guide](./features/low-vram.md) to optimize performance.
- Check that your generations are happening on your GPU (if you have one). Invoke will log what is being used for generation upon startup. If your GPU isn't used, re-install to and ensure you select the appropriate GPU option.
- If you are on Windows with an Nvidia GPU, you may have exceeded your GPU's VRAM capacity and are triggering Nvidia's "sysmem fallback". There's a guide to opt out of this behaviour in the [Low-VRAM mode guide](./features/low-vram.md).
## Triton error on startup
This can be safely ignored. InvokeAI doesn't use Triton, but if you are on Linux and wish to dismiss the error, you can install Triton.
This can be safely ignored. Invoke doesn't use Triton, but if you are on Linux and wish to dismiss the error, you can install Triton.
## Updated to 3.4.0 and xformers cant load C++/CUDA
## Unable to Copy on Firefox
An issue occurred with your PyTorch update. Follow these steps to fix :
Firefox does not allow Invoke to directly access the clipboard by default. As a result, you may be unable to use certain copy functions. You can fix this by configuring Firefox to allow access to write to the clipboard:
1. Launch your invoke.bat / invoke.sh and select the option to open the developer console
2. Run:`pip install ".[xformers]" --upgrade --force-reinstall --extra-index-url https://download.pytorch.org/whl/cu121`
- If you run into an error with `typing_extensions`, re-open the developer console and run: `pip install -U typing-extensions`
Note that v3.4.0 is an old, unsupported version. Please upgrade to the [latest release].
## Install failed and says `pip` is out of date
An out of date `pip` typically won't cause an installation to fail. The cause of the error can likely be found above the message that says `pip` is out of date.
If you saw that warning but the install went well, don't worry about it (but you can update `pip` afterwards if you'd like).
- Go to `about:config` and click the Accept button
- Search for `dom.events.asyncClipboard.clipboardItem`
- Set it to `true` by clicking the toggle button
- Restart Firefox
## Replicate image found online
Most example images with prompts that you'll find on the internet have been generated using different software, so you can't expect to get identical results. In order to reproduce an image, you need to replicate the exact settings and processing steps, including (but not limited to) the model, the positive and negative prompts, the seed, the sampler, the exact image size, any upscaling steps, etc.
## OSErrors on Windows while installing dependencies
During a zip file installation or an update, installation stops with an error like this:
![broken-dependency-screenshot](./assets/troubleshooting/broken-dependency.png){:width="800px"}
To resolve this, re-install the application as described above.
## HuggingFace install failed due to invalid access token
Some HuggingFace models require you to authenticate using an [access token].
Invoke doesn't manage this token for you, but it's easy to set it up:
- Follow the instructions in the link above to create an access token. Copy it.
- Run the launcher script.
- Select option 2 (developer console).
- Paste the following command:
```sh
python -c "import huggingface_hub; huggingface_hub.login()"
```
- Paste your access token when prompted and press Enter. You won't see anything when you paste it.
- Type `n` if prompted about git credentials.
If you get an error, try the command again - maybe the token didn't paste correctly.
Once your token is set, start Invoke and try downloading the model again. The installer will automatically use the access token.
If the install still fails, you may not have access to the model.
## Stable Diffusion XL generation fails after trying to load UNet
InvokeAI is working in other respects, but when trying to generate
images with Stable Diffusion XL you get a "Server Error". The text log
in the launch window contains this log line above several more lines of
error messages:
`INFO --> Loading model:D:\LONG\PATH\TO\MODEL, type sdxl:main:unet`
This failure mode occurs when there is a network glitch during
downloading the very large SDXL model.
To address this, first go to the Model Manager and delete the
Stable-Diffusion-XL-base-1.X model. Then, click the HuggingFace tab,
paste the Repo ID stabilityai/stable-diffusion-xl-base-1.0 and install
the model.
## Package dependency conflicts during installation or update
If you have previously installed InvokeAI or another Stable Diffusion
package, the installer may occasionally pick up outdated libraries and
either the installer or `invoke` will fail with complaints about
library conflicts.
To resolve this, re-install the application as described above.
## Invalid configuration file
Everything seems to install ok, you get a `ValidationError` when starting up the app.
@@ -154,64 +58,9 @@ This is caused by an invalid setting in the `invokeai.yaml` configuration file.
Check the [configuration docs] for more detail about the settings and how to specify them.
## `ModuleNotFoundError: No module named 'controlnet_aux'`
## Out of Memory Errors
`controlnet_aux` is a dependency of Invoke and appears to have been packaged or distributed strangely. Sometimes, it doesn't install correctly. This is outside our control.
If you encounter this error, the solution is to remove the package from the `pip` cache and re-run the Invoke installer so a fresh, working version of `controlnet_aux` can be downloaded and installed:
- Run the Invoke launcher
- Choose the developer console option
- Run this command: `pip cache remove controlnet_aux`
- Close the terminal window
- Download and run the [installer][latest release], selecting your current install location
## Out of Memory Issues
The models are large, VRAM is expensive, and you may find yourself
faced with Out of Memory errors when generating images. Here are some
tips to reduce the problem:
!!! info "Optimizing for GPU VRAM"
=== "4GB VRAM GPU"
This should be adequate for 512x512 pixel images using Stable Diffusion 1.5
and derived models, provided that you do not use the NSFW checker. It won't be loaded unless you go into the UI settings and turn it on.
If you are on a CUDA-enabled GPU, we will automatically use xformers or torch-sdp to reduce VRAM requirements, though you can explicitly configure this. See the [configuration docs].
=== "6GB VRAM GPU"
This is a border case. Using the SD 1.5 series you should be able to
generate images up to 640x640 with the NSFW checker enabled, and up to
1024x1024 with it disabled.
If you run into persistent memory issues there are a series of
environment variables that you can set before launching InvokeAI that
alter how the PyTorch machine learning library manages memory. See
<https://pytorch.org/docs/stable/notes/cuda.html#memory-management> for
a list of these tweaks.
=== "12GB VRAM GPU"
This should be sufficient to generate larger images up to about 1280x1280.
## Checkpoint Models Load Slowly or Use Too Much RAM
The difference between diffusers models (a folder containing multiple
subfolders) and checkpoint models (a file ending with .safetensors or
.ckpt) is that InvokeAI is able to load diffusers models into memory
incrementally, while checkpoint models must be loaded all at
once. With very large models, or systems with limited RAM, you may
experience slowdowns and other memory-related issues when loading
checkpoint models.
To solve this, go to the Model Manager tab (the cube), select the
checkpoint model that's giving you trouble, and press the "Convert"
button in the upper right of your browser window. This will convert the
checkpoint into a diffusers model, after which loading should be
faster and less memory-intensive.
The models are large, VRAM is expensive, and you may find yourself faced with Out of Memory errors when generating images. Follow our [Low-VRAM mode guide](./features/low-vram.md) to configure Invoke to prevent these.
## Memory Leak (Linux)
@@ -253,8 +102,6 @@ Note the differences between memory allocated as chunks in an arena vs. memory a
[model install docs]: ./installation/models.md
[system requirements]: ./installation/requirements.md
[latest release]: https://github.com/invoke-ai/InvokeAI/releases/latest
[create an issue]: https://github.com/invoke-ai/InvokeAI/issues
[discord]: https://discord.gg/ZmtBAhwWhy
[configuration docs]: ./configuration.md
[access token]: https://huggingface.co/docs/hub/security-tokens#how-to-manage-user-access-tokens

View File

@@ -99,6 +99,20 @@ We recommend watching our [Getting Started Playlist](https://www.youtube.com/pla
- Using control layers and reference guides.
- Refining images with advanced workflows.
## Troubleshooting
If installation fails, retrying the install in Repair Mode may fix it. There's a checkbox to enable this on the Review step of the install flow.
If that doesn't fix it, [clearing the `uv` cache](https://docs.astral.sh/uv/reference/cli/#uv-cache-clean) might do the trick:
- Open and start the dev console (button at the bottom-left of the launcher).
- Run `uv cache clean`.
- Retry the installation. Enable Repair Mode for good measure.
If you are still unable to install, try installing to a different location and see if that works.
If you still have problems, ask for help on the Invoke [discord](https://discord.gg/ZmtBAhwWhy).
## Other Installation Methods
- You can install the Invoke application as a python package. See our [manual install](./manual.md) docs.

View File

@@ -4,7 +4,9 @@ Invoke runs on Windows 10+, macOS 14+ and Linux (Ubuntu 20.04+ is well-tested).
## Hardware
Hardware requirements vary significantly depending on model and image output size. The requirements below are rough guidelines.
Hardware requirements vary significantly depending on model and image output size.
The requirements below are rough guidelines for best performance. GPUs with less VRAM typically still work, if a bit slower. Follow the [Low-VRAM mode guide](./features/low-vram.md) to optimize performance.
- All Apple Silicon (M1, M2, etc) Macs work, but 16GB+ memory is recommended.
- AMD GPUs are supported on Linux only. The VRAM requirements are the same as Nvidia GPUs.

View File

@@ -898,7 +898,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
### inpaint
mask, masked_latents, is_gradient_mask = self.prep_inpaint_mask(context, latents)
# NOTE: We used to identify inpainting models by inpecting the shape of the loaded UNet model weights. Now we
# NOTE: We used to identify inpainting models by inspecting the shape of the loaded UNet model weights. Now we
# use the ModelVariantType config. During testing, there was a report of a user with models that had an
# incorrect ModelVariantType value. Re-installing the model fixed the issue. If this issue turns out to be
# prevalent, we will have to revisit how we initialize the inpainting extensions.

View File

@@ -918,7 +918,7 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata, WithBoard):
invert_channel: bool = InputField(default=False, description="Invert the channel after scaling")
def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.images.get_pil(self.image.image_name)
image = context.images.get_pil(self.image.image_name, "RGBA")
# extract the channel and mode from the input and reference tuple
mode = CHANNEL_FORMATS[self.channel][0]

View File

@@ -18,6 +18,7 @@ from invokeai.app.invocations.fields import (
UIType,
)
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import StringOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.controlnet_utils import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES
from invokeai.version.invokeai_version import __version__
@@ -275,3 +276,33 @@ class CoreMetadataInvocation(BaseInvocation):
return MetadataOutput(metadata=MetadataField.model_validate(as_dict))
model_config = ConfigDict(extra="allow")
@invocation(
"metadata_field_extractor",
title="Metadata Field Extractor",
tags=["metadata"],
category="metadata",
version="1.0.0",
)
class MetadataFieldExtractorInvocation(BaseInvocation):
"""Extracts the text value from an image's metadata given a key.
Raises an error if the image has no metadata or if the value is not a string (nesting not permitted)."""
image: ImageField = InputField(description="The image to extract metadata from")
key: str = InputField(description="The key in the image's metadata to extract the value from")
def invoke(self, context: InvocationContext) -> StringOutput:
image_name = self.image.image_name
metadata = context.images.get_metadata(image_name=image_name)
if not metadata:
raise ValueError(f"No metadata found on image {image_name}")
try:
val = metadata.root[self.key]
if not isinstance(val, str):
raise ValueError(f"Metadata at key '{self.key}' must be a string")
return StringOutput(value=val)
except KeyError as e:
raise ValueError(f"No key '{self.key}' found in the metadata for {image_name}") from e

View File

@@ -1,4 +1,4 @@
# TODO: Should these excpetions subclass existing python exceptions?
# TODO: Should these exceptions subclass existing python exceptions?
class ModelImageFileNotFoundException(Exception):
"""Raised when an image file is not found in storage."""

View File

@@ -23,6 +23,12 @@ module.exports = {
property: 'randomUUID',
message: 'Use of crypto.randomUUID is not allowed as it is not available in all browsers.',
},
{
object: 'navigator',
property: 'clipboard',
message:
'The Clipboard API is not available by default in Firefox. Use the `useClipboard` hook instead, which wraps clipboard access to prevent errors.',
},
],
},
overrides: [

View File

@@ -11,9 +11,11 @@
<link id="invoke-favicon" rel="icon" type="icon" href="assets/images/invoke-favicon.svg" />
<style>
html,
body {
body,
#root {
padding: 0;
margin: 0;
overflow: hidden;
}
</style>
</head>
@@ -23,4 +25,4 @@
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
</html>

View File

@@ -58,10 +58,11 @@
"@dagrejs/dagre": "^1.1.4",
"@dagrejs/graphlib": "^2.2.4",
"@fontsource-variable/inter": "^5.1.0",
"@invoke-ai/ui-library": "^0.0.44",
"@invoke-ai/ui-library": "^0.0.46",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",
"@xyflow/react": "^12.4.2",
"async-mutex": "^0.5.0",
"chakra-react-select": "^4.9.2",
"cmdk": "^1.0.0",
@@ -96,9 +97,9 @@
"react-icons": "^5.3.0",
"react-redux": "9.1.2",
"react-resizable-panels": "^2.1.4",
"react-textarea-autosize": "^8.5.7",
"react-use": "^17.5.1",
"react-virtuoso": "^4.10.4",
"reactflow": "^11.11.4",
"redux-dynamic-middlewares": "^2.2.0",
"redux-remember": "^5.1.0",
"redux-undo": "^1.1.0",
@@ -126,7 +127,7 @@
"@storybook/addon-storysource": "^8.3.4",
"@storybook/manager-api": "^8.3.4",
"@storybook/react": "^8.3.4",
"@storybook/react-vite": "^8.3.4",
"@storybook/react-vite": "^8.5.5",
"@storybook/theming": "^8.3.4",
"@types/dateformat": "^5.0.2",
"@types/lodash-es": "^4.17.12",
@@ -134,9 +135,9 @@
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react-swc": "^3.7.1",
"@vitest/coverage-v8": "^1.6.0",
"@vitest/ui": "^1.6.0",
"@vitejs/plugin-react-swc": "^3.8.0",
"@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.5",
"concurrently": "^8.2.2",
"csstype": "^3.1.3",
"dpdm": "^3.14.0",
@@ -152,11 +153,11 @@
"tsafe": "^1.7.5",
"type-fest": "^4.26.1",
"typescript": "^5.6.2",
"vite": "^5.4.8",
"vite": "^6.1.0",
"vite-plugin-css-injected-by-js": "^3.5.2",
"vite-plugin-dts": "^3.9.1",
"vite-plugin-dts": "^4.5.0",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.3.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^1.6.0"
},
"engines": {

File diff suppressed because it is too large Load Diff

View File

@@ -187,7 +187,10 @@
"values": "Values",
"resetToDefaults": "Reset to Defaults",
"seed": "Seed",
"combinatorial": "Combinatorial"
"combinatorial": "Combinatorial",
"layout": "Layout",
"row": "Row",
"column": "Column"
},
"hrf": {
"hrf": "High Resolution Fix",
@@ -305,6 +308,7 @@
},
"gallery": {
"gallery": "Gallery",
"assets": "Assets",
"alwaysShowImageSizeBadge": "Always Show Image Size Badge",
"assetsTab": "Files youve uploaded for use in your projects.",
"autoAssignBoardOnClick": "Auto-Assign Board on Click",
@@ -930,6 +934,7 @@
"noWorkflows": "No Workflows",
"noMatchingWorkflows": "No Matching Workflows",
"noWorkflow": "No Workflow",
"unableToUpdateNode": "Node update failed: node {{node}} of type {{type}} (may require deleting and recreating)",
"mismatchedVersion": "Invalid node: node {{node}} of type {{type}} has mismatched version (try updating?)",
"missingTemplate": "Invalid node: node {{node}} of type {{type}} missing template (not installed?)",
"sourceNodeDoesNotExist": "Invalid edge: source/output node {{node}} does not exist",
@@ -937,6 +942,7 @@
"sourceNodeFieldDoesNotExist": "Invalid edge: source/output field {{node}}.{{field}} does not exist",
"targetNodeFieldDoesNotExist": "Invalid edge: target/input field {{node}}.{{field}} does not exist",
"deletedInvalidEdge": "Deleted invalid edge {{source}} -> {{target}}",
"deletedMissingNodeFieldFormElement": "Deleted missing form field: node {{nodeId}} field {{fieldName}}",
"noConnectionInProgress": "No connection in progress",
"node": "Node",
"nodeOutputs": "Node Outputs",
@@ -951,6 +957,7 @@
"nodeVersion": "Node Version",
"noOutputRecorded": "No outputs recorded",
"notes": "Notes",
"description": "Description",
"notesDescription": "Add notes about your workflow",
"problemSettingTitle": "Problem Setting Title",
"resetToDefaultValue": "Reset to default value",
@@ -1265,7 +1272,10 @@
"workflowLoaded": "Workflow Loaded",
"problemRetrievingWorkflow": "Problem Retrieving Workflow",
"workflowDeleted": "Workflow Deleted",
"problemDeletingWorkflow": "Problem Deleting Workflow"
"problemDeletingWorkflow": "Problem Deleting Workflow",
"unableToCopy": "Unable to Copy",
"unableToCopyDesc": "Your browser does not support clipboard access. Firefox users may be able to fix this by following ",
"unableToCopyDesc_theseSteps": "these steps"
},
"popovers": {
"clipSkip": {
@@ -1690,7 +1700,26 @@
"download": "Download",
"copyShareLink": "Copy Share Link",
"copyShareLinkForWorkflow": "Copy Share Link for Workflow",
"delete": "Delete"
"delete": "Delete",
"builder": {
"builder": "Builder",
"layout": "Layout",
"row": "Row",
"column": "Column",
"label": "Label",
"description": "Description",
"component": "Component",
"numberInput": "Number Input",
"slider": "Slider",
"both": "Both",
"emptyRootPlaceholderViewMode": "Click Edit to start building a form for this workflow.",
"emptyRootPlaceholderEditMode": "Drag a form element or node field here to get started.",
"containerPlaceholder": "Empty Container",
"containerPlaceholderDesc": "Drag a form element or node field into this container.",
"headingPlaceholder": "Empty Heading",
"textPlaceholder": "Empty Text",
"workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release."
}
},
"controlLayers": {
"regional": "Regional",
@@ -1869,6 +1898,10 @@
"rgAutoNegativeNotSupported": "Auto-Negative not supported for selected base model",
"rgNoRegion": "no region drawn"
},
"errors": {
"unableToFindImage": "Unable to find image",
"unableToLoadImage": "Unable to Load Image"
},
"controlMode": {
"controlMode": "Control Mode",
"balanced": "Balanced (recommended)",

View File

@@ -301,7 +301,9 @@
"hfTokenHelperText": "Un token HF est requis pour utiliser certains modèles. Cliquez ici pour créer ou obtenir votre token.",
"hfTokenInvalid": "Token HF invalide ou manquant",
"hfForbidden": "Vous n'avez pas accès à ce modèle HF.",
"hfTokenInvalidErrorMessage2": "Mettre à jour dans le "
"hfTokenInvalidErrorMessage2": "Mettre à jour dans le ",
"controlLora": "Controle LoRA",
"urlUnauthorizedErrorMessage2": "Découvrir comment ici."
},
"parameters": {
"images": "Images",
@@ -332,7 +334,7 @@
"showOptionsPanel": "Afficher le panneau latéral (O ou T)",
"invoke": {
"noPrompts": "Aucun prompts généré",
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} entrée manquante",
"missingInputForField": "entrée manquante",
"missingFieldTemplate": "Modèle de champ manquant",
"invoke": "Invoke",
"addingImagesTo": "Ajouter des images à",
@@ -353,7 +355,9 @@
"canvasIsCompositing": "La toile est en train de composer",
"collectionTooFewItems": "{{nodeLabel}} -> {{fieldLabel}} : trop peu d'éléments, minimum {{minItems}}",
"collectionTooManyItems": "{{nodeLabel}} -> {{fieldLabel}} : trop d'éléments, maximum {{maxItems}}",
"canvasIsSelectingObject": "La toile est occupée (sélection d'objet)"
"canvasIsSelectingObject": "La toile est occupée (sélection d'objet)",
"emptyBatches": "lots vides",
"batchNodeNotConnected": "Noeud de lots non connecté : {{label}}"
},
"negativePromptPlaceholder": "Prompt Négatif",
"positivePromptPlaceholder": "Prompt Positif",
@@ -1630,7 +1634,26 @@
"boardAccessError": "Impossible de trouver la planche {{board_id}}, réinitialisation à la valeur par défaut",
"workflowHelpText": "Besoin d'aide? Consultez notre guide sur <LinkComponent>Comment commencer avec les Workflows</LinkComponent>.",
"noWorkflows": "Aucun Workflows",
"noMatchingWorkflows": "Aucun Workflows correspondant"
"noMatchingWorkflows": "Aucun Workflows correspondant",
"arithmeticSequence": "Séquence Arithmétique",
"uniformRandomDistribution": "Distribution Aléatoire Uniforme",
"noBatchGroup": "aucun groupe",
"generatorLoading": "chargement",
"generatorLoadFromFile": "Charger depuis un Fichier",
"dynamicPromptsRandom": "Prompts Dynamiques (Aléatoire)",
"integerRangeGenerator": "Générateur d'interval d'entiers",
"generateValues": "Générer Valeurs",
"linearDistribution": "Distribution Linéaire",
"floatRangeGenerator": "Générateur d'interval de nombres décimaux",
"generatorNRandomValues_one": "{{count}} valeur aléatoire",
"generatorNRandomValues_many": "{{count}} valeurs aléatoires",
"generatorNRandomValues_other": "{{count}} valeurs aléatoires",
"dynamicPromptsCombinatorial": "Prompts Dynamiques (Combinatoire)",
"parseString": "Analyser la chaine de charactères",
"internalDesc": "Cette invocation est utilisée internalement par Invoke. En fonction des mises à jours il est possible que des changements y soit effectués ou qu'elle soit supprimé sans prévention.",
"splitOn": "Diviser sur",
"generatorNoValues": "vide",
"addItem": "Ajouter un élément"
},
"models": {
"noMatchingModels": "Aucun modèle correspondant",

View File

@@ -172,7 +172,8 @@
"imagesTab": "Immagini create e salvate in Invoke.",
"assetsTab": "File che hai caricato per usarli nei tuoi progetti.",
"boardsSettings": "Impostazioni Bacheche",
"imagesSettings": "Impostazioni Immagini Galleria"
"imagesSettings": "Impostazioni Immagini Galleria",
"assets": "Risorse"
},
"hotkeys": {
"searchHotkeys": "Cerca tasti di scelta rapida",
@@ -832,7 +833,12 @@
"uploadFailedInvalidUploadDesc_withCount_one": "Devi caricare al massimo 1 immagine PNG o JPEG.",
"uploadFailedInvalidUploadDesc_withCount_many": "Devi caricare al massimo {{count}} immagini PNG o JPEG.",
"uploadFailedInvalidUploadDesc_withCount_other": "Devi caricare al massimo {{count}} immagini PNG o JPEG.",
"outOfMemoryErrorDescLocal": "Segui la nostra <LinkComponent>guida per bassa VRAM</LinkComponent> per ridurre gli OOM."
"outOfMemoryErrorDescLocal": "Segui la nostra <LinkComponent>guida per bassa VRAM</LinkComponent> per ridurre gli OOM.",
"pasteFailed": "Incolla non riuscita",
"pasteSuccess": "Incollato su {{destination}}",
"unableToCopy": "Impossibile copiare",
"unableToCopyDesc": "Il tuo browser non supporta l'accesso agli appunti. Gli utenti di Firefox potrebbero risolvere il problema seguendo ",
"unableToCopyDesc_theseSteps": "questi passaggi"
},
"accessibility": {
"invokeProgressBar": "Barra di avanzamento generazione",
@@ -2092,7 +2098,10 @@
"saveCanvasToGallery": "Salva la Tela nella Galleria",
"saveToGalleryGroup": "Salva nella Galleria",
"newInpaintMask": "Nuova maschera Inpaint",
"newRegionalGuidance": "Nuova Guida Regionale"
"newRegionalGuidance": "Nuova Guida Regionale",
"copyToClipboard": "Copia negli appunti",
"copyCanvasToClipboard": "Copia la tela negli appunti",
"copyBboxToClipboard": "Copia il riquadro di delimitazione negli appunti"
},
"newImg2ImgCanvasFromImage": "Nuova Immagine da immagine",
"copyRasterLayerTo": "Copia $t(controlLayers.rasterLayer) in",
@@ -2155,7 +2164,17 @@
"ipAdapterIncompatibleBaseModel": "modello base dell'immagine di riferimento incompatibile",
"ipAdapterNoImageSelected": "nessuna immagine di riferimento selezionata",
"rgAutoNegativeNotSupported": "Auto-Negativo non supportato per il modello base selezionato"
}
},
"pasteTo": "Incolla su",
"pasteToBboxDesc": "Nuovo livello (nel riquadro di delimitazione)",
"pasteToAssets": "Risorse",
"copyRegionError": "Errore durante la copia di {{region}}",
"pasteToAssetsDesc": "Incolla in Risorse",
"pasteToBbox": "Riquadro di delimitazione",
"pasteToCanvas": "Tela",
"pasteToCanvasDesc": "Nuovo livello (nella Tela)",
"pastedTo": "Incollato su {{destination}}",
"regionCopiedToClipboard": "{{region}} Copiato negli appunti"
},
"ui": {
"tabs": {
@@ -2254,11 +2273,12 @@
"watchRecentReleaseVideos": "Guarda i video su questa versione",
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
"items": [
"Modalità Bassa-VRAM",
"Gestione dinamica della memoria",
"Tempi di caricamento del modello più rapidi",
"Meno errori di memoria",
"Funzionalità lotto del flusso di lavoro ampliate"
"Impostazioni predefinite VRAM migliorate",
"Cancellazione della cache del modello su richiesta",
"Compatibilità estesa FLUX LoRA",
"Filtro Regola Immagine su Tela",
"Annulla tutto tranne l'elemento della coda corrente",
"Copia da e incolla sulla Tela"
]
},
"system": {

View File

@@ -117,7 +117,8 @@
"unstarImage": "Ngừng Gắn Sao Cho Ảnh",
"compareHelp2": "Nhấn <Kbd>M</Kbd> để tuần hoàn trong chế độ so sánh.",
"boardsSettings": "Thiết Lập Bảng",
"imagesSettings": "Cài Đặt Thư Viện Ảnh"
"imagesSettings": "Cài Đặt Thư Viện Ảnh",
"assets": "Tài Nguyên"
},
"common": {
"ipAdapter": "IP Adapter",
@@ -303,7 +304,11 @@
"completedIn": "Hoàn tất trong",
"graphQueued": "Đồ Thị Đã Vào Hàng",
"batchQueuedDesc_other": "Thêm {{count}} phiên vào {{direction}} của hàng",
"batchSize": "Kích Thước Lô"
"batchSize": "Kích Thước Lô",
"cancelAllExceptCurrentQueueItemAlertDialog": "Huỷ tất cả mục đang xếp hàng ngoại trừ mục hiện tại, sẽ dừng các mục đang chờ nhưng cho phép các mục đang chạy được hoàn tất.",
"cancelAllExceptCurrentQueueItemAlertDialog2": "Bạn có chắc muốn huỷ tất cả mục đang chờ?",
"cancelAllExceptCurrentTooltip": "Huỷ Bỏ Tất Cả Ngoại Trừ Mục Hiện Tại",
"confirm": "Đồng Ý"
},
"hotkeys": {
"canvas": {
@@ -1797,7 +1802,10 @@
"newControlLayer": "Layer Điều Khiển Được Mới",
"newRasterLayer": "Layer Dạng Raster Mới",
"bboxGroup": "Được Tạo Từ Hộp Giới Hạn",
"canvasGroup": "Canvas"
"canvasGroup": "Canvas",
"copyCanvasToClipboard": "Sao Chép Canvas Vào Clipboard",
"copyToClipboard": "Sao Chép Vào Clipboard",
"copyBboxToClipboard": "Sao Chép Hộp Giới Hạn Vào Clipboard"
},
"stagingArea": {
"saveToGallery": "Lưu Vào Thư Viện",
@@ -1914,6 +1922,30 @@
"gaussian_type": "Gaussian",
"noise_color": "Màu Nhiễu",
"size": "Cỡ Nhiễu"
},
"adjust_image": {
"channel": "Kênh Màu",
"cyan": "Lục Lam (Cmyk)",
"value_setting": "Giá Trị",
"scale_values": "Giá Trị Theo Tỉ Lệ",
"red": "Đỏ (Rgba)",
"green": "Lục (rGba)",
"blue": "Lam (rgBa)",
"alpha": "Độ Trong Suốt (rgbA)",
"luminosity": "Độ Sáng (Lab)",
"magenta": "Hồng Đỏ (cMyk)",
"yellow": "Vàng (cmYk)",
"description": "Điều chỉnh kênh màu được chọn của ảnh.",
"black": "Đen (cmyK)",
"cr": "Cr (ycC)",
"label": "Điều Chỉnh Ảnh",
"value": "Độ Sáng (hsV)",
"saturation": "Độ Bão Hoà (hSv)",
"hue": "Vùng Màu (Hsv)",
"a": "A (lAb)",
"b": "B (laB)",
"y": "Y (Ycc)",
"cb": "Cb (yCc)"
}
},
"transform": {
@@ -1992,6 +2024,20 @@
"rgReferenceImagesNotSupported": "Ảnh Mẫu Khu Vực không được hỗ trợ cho model cơ sở được chọn",
"rgAutoNegativeNotSupported": "Tự Động Đảo Chiều không được hỗ trợ cho model cơ sở được chọn",
"rgNoRegion": "không có khu vực được vẽ"
},
"pasteTo": "Dán Vào",
"pasteToAssets": "Tài Nguyên",
"pasteToAssetsDesc": "Dán Vào Tài Nguyên",
"pasteToBbox": "Hộp Giới Hạn",
"pasteToBboxDesc": "Layer Mới (Trong Hộp Giới Hạn)",
"pasteToCanvas": "Canvas",
"pasteToCanvasDesc": "Layer Mới (Trong Canvas)",
"pastedTo": "Dán Vào {{destination}}",
"regionCopiedToClipboard": "Sao Chép {{region}} Vào Clipboard",
"copyRegionError": "Lỗi khi sao chép {{region}}",
"errors": {
"unableToLoadImage": "Không Thể Tải Hình Ảnh",
"unableToFindImage": "Không Thể Tìm Hình Ảnh"
}
},
"stylePresets": {
@@ -2123,7 +2169,12 @@
"problemDownloadingImage": "Không Thể Tải Xuống Ảnh",
"problemCopyingLayer": "Không Thể Sao Chép Layer",
"problemSavingLayer": "Không Thể Lưu Layer",
"outOfMemoryErrorDescLocal": "Làm theo <LinkComponent>hướng dẫn VRAM Thấp</LinkComponent> của chúng tôi để hạn chế OOM (Tràn bộ nhớ)."
"outOfMemoryErrorDescLocal": "Làm theo <LinkComponent>hướng dẫn VRAM Thấp</LinkComponent> của chúng tôi để hạn chế OOM (Tràn bộ nhớ).",
"unableToCopy": "Không Thể Sao Chép",
"unableToCopyDesc_theseSteps": "các bước sau",
"unableToCopyDesc": "Trình duyệt của bạn không hỗ trợ tính năng clipboard. Người dùng Firefox có thể khắc phục theo ",
"pasteSuccess": "Dán Vào {{destination}}",
"pasteFailed": "Dán Thất Bại"
},
"ui": {
"tabs": {
@@ -2218,11 +2269,12 @@
"watchRecentReleaseVideos": "Xem Video Phát Hành Mới Nhất",
"watchUiUpdatesOverview": "Xem Tổng Quan Về Những Cập Nhật Cho Giao Diện Người Dùng",
"items": [
"Chế độ VRAM thấp",
"Trình quản lý bộ nhớ động",
"Tải model nhanh hơn",
"Ít lỗi bộ nhớ hơn",
"Mở rộng khả năng xử lý hàng loạt workflow"
"Cải thiện các thiết lập mặc định của VRAM",
"Xoá bộ nhớ đệm của model theo yêu cầu",
"Mở rộng khả năng tương thích LoRA trên FLUX",
"Bộ lọc điều chỉnh ảnh trên Canvas",
"Huỷ tất cả trừ mục đang xếp hàng hiện tại",
"Sao chép và dán trên Canvas"
]
},
"upsell": {

View File

@@ -1,6 +1,7 @@
import { Button, Flex, Heading, Image, Link, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useClipboard } from 'common/hooks/useClipboard';
import { selectConfigSlice } from 'features/system/store/configSlice';
import { toast } from 'features/toast/toast';
import newGithubIssueUrl from 'new-github-issue-url';
@@ -20,15 +21,17 @@ const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLoc
const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
const { t } = useTranslation();
const isLocal = useAppSelector(selectIsLocal);
const clipboard = useClipboard();
const handleCopy = useCallback(() => {
const text = JSON.stringify(serializeError(error), null, 2);
navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``);
toast({
id: 'ERROR_COPIED',
title: t('toast.errorCopied'),
clipboard.writeText(`\`\`\`\n${text}\n\`\`\``, () => {
toast({
id: 'ERROR_COPIED',
title: t('toast.errorCopied'),
});
});
}, [error, t]);
}, [clipboard, error, t]);
const url = useMemo(() => {
if (isLocal) {

View File

@@ -1,5 +1,6 @@
import '@fontsource-variable/inter';
import 'overlayscrollbars/overlayscrollbars.css';
import '@xyflow/react/dist/base.css';
import { ChakraProvider, DarkMode, extendTheme, theme as _theme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
import type { ReactNode } from 'react';

View File

@@ -8,12 +8,13 @@ import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { isImageFieldInputInstance } from 'features/nodes/types/field';
import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { forEach, intersectionBy } from 'lodash-es';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import type { Param0 } from 'tsafe';
const log = logger('gallery');
@@ -21,6 +22,7 @@ const log = logger('gallery');
// Some utils to delete images from different parts of the app
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
const actions: Param0<typeof dispatch>[] = [];
state.nodes.present.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
@@ -28,16 +30,28 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im
forEach(node.data.inputs, (input) => {
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
dispatch(
actions.push(
fieldImageValueChanged({
nodeId: node.data.id,
fieldName: input.name,
value: undefined,
})
);
return;
}
if (isImageFieldCollectionInputInstance(input)) {
actions.push(
fieldImageCollectionValueChanged({
nodeId: node.data.id,
fieldName: input.name,
value: input.value?.filter((value) => value?.image_name !== imageDTO.image_name),
})
);
}
});
});
actions.forEach(dispatch);
};
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {

View File

@@ -1,6 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
import { $nodeExecutionStates } from 'features/nodes/hooks/useNodeExecutionState';
import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions';
import { $templates } from 'features/nodes/store/nodesSlice';
import { $needsFit } from 'features/nodes/store/reactFlowInstance';

View File

@@ -1,5 +1,6 @@
import type { As, ChakraProps, FlexProps } from '@invoke-ai/ui-library';
import type { ChakraProps, FlexProps } from '@invoke-ai/ui-library';
import { Flex, Icon, Skeleton, Spinner, Text } from '@invoke-ai/ui-library';
import type { ElementType } from 'react';
import { memo, useMemo } from 'react';
import { PiImageBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
@@ -28,7 +29,7 @@ IAILoadingImageFallback.displayName = 'IAILoadingImageFallback';
type IAINoImageFallbackProps = FlexProps & {
label?: string;
icon?: As | null;
icon?: ElementType | null;
boxSize?: ChakraProps['boxSize'];
};

View File

@@ -1,39 +0,0 @@
import { Box } from '@invoke-ai/ui-library';
import { memo, useMemo } from 'react';
type Props = {
isSelected: boolean;
isHovered: boolean;
};
const SelectionOverlay = ({ isSelected, isHovered }: Props) => {
const shadow = useMemo(() => {
if (isSelected && isHovered) {
return 'nodeHoveredSelected';
}
if (isSelected) {
return 'nodeSelected';
}
if (isHovered) {
return 'nodeHovered';
}
return undefined;
}, [isHovered, isSelected]);
return (
<Box
className="selection-box"
position="absolute"
top={0}
insetInlineEnd={0}
bottom={0}
insetInlineStart={0}
borderRadius="base"
opacity={isSelected || isHovered ? 1 : 0.5}
transitionProperty="common"
transitionDuration="0.1s"
pointerEvents="none"
shadow={shadow}
/>
);
};
export default memo(SelectionOverlay);

View File

@@ -1,238 +0,0 @@
import { useToken } from '@invoke-ai/ui-library';
export const useChakraThemeTokens = () => {
const [
base50,
base100,
base150,
base200,
base250,
base300,
base350,
base400,
base450,
base500,
base550,
base600,
base650,
base700,
base750,
base800,
base850,
base900,
base950,
accent50,
accent100,
accent150,
accent200,
accent250,
accent300,
accent350,
accent400,
accent450,
accent500,
accent550,
accent600,
accent650,
accent700,
accent750,
accent800,
accent850,
accent900,
accent950,
baseAlpha50,
baseAlpha100,
baseAlpha150,
baseAlpha200,
baseAlpha250,
baseAlpha300,
baseAlpha350,
baseAlpha400,
baseAlpha450,
baseAlpha500,
baseAlpha550,
baseAlpha600,
baseAlpha650,
baseAlpha700,
baseAlpha750,
baseAlpha800,
baseAlpha850,
baseAlpha900,
baseAlpha950,
accentAlpha50,
accentAlpha100,
accentAlpha150,
accentAlpha200,
accentAlpha250,
accentAlpha300,
accentAlpha350,
accentAlpha400,
accentAlpha450,
accentAlpha500,
accentAlpha550,
accentAlpha600,
accentAlpha650,
accentAlpha700,
accentAlpha750,
accentAlpha800,
accentAlpha850,
accentAlpha900,
accentAlpha950,
] = useToken('colors', [
'base.50',
'base.100',
'base.150',
'base.200',
'base.250',
'base.300',
'base.350',
'base.400',
'base.450',
'base.500',
'base.550',
'base.600',
'base.650',
'base.700',
'base.750',
'base.800',
'base.850',
'base.900',
'base.950',
'accent.50',
'accent.100',
'accent.150',
'accent.200',
'accent.250',
'accent.300',
'accent.350',
'accent.400',
'accent.450',
'accent.500',
'accent.550',
'accent.600',
'accent.650',
'accent.700',
'accent.750',
'accent.800',
'accent.850',
'accent.900',
'accent.950',
'baseAlpha.50',
'baseAlpha.100',
'baseAlpha.150',
'baseAlpha.200',
'baseAlpha.250',
'baseAlpha.300',
'baseAlpha.350',
'baseAlpha.400',
'baseAlpha.450',
'baseAlpha.500',
'baseAlpha.550',
'baseAlpha.600',
'baseAlpha.650',
'baseAlpha.700',
'baseAlpha.750',
'baseAlpha.800',
'baseAlpha.850',
'baseAlpha.900',
'baseAlpha.950',
'accentAlpha.50',
'accentAlpha.100',
'accentAlpha.150',
'accentAlpha.200',
'accentAlpha.250',
'accentAlpha.300',
'accentAlpha.350',
'accentAlpha.400',
'accentAlpha.450',
'accentAlpha.500',
'accentAlpha.550',
'accentAlpha.600',
'accentAlpha.650',
'accentAlpha.700',
'accentAlpha.750',
'accentAlpha.800',
'accentAlpha.850',
'accentAlpha.900',
'accentAlpha.950',
]);
return {
base50,
base100,
base150,
base200,
base250,
base300,
base350,
base400,
base450,
base500,
base550,
base600,
base650,
base700,
base750,
base800,
base850,
base900,
base950,
accent50,
accent100,
accent150,
accent200,
accent250,
accent300,
accent350,
accent400,
accent450,
accent500,
accent550,
accent600,
accent650,
accent700,
accent750,
accent800,
accent850,
accent900,
accent950,
baseAlpha50,
baseAlpha100,
baseAlpha150,
baseAlpha200,
baseAlpha250,
baseAlpha300,
baseAlpha350,
baseAlpha400,
baseAlpha450,
baseAlpha500,
baseAlpha550,
baseAlpha600,
baseAlpha650,
baseAlpha700,
baseAlpha750,
baseAlpha800,
baseAlpha850,
baseAlpha900,
baseAlpha950,
accentAlpha50,
accentAlpha100,
accentAlpha150,
accentAlpha200,
accentAlpha250,
accentAlpha300,
accentAlpha350,
accentAlpha400,
accentAlpha450,
accentAlpha500,
accentAlpha550,
accentAlpha600,
accentAlpha650,
accentAlpha700,
accentAlpha750,
accentAlpha800,
accentAlpha850,
accentAlpha900,
accentAlpha950,
};
};

View File

@@ -0,0 +1,81 @@
/* eslint-disable no-restricted-properties */
import { ExternalLink, Text } from '@invoke-ai/ui-library';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { Param0 } from 'tsafe';
const CLIPBOARD_FAQ_URL = 'https://invoke-ai.github.io/InvokeAI/faq/#unable-to-copy-on-firefox';
export const useClipboard = () => {
const { t } = useTranslation();
const alertClipboardNotAvailable = useCallback(() => {
toast({
id: 'CLIPBOARD_UNAVAILABLE',
title: t('toast.unableToCopy'),
description: (
<>
<Text fontSize="md">
{t('toast.unableToCopyDesc')}
<ExternalLink
display="inline"
fontWeight="semibold"
href={CLIPBOARD_FAQ_URL}
label={t('toast.unableToCopyDesc_theseSteps')}
/>
.
</Text>
</>
),
status: 'error',
});
}, [t]);
const isAvailable = useMemo(() => {
if (!navigator.clipboard || !window.ClipboardItem) {
return false;
}
// TODO(psyche): Should we query the permissions API?
return true;
}, []);
const writeText = useCallback(
(data: Param0<Clipboard['writeText']>, onCopy?: () => void) => {
if (!isAvailable) {
alertClipboardNotAvailable();
return;
}
navigator.clipboard.writeText(data);
onCopy?.();
},
[alertClipboardNotAvailable, isAvailable]
);
const write = useCallback(
(data: Param0<Clipboard['write']>, onCopy?: () => void) => {
if (!isAvailable) {
alertClipboardNotAvailable();
return;
}
navigator.clipboard.write(data);
onCopy?.();
},
[alertClipboardNotAvailable, isAvailable]
);
const writeImage = useCallback(
(blob: Blob, onCopy?: () => void) => {
if (!isAvailable) {
alertClipboardNotAvailable();
return;
}
const data = [new ClipboardItem({ ['image/png']: blob })];
navigator.clipboard.write(data);
onCopy?.();
},
[alertClipboardNotAvailable, isAvailable]
);
return { isAvailable, writeText, write, writeImage };
};

View File

@@ -1,26 +1,15 @@
import { useClipboard } from 'common/hooks/useClipboard';
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const useCopyImageToClipboard = () => {
const { t } = useTranslation();
const isClipboardAPIAvailable = useMemo(() => {
return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem);
}, []);
const clipboard = useClipboard();
const copyImageToClipboard = useCallback(
async (image_url: string) => {
if (!isClipboardAPIAvailable) {
toast({
id: 'PROBLEM_COPYING_IMAGE',
title: t('toast.problemCopyingImage'),
description: "Your browser doesn't support the Clipboard API.",
status: 'error',
});
}
try {
const blob = await convertImageUrlToBlob(image_url);
@@ -28,12 +17,12 @@ export const useCopyImageToClipboard = () => {
throw new Error('Unable to create Blob');
}
copyBlobToClipboard(blob);
toast({
id: 'IMAGE_COPIED',
title: t('toast.imageCopied'),
status: 'success',
clipboard.writeImage(blob, () => {
toast({
id: 'IMAGE_COPIED',
title: t('toast.imageCopied'),
status: 'success',
});
});
} catch (err) {
toast({
@@ -44,8 +33,8 @@ export const useCopyImageToClipboard = () => {
});
}
},
[isClipboardAPIAvailable, t]
[clipboard, t]
);
return { isClipboardAPIAvailable, copyImageToClipboard };
return copyImageToClipboard;
};

View File

@@ -0,0 +1,72 @@
import type { ChangeEvent, KeyboardEvent, RefObject } from 'react';
import { useCallback, useEffect, useState } from 'react';
type UseEditableArg = {
value: string;
defaultValue: string;
onChange: (value: string) => void;
onStartEditing?: () => void;
inputRef?: RefObject<HTMLInputElement | HTMLTextAreaElement>;
};
export const useEditable = ({ value, defaultValue, onChange: _onChange, onStartEditing, inputRef }: UseEditableArg) => {
const [isEditing, setIsEditing] = useState(false);
const [localValue, setLocalValue] = useState(value);
const onBlur = useCallback(() => {
const trimmedValue = localValue.trim();
const newValue = trimmedValue || defaultValue;
setLocalValue(newValue);
if (newValue !== value) {
_onChange(newValue);
}
setIsEditing(false);
inputRef?.current?.setSelectionRange(0, 0);
}, [localValue, defaultValue, value, inputRef, _onChange]);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setLocalValue(e.target.value);
}, []);
const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
onBlur();
} else if (e.key === 'Escape') {
setLocalValue(value);
_onChange(value);
setIsEditing(false);
}
},
[_onChange, onBlur, value]
);
const startEditing = useCallback(() => {
setIsEditing(true);
onStartEditing?.();
}, [onStartEditing]);
useEffect(() => {
// Another component may change the title; sync local title with global state
setLocalValue(value);
}, [value]);
useEffect(() => {
if (isEditing) {
inputRef?.current?.focus();
inputRef?.current?.select();
}
}, [inputRef, isEditing]);
return {
isEditing,
startEditing,
value: localValue,
inputProps: {
value: localValue,
onChange,
onKeyDown,
onBlur,
},
};
};

View File

@@ -1,67 +1,43 @@
import { Input } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useBoolean } from 'common/hooks/useBoolean';
import { useEditable } from 'common/hooks/useEditable';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle';
import { useEntityName, useEntityTypeName } from 'features/controlLayers/hooks/useEntityTitle';
import { entityNameChanged } from 'features/controlLayers/store/canvasSlice';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { memo, useCallback, useRef } from 'react';
export const CanvasEntityEditableTitle = memo(() => {
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const title = useEntityTitle(entityIdentifier);
const isEditing = useBoolean(false);
const [localTitle, setLocalTitle] = useState(title);
const ref = useRef<HTMLInputElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const name = useEntityName(entityIdentifier);
const typeName = useEntityTypeName(entityIdentifier.type);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setLocalTitle(e.target.value);
}, []);
const onBlur = useCallback(() => {
const trimmedTitle = localTitle.trim();
if (trimmedTitle.length === 0) {
dispatch(entityNameChanged({ entityIdentifier, name: null }));
} else if (trimmedTitle !== title) {
dispatch(entityNameChanged({ entityIdentifier, name: trimmedTitle }));
}
isEditing.setFalse();
}, [dispatch, entityIdentifier, isEditing, localTitle, title]);
const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onBlur();
} else if (e.key === 'Escape') {
setLocalTitle(title);
isEditing.setFalse();
}
const onChange = useCallback(
(name: string) => {
dispatch(entityNameChanged({ entityIdentifier, name }));
},
[isEditing, onBlur, title]
[dispatch, entityIdentifier]
);
useEffect(() => {
if (isEditing.isTrue) {
ref.current?.focus();
ref.current?.select();
}
}, [isEditing.isTrue]);
const editable = useEditable({
value: name || typeName,
defaultValue: typeName,
onChange,
inputRef,
});
if (!isEditing.isTrue) {
return <CanvasEntityTitle cursor="text" onDoubleClick={isEditing.setTrue} />;
if (!editable.isEditing) {
return <CanvasEntityTitle cursor="text" onDoubleClick={editable.startEditing} />;
}
return (
<Input
ref={ref}
value={localTitle}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
ref={inputRef}
{...editable.inputProps}
variant="outline"
_focusVisible={{ borderWidth: 1, borderColor: 'invokeBlueAlpha.400', borderRadius: 'base' }}
_focusVisible={{ borderRadius: 'base', h: 'unset' }}
/>
);
});

View File

@@ -1,12 +1,11 @@
import { logger } from 'app/logging/logger';
import { withResultAsync } from 'common/util/result';
import { useClipboard } from 'common/hooks/useClipboard';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance';
import { canvasToBlob } from 'features/controlLayers/konva/util';
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { startCase } from 'lodash-es';
import { useCallback } from 'react';
@@ -17,6 +16,7 @@ const log = logger('canvas');
export const useCopyLayerToClipboard = () => {
const { t } = useTranslation();
const clipboard = useClipboard();
const copyLayerToCipboard = useCallback(
async (
adapter:
@@ -30,27 +30,25 @@ export const useCopyLayerToClipboard = () => {
return;
}
const result = await withResultAsync(async () => {
try {
const canvas = adapter.getCanvas();
const blob = await canvasToBlob(canvas);
copyBlobToClipboard(blob);
});
if (result.isOk()) {
log.trace('Layer copied to clipboard');
toast({
status: 'info',
title: t('toast.layerCopiedToClipboard'),
clipboard.writeImage(blob, () => {
log.trace('Layer copied to clipboard');
toast({
status: 'info',
title: t('toast.layerCopiedToClipboard'),
});
});
} else {
log.error({ error: serializeError(result.error) }, 'Problem copying layer to clipboard');
} catch (error) {
log.error({ error: serializeError(error) }, 'Problem copying layer to clipboard');
toast({
status: 'error',
title: t('toast.problemCopyingLayer'),
});
}
},
[t]
[clipboard, t]
);
return copyLayerToCipboard;
@@ -58,6 +56,7 @@ export const useCopyLayerToClipboard = () => {
export const useCopyCanvasToClipboard = (region: 'canvas' | 'bbox') => {
const { t } = useTranslation();
const clipboard = useClipboard();
const canvasManager = useCanvasManager();
const copyCanvasToClipboard = useCallback(async () => {
const rect =
@@ -74,20 +73,19 @@ export const useCopyCanvasToClipboard = (region: 'canvas' | 'bbox') => {
return;
}
const result = await withResultAsync(async () => {
try {
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
const canvasElement = canvasManager.compositor.getCompositeCanvas(rasterAdapters, rect);
const blob = await canvasToBlob(canvasElement);
copyBlobToClipboard(blob);
});
if (result.isOk()) {
toast({ title: t('controlLayers.regionCopiedToClipboard', { region: startCase(region) }) });
} else {
log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery');
clipboard.writeImage(blob, () => {
log.trace('Region copied to clipboard');
toast({ title: t('controlLayers.regionCopiedToClipboard', { region: startCase(region) }) });
});
} catch (error) {
log.error({ error: serializeError(error) }, 'Failed to save canvas to gallery');
toast({ title: t('controlLayers.copyRegionError', { region: startCase(region) }), status: 'error' });
}
}, [canvasManager.compositor, canvasManager.stateApi, region, t]);
}, [canvasManager.compositor, canvasManager.stateApi, clipboard, region, t]);
return copyCanvasToClipboard;
};

View File

@@ -15,17 +15,17 @@ const createSelectName = (entityIdentifier: CanvasEntityIdentifier) =>
return entity.name;
});
export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
const { t } = useTranslation();
export const useEntityName = (entityIdentifier: CanvasEntityIdentifier) => {
const selectName = useMemo(() => createSelectName(entityIdentifier), [entityIdentifier]);
const name = useAppSelector(selectName);
return name;
};
const title = useMemo(() => {
if (name) {
return name;
}
export const useEntityTypeName = (type: CanvasEntityIdentifier['type']) => {
const { t } = useTranslation();
switch (entityIdentifier.type) {
const typeName = useMemo(() => {
switch (type) {
case 'inpaint_mask':
return t('controlLayers.inpaintMask');
case 'control_layer':
@@ -39,7 +39,15 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
default:
assert(false, 'Unexpected entity type');
}
}, [entityIdentifier.type, name, t]);
}, [type, t]);
return typeName;
};
export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
const name = useEntityName(entityIdentifier);
const typeName = useEntityTypeName(entityIdentifier.type);
const title = useMemo(() => name || typeName, [name, typeName]);
return title;
};

View File

@@ -1,5 +1,6 @@
import { Mutex } from 'async-mutex';
import { deepClone } from 'common/util/deepClone';
import { withResultAsync } from 'common/util/result';
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer';
import type { CanvasEntityFilterer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer';
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
@@ -7,7 +8,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import type { CanvasSegmentAnythingModule } from 'features/controlLayers/konva/CanvasSegmentAnythingModule';
import type { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStagingAreaModule';
import { loadImage } from 'features/controlLayers/konva/util';
import { getKonvaNodeDebugAttrs, loadImage } from 'features/controlLayers/konva/util';
import type { CanvasImageState } from 'features/controlLayers/store/types';
import { t } from 'i18next';
import Konva from 'konva';
@@ -94,36 +95,41 @@ export class CanvasObjectImage extends CanvasModuleBase {
}
updateImageSource = async (imageName: string) => {
try {
this.log.trace({ imageName }, 'Updating image source');
this.log.trace({ imageName }, 'Updating image source');
this.isLoading = true;
this.konva.group.visible(true);
this.isLoading = true;
this.konva.group.visible(true);
if (!this.konva.image) {
this.konva.placeholder.group.visible(false);
this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image'));
}
const imageDTO = await getImageDTOSafe(imageName);
if (imageDTO === null) {
this.onFailedToLoadImage();
return;
}
this.imageElement = await loadImage(imageDTO.image_url);
await this.updateImageElement();
} catch {
this.onFailedToLoadImage();
if (!this.konva.image) {
this.konva.placeholder.group.visible(false);
this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image'));
}
const imageDTO = await getImageDTOSafe(imageName);
if (imageDTO === null) {
// ImageDTO not found (or network error)
this.onFailedToLoadImage(t('controlLayers.unableToFindImage', 'Unable to find image'));
return;
}
const imageElementResult = await withResultAsync(() => loadImage(imageDTO.image_url));
if (imageElementResult.isErr()) {
// Image loading failed (e.g. the URL to the "physical" image is invalid)
this.onFailedToLoadImage(t('controlLayers.unableToLoadImage', 'Unable to load image'));
return;
}
this.imageElement = imageElementResult.value;
await this.updateImageElement();
};
onFailedToLoadImage = () => {
this.log({ image: this.state.image }, 'Failed to load image');
onFailedToLoadImage = (message: string) => {
this.log({ image: this.state.image }, message);
this.konva.image?.visible(false);
this.isLoading = false;
this.isError = true;
this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load'));
this.konva.placeholder.text.text(message);
this.konva.placeholder.group.visible(true);
};
@@ -140,6 +146,7 @@ export class CanvasObjectImage extends CanvasModuleBase {
image: this.imageElement,
width,
height,
visible: true,
});
} else {
this.log.trace('Creating new Konva image');
@@ -202,6 +209,10 @@ export class CanvasObjectImage extends CanvasModuleBase {
isLoading: this.isLoading,
isError: this.isError,
state: deepClone(this.state),
konva: {
group: getKonvaNodeDebugAttrs(this.konva.group),
image: this.konva.image ? getKonvaNodeDebugAttrs(this.konva.image) : null,
},
};
};
}

View File

@@ -75,7 +75,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
this.log.trace('Rendering staging area');
const stagingArea = this.manager.stateApi.runSelector(selectCanvasStagingAreaSlice);
const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
const { x, y } = this.manager.stateApi.getBbox().rect;
const shouldShowStagedImage = this.$shouldShowStagedImage.get();
this.selectedImage = stagingArea.stagedImages[stagingArea.selectedStagedImageIndex] ?? null;
@@ -83,27 +83,51 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
if (this.selectedImage) {
const { imageDTO } = this.selectedImage;
const image = imageDTOToImageWithDims(imageDTO);
/**
* When the final output image of a generation is received, we should clear that generation's last progress image.
*
* It's possible that we have already rendered the progress image from the next generation before the output image
* from the previous is fully loaded/rendered. This race condition results in a flicker:
* - LAST GENERATION: Render the final progress image
* - LAST GENERATION: Start loading the final output image...
* - NEXT GENERATION: Render the first progress image
* - LAST GENERATION: ...Finish loading the final output image & render it, clearing the progress image <-- Flicker!
* - NEXT GENERATION: Render the next progress image
*
* We can detect the race condition by stashing the session ID of the last progress image when we begin loading
* that session's output image. After we render it, if the progress image's session ID is the same as the one we
* stashed, we know that we have not yet gotten that next generation's first progress image. We can clear the
* progress image without causing a flicker.
*/
const lastProgressEventSessionId = this.manager.progressImage.$lastProgressEvent.get()?.session_id;
const hideProgressIfSameSession = () => {
const currentProgressEventSessionId = this.manager.progressImage.$lastProgressEvent.get()?.session_id;
if (lastProgressEventSessionId === currentProgressEventSessionId) {
this.manager.progressImage.$lastProgressEvent.set(null);
}
};
if (!this.image) {
const { image_name } = imageDTO;
this.image = new CanvasObjectImage(
{
id: 'staging-area-image',
type: 'image',
image: {
image_name: image_name,
width,
height,
},
image,
},
this
);
await this.image.update(this.image.state, true);
this.konva.group.add(this.image.konva.group);
}
if (!this.image.isLoading && !this.image.isError) {
await this.image.update({ ...this.image.state, image: imageDTOToImageWithDims(imageDTO) }, true);
this.manager.progressImage.$lastProgressEvent.set(null);
hideProgressIfSameSession();
} else if (this.image.isLoading) {
// noop - just wait for the image to load
} else if (this.image.state.image.image_name !== image.image_name) {
await this.image.update({ ...this.image.state, image }, true);
hideProgressIfSameSession();
} else if (this.image.isError) {
hideProgressIfSameSession();
}
this.image.konva.group.visible(shouldShowStagedImage);
} else {
@@ -136,6 +160,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
selectedImage: this.selectedImage,
$shouldShowStagedImage: this.$shouldShowStagedImage.get(),
$isStaging: this.$isStaging.get(),
image: this.image?.repr() ?? null,
};
};
}

View File

@@ -484,9 +484,10 @@ export function loadImage(src: string): Promise<HTMLImageElement> {
export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
export function getPrefixedId(
prefix: CanvasEntityIdentifier['type'] | CanvasObjectState['type'] | (string & Record<never, never>)
prefix: CanvasEntityIdentifier['type'] | CanvasObjectState['type'] | (string & Record<never, never>),
separator = ':'
): string {
return `${prefix}:${nanoid()}`;
return `${prefix}${separator}${nanoid()}`;
}
export const getEmptyRect = (): Rect => {

View File

@@ -49,6 +49,8 @@ export type ParamsState = {
optimizedDenoisingEnabled: boolean;
iterations: number;
scheduler: ParameterScheduler;
upscaleScheduler: ParameterScheduler;
upscaleCfgScale: ParameterCFGScale;
seed: ParameterSeed;
shouldRandomizeSeed: boolean;
steps: ParameterSteps;
@@ -96,6 +98,8 @@ const initialState: ParamsState = {
optimizedDenoisingEnabled: true,
iterations: 1,
scheduler: 'dpmpp_3m_k',
upscaleScheduler: 'kdpm_2',
upscaleCfgScale: 2,
seed: 0,
shouldRandomizeSeed: true,
steps: 30,
@@ -139,6 +143,9 @@ export const paramsSlice = createSlice({
setCfgScale: (state, action: PayloadAction<ParameterCFGScale>) => {
state.cfgScale = action.payload;
},
setUpscaleCfgScale: (state, action: PayloadAction<ParameterCFGScale>) => {
state.upscaleCfgScale = action.payload;
},
setGuidance: (state, action: PayloadAction<ParameterGuidance>) => {
state.guidance = action.payload;
},
@@ -148,6 +155,10 @@ export const paramsSlice = createSlice({
setScheduler: (state, action: PayloadAction<ParameterScheduler>) => {
state.scheduler = action.payload;
},
setUpscaleScheduler: (state, action: PayloadAction<ParameterScheduler>) => {
state.upscaleScheduler = action.payload;
},
setSeed: (state, action: PayloadAction<number>) => {
state.seed = action.payload;
state.shouldRandomizeSeed = false;
@@ -315,6 +326,8 @@ export const {
setCfgRescaleMultiplier,
setGuidance,
setScheduler,
setUpscaleScheduler,
setUpscaleCfgScale,
setSeed,
setImg2imgStrength,
setOptimizedDenoisingEnabled,
@@ -409,6 +422,9 @@ export const selectVAEPrecision = createParamsSelector((params) => params.vaePre
export const selectIterations = createParamsSelector((params) => params.iterations);
export const selectShouldUseCPUNoise = createParamsSelector((params) => params.shouldUseCpuNoise);
export const selectUpscaleScheduler = createParamsSelector((params) => params.upscaleScheduler);
export const selectUpscaleCfgScale = createParamsSelector((params) => params.upscaleCfgScale);
export const selectRefinerCFGScale = createParamsSelector((params) => params.refinerCFGScale);
export const selectRefinerModel = createParamsSelector((params) => params.refinerModel);
export const selectIsRefinerModelSelected = createParamsSelector((params) => Boolean(params.refinerModel));

View File

@@ -9,7 +9,8 @@ import type { DndListTargetState } from 'features/dnd/types';
*/
const line = {
thickness: 2,
backgroundColor: 'base.500',
backgroundColor: 'red',
// backgroundColor: 'base.500',
};
type DropIndicatorProps = {
@@ -104,7 +105,7 @@ function DndDropIndicatorInternal({ edge, gap = '0px' }: DropIndicatorProps) {
);
}
export const DndListDropIndicator = ({ dndState }: { dndState: DndListTargetState }) => {
export const DndListDropIndicator = ({ dndState, gap }: { dndState: DndListTargetState; gap?: string }) => {
if (dndState.type !== 'is-dragging-over') {
return null;
}
@@ -117,7 +118,7 @@ export const DndListDropIndicator = ({ dndState }: { dndState: DndListTargetStat
<DndDropIndicatorInternal
edge={dndState.closestEdge}
// This is the gap between items in the list, used to calculate the position of the drop indicator
gap="var(--invoke-space-2)"
gap={gap || 'var(--invoke-space-2)'}
/>
);
};

View File

@@ -108,18 +108,6 @@ export const singleCanvasEntityDndSource: DndSource<SingleCanvasEntityDndSourceD
getData: buildGetData(_singleCanvasEntity.key, _singleCanvasEntity.type),
};
const _singleWorkflowField = buildTypeAndKey('single-workflow-field');
type SingleWorkflowFieldDndSourceData = DndData<
typeof _singleWorkflowField.type,
typeof _singleWorkflowField.key,
{ fieldIdentifier: FieldIdentifier }
>;
export const singleWorkflowFieldDndSource: DndSource<SingleWorkflowFieldDndSourceData> = {
..._singleWorkflowField,
typeGuard: buildTypeGuard(_singleWorkflowField.key),
getData: buildGetData(_singleWorkflowField.key, _singleWorkflowField.type),
};
type DndTarget<TargetData extends DndData, SourceData extends DndData> = {
key: symbol;
type: TargetData['type'];

View File

@@ -1,9 +1,9 @@
import { Flex, IconButton, Input, Text } from '@invoke-ai/ui-library';
import { useBoolean } from 'common/hooks/useBoolean';
import { useEditable } from 'common/hooks/useEditable';
import { withResultAsync } from 'common/util/result';
import { toast } from 'features/toast/toast';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPencilBold } from 'react-icons/pi';
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
@@ -16,85 +16,54 @@ type Props = {
export const BoardEditableTitle = memo(({ board, isSelected }: Props) => {
const { t } = useTranslation();
const isEditing = useBoolean(false);
const [isHovering, setIsHovering] = useState(false);
const [localTitle, setLocalTitle] = useState(board.board_name);
const ref = useRef<HTMLInputElement>(null);
const isHovering = useBoolean(false);
const inputRef = useRef<HTMLInputElement>(null);
const [updateBoard, updateBoardResult] = useUpdateBoardMutation();
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setLocalTitle(e.target.value);
}, []);
const onEdit = useCallback(() => {
isEditing.setTrue();
setIsHovering(false);
}, [isEditing]);
const onBlur = useCallback(async () => {
const trimmedTitle = localTitle.trim();
isEditing.setFalse();
if (trimmedTitle.length === 0) {
setLocalTitle(board.board_name);
} else if (trimmedTitle !== board.board_name) {
setLocalTitle(trimmedTitle);
const onChange = useCallback(
async (board_name: string) => {
const result = await withResultAsync(() =>
updateBoard({ board_id: board.board_id, changes: { board_name: trimmedTitle } }).unwrap()
updateBoard({ board_id: board.board_id, changes: { board_name } }).unwrap()
);
if (result.isErr()) {
setLocalTitle(board.board_name);
toast({
status: 'error',
title: t('boards.updateBoardError'),
});
} else {
setLocalTitle(result.value.board_name);
}
}
}, [board.board_id, board.board_name, isEditing, localTitle, updateBoard, t]);
const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onBlur();
} else if (e.key === 'Escape') {
setLocalTitle(board.board_name);
isEditing.setFalse();
}
},
[board.board_name, isEditing, onBlur]
[board.board_id, t, updateBoard]
);
const handleMouseOver = useCallback(() => {
setIsHovering(true);
}, []);
const editable = useEditable({
value: board.board_name,
defaultValue: board.board_name,
onChange,
inputRef,
onStartEditing: isHovering.setTrue,
});
const handleMouseOut = useCallback(() => {
setIsHovering(false);
}, []);
useEffect(() => {
if (isEditing.isTrue) {
ref.current?.focus();
ref.current?.select();
}
}, [isEditing.isTrue]);
if (!isEditing.isTrue) {
if (!editable.isEditing) {
return (
<Flex alignItems="center" gap={3} onMouseOver={handleMouseOver} onMouseOut={handleMouseOut}>
<Flex alignItems="center" gap={3} onMouseOver={isHovering.setTrue} onMouseOut={isHovering.setFalse}>
<Text
size="sm"
fontWeight="semibold"
userSelect="none"
color={isSelected ? 'base.100' : 'base.300'}
onDoubleClick={onEdit}
onDoubleClick={editable.startEditing}
cursor="text"
>
{localTitle}
{editable.value}
</Text>
{isHovering && (
<IconButton aria-label="edit name" icon={<PiPencilBold />} size="sm" variant="ghost" onClick={onEdit} />
<IconButton
aria-label="edit name"
icon={<PiPencilBold />}
size="sm"
variant="ghost"
onClick={editable.startEditing}
/>
)}
</Flex>
);
@@ -102,11 +71,8 @@ export const BoardEditableTitle = memo(({ board, isSelected }: Props) => {
return (
<Input
ref={ref}
value={localTitle}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
ref={inputRef}
{...editable.inputProps}
variant="outline"
isDisabled={updateBoardResult.isLoading}
_focusVisible={{ borderWidth: 1, borderColor: 'invokeBlueAlpha.400', borderRadius: 'base' }}

View File

@@ -8,16 +8,12 @@ import { PiCopyBold } from 'react-icons/pi';
export const ImageMenuItemCopy = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const { isClipboardAPIAvailable, copyImageToClipboard } = useCopyImageToClipboard();
const copyImageToClipboard = useCopyImageToClipboard();
const onClick = useCallback(() => {
copyImageToClipboard(imageDTO.image_url);
}, [copyImageToClipboard, imageDTO.image_url]);
if (!isClipboardAPIAvailable) {
return null;
}
return (
<IconMenuItem
icon={<PiCopyBold />}

View File

@@ -1,5 +1,6 @@
import { Box, Flex, IconButton, Tooltip, useShiftModifier } from '@invoke-ai/ui-library';
import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { useClipboard } from 'common/hooks/useClipboard';
import { Formatter } from 'fracturedjsonjs';
import { isString } from 'lodash-es';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
@@ -25,9 +26,10 @@ const DataViewer = (props: Props) => {
const { label, data, fileName, withDownload = true, withCopy = true, extraCopyActions } = props;
const dataString = useMemo(() => (isString(data) ? data : formatter.Serialize(data)) ?? '', [data]);
const shift = useShiftModifier();
const clipboard = useClipboard();
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(dataString);
}, [dataString]);
clipboard.writeText(dataString);
}, [clipboard, dataString]);
const handleDownload = useCallback(() => {
const blob = new Blob([dataString]);
@@ -94,9 +96,10 @@ type ExtraCopyActionProps = {
};
const ExtraCopyAction = ({ label, data, getData }: ExtraCopyActionProps) => {
const { t } = useTranslation();
const clipboard = useClipboard();
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(JSON.stringify(getData(data), null, 2));
}, [data, getData]);
clipboard.writeText(JSON.stringify(getData(data), null, 2));
}, [clipboard, data, getData]);
return (
<Tooltip label={`${t('gallery.copy')} ${label} JSON`}>

View File

@@ -21,7 +21,7 @@ import type { ImageDTO } from 'services/api/types';
*
* We can hack around this, thanks to the fact that the image viewer is always opened on the first app startup. By the
* time the user closes it, the resizable panels library has already done its one extra resize and the DOM layout has
* stablized. So we can track the first time the image viewer is closed and fit the layers to the stage at that time,
* stabilized. So we can track the first time the image viewer is closed and fit the layers to the stage at that time,
* ensuring that the bbox is centered in the canvas stage on that first app startup.
*
* TODO(psyche): Figure out a better way to do handle this...

View File

@@ -152,7 +152,7 @@ type UseGalleryNavigationReturn = {
/**
* Provides access to the gallery navigation via arrow keys.
* Also provides information about the current image's position in the gallery,
* useful for determining whether to load more images or display navigatin
* useful for determining whether to load more images or display navigation
* buttons.
*/
export const useGalleryNavigation = (): UseGalleryNavigationReturn => {

View File

@@ -1,5 +1,3 @@
import 'reactflow/dist/style.css';
import { Flex } from '@invoke-ai/ui-library';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { useFocusRegion } from 'common/hooks/focus';

View File

@@ -12,6 +12,7 @@ import {
Text,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import type { EdgeChange, NodeChange } from '@xyflow/react';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
@@ -31,6 +32,7 @@ import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupied
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { toast } from 'features/toast/toast';
@@ -41,8 +43,8 @@ import type { ChangeEvent } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCircuitryBold, PiFlaskBold, PiHammerBold, PiLightningFill } from 'react-icons/pi';
import type { EdgeChange, NodeChange } from 'reactflow';
import type { S } from 'services/api/types';
import { objectEntries } from 'tsafe';
const useThrottle = <T,>(value: T, limit: number) => {
const [throttledValue, setThrottledValue] = useState(value);
@@ -95,8 +97,8 @@ const useAddNode = () => {
node.selected = true;
// Deselect all other nodes and edges
const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
const edgeChanges: EdgeChange[] = [];
const nodeChanges: NodeChange<AnyNode>[] = [{ type: 'add', item: node }];
const edgeChanges: EdgeChange<AnyEdge>[] = [];
nodes.forEach(({ id, selected }) => {
if (selected) {
nodeChanges.push({ type: 'select', id, selected: false });
@@ -381,11 +383,11 @@ const NodeCommandList = memo(({ searchTerm, onSelect }: { searchTerm: string; on
if (filter(template, searchTerm)) {
const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
for (const field of Object.values(candidateFields)) {
for (const [_fieldName, fieldTemplate] of objectEntries(candidateFields)) {
const sourceType =
pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
pendingConnection.handleType === 'source' ? pendingConnection.fieldTemplate.type : fieldTemplate.type;
const targetType =
pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
pendingConnection.handleType === 'target' ? pendingConnection.fieldTemplate.type : fieldTemplate.type;
if (validateConnectionTypes(sourceType, targetType)) {
_items.push({

View File

@@ -1,11 +1,25 @@
import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import type {
EdgeChange,
HandleType,
NodeChange,
OnEdgesChange,
OnInit,
OnMoveEnd,
OnNodesChange,
OnReconnect,
ProOptions,
ReactFlowProps,
ReactFlowState,
} from '@xyflow/react';
import { Background, ReactFlow, useStore as useReactFlowStore, useUpdateNodeInternals } from '@xyflow/react';
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
import { useConnection } from 'features/nodes/hooks/useConnection';
import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste';
import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
import { useNodeCopyPaste } from 'features/nodes/hooks/useNodeCopyPaste';
import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
import {
$addNodeCmdk,
@@ -30,23 +44,11 @@ import {
} from 'features/nodes/store/selectors';
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
import { selectSelectionMode, selectShouldSnapToGrid } from 'features/nodes/store/workflowSettingsSlice';
import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import type { CSSProperties, MouseEvent } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import type {
EdgeChange,
NodeChange,
OnEdgesChange,
OnEdgeUpdateFunc,
OnInit,
OnMoveEnd,
OnNodesChange,
ProOptions,
ReactFlowProps,
ReactFlowState,
} from 'reactflow';
import { Background, ReactFlow, useStore as useReactFlowStore, useUpdateNodeInternals } from 'reactflow';
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
@@ -58,13 +60,13 @@ import NotesNode from './nodes/Notes/NotesNode';
const edgeTypes = {
collapsed: InvocationCollapsedEdge,
default: InvocationDefaultEdge,
};
} as const;
const nodeTypes = {
invocation: InvocationNodeWrapper,
current_image: CurrentImageNode,
notes: NotesNode,
};
} as const;
// TODO: can we support reactflow? if not, we could style the attribution so it matches the app
const proOptions: ProOptions = { hideAttribution: true };
@@ -97,7 +99,7 @@ export const Flow = memo(() => {
const [borderRadius] = useToken('radii', ['base']);
const flowStyles = useMemo<CSSProperties>(() => ({ borderRadius }), [borderRadius]);
const onNodesChange: OnNodesChange = useCallback(
const onNodesChange: OnNodesChange<AnyNode> = useCallback(
(nodeChanges) => {
dispatch(nodesChanged(nodeChanges));
const flow = $flow.get();
@@ -112,7 +114,7 @@ export const Flow = memo(() => {
[dispatch, needsFit]
);
const onEdgesChange: OnEdgesChange = useCallback(
const onEdgesChange: OnEdgesChange<AnyEdge> = useCallback(
(changes) => {
if (changes.length > 0) {
dispatch(edgesChanged(changes));
@@ -130,7 +132,7 @@ export const Flow = memo(() => {
onCloseGlobal();
}, [onCloseGlobal]);
const onInit: OnInit = useCallback((flow) => {
const onInit: OnInit<AnyNode, AnyEdge> = useCallback((flow) => {
$flow.set(flow);
flow.fitView();
}, []);
@@ -158,13 +160,13 @@ export const Flow = memo(() => {
* where the edge is deleted if you click it accidentally).
*/
const onEdgeUpdateStart: NonNullable<ReactFlowProps['onEdgeUpdateStart']> = useCallback((e, edge, _handleType) => {
const onReconnectStart = useCallback((event: MouseEvent, edge: AnyEdge, _handleType: HandleType) => {
$edgePendingUpdate.set(edge);
$didUpdateEdge.set(false);
$lastEdgeUpdateMouseEvent.set(e);
$lastEdgeUpdateMouseEvent.set(event);
}, []);
const onEdgeUpdate: OnEdgeUpdateFunc = useCallback(
const onReconnect: OnReconnect = useCallback(
(oldEdge, newConnection) => {
// This event is fired when an edge update is successful
$didUpdateEdge.set(true);
@@ -183,7 +185,7 @@ export const Flow = memo(() => {
[dispatch, updateNodeInternals]
);
const onEdgeUpdateEnd: NonNullable<ReactFlowProps['onEdgeUpdateEnd']> = useCallback(
const onReconnectEnd: NonNullable<ReactFlowProps['onReconnectEnd']> = useCallback(
(e, edge, _handleType) => {
const didUpdateEdge = $didUpdateEdge.get();
// Fall back to a reasonable default event
@@ -208,7 +210,7 @@ export const Flow = memo(() => {
// #endregion
const { copySelection, pasteSelection, pasteSelectionWithEdges } = useCopyPaste();
const { copySelection, pasteSelection, pasteSelectionWithEdges } = useNodeCopyPaste();
useRegisteredHotkeys({
id: 'copySelection',
@@ -220,8 +222,8 @@ export const Flow = memo(() => {
const selectAll = useCallback(() => {
const { nodes, edges } = selectNodesSlice(store.getState());
const nodeChanges: NodeChange[] = [];
const edgeChanges: EdgeChange[] = [];
const nodeChanges: NodeChange<AnyNode>[] = [];
const edgeChanges: EdgeChange<AnyEdge>[] = [];
nodes.forEach(({ id, selected }) => {
if (!selected) {
nodeChanges.push({ type: 'select', id, selected: true });
@@ -294,8 +296,8 @@ export const Flow = memo(() => {
const deleteSelection = useCallback(() => {
const { nodes, edges } = selectNodesSlice(store.getState());
const nodeChanges: NodeChange[] = [];
const edgeChanges: EdgeChange[] = [];
const nodeChanges: NodeChange<AnyNode>[] = [];
const edgeChanges: EdgeChange<AnyEdge>[] = [];
nodes
.filter((n) => n.selected)
.forEach(({ id }) => {
@@ -322,7 +324,7 @@ export const Flow = memo(() => {
});
return (
<ReactFlow
<ReactFlow<AnyNode, AnyEdge>
id="workflow-editor"
ref={flowWrapper}
defaultViewport={viewport}
@@ -334,9 +336,9 @@ export const Flow = memo(() => {
onMouseMove={onMouseMove}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onEdgeUpdate={onEdgeUpdate}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onConnectStart={onConnectStart}
onConnect={onConnect}
onConnectEnd={onConnectEnd}

View File

@@ -1,4 +1,6 @@
import { useStore } from '@nanostores/react';
import type { ConnectionLineComponentProps } from '@xyflow/react';
import { getBezierPath } from '@xyflow/react';
import { useAppSelector } from 'app/store/storeHooks';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
@@ -6,8 +8,6 @@ import { $pendingConnection } from 'features/nodes/store/nodesSlice';
import { selectShouldAnimateEdges, selectShouldColorEdges } from 'features/nodes/store/workflowSettingsSlice';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import type { ConnectionLineComponentProps } from 'reactflow';
import { getBezierPath } from 'reactflow';
const pathStyles: CSSProperties = { opacity: 0.8 };

View File

@@ -1,13 +1,39 @@
import { Badge, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Badge, Box, chakra } from '@invoke-ai/ui-library';
import type { EdgeProps } from '@xyflow/react';
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@xyflow/react';
import { useAppSelector } from 'app/store/storeHooks';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { getEdgeStyles } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import { makeEdgeSelector } from 'features/nodes/components/flow/edges/util/makeEdgeSelector';
import { $templates } from 'features/nodes/store/nodesSlice';
import { buildSelectAreConnectedNodesSelected } from 'features/nodes/components/flow/edges/util/buildEdgeSelectors';
import { selectShouldAnimateEdges } from 'features/nodes/store/workflowSettingsSlice';
import type { CollapsedInvocationNodeEdge } from 'features/nodes/types/invocation';
import { memo, useMemo } from 'react';
import type { EdgeProps } from 'reactflow';
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow';
const ChakraBaseEdge = chakra(BaseEdge);
const baseEdgeSx: SystemStyleObject = {
strokeWidth: '3px !important',
stroke: 'base.500 !important',
opacity: '0.5 !important',
strokeDasharray: 'none',
'&[data-selected="true"]': {
opacity: '1 !important',
},
'&[data-selected="true"], &[data-are-connected-nodes-selected="true"]': {
strokeDasharray: '5 !important',
},
'&[data-should-animate-edges="true"]': {
animation: 'dashdraw 0.5s linear infinite !important',
},
};
const badgeSx: SystemStyleObject = {
bg: 'base.500',
opacity: 0.5,
shadow: 'base',
'&[data-selected="true"]': {
opacity: 1,
},
};
const InvocationCollapsedEdge = ({
sourceX,
@@ -20,17 +46,15 @@ const InvocationCollapsedEdge = ({
data,
selected = false,
source,
sourceHandleId,
target,
targetHandleId,
}: EdgeProps<{ count: number }>) => {
const templates = useStore($templates);
const selector = useMemo(
() => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId),
[templates, source, sourceHandleId, target, targetHandleId]
}: EdgeProps<CollapsedInvocationNodeEdge>) => {
const shouldAnimateEdges = useAppSelector(selectShouldAnimateEdges);
const selectAreConnectedNodesSelected = useMemo(
() => buildSelectAreConnectedNodesSelected(source, target),
[source, target]
);
const { shouldAnimateEdges, areConnectedNodesSelected } = useAppSelector(selector);
const areConnectedNodesSelected = useAppSelector(selectAreConnectedNodesSelected);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
@@ -41,31 +65,29 @@ const InvocationCollapsedEdge = ({
targetPosition,
});
const { base500 } = useChakraThemeTokens();
const edgeStyles = useMemo(
() => getEdgeStyles(base500, selected, shouldAnimateEdges, areConnectedNodesSelected),
[areConnectedNodesSelected, base500, selected, shouldAnimateEdges]
);
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={edgeStyles} />
{data?.count && data.count > 1 && (
<ChakraBaseEdge
path={edgePath}
markerEnd={markerEnd}
sx={baseEdgeSx}
data-selected={selected}
data-are-connected-nodes-selected={areConnectedNodesSelected}
data-should-animate-edges={shouldAnimateEdges}
/>
{data?.count !== undefined && (
<EdgeLabelRenderer>
<Flex
data-testid="asdfasdfasdf"
<Box
position="absolute"
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
className="nodrag nopan"
// Unfortunately edge labels do not get the same zIndex treatment as edges do, so we need to manage this ourselves
className="edge-label-renderer__custom-edge nodrag nopan" // Unfortunately edge labels do not get the same zIndex treatment as edges do, so we need to manage this ourselves
// See: https://github.com/xyflow/xyflow/issues/3658
zIndex={1001}
>
<Badge variant="solid" bg="base.500" opacity={selected ? 0.8 : 0.5} boxShadow="base">
<Badge variant="solid" sx={badgeSx} data-selected={selected}>
{data.count}
</Badge>
</Flex>
</Box>
</EdgeLabelRenderer>
)}
</>

View File

@@ -1,14 +1,61 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { chakra, Flex, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import type { EdgeProps } from '@xyflow/react';
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@xyflow/react';
import { useAppSelector } from 'app/store/storeHooks';
import { getEdgeStyles } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectShouldShowEdgeLabels } from 'features/nodes/store/workflowSettingsSlice';
import { selectShouldAnimateEdges, selectShouldShowEdgeLabels } from 'features/nodes/store/workflowSettingsSlice';
import type { DefaultInvocationNodeEdge } from 'features/nodes/types/invocation';
import { memo, useMemo } from 'react';
import type { EdgeProps } from 'reactflow';
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow';
import { makeEdgeSelector } from './util/makeEdgeSelector';
import {
buildSelectAreConnectedNodesSelected,
buildSelectEdgeColor,
buildSelectEdgeLabel,
} from './util/buildEdgeSelectors';
const ChakraBaseEdge = chakra(BaseEdge);
const baseEdgeSx: SystemStyleObject = {
strokeWidth: '3px !important',
opacity: '0.5 !important',
strokeDasharray: 'none',
'&[data-selected="true"]': {
opacity: '1 !important',
},
'&[data-should-animate-edges="true"]': {
animation: 'dashdraw 0.5s linear infinite !important',
'&[data-selected="true"], &[data-are-connected-nodes-selected="true"]': {
strokeDasharray: '5 !important',
},
},
};
const edgeLabelWrapperSx: SystemStyleObject = {
pointerEvents: 'all',
position: 'absolute',
bg: 'base.800',
borderRadius: 'base',
borderWidth: 1,
opacity: 0.5,
borderColor: 'transparent',
py: 1,
px: 3,
shadow: 'md',
'&[data-selected="true"]': {
opacity: 1,
borderColor: undefined,
},
};
const edgeLabelTextSx: SystemStyleObject = {
fontWeight: 'semibold',
color: 'base.300',
'&[data-selected="true"]': {
color: 'base.100',
},
};
const InvocationDefaultEdge = ({
sourceX,
@@ -23,15 +70,26 @@ const InvocationDefaultEdge = ({
target,
sourceHandleId,
targetHandleId,
}: EdgeProps) => {
}: EdgeProps<DefaultInvocationNodeEdge>) => {
const templates = useStore($templates);
const selector = useMemo(
() => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId),
const shouldAnimateEdges = useAppSelector(selectShouldAnimateEdges);
const shouldShowEdgeLabels = useAppSelector(selectShouldShowEdgeLabels);
const selectAreConnectedNodesSelected = useMemo(
() => buildSelectAreConnectedNodesSelected(source, target),
[source, target]
);
const selectStrokeColor = useMemo(
() => buildSelectEdgeColor(templates, source, sourceHandleId, target, targetHandleId),
[templates, source, sourceHandleId, target, targetHandleId]
);
const { shouldAnimateEdges, areConnectedNodesSelected, stroke, label } = useAppSelector(selector);
const shouldShowEdgeLabels = useAppSelector(selectShouldShowEdgeLabels);
const selectEdgeLabel = useMemo(
() => buildSelectEdgeLabel(templates, source, sourceHandleId, target, targetHandleId),
[templates, source, sourceHandleId, target, targetHandleId]
);
const areConnectedNodesSelected = useAppSelector(selectAreConnectedNodesSelected);
const stroke = useAppSelector(selectStrokeColor);
const label = useAppSelector(selectEdgeLabel);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
@@ -42,31 +100,26 @@ const InvocationDefaultEdge = ({
targetPosition,
});
const edgeStyles = useMemo(
() => getEdgeStyles(stroke, selected, shouldAnimateEdges, areConnectedNodesSelected),
[areConnectedNodesSelected, stroke, selected, shouldAnimateEdges]
);
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={edgeStyles} />
<ChakraBaseEdge
path={edgePath}
markerEnd={markerEnd}
sx={baseEdgeSx}
stroke={`${stroke} !important`}
data-selected={selected}
data-are-connected-nodes-selected={areConnectedNodesSelected}
data-should-animate-edges={shouldAnimateEdges}
/>
{label && shouldShowEdgeLabels && (
<EdgeLabelRenderer>
<Flex
className="nodrag nopan"
pointerEvents="all"
position="absolute"
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
bg="base.800"
borderRadius="base"
borderWidth={1}
borderColor={selected ? 'undefined' : 'transparent'}
opacity={selected ? 1 : 0.5}
py={1}
px={3}
shadow="md"
data-selected={selected}
sx={edgeLabelWrapperSx}
>
<Text size="sm" fontWeight="semibold" color={selected ? 'base.100' : 'base.300'}>
<Text size="sm" sx={edgeLabelTextSx} data-selected={selected}>
{label}
</Text>
</Flex>

View File

@@ -0,0 +1,62 @@
import { createSelector } from '@reduxjs/toolkit';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { Templates } from 'features/nodes/store/types';
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { getFieldColor } from './getEdgeColor';
export const buildSelectAreConnectedNodesSelected = (source: string, target: string) =>
createSelector(selectNodesSlice, (nodes): boolean => {
const sourceNode = nodes.nodes.find((node) => node.id === source);
const targetNode = nodes.nodes.find((node) => node.id === target);
return Boolean(sourceNode?.selected || targetNode?.selected);
});
export const buildSelectEdgeColor = (
templates: Templates,
source: string,
sourceHandleId: string | null | undefined,
target: string,
targetHandleId: string | null | undefined
) =>
createSelector(selectNodesSlice, selectWorkflowSettingsSlice, (nodes, workflowSettings): string => {
const { shouldColorEdges } = workflowSettings;
const sourceNode = nodes.nodes.find((node) => node.id === source);
const targetNode = nodes.nodes.find((node) => node.id === target);
if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) {
return colorTokenToCssVar('base.500');
}
const sourceNodeTemplate = templates[sourceNode.data.type];
const isInvocationToInvocationEdge = isInvocationNode(sourceNode) && isInvocationNode(targetNode);
const outputFieldTemplate = sourceNodeTemplate?.outputs[sourceHandleId];
const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined;
return sourceType && shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
});
export const buildSelectEdgeLabel = (
templates: Templates,
source: string,
sourceHandleId: string | null | undefined,
target: string,
targetHandleId: string | null | undefined
) =>
createSelector(selectNodesSlice, (nodes): string | null => {
const sourceNode = nodes.nodes.find((node) => node.id === source);
const targetNode = nodes.nodes.find((node) => node.id === target);
if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) {
return null;
}
const sourceNodeTemplate = templates[sourceNode.data.type];
const targetNodeTemplate = templates[targetNode.data.type];
return `${sourceNodeTemplate?.title || sourceNode.data?.label} -> ${targetNodeTemplate?.title || targetNode.data?.label}`;
});

View File

@@ -1,7 +1,6 @@
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { FIELD_COLORS } from 'features/nodes/types/constants';
import type { FieldType } from 'features/nodes/types/field';
import type { CSSProperties } from 'react';
export const getFieldColor = (fieldType: FieldType | null): string => {
if (!fieldType) {
@@ -11,16 +10,3 @@ export const getFieldColor = (fieldType: FieldType | null): string => {
return color ? colorTokenToCssVar(color) : colorTokenToCssVar('base.500');
};
export const getEdgeStyles = (
stroke: string,
selected: boolean,
shouldAnimateEdges: boolean,
areConnectedNodesSelected: boolean
): CSSProperties => ({
strokeWidth: 3,
stroke,
opacity: selected ? 1 : 0.5,
animation: shouldAnimateEdges ? 'dashdraw 0.5s linear infinite' : undefined,
strokeDasharray: selected || areConnectedNodesSelected ? 5 : 'none',
});

View File

@@ -1,58 +0,0 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { deepClone } from 'common/util/deepClone';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { Templates } from 'features/nodes/store/types';
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { getFieldColor } from './getEdgeColor';
const defaultReturnValue = {
areConnectedNodesSelected: false,
shouldAnimateEdges: false,
stroke: colorTokenToCssVar('base.500'),
label: '',
};
export const makeEdgeSelector = (
templates: Templates,
source: string,
sourceHandleId: string | null | undefined,
target: string,
targetHandleId: string | null | undefined
) =>
createMemoizedSelector(
selectNodesSlice,
selectWorkflowSettingsSlice,
(
nodes,
workflowSettings
): { areConnectedNodesSelected: boolean; shouldAnimateEdges: boolean; stroke: string; label: string } => {
const { shouldAnimateEdges, shouldColorEdges } = workflowSettings;
const sourceNode = nodes.nodes.find((node) => node.id === source);
const targetNode = nodes.nodes.find((node) => node.id === target);
const returnValue = deepClone(defaultReturnValue);
returnValue.shouldAnimateEdges = shouldAnimateEdges;
const isInvocationToInvocationEdge = isInvocationNode(sourceNode) && isInvocationNode(targetNode);
returnValue.areConnectedNodesSelected = Boolean(sourceNode?.selected || targetNode?.selected);
if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) {
return returnValue;
}
const sourceNodeTemplate = templates[sourceNode.data.type];
const targetNodeTemplate = templates[targetNode.data.type];
const outputFieldTemplate = sourceNodeTemplate?.outputs[sourceHandleId];
const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined;
returnValue.stroke = sourceType && shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
returnValue.label = `${sourceNodeTemplate?.title || sourceNode.data?.label} -> ${targetNodeTemplate?.title || targetNode.data?.label}`;
return returnValue;
}
);

View File

@@ -1,5 +1,6 @@
import { Flex, Image, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import type { NodeProps } from '@xyflow/react';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { DndImage } from 'features/dnd/DndImage';
@@ -12,7 +13,6 @@ import { motion } from 'framer-motion';
import type { CSSProperties, PropsWithChildren } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { NodeProps } from 'reactflow';
import { $lastProgressEvent } from 'services/events/stores';
const CurrentImageNode = (props: NodeProps) => {

View File

@@ -1,13 +1,14 @@
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
import { useFieldNames } from 'features/nodes/hooks/useFieldNames';
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { OutputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate';
import { OutputFieldNodesEditorView } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldNodesEditorView';
import { useInputFieldNamesByStatus } from 'features/nodes/hooks/useInputFieldNamesByStatus';
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
import { memo } from 'react';
import InputField from './fields/InputField';
import OutputField from './fields/OutputField';
import { InputFieldEditModeNodes } from './fields/InputFieldEditModeNodes';
import InvocationNodeFooter from './InvocationNodeFooter';
import InvocationNodeHeader from './InvocationNodeHeader';
@@ -20,7 +21,7 @@ type Props = {
};
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
const fieldNames = useFieldNames(nodeId);
const fieldNames = useInputFieldNamesByStatus(nodeId);
const withFooter = useWithFooter(nodeId);
const outputFieldNames = useOutputFieldNames(nodeId);
@@ -42,34 +43,28 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
<Grid gridTemplateColumns="1fr auto" gridAutoRows="1fr">
{fieldNames.connectionFields.map((fieldName, i) => (
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
<InputField nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck>
<InputFieldGate nodeId={nodeId} fieldName={fieldName}>
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
</GridItem>
))}
{outputFieldNames.map((fieldName, i) => (
<GridItem gridColumnStart={2} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.output-field`}>
<OutputField nodeId={nodeId} fieldName={fieldName} />
<OutputFieldGate nodeId={nodeId} fieldName={fieldName}>
<OutputFieldNodesEditorView nodeId={nodeId} fieldName={fieldName} />
</OutputFieldGate>
</GridItem>
))}
</Grid>
{fieldNames.anyOrDirectFields.map((fieldName) => (
<InvocationInputFieldCheck
key={`${nodeId}.${fieldName}.input-field`}
nodeId={nodeId}
fieldName={fieldName}
>
<InputField nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck>
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
))}
{fieldNames.missingFields.map((fieldName) => (
<InvocationInputFieldCheck
key={`${nodeId}.${fieldName}.input-field`}
nodeId={nodeId}
fieldName={fieldName}
>
<InputField nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck>
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
))}
</Flex>
</Flex>

View File

@@ -1,40 +1,25 @@
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { Handle, Position } from '@xyflow/react';
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { map } from 'lodash-es';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import { Handle, Position } from 'reactflow';
import { memo } from 'react';
interface Props {
nodeId: string;
}
const hiddenHandleStyles: CSSProperties = { visibility: 'hidden' };
const collapsedHandleStyles: CSSProperties = {
borderWidth: 0,
borderRadius: '3px',
width: '1rem',
height: '1rem',
backgroundColor: 'var(--invoke-colors-base-600)',
zIndex: -1,
};
const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
const template = useNodeTemplate(nodeId);
const { base600 } = useChakraThemeTokens();
const dummyHandleStyles: CSSProperties = useMemo(
() => ({
borderWidth: 0,
borderRadius: '3px',
width: '1rem',
height: '1rem',
backgroundColor: base600,
zIndex: -1,
}),
[base600]
);
const collapsedTargetStyles: CSSProperties = useMemo(
() => ({ ...dummyHandleStyles, left: '-0.5rem' }),
[dummyHandleStyles]
);
const collapsedSourceStyles: CSSProperties = useMemo(
() => ({ ...dummyHandleStyles, right: '-0.5rem' }),
[dummyHandleStyles]
);
if (!template) {
return null;
@@ -47,7 +32,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={`${nodeId}-collapsed-target`}
isConnectable={false}
position={Position.Left}
style={collapsedTargetStyles}
style={collapsedHandleStyles}
/>
{map(template.inputs, (input) => (
<Handle
@@ -64,7 +49,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={`${nodeId}-collapsed-source`}
isConnectable={false}
position={Position.Right}
style={collapsedSourceStyles}
style={collapsedHandleStyles}
/>
{map(template.outputs, (output) => (
<Handle

View File

@@ -1,6 +1,6 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
import { useNodeHasImageOutput } from 'features/nodes/hooks/useNodeHasImageOutput';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
@@ -15,7 +15,7 @@ type Props = {
const props: ChakraProps = { w: 'unset' };
const InvocationNodeFooter = ({ nodeId }: Props) => {
const hasImageOutput = useHasImageOutput(nodeId);
const hasImageOutput = useNodeHasImageOutput(nodeId);
const isCacheEnabled = useFeatureStatus('invocationCache');
return (
<Flex

View File

@@ -5,7 +5,7 @@ import InvocationNodeClassificationIcon from 'features/nodes/components/flow/nod
import { memo } from 'react';
import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles';
import InvocationNodeInfoIcon from './InvocationNodeInfoIcon';
import { InvocationNodeInfoIcon } from './InvocationNodeInfoIcon';
import InvocationNodeStatusIndicator from './InvocationNodeStatusIndicator';
type Props = {

View File

@@ -1,9 +1,10 @@
import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
import { compare } from 'compare-versions';
import { useNode } from 'features/nodes/hooks/useNode';
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
import { useInvocationNodeNotes } from 'features/nodes/hooks/useNodeNotes';
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useNodeVersion } from 'features/nodes/hooks/useNodeVersion';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiInfoBold } from 'react-icons/pi';
@@ -12,7 +13,7 @@ interface Props {
nodeId: string;
}
const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
export const InvocationNodeInfoIcon = memo(({ nodeId }: Props) => {
const needsUpdate = useNodeNeedsUpdate(nodeId);
return (
@@ -20,96 +21,66 @@ const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
<Icon as={PiInfoBold} display="block" boxSize={4} w={8} color={needsUpdate ? 'error.400' : 'base.400'} />
</Tooltip>
);
};
});
export default memo(InvocationNodeInfoIcon);
InvocationNodeInfoIcon.displayName = 'InvocationNodeInfoIcon';
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
const node = useNode(nodeId);
const notes = useInvocationNodeNotes(nodeId);
const label = useNodeLabel(nodeId);
const version = useNodeVersion(nodeId);
const nodeTemplate = useNodeTemplate(nodeId);
const { t } = useTranslation();
const title = useMemo(() => {
if (node.data?.label && nodeTemplate?.title) {
return `${node.data.label} (${nodeTemplate.title})`;
if (label) {
return `${label} (${nodeTemplate.title})`;
}
if (node.data?.label && !nodeTemplate) {
return node.data.label;
}
if (!node.data?.label && nodeTemplate) {
return nodeTemplate.title;
}
return t('nodes.unknownNode');
}, [node.data.label, nodeTemplate, t]);
const versionComponent = useMemo(() => {
if (!isInvocationNode(node) || !nodeTemplate) {
return null;
}
if (!node.data.version) {
return (
<Text as="span" color="error.500">
{t('nodes.versionUnknown')}
</Text>
);
}
if (!nodeTemplate.version) {
return (
<Text as="span" color="error.500">
{t('nodes.version')} {node.data.version} ({t('nodes.unknownTemplate')})
</Text>
);
}
if (compare(node.data.version, nodeTemplate.version, '<')) {
return (
<Text as="span" color="error.500">
{t('nodes.version')} {node.data.version} ({t('nodes.updateNode')})
</Text>
);
}
if (compare(node.data.version, nodeTemplate.version, '>')) {
return (
<Text as="span" color="error.500">
{t('nodes.version')} {node.data.version} ({t('nodes.updateApp')})
</Text>
);
}
return (
<Text as="span">
{t('nodes.version')} {node.data.version}
</Text>
);
}, [node, nodeTemplate, t]);
if (!isInvocationNode(node)) {
return <Text fontWeight="semibold">{t('nodes.unknownNode')}</Text>;
}
return nodeTemplate.title;
}, [label, nodeTemplate.title]);
return (
<Flex flexDir="column">
<Text as="span" fontWeight="semibold">
{title}
</Text>
{nodeTemplate?.nodePack && (
<Text opacity={0.7}>
{t('nodes.nodePack')}: {nodeTemplate.nodePack}
</Text>
)}
<Text opacity={0.7} fontStyle="oblique 5deg">
{nodeTemplate?.description}
<Text opacity={0.7}>
{t('nodes.nodePack')}: {nodeTemplate.nodePack}
</Text>
{versionComponent}
{node.data?.notes && <Text>{node.data.notes}</Text>}
<Text opacity={0.7} fontStyle="oblique 5deg">
{nodeTemplate.description}
</Text>
<Version nodeVersion={version} templateVersion={nodeTemplate.version} />
{notes && <Text>{notes}</Text>}
</Flex>
);
});
TooltipContent.displayName = 'TooltipContent';
const Version = ({ nodeVersion, templateVersion }: { nodeVersion: string; templateVersion: string }) => {
const { t } = useTranslation();
if (compare(nodeVersion, templateVersion, '<')) {
return (
<Text as="span" color="error.500">
{t('nodes.version')} {nodeVersion} ({t('nodes.updateNode')})
</Text>
);
}
if (compare(nodeVersion, templateVersion, '>')) {
return (
<Text as="span" color="error.500">
{t('nodes.version')} {nodeVersion} ({t('nodes.updateApp')})
</Text>
);
}
return (
<Text as="span">
{t('nodes.version')} {nodeVersion}
</Text>
);
};

View File

@@ -1,31 +1,31 @@
import { FormControl, FormLabel, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useNode } from 'features/nodes/hooks/useNode';
import { useInvocationNodeNotes } from 'features/nodes/hooks/useNodeNotes';
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
type Props = {
nodeId: string;
};
export const InvocationNodeNotesTextarea = memo(({ nodeId }: Props) => {
const dispatch = useAppDispatch();
const node = useNode(nodeId);
const { t } = useTranslation();
const notes = useInvocationNodeNotes(nodeId);
const handleNotesChanged = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
},
[dispatch, nodeId]
);
if (!isInvocationNode(node)) {
return null;
}
return (
<FormControl orientation="vertical" h="full">
<FormLabel>{t('nodes.notes')}</FormLabel>
<Textarea value={node.data?.notes} onChange={handleNotesChanged} rows={10} resize="none" />
<Textarea value={notes} onChange={handleNotesChanged} rows={10} resize="none" />
</FormControl>
);
};
});
export default memo(NotesTextarea);
InvocationNodeNotesTextarea.displayName = 'InvocationNodeNotesTextarea';

View File

@@ -1,6 +1,6 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Badge, CircularProgress, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import type { NodeExecutionState } from 'features/nodes/types/invocation';
import { zNodeStatus } from 'features/nodes/types/invocation';
@@ -22,7 +22,7 @@ const circleStyles: SystemStyleObject = {
};
const InvocationNodeStatusIndicator = ({ nodeId }: Props) => {
const nodeExecutionState = useExecutionState(nodeId);
const nodeExecutionState = useNodeExecutionState(nodeId);
if (!nodeExecutionState) {
return null;

View File

@@ -1,16 +1,16 @@
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import type { Node, NodeProps } from '@xyflow/react';
import { useAppSelector } from 'app/store/storeHooks';
import InvocationNode from 'features/nodes/components/flow/nodes/Invocation/InvocationNode';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectNodes } from 'features/nodes/store/selectors';
import type { InvocationNodeData } from 'features/nodes/types/invocation';
import { memo, useMemo } from 'react';
import type { NodeProps } from 'reactflow';
import InvocationNodeUnknownFallback from './InvocationNodeUnknownFallback';
const InvocationNodeWrapper = (props: NodeProps<InvocationNodeData>) => {
const InvocationNodeWrapper = (props: NodeProps<Node<InvocationNodeData>>) => {
const { data, selected } = props;
const { id: nodeId, type, isOpen, label } = data;
const templates = useStore($templates);

View File

@@ -1,7 +1,7 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
import { useIsIntermediate } from 'features/nodes/hooks/useIsIntermediate';
import { useNodeHasImageOutput } from 'features/nodes/hooks/useNodeHasImageOutput';
import { useNodeIsIntermediate } from 'features/nodes/hooks/useNodeIsIntermediate';
import { nodeIsIntermediateChanged } from 'features/nodes/store/nodesSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
@@ -10,8 +10,8 @@ import { useTranslation } from 'react-i18next';
const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const hasImageOutput = useHasImageOutput(nodeId);
const isIntermediate = useIsIntermediate(nodeId);
const hasImageOutput = useNodeHasImageOutput(nodeId);
const isIntermediate = useNodeIsIntermediate(nodeId);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(
@@ -30,7 +30,7 @@ const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
return (
<FormControl className="nopan">
<FormLabel>{t('nodes.saveToGallery')} </FormLabel>
<FormLabel m={0}>{t('nodes.saveToGallery')} </FormLabel>
<Checkbox onChange={handleChange} isChecked={!isIntermediate} />
</FormControl>
);

View File

@@ -23,7 +23,7 @@ const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => {
const { t } = useTranslation();
return (
<FormControl>
<FormLabel>{t('invocationCache.useCache')}</FormLabel>
<FormLabel m={0}>{t('invocationCache.useCache')}</FormLabel>
<Checkbox className="nopan" onChange={handleChange} isChecked={useCache} />
</FormControl>
);

View File

@@ -1,143 +0,0 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import {
Editable,
EditableInput,
EditablePreview,
Flex,
forwardRef,
Tooltip,
useEditableControls,
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import type { MouseEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import FieldTooltipContent from './FieldTooltipContent';
interface Props {
nodeId: string;
fieldName: string;
kind: 'inputs' | 'outputs';
isInvalid?: boolean;
withTooltip?: boolean;
shouldDim?: boolean;
}
const EditableFieldTitle = forwardRef((props: Props, ref) => {
const { nodeId, fieldName, kind, isInvalid = false, withTooltip = false, shouldDim = false } = props;
const label = useFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [localTitle, setLocalTitle] = useState(label || fieldTemplateTitle || t('nodes.unknownField'));
const handleSubmit = useCallback(
(newTitleRaw: string) => {
const newTitle = newTitleRaw.trim();
const finalTitle = newTitle || fieldTemplateTitle || t('nodes.unknownField');
setLocalTitle(finalTitle);
dispatch(fieldLabelChanged({ nodeId, fieldName, label: finalTitle }));
},
[fieldTemplateTitle, dispatch, nodeId, fieldName, t]
);
const handleChange = useCallback((newTitle: string) => {
setLocalTitle(newTitle);
}, []);
useEffect(() => {
// Another component may change the title; sync local title with global state
setLocalTitle(label || fieldTemplateTitle || t('nodes.unknownField'));
}, [label, fieldTemplateTitle, t]);
return (
<Editable
value={localTitle}
onChange={handleChange}
onSubmit={handleSubmit}
as={Flex}
ref={ref}
position="relative"
overflow="hidden"
alignItems="center"
justifyContent="flex-start"
gap={1}
w="full"
>
<Tooltip
label={withTooltip ? <FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="inputs" /> : undefined}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
>
<EditablePreview
fontWeight="semibold"
sx={editablePreviewStyles}
noOfLines={1}
color={isInvalid ? 'error.300' : 'base.300'}
opacity={shouldDim ? 0.5 : 1}
/>
</Tooltip>
<EditableInput className="nodrag" sx={editableInputStyles} />
<EditableControls />
</Editable>
);
});
const editableInputStyles: SystemStyleObject = {
p: 0,
w: 'full',
fontWeight: 'semibold',
color: 'base.100',
_focusVisible: {
p: 0,
textAlign: 'left',
boxShadow: 'none',
},
};
const editablePreviewStyles: SystemStyleObject = {
p: 0,
textAlign: 'left',
_hover: {
fontWeight: 'semibold !important',
},
};
export default memo(EditableFieldTitle);
const EditableControls = memo(() => {
const { isEditing, getEditButtonProps } = useEditableControls();
const handleClick = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
const { onClick } = getEditButtonProps();
if (!onClick) {
return;
}
onClick(e);
e.preventDefault();
},
[getEditButtonProps]
);
if (isEditing) {
return null;
}
return (
<Flex
onClick={handleClick}
position="absolute"
w="min-content"
h="full"
top={0}
insetInlineStart={0}
cursor="text"
/>
);
});
EditableControls.displayName = 'EditableControls';

View File

@@ -1,94 +0,0 @@
import { Tooltip } from '@invoke-ai/ui-library';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import type { ValidationResult } from 'features/nodes/store/util/validateConnection';
import { HANDLE_TOOLTIP_OPEN_DELAY, MODEL_TYPES } from 'features/nodes/types/constants';
import { type FieldInputTemplate, type FieldOutputTemplate, isSingle } from 'features/nodes/types/field';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { HandleType } from 'reactflow';
import { Handle, Position } from 'reactflow';
type FieldHandleProps = {
fieldTemplate: FieldInputTemplate | FieldOutputTemplate;
handleType: HandleType;
isConnectionInProgress: boolean;
isConnectionStartField: boolean;
validationResult: ValidationResult;
};
const FieldHandle = (props: FieldHandleProps) => {
const { fieldTemplate, handleType, isConnectionInProgress, isConnectionStartField, validationResult } = props;
const { t } = useTranslation();
const { name } = fieldTemplate;
const type = fieldTemplate.type;
const fieldTypeName = useFieldTypeName(type);
const styles: CSSProperties = useMemo(() => {
const isModelType = MODEL_TYPES.some((t) => t === type.name);
const color = getFieldColor(type);
const s: CSSProperties = {
backgroundColor: !isSingle(type) ? colorTokenToCssVar('base.900') : color,
position: 'absolute',
width: '1rem',
height: '1rem',
borderWidth: !isSingle(type) ? 4 : 0,
borderStyle: 'solid',
borderColor: color,
borderRadius: isModelType || type.batch ? 4 : '100%',
zIndex: 1,
transformOrigin: 'center',
};
if (type.batch) {
s.transform = 'rotate(45deg) translateX(-0.3rem) translateY(-0.3rem)';
}
if (handleType === 'target') {
s.insetInlineStart = '-1rem';
} else {
s.insetInlineEnd = '-1rem';
}
if (isConnectionInProgress && !isConnectionStartField && !validationResult.isValid) {
s.filter = 'opacity(0.4) grayscale(0.7)';
}
if (isConnectionInProgress && !validationResult.isValid) {
if (isConnectionStartField) {
s.cursor = 'grab';
} else {
s.cursor = 'not-allowed';
}
} else {
s.cursor = 'crosshair';
}
return s;
}, [handleType, isConnectionInProgress, isConnectionStartField, type, validationResult.isValid]);
const tooltip = useMemo(() => {
if (isConnectionInProgress && validationResult.messageTKey) {
return t(validationResult.messageTKey);
}
return fieldTypeName;
}, [fieldTypeName, isConnectionInProgress, t, validationResult.messageTKey]);
return (
<Tooltip
label={tooltip}
placement={handleType === 'target' ? 'start' : 'end'}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
>
<Handle
type={handleType}
id={name}
position={handleType === 'target' ? Position.Left : Position.Right}
style={styles}
/>
</Tooltip>
);
};
export default memo(FieldHandle);

View File

@@ -1,68 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
import {
selectWorkflowSlice,
workflowExposedFieldAdded,
workflowExposedFieldRemoved,
} from 'features/nodes/store/workflowSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiMinusBold, PiPlusBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const value = useFieldValue(nodeId, fieldName);
const selectIsExposed = useMemo(
() =>
createSelector(selectWorkflowSlice, (workflow) => {
return Boolean(workflow.exposedFields.find((f) => f.nodeId === nodeId && f.fieldName === fieldName));
}),
[fieldName, nodeId]
);
const isExposed = useAppSelector(selectIsExposed);
const handleExposeField = useCallback(() => {
dispatch(workflowExposedFieldAdded({ nodeId, fieldName, value }));
}, [dispatch, fieldName, nodeId, value]);
const handleUnexposeField = useCallback(() => {
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
}, [dispatch, fieldName, nodeId]);
if (!isExposed) {
return (
<IconButton
variant="ghost"
tooltip={t('nodes.addLinearView')}
aria-label={t('nodes.addLinearView')}
icon={<PiPlusBold />}
onClick={handleExposeField}
pointerEvents="auto"
size="xs"
/>
);
} else {
return (
<IconButton
variant="ghost"
tooltip={t('nodes.removeLinearView')}
aria-label={t('nodes.removeLinearView')}
icon={<PiMinusBold />}
onClick={handleUnexposeField}
pointerEvents="auto"
size="xs"
/>
);
}
};
export default memo(FieldLinearViewToggle);

View File

@@ -1,42 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
import { isEqual } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
const FieldResetToDefaultValueButton = ({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const value = useFieldValue(nodeId, fieldName);
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
const isDisabled = useMemo(() => {
return isEqual(value, fieldTemplate.default);
}, [value, fieldTemplate.default]);
const onClick = useCallback(() => {
dispatch(fieldValueReset({ nodeId, fieldName, value: fieldTemplate.default }));
}, [dispatch, fieldName, fieldTemplate.default, nodeId]);
return (
<IconButton
variant="ghost"
tooltip={t('nodes.resetToDefaultValue')}
aria-label={t('nodes.resetToDefaultValue')}
icon={<PiArrowCounterClockwiseBold />}
onClick={onClick}
isDisabled={isDisabled}
pointerEvents="auto"
size="xs"
/>
);
};
export default memo(FieldResetToDefaultValueButton);

View File

@@ -1,63 +0,0 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import { isFieldInputInstance, isFieldInputTemplate } from 'features/nodes/types/field';
import { startCase } from 'lodash-es';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
fieldName: string;
kind: 'inputs' | 'outputs';
}
const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
const field = useFieldInputInstance(nodeId, fieldName);
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
const isInputTemplate = isFieldInputTemplate(fieldTemplate);
const fieldTypeName = useFieldTypeName(fieldTemplate?.type);
const { t } = useTranslation();
const fieldTitle = useMemo(() => {
if (isFieldInputInstance(field)) {
if (field.label && fieldTemplate?.title) {
return `${field.label} (${fieldTemplate.title})`;
}
if (field.label && !fieldTemplate) {
return field.label;
}
if (!field.label && fieldTemplate) {
return fieldTemplate.title;
}
return t('nodes.unknownField');
} else {
return fieldTemplate?.title || t('nodes.unknownField');
}
}, [field, fieldTemplate, t]);
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{fieldTitle}</Text>
{fieldTemplate && (
<Text opacity={0.7} fontStyle="oblique 5deg">
{fieldTemplate.description}
</Text>
)}
{fieldTypeName && (
<Text>
{t('parameters.type')}: {fieldTypeName}
</Text>
)}
{isInputTemplate && (
<Text>
{t('common.input')}: {startCase(fieldTemplate.input)}
</Text>
)}
</Flex>
);
};
export default memo(FieldTooltipContent);

View File

@@ -0,0 +1,25 @@
import { CompositeNumberInput } from '@invoke-ai/ui-library';
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
export const FloatFieldInput = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
return (
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
flex="1 1 0"
/>
);
});
FloatFieldInput.displayName = 'FloatFieldInput ';

View File

@@ -0,0 +1,42 @@
import { CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
export const FloatFieldInputAndSlider = memo(
(props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
return (
<>
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
marks
withThumbTooltip
flex="1 1 0"
/>
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
flex="1 1 0"
/>
</>
);
}
);
FloatFieldInputAndSlider.displayName = 'FloatFieldInputAndSlider ';

View File

@@ -0,0 +1,27 @@
import { CompositeSlider } from '@invoke-ai/ui-library';
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
export const FloatFieldSlider = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
return (
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
marks
withThumbTooltip
flex="1 1 0"
/>
);
});
FloatFieldSlider.displayName = 'FloatFieldSlider ';

View File

@@ -0,0 +1,65 @@
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 { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import { isNil } from 'lodash-es';
import { useCallback, useMemo } from 'react';
export const useFloatField = (props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
const { nodeId, field, fieldTemplate } = props;
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 (isNil(fieldTemplate.multipleOf)) {
return 0.1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf]);
const fineStep = useMemo(() => {
if (isNil(fieldTemplate.multipleOf)) {
return 0.01;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf]);
return {
defaultValue: fieldTemplate.default,
onChange,
value: field.value,
min,
max,
step,
fineStep,
};
};

View File

@@ -1,94 +0,0 @@
import { Flex, FormControl } from '@invoke-ai/ui-library';
import FieldResetToDefaultValueButton from 'features/nodes/components/flow/nodes/Invocation/fields/FieldResetToDefaultValueButton';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { memo, useCallback, useState } from 'react';
import EditableFieldTitle from './EditableFieldTitle';
import FieldHandle from './FieldHandle';
import FieldLinearViewToggle from './FieldLinearViewToggle';
import InputFieldRenderer from './InputFieldRenderer';
import { InputFieldWrapper } from './InputFieldWrapper';
interface Props {
nodeId: string;
fieldName: string;
}
const InputField = ({ nodeId, fieldName }: Props) => {
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
const [isHovered, setIsHovered] = useState(false);
const isInvalid = useFieldIsInvalid(nodeId, fieldName);
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
useConnectionState({ nodeId, fieldName, kind: 'inputs' });
const onMouseEnter = useCallback(() => {
setIsHovered(true);
}, []);
const onMouseLeave = useCallback(() => {
setIsHovered(false);
}, []);
if (fieldTemplate.input === 'connection' || isConnected) {
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
<EditableFieldTitle
nodeId={nodeId}
fieldName={fieldName}
kind="inputs"
isInvalid={isInvalid}
withTooltip
shouldDim
/>
</FormControl>
<FieldHandle
fieldTemplate={fieldTemplate}
handleType="target"
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
validationResult={validationResult}
/>
</InputFieldWrapper>
);
}
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl
isInvalid={isInvalid}
isDisabled={isConnected}
// Without pointerEvents prop, disabled inputs don't trigger reactflow events. For example, when making a
// connection, the mouse up to end the connection won't fire, leaving the connection in-progress.
pointerEvents={isConnected ? 'none' : 'auto'}
orientation="vertical"
px={2}
>
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Flex gap={1}>
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" isInvalid={isInvalid} withTooltip />
{isHovered && <FieldResetToDefaultValueButton nodeId={nodeId} fieldName={fieldName} />}
{isHovered && <FieldLinearViewToggle nodeId={nodeId} fieldName={fieldName} />}
</Flex>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</Flex>
</FormControl>
{fieldTemplate.input !== 'direct' && (
<FieldHandle
fieldTemplate={fieldTemplate}
handleType="target"
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
validationResult={validationResult}
/>
)}
</InputFieldWrapper>
);
};
export default memo(InputField);

View File

@@ -0,0 +1,64 @@
import {
FormControl,
FormLabel,
IconButton,
Popover,
PopoverContent,
PopoverTrigger,
Textarea,
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiNoteBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldDescriptionPopover = memo(({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const description = useInputFieldDescription(nodeId, fieldName);
const onChange = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(fieldDescriptionChanged({ nodeId, fieldName, val: e.target.value }));
},
[dispatch, fieldName, nodeId]
);
return (
<Popover>
<PopoverTrigger>
<IconButton
variant="ghost"
tooltip={t('nodes.description')}
aria-label={t('nodes.description')}
icon={<PiNoteBold />}
pointerEvents="auto"
size="xs"
/>
</PopoverTrigger>
<PopoverContent p={2} w={256}>
<FormControl orientation="vertical">
<FormLabel>{t('nodes.description')}</FormLabel>
<Textarea
className="nodrag nopan nowheel"
fontSize="sm"
value={description ?? ''}
onChange={onChange}
p={2}
resize="none"
rows={5}
/>
</FormControl>
</PopoverContent>
</Popover>
);
});
InputFieldDescriptionPopover.displayName = 'InputFieldDescriptionPopover';

View File

@@ -0,0 +1,105 @@
import { Flex, FormControl, Spacer } from '@invoke-ai/ui-library';
import { InputFieldDescriptionPopover } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover';
import { InputFieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle';
import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton';
import { useNodeFieldDnd } from 'features/nodes/components/sidePanel/builder/dnd';
import { useInputFieldConnectionState } from 'features/nodes/hooks/useInputFieldConnectionState';
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { memo, useCallback, useRef, useState } from 'react';
import { InputFieldRenderer } from './InputFieldRenderer';
import { InputFieldTitle } from './InputFieldTitle';
import { InputFieldWrapper } from './InputFieldWrapper';
interface Props {
nodeId: string;
fieldName: string;
}
export const InputFieldEditModeNodes = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const draggableRef = useRef<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const isInvalid = useInputFieldIsInvalid(nodeId, fieldName);
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
const { isConnectionInProgress, isConnectionStartField, validationResult } = useInputFieldConnectionState(
nodeId,
fieldName
);
const onMouseEnter = useCallback(() => {
setIsHovered(true);
}, []);
const onMouseLeave = useCallback(() => {
setIsHovered(false);
}, []);
const isDragging = useNodeFieldDnd({ nodeId, fieldName }, fieldTemplate, draggableRef, dragHandleRef);
if (fieldTemplate.input === 'connection' || isConnected) {
return (
<InputFieldWrapper>
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
<InputFieldTitle
nodeId={nodeId}
fieldName={fieldName}
isInvalid={isInvalid}
isDisabled={(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField) || isConnected}
/>
</FormControl>
<InputFieldHandle
fieldTemplate={fieldTemplate}
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
validationResult={validationResult}
/>
</InputFieldWrapper>
);
}
return (
<InputFieldWrapper>
<FormControl
ref={draggableRef}
isInvalid={isInvalid}
isDisabled={isConnected}
// Without pointerEvents prop, disabled inputs don't trigger reactflow events. For example, when making a
// connection, the mouse up to end the connection won't fire, leaving the connection in-progress.
pointerEvents={isConnected ? 'none' : 'auto'}
orientation="vertical"
px={2}
opacity={isDragging ? 0.3 : 1}
>
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Flex className="nodrag" ref={dragHandleRef} gap={1}>
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} isInvalid={isInvalid} />
<Spacer />
{isHovered && (
<>
<InputFieldDescriptionPopover nodeId={nodeId} fieldName={fieldName} />
<InputFieldResetToDefaultValueIconButton nodeId={nodeId} fieldName={fieldName} />
</>
)}
</Flex>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</Flex>
</FormControl>
{fieldTemplate.input !== 'direct' && (
<InputFieldHandle
fieldTemplate={fieldTemplate}
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
validationResult={validationResult}
/>
)}
</InputFieldWrapper>
);
});
InputFieldEditModeNodes.displayName = 'InputFieldEditModeNodes';

View File

@@ -0,0 +1,23 @@
import { InputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder';
import { useInputFieldInstanceExists } from 'features/nodes/hooks/useInputFieldInstanceExists';
import { useInputFieldTemplateExists } from 'features/nodes/hooks/useInputFieldTemplateExists';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
}>;
export const InputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
const hasInstance = useInputFieldInstanceExists(nodeId, fieldName);
const hasTemplate = useInputFieldTemplateExists(nodeId, fieldName);
if (!hasTemplate || !hasInstance) {
return <InputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
}
return children;
});
InputFieldGate.displayName = 'InputFieldGate';

View File

@@ -0,0 +1,93 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Tooltip } from '@invoke-ai/ui-library';
import { Handle, Position } from '@xyflow/react';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import type { ValidationResult } from 'features/nodes/store/util/validateConnection';
import { HANDLE_TOOLTIP_OPEN_DELAY, MODEL_TYPES } from 'features/nodes/types/constants';
import type { FieldInputTemplate } from 'features/nodes/types/field';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type InputFieldHandleProps = {
fieldTemplate: FieldInputTemplate;
isConnectionInProgress: boolean;
isConnectionStartField: boolean;
validationResult: ValidationResult;
};
const sx = {
position: 'relative',
width: 'full',
height: 'full',
borderStyle: 'solid',
borderWidth: 4,
pointerEvents: 'none',
'&[data-cardinality="SINGLE"]': {
borderWidth: 0,
},
borderRadius: '100%',
'&[data-is-model-field="true"], &[data-is-batch-field="true"]': {
borderRadius: 4,
},
'&[data-is-batch-field="true"]': {
transform: 'rotate(45deg)',
},
'&[data-is-connection-in-progress="true"][data-is-connection-start-field="false"][data-is-connection-valid="false"]':
{
filter: 'opacity(0.4) grayscale(0.7)',
cursor: 'not-allowed',
},
'&[data-is-connection-in-progress="true"][data-is-connection-start-field="true"][data-is-connection-valid="false"]': {
cursor: 'grab',
},
'&[data-is-connection-in-progress="false"] &[data-is-connection-valid="true"]': {
cursor: 'crosshair',
},
} satisfies SystemStyleObject;
const handleStyles = {
position: 'absolute',
width: '1rem',
height: '1rem',
zIndex: 1,
background: 'none',
border: 'none',
insetInlineStart: '-0.5rem',
} satisfies CSSProperties;
export const InputFieldHandle = memo((props: InputFieldHandleProps) => {
const { fieldTemplate, isConnectionInProgress, isConnectionStartField, validationResult } = props;
const { t } = useTranslation();
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
const isModelField = useMemo(() => MODEL_TYPES.some((t) => t === fieldTemplate.type.name), [fieldTemplate.type]);
const tooltip = useMemo(() => {
if (isConnectionInProgress && validationResult.messageTKey) {
return t(validationResult.messageTKey);
}
return fieldTypeName;
}, [fieldTypeName, isConnectionInProgress, t, validationResult.messageTKey]);
return (
<Tooltip label={tooltip} placement="start" openDelay={HANDLE_TOOLTIP_OPEN_DELAY}>
<Handle type="target" id={fieldTemplate.name} position={Position.Left} style={handleStyles}>
<Box
sx={sx}
data-cardinality={fieldTemplate.type.cardinality}
data-is-batch-field={fieldTemplate.type.batch}
data-is-model-field={isModelField}
data-is-connection-in-progress={isConnectionInProgress}
data-is-connection-start-field={isConnectionStartField}
data-is-connection-valid={validationResult.isValid}
backgroundColor={fieldTemplate.type.cardinality === 'SINGLE' ? fieldColor : 'base.900'}
borderColor={fieldColor}
/>
</Handle>
</Tooltip>
);
});
InputFieldHandle.displayName = 'InputFieldHandle';

View File

@@ -1,12 +1,21 @@
import { FloatFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput';
import { FloatFieldInputAndSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInputAndSlider';
import { FloatFieldSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldSlider';
import { FloatFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatFieldCollectionInputComponent';
import { FloatGeneratorFieldInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatGeneratorFieldComponent';
import { ImageFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent';
import { IntegerFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerFieldCollectionInputComponent';
import { IntegerGeneratorFieldInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerGeneratorFieldComponent';
import ModelIdentifierFieldInputComponent from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent';
import { NumberFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberFieldCollectionInputComponent';
import { StringFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent';
import { StringGeneratorFieldInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorFieldComponent';
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { IntegerFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput';
import { IntegerFieldInputAndSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInputAndSlider';
import { IntegerFieldSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider';
import { StringFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput';
import { StringFieldTextarea } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import {
isBoardFieldInputInstance,
isBoardFieldInputTemplate,
@@ -77,6 +86,7 @@ import {
isVAEModelFieldInputInstance,
isVAEModelFieldInputTemplate,
} from 'features/nodes/types/field';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { memo } from 'react';
import BoardFieldInputComponent from './inputs/BoardFieldInputComponent';
@@ -94,174 +104,292 @@ import ImageFieldInputComponent from './inputs/ImageFieldInputComponent';
import IPAdapterModelFieldInputComponent from './inputs/IPAdapterModelFieldInputComponent';
import LoRAModelFieldInputComponent from './inputs/LoRAModelFieldInputComponent';
import MainModelFieldInputComponent from './inputs/MainModelFieldInputComponent';
import NumberFieldInputComponent from './inputs/NumberFieldInputComponent';
import RefinerModelFieldInputComponent from './inputs/RefinerModelFieldInputComponent';
import SchedulerFieldInputComponent from './inputs/SchedulerFieldInputComponent';
import SD3MainModelFieldInputComponent from './inputs/SD3MainModelFieldInputComponent';
import SDXLMainModelFieldInputComponent from './inputs/SDXLMainModelFieldInputComponent';
import SpandrelImageToImageModelFieldInputComponent from './inputs/SpandrelImageToImageModelFieldInputComponent';
import StringFieldInputComponent from './inputs/StringFieldInputComponent';
import T2IAdapterModelFieldInputComponent from './inputs/T2IAdapterModelFieldInputComponent';
import T5EncoderModelFieldInputComponent from './inputs/T5EncoderModelFieldInputComponent';
import VAEModelFieldInputComponent from './inputs/VAEModelFieldInputComponent';
type InputFieldProps = {
type Props = {
nodeId: string;
fieldName: string;
settings?: NodeFieldElement['data']['settings'];
};
const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
const fieldInstance = useFieldInputInstance(nodeId, fieldName);
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) => {
const field = useInputFieldInstance(nodeId, fieldName);
const template = useInputFieldTemplate(nodeId, fieldName);
if (isStringFieldCollectionInputInstance(fieldInstance) && isStringFieldCollectionInputTemplate(fieldTemplate)) {
return <StringFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isStringFieldCollectionInputTemplate(template)) {
if (!isStringFieldCollectionInputInstance(field)) {
return null;
}
return <StringFieldCollectionInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isStringFieldInputInstance(fieldInstance) && isStringFieldInputTemplate(fieldTemplate)) {
return <StringFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isStringFieldInputTemplate(template)) {
if (!isStringFieldInputInstance(field)) {
return null;
}
if (settings?.type !== 'string-field-config') {
if (template.ui_component === 'textarea') {
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
} else {
return <StringFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
}
}
if (settings.component === 'input') {
return <StringFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
} else if (settings.component === 'textarea') {
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
}
}
if (isBooleanFieldInputInstance(fieldInstance) && isBooleanFieldInputTemplate(fieldTemplate)) {
return <BooleanFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isBooleanFieldInputTemplate(template)) {
if (!isBooleanFieldInputInstance(field)) {
return null;
}
return <BooleanFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isIntegerFieldInputInstance(fieldInstance) && isIntegerFieldInputTemplate(fieldTemplate)) {
return <NumberFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isIntegerFieldInputTemplate(template)) {
if (!isIntegerFieldInputInstance(field)) {
return null;
}
if (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} />;
}
}
if (isFloatFieldInputInstance(fieldInstance) && isFloatFieldInputTemplate(fieldTemplate)) {
return <NumberFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isFloatFieldInputTemplate(template)) {
if (!isFloatFieldInputInstance(field)) {
return null;
}
if (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} />;
}
}
if (isIntegerFieldCollectionInputInstance(fieldInstance) && isIntegerFieldCollectionInputTemplate(fieldTemplate)) {
return <NumberFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isIntegerFieldCollectionInputTemplate(template)) {
if (!isIntegerFieldCollectionInputInstance(field)) {
return null;
}
return <IntegerFieldCollectionInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isFloatFieldCollectionInputInstance(fieldInstance) && isFloatFieldCollectionInputTemplate(fieldTemplate)) {
return <NumberFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isFloatFieldCollectionInputTemplate(template)) {
if (!isFloatFieldCollectionInputInstance(field)) {
return null;
}
return <FloatFieldCollectionInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isEnumFieldInputInstance(fieldInstance) && isEnumFieldInputTemplate(fieldTemplate)) {
return <EnumFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isEnumFieldInputTemplate(template)) {
if (!isEnumFieldInputInstance(field)) {
return null;
}
return <EnumFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isImageFieldCollectionInputInstance(fieldInstance) && isImageFieldCollectionInputTemplate(fieldTemplate)) {
return <ImageFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isImageFieldCollectionInputTemplate(template)) {
if (!isImageFieldCollectionInputInstance(field)) {
return null;
}
return <ImageFieldCollectionInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isImageFieldInputInstance(fieldInstance) && isImageFieldInputTemplate(fieldTemplate)) {
return <ImageFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isImageFieldInputTemplate(template)) {
if (!isImageFieldInputInstance(field)) {
return null;
}
return <ImageFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isBoardFieldInputInstance(fieldInstance) && isBoardFieldInputTemplate(fieldTemplate)) {
return <BoardFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isBoardFieldInputTemplate(template)) {
if (!isBoardFieldInputInstance(field)) {
return null;
}
return <BoardFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isMainModelFieldInputInstance(fieldInstance) && isMainModelFieldInputTemplate(fieldTemplate)) {
return <MainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isMainModelFieldInputTemplate(template)) {
if (!isMainModelFieldInputInstance(field)) {
return null;
}
return <MainModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isModelIdentifierFieldInputInstance(fieldInstance) && isModelIdentifierFieldInputTemplate(fieldTemplate)) {
return <ModelIdentifierFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isModelIdentifierFieldInputTemplate(template)) {
if (!isModelIdentifierFieldInputInstance(field)) {
return null;
}
return <ModelIdentifierFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isSDXLRefinerModelFieldInputInstance(fieldInstance) && isSDXLRefinerModelFieldInputTemplate(fieldTemplate)) {
return <RefinerModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isSDXLRefinerModelFieldInputTemplate(template)) {
if (!isSDXLRefinerModelFieldInputInstance(field)) {
return null;
}
return <RefinerModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isVAEModelFieldInputInstance(fieldInstance) && isVAEModelFieldInputTemplate(fieldTemplate)) {
return <VAEModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isVAEModelFieldInputTemplate(template)) {
if (!isVAEModelFieldInputInstance(field)) {
return null;
}
return <VAEModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isT5EncoderModelFieldInputInstance(fieldInstance) && isT5EncoderModelFieldInputTemplate(fieldTemplate)) {
return <T5EncoderModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isT5EncoderModelFieldInputTemplate(template)) {
if (!isT5EncoderModelFieldInputInstance(field)) {
return null;
}
return <T5EncoderModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isCLIPEmbedModelFieldInputInstance(fieldInstance) && isCLIPEmbedModelFieldInputTemplate(fieldTemplate)) {
return <CLIPEmbedModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isCLIPEmbedModelFieldInputTemplate(template)) {
if (!isCLIPEmbedModelFieldInputInstance(field)) {
return null;
}
return <CLIPEmbedModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isCLIPLEmbedModelFieldInputInstance(fieldInstance) && isCLIPLEmbedModelFieldInputTemplate(fieldTemplate)) {
return <CLIPLEmbedModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isCLIPLEmbedModelFieldInputTemplate(template)) {
if (!isCLIPLEmbedModelFieldInputInstance(field)) {
return null;
}
return <CLIPLEmbedModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isCLIPGEmbedModelFieldInputInstance(fieldInstance) && isCLIPGEmbedModelFieldInputTemplate(fieldTemplate)) {
return <CLIPGEmbedModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isCLIPGEmbedModelFieldInputTemplate(template)) {
if (!isCLIPGEmbedModelFieldInputInstance(field)) {
return null;
}
return <CLIPGEmbedModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isControlLoRAModelFieldInputInstance(fieldInstance) && isControlLoRAModelFieldInputTemplate(fieldTemplate)) {
return <ControlLoRAModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isControlLoRAModelFieldInputTemplate(template)) {
if (!isControlLoRAModelFieldInputInstance(field)) {
return null;
}
return <ControlLoRAModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isFluxVAEModelFieldInputInstance(fieldInstance) && isFluxVAEModelFieldInputTemplate(fieldTemplate)) {
return <FluxVAEModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isFluxVAEModelFieldInputTemplate(template)) {
if (!isFluxVAEModelFieldInputInstance(field)) {
return null;
}
return <FluxVAEModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isLoRAModelFieldInputInstance(fieldInstance) && isLoRAModelFieldInputTemplate(fieldTemplate)) {
return <LoRAModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isLoRAModelFieldInputTemplate(template)) {
if (!isLoRAModelFieldInputInstance(field)) {
return null;
}
return <LoRAModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isControlNetModelFieldInputInstance(fieldInstance) && isControlNetModelFieldInputTemplate(fieldTemplate)) {
return <ControlNetModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isControlNetModelFieldInputTemplate(template)) {
if (!isControlNetModelFieldInputInstance(field)) {
return null;
}
return <ControlNetModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isIPAdapterModelFieldInputInstance(fieldInstance) && isIPAdapterModelFieldInputTemplate(fieldTemplate)) {
return <IPAdapterModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isIPAdapterModelFieldInputTemplate(template)) {
if (!isIPAdapterModelFieldInputInstance(field)) {
return null;
}
return <IPAdapterModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isT2IAdapterModelFieldInputInstance(fieldInstance) && isT2IAdapterModelFieldInputTemplate(fieldTemplate)) {
return <T2IAdapterModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isT2IAdapterModelFieldInputTemplate(template)) {
if (!isT2IAdapterModelFieldInputInstance(field)) {
return null;
}
return <T2IAdapterModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (
isSpandrelImageToImageModelFieldInputInstance(fieldInstance) &&
isSpandrelImageToImageModelFieldInputTemplate(fieldTemplate)
) {
return (
<SpandrelImageToImageModelFieldInputComponent
nodeId={nodeId}
field={fieldInstance}
fieldTemplate={fieldTemplate}
/>
);
if (isSpandrelImageToImageModelFieldInputTemplate(template)) {
if (!isSpandrelImageToImageModelFieldInputInstance(field)) {
return null;
}
return <SpandrelImageToImageModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isColorFieldInputInstance(fieldInstance) && isColorFieldInputTemplate(fieldTemplate)) {
return <ColorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isColorFieldInputTemplate(template)) {
if (!isColorFieldInputInstance(field)) {
return null;
}
return <ColorFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isFluxMainModelFieldInputInstance(fieldInstance) && isFluxMainModelFieldInputTemplate(fieldTemplate)) {
return <FluxMainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isFluxMainModelFieldInputTemplate(template)) {
if (!isFluxMainModelFieldInputInstance(field)) {
return null;
}
return <FluxMainModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isSD3MainModelFieldInputInstance(fieldInstance) && isSD3MainModelFieldInputTemplate(fieldTemplate)) {
return <SD3MainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isSD3MainModelFieldInputTemplate(template)) {
if (!isSD3MainModelFieldInputInstance(field)) {
return null;
}
return <SD3MainModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isSDXLMainModelFieldInputInstance(fieldInstance) && isSDXLMainModelFieldInputTemplate(fieldTemplate)) {
return <SDXLMainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isSDXLMainModelFieldInputTemplate(template)) {
if (!isSDXLMainModelFieldInputInstance(field)) {
return null;
}
return <SDXLMainModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isSchedulerFieldInputInstance(fieldInstance) && isSchedulerFieldInputTemplate(fieldTemplate)) {
return <SchedulerFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isSchedulerFieldInputTemplate(template)) {
if (!isSchedulerFieldInputInstance(field)) {
return null;
}
return <SchedulerFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isFloatGeneratorFieldInputInstance(fieldInstance) && isFloatGeneratorFieldInputTemplate(fieldTemplate)) {
return <FloatGeneratorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isFloatGeneratorFieldInputTemplate(template)) {
if (!isFloatGeneratorFieldInputInstance(field)) {
return null;
}
return <FloatGeneratorFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isIntegerGeneratorFieldInputInstance(fieldInstance) && isIntegerGeneratorFieldInputTemplate(fieldTemplate)) {
return <IntegerGeneratorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isIntegerGeneratorFieldInputTemplate(template)) {
if (!isIntegerGeneratorFieldInputInstance(field)) {
return null;
}
return <IntegerGeneratorFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isStringGeneratorFieldInputInstance(fieldInstance) && isStringGeneratorFieldInputTemplate(fieldTemplate)) {
return <StringGeneratorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isStringGeneratorFieldInputTemplate(template)) {
if (!isStringGeneratorFieldInputInstance(field)) {
return null;
}
return <StringGeneratorFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (fieldTemplate) {
// Fallback for when there is no component for the type
return null;
}
};
return null;
});
export default memo(InputFieldRenderer);
InputFieldRenderer.displayName = 'InputFieldRenderer';

View File

@@ -0,0 +1,30 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useInputFieldDefaultValue } from 'features/nodes/hooks/useInputFieldDefaultValue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldResetToDefaultValueIconButton = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const { isValueChanged, resetToDefaultValue } = useInputFieldDefaultValue(nodeId, fieldName);
return (
<IconButton
variant="ghost"
tooltip={t('nodes.resetToDefaultValue')}
aria-label={t('nodes.resetToDefaultValue')}
icon={<PiArrowCounterClockwiseBold />}
pointerEvents="auto"
size="xs"
onClick={resetToDefaultValue}
isDisabled={!isValueChanged}
/>
);
});
InputFieldResetToDefaultValueIconButton.displayName = 'InputFieldResetToDefaultValueIconButton';

View File

@@ -0,0 +1,81 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Input, Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEditable } from 'common/hooks/useEditable';
import { InputFieldTooltipContent } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent';
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
const labelSx: SystemStyleObject = {
p: 0,
fontWeight: 'semibold',
textAlign: 'left',
color: 'base.300',
_hover: {
fontWeight: 'semibold !important',
},
'&[data-is-invalid="true"]': {
color: 'error.300',
},
'&[data-is-disabled="true"]': {
opacity: 0.5,
},
};
interface Props {
nodeId: string;
fieldName: string;
isInvalid?: boolean;
isDisabled?: boolean;
}
export const InputFieldTitle = memo((props: Props) => {
const { nodeId, fieldName, isInvalid, isDisabled } = props;
const inputRef = useRef<HTMLInputElement>(null);
const label = useInputFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const defaultTitle = useMemo(() => fieldTemplateTitle || t('nodes.unknownField'), [fieldTemplateTitle, t]);
const onChange = useCallback(
(label: string) => {
dispatch(fieldLabelChanged({ nodeId, fieldName, label }));
},
[dispatch, nodeId, fieldName]
);
const editable = useEditable({
value: label || defaultTitle,
defaultValue: defaultTitle,
onChange,
inputRef,
});
if (!editable.isEditing) {
return (
<Tooltip
label={<InputFieldTooltipContent nodeId={nodeId} fieldName={fieldName} />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
>
<Text
sx={labelSx}
noOfLines={1}
data-is-invalid={isInvalid}
data-is-disabled={isDisabled}
onDoubleClick={editable.startEditing}
>
{editable.value}
</Text>
</Tooltip>
);
}
return <Input ref={inputRef} variant="outline" {...editable.inputProps} />;
});
InputFieldTitle.displayName = 'InputFieldTitle';

View File

@@ -0,0 +1,49 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import { startCase } from 'lodash-es';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
fieldName: string;
}
export const InputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const fieldInstance = useInputFieldInstance(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
const fieldTitle = useMemo(() => {
if (fieldInstance.label && fieldTemplate.title) {
return `${fieldInstance.label} (${fieldTemplate.title})`;
}
if (fieldInstance.label && !fieldTemplate.title) {
return fieldInstance.label;
}
return fieldTemplate.title;
}, [fieldInstance, fieldTemplate]);
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{fieldTitle}</Text>
<Text opacity={0.7} fontStyle="oblique 5deg">
{fieldTemplate.description}
</Text>
<Text>
{t('parameters.type')}: {fieldTypeName}
</Text>
<Text>
{t('common.input')}: {startCase(fieldTemplate.input)}
</Text>
</Flex>
);
});
InputFieldTooltipContent.displayName = 'InputFieldTooltipContent';

View File

@@ -0,0 +1,27 @@
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
import { useInputFieldName } from 'features/nodes/hooks/useInputFieldName';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const name = useInputFieldName(nodeId, fieldName);
return (
<InputFieldWrapper>
<FormControl isInvalid={true} alignItems="stretch" justifyContent="center" gap={2} h="full" w="full">
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
{t('nodes.unknownInput', { name })}
</FormLabel>
</FormControl>
</InputFieldWrapper>
);
});
InputFieldUnknownPlaceholder.displayName = 'InputFieldUnknownPlaceholder';

View File

@@ -1,27 +1,21 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type InputFieldWrapperProps = PropsWithChildren<{
shouldDim: boolean;
}>;
const sx = {
position: 'relative',
minH: 8,
py: 0.5,
alignItems: 'center',
transitionProperty: 'opacity',
transitionDuration: '0.1s',
w: 'full',
h: 'full',
} satisfies SystemStyleObject;
export const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
return (
<Flex
position="relative"
minH={8}
py={0.5}
alignItems="center"
opacity={shouldDim ? 0.5 : 1}
transitionProperty="opacity"
transitionDuration="0.1s"
w="full"
h="full"
>
{children}
</Flex>
);
export const InputFieldWrapper = memo(({ children }: PropsWithChildren) => {
return <Flex sx={sx}>{children}</Flex>;
});
InputFieldWrapper.displayName = 'InputFieldWrapper';

View File

@@ -0,0 +1,27 @@
import { CompositeNumberInput } from '@invoke-ai/ui-library';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
export const IntegerFieldInput = memo(
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
return (
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
flex="1 1 0"
/>
);
}
);
IntegerFieldInput.displayName = 'IntegerFieldInput';

View File

@@ -0,0 +1,42 @@
import { CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
export const IntegerFieldInputAndSlider = memo(
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
return (
<>
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
marks
withThumbTooltip
flex="1 1 0"
/>
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
flex="1 1 0"
/>
</>
);
}
);
IntegerFieldInputAndSlider.displayName = 'IntegerFieldInputAndSlider';

View File

@@ -0,0 +1,29 @@
import { CompositeSlider } from '@invoke-ai/ui-library';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
export const IntegerFieldSlider = memo(
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
return (
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
marks
withThumbTooltip
flex="1 1 0"
/>
);
}
);
IntegerFieldSlider.displayName = 'IntegerFieldSlider';

View File

@@ -0,0 +1,65 @@
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 { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import { isNil } from 'lodash-es';
import { useCallback, useMemo } from 'react';
export const useIntegerField = (props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
const { nodeId, field, fieldTemplate } = props;
const dispatch = useAppDispatch();
const onChange = useCallback(
(value: number) => {
dispatch(fieldIntegerValueChanged({ nodeId, fieldName: field.name, value: Math.floor(Number(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 + 1;
}
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 - 1;
}
return max;
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
const step = useMemo(() => {
if (isNil(fieldTemplate.multipleOf)) {
return 1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf]);
const fineStep = useMemo(() => {
if (isNil(fieldTemplate.multipleOf)) {
return 1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf]);
return {
defaultValue: fieldTemplate.default,
onChange,
value: field.value,
min,
max,
step,
fineStep,
};
};

View File

@@ -1,59 +0,0 @@
import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
import type { PropsWithChildren } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
}>;
export const InvocationInputFieldCheck = memo(({ nodeId, fieldName, children }: Props) => {
const { t } = useTranslation();
const templates = useStore($templates);
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodesSlice) => {
const node = selectInvocationNode(nodesSlice, nodeId);
const instance = node.data.inputs[fieldName];
const template = templates[node.data.type];
const fieldTemplate = template?.inputs[fieldName];
return {
name: instance?.label || fieldTemplate?.title || fieldName,
hasInstance: Boolean(instance),
hasTemplate: Boolean(fieldTemplate),
};
}),
[fieldName, nodeId, templates]
);
const { hasInstance, hasTemplate, name } = useAppSelector(selector);
if (!hasTemplate || !hasInstance) {
return (
<Flex position="relative" minH={8} py={0.5} alignItems="center" w="full" h="full">
<FormControl
isInvalid={true}
alignItems="stretch"
justifyContent="center"
flexDir="column"
gap={2}
h="full"
w="full"
>
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
{t('nodes.unknownInput', { name })}
</FormLabel>
</FormControl>
</Flex>
);
}
return children;
});
InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck';

View File

@@ -1,116 +0,0 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Circle, Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
import { useLinearViewFieldDnd } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
import EditableFieldTitle from './EditableFieldTitle';
import FieldTooltipContent from './FieldTooltipContent';
import InputFieldRenderer from './InputFieldRenderer';
type Props = {
fieldIdentifier: FieldIdentifier;
};
const sx = {
layerStyle: 'second',
alignItems: 'center',
position: 'relative',
borderRadius: 'base',
w: 'full',
p: 2,
'&[data-is-dragging=true]': {
opacity: 0.3,
},
transitionProperty: 'common',
} satisfies SystemStyleObject;
const LinearViewFieldInternal = ({ fieldIdentifier }: Props) => {
const dispatch = useAppDispatch();
const { isValueChanged, onReset } = useFieldOriginalValue(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(fieldIdentifier.nodeId);
const { t } = useTranslation();
const handleRemoveField = useCallback(() => {
dispatch(workflowExposedFieldRemoved(fieldIdentifier));
}, [dispatch, fieldIdentifier]);
const ref = useRef<HTMLDivElement>(null);
const [dndListState, isDragging] = useLinearViewFieldDnd(ref, fieldIdentifier);
return (
<Box position="relative" w="full">
<Flex
ref={ref}
// This is used to trigger the post-move flash animation
data-field-name={`${fieldIdentifier.nodeId}-${fieldIdentifier.fieldName}`}
data-is-dragging={isDragging}
onMouseEnter={handleMouseOver}
onMouseLeave={handleMouseOut}
sx={sx}
>
<Flex flexDir="column" w="full">
<Flex alignItems="center" gap={2}>
<EditableFieldTitle nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} kind="inputs" />
<Spacer />
{isMouseOverNode && <Circle me={2} size={2} borderRadius="full" bg="invokeBlue.500" />}
{isValueChanged && (
<IconButton
aria-label={t('nodes.resetToDefaultValue')}
tooltip={t('nodes.resetToDefaultValue')}
variant="ghost"
size="sm"
onClick={onReset}
icon={<PiArrowCounterClockwiseBold />}
/>
)}
<Tooltip
label={
<FieldTooltipContent
nodeId={fieldIdentifier.nodeId}
fieldName={fieldIdentifier.fieldName}
kind="inputs"
/>
}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
>
<Flex h="full" alignItems="center">
<Icon fontSize="sm" color="base.300" as={PiInfoBold} />
</Flex>
</Tooltip>
<IconButton
aria-label={t('nodes.removeLinearView')}
tooltip={t('nodes.removeLinearView')}
variant="ghost"
size="sm"
onClick={handleRemoveField}
icon={<PiTrashSimpleBold />}
/>
</Flex>
<InputFieldRenderer nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
</Flex>
</Flex>
<DndListDropIndicator dndState={dndListState} />
</Box>
);
};
const LinearViewField = ({ fieldIdentifier }: Props) => {
return (
<InvocationInputFieldCheck nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
<LinearViewFieldInternal fieldIdentifier={fieldIdentifier} />
</InvocationInputFieldCheck>
);
};
export default memo(LinearViewField);

View File

@@ -0,0 +1,32 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useInputFieldInitialFormValue } from 'features/nodes/hooks/useInputFieldInitialFormValue';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
type Props = {
element: NodeFieldElement;
};
export const NodeFieldElementResetToInitialValueIconButton = memo(({ element }: Props) => {
const { t } = useTranslation();
const { id, data } = element;
const { nodeId, fieldName } = data.fieldIdentifier;
const { isValueChanged, resetToInitialValue } = useInputFieldInitialFormValue(id, nodeId, fieldName);
return (
<IconButton
variant="link"
size="sm"
alignSelf="stretch"
tooltip={t('nodes.resetToDefaultValue')}
aria-label={t('nodes.resetToDefaultValue')}
icon={<PiArrowCounterClockwiseBold />}
onClick={resetToInitialValue}
isDisabled={!isValueChanged}
/>
);
});
NodeFieldElementResetToInitialValueIconButton.displayName = 'NodeFieldElementResetToInitialValueIconButton';

View File

@@ -1,82 +0,0 @@
import { Flex, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useFieldOutputTemplate } from 'features/nodes/hooks/useFieldOutputTemplate';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import FieldHandle from './FieldHandle';
import FieldTooltipContent from './FieldTooltipContent';
interface Props {
nodeId: string;
fieldName: string;
}
const OutputField = ({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const fieldTemplate = useFieldOutputTemplate(nodeId, fieldName);
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
useConnectionState({ nodeId, fieldName, kind: 'outputs' });
if (!fieldTemplate) {
return (
<OutputFieldWrapper shouldDim={shouldDim}>
<FormControl alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
{t('nodes.unknownOutput', {
name: fieldName,
})}
</FormLabel>
</FormControl>
</OutputFieldWrapper>
);
}
return (
<OutputFieldWrapper shouldDim={shouldDim}>
<Tooltip
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="outputs" />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
shouldWrapChildren
>
<FormControl isDisabled={isConnected} pe={2}>
<FormLabel mb={0}>{fieldTemplate?.title}</FormLabel>
</FormControl>
</Tooltip>
<FieldHandle
fieldTemplate={fieldTemplate}
handleType="source"
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
validationResult={validationResult}
/>
</OutputFieldWrapper>
);
};
export default memo(OutputField);
type OutputFieldWrapperProps = PropsWithChildren<{
shouldDim: boolean;
}>;
const OutputFieldWrapper = memo(({ shouldDim, children }: OutputFieldWrapperProps) => (
<Flex
position="relative"
minH={8}
py={0.5}
alignItems="center"
opacity={shouldDim ? 0.5 : 1}
transitionProperty="opacity"
transitionDuration="0.1s"
justifyContent="flex-end"
>
{children}
</Flex>
));
OutputFieldWrapper.displayName = 'OutputFieldWrapper';

View File

@@ -0,0 +1,21 @@
import { OutputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder';
import { useOutputFieldTemplateExists } from 'features/nodes/hooks/useOutputFieldTemplateExists';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
}>;
export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
const hasTemplate = useOutputFieldTemplateExists(nodeId, fieldName);
if (!hasTemplate) {
return <OutputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
}
return children;
});
OutputFieldGate.displayName = 'OutputFieldGate';

View File

@@ -0,0 +1,93 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Tooltip } from '@invoke-ai/ui-library';
import { Handle, Position } from '@xyflow/react';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import type { ValidationResult } from 'features/nodes/store/util/validateConnection';
import { HANDLE_TOOLTIP_OPEN_DELAY, MODEL_TYPES } from 'features/nodes/types/constants';
import type { FieldOutputTemplate } from 'features/nodes/types/field';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
fieldTemplate: FieldOutputTemplate;
isConnectionInProgress: boolean;
isConnectionStartField: boolean;
validationResult: ValidationResult;
};
const sx = {
position: 'relative',
width: 'full',
height: 'full',
borderStyle: 'solid',
borderWidth: 4,
pointerEvents: 'none',
'&[data-cardinality="SINGLE"]': {
borderWidth: 0,
},
borderRadius: '100%',
'&[data-is-model-field="true"], &[data-is-batch-field="true"]': {
borderRadius: 4,
},
'&[data-is-batch-field="true"]': {
transform: 'rotate(45deg)',
},
'&[data-is-connection-in-progress="true"][data-is-connection-start-field="false"][data-is-connection-valid="false"]':
{
filter: 'opacity(0.4) grayscale(0.7)',
cursor: 'not-allowed',
},
'&[data-is-connection-in-progress="true"][data-is-connection-start-field="true"][data-is-connection-valid="false"]': {
cursor: 'grab',
},
'&[data-is-connection-in-progress="false"] &[data-is-connection-valid="true"]': {
cursor: 'crosshair',
},
} satisfies SystemStyleObject;
const handleStyles = {
position: 'absolute',
width: '1rem',
height: '1rem',
zIndex: 1,
background: 'none',
border: 'none',
insetInlineEnd: '-0.5rem',
} satisfies CSSProperties;
export const OutputFieldHandle = memo((props: Props) => {
const { fieldTemplate, isConnectionInProgress, isConnectionStartField, validationResult } = props;
const { t } = useTranslation();
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
const isModelField = useMemo(() => MODEL_TYPES.some((t) => t === fieldTemplate.type.name), [fieldTemplate.type]);
const tooltip = useMemo(() => {
if (isConnectionInProgress && validationResult.messageTKey) {
return t(validationResult.messageTKey);
}
return fieldTypeName;
}, [fieldTypeName, isConnectionInProgress, t, validationResult.messageTKey]);
return (
<Tooltip label={tooltip} placement="end" openDelay={HANDLE_TOOLTIP_OPEN_DELAY}>
<Handle type="source" id={fieldTemplate.name} position={Position.Right} style={handleStyles}>
<Box
sx={sx}
data-cardinality={fieldTemplate.type.cardinality}
data-is-batch-field={fieldTemplate.type.batch}
data-is-model-field={isModelField}
data-is-connection-in-progress={isConnectionInProgress}
data-is-connection-start-field={isConnectionStartField}
data-is-connection-valid={validationResult.isValid}
backgroundColor={fieldTemplate.type.cardinality === 'SINGLE' ? fieldColor : 'base.900'}
borderColor={fieldColor}
/>
</Handle>
</Tooltip>
);
});
OutputFieldHandle.displayName = 'OutputFieldHandle';

View File

@@ -0,0 +1,40 @@
import { OutputFieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldHandle';
import { OutputFieldTitle } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle';
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
import { useOutputFieldConnectionState } from 'features/nodes/hooks/useOutputFieldConnectionState';
import { useOutputFieldIsConnected } from 'features/nodes/hooks/useOutputFieldIsConnected';
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
}>;
export const OutputFieldNodesEditorView = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
const isConnected = useOutputFieldIsConnected(nodeId, fieldName);
const { isConnectionInProgress, isConnectionStartField, validationResult } = useOutputFieldConnectionState(
nodeId,
fieldName
);
return (
<OutputFieldWrapper>
<OutputFieldTitle
nodeId={nodeId}
fieldName={fieldName}
isDisabled={(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField) || isConnected}
/>
<OutputFieldHandle
fieldTemplate={fieldTemplate}
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
validationResult={validationResult}
/>
</OutputFieldWrapper>
);
});
OutputFieldNodesEditorView.displayName = 'OutputFieldNodesEditorView';

View File

@@ -0,0 +1,41 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Text, Tooltip } from '@invoke-ai/ui-library';
import { OutputFieldTooltipContent } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTooltipContent';
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
const sx = {
fontSize: 'sm',
color: 'base.300',
fontWeight: 'semibold',
pe: 2,
'&[data-is-disabled="true"]': {
opacity: 0.5,
},
} satisfies SystemStyleObject;
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
isDisabled?: boolean;
}>;
export const OutputFieldTitle = memo(({ nodeId, fieldName, isDisabled }: Props) => {
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
return (
<Tooltip
label={<OutputFieldTooltipContent nodeId={nodeId} fieldName={fieldName} />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
>
<Text data-is-disabled={isDisabled} sx={sx}>
{fieldTemplate.title}
</Text>
</Tooltip>
);
});
OutputFieldTitle.displayName = 'OutputFieldTitle';

View File

@@ -0,0 +1,30 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
fieldName: string;
}
export const OutputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{fieldTemplate.title}</Text>
<Text opacity={0.7} fontStyle="oblique 5deg">
{fieldTemplate.description}
</Text>
<Text>
{t('parameters.type')}: {fieldTypeName}
</Text>
</Flex>
);
});
OutputFieldTooltipContent.displayName = 'OutputFieldTooltipContent';

View File

@@ -0,0 +1,27 @@
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
nodeId: string;
fieldName: string;
};
export const OutputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const name = useOutputFieldName(nodeId, fieldName);
return (
<OutputFieldWrapper>
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
{t('nodes.unknownOutput', { name })}
</FormLabel>
</FormControl>
</OutputFieldWrapper>
);
});
OutputFieldUnknownPlaceholder.displayName = 'OutputFieldUnknownPlaceholder';

View File

@@ -0,0 +1,18 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
const sx = {
position: 'relative',
minH: 8,
py: 0.5,
alignItems: 'center',
transitionProperty: 'opacity',
transitionDuration: '0.1s',
justifyContent: 'flex-end',
} satisfies SystemStyleObject;
export const OutputFieldWrapper = memo(({ children }: PropsWithChildren) => <Flex sx={sx}>{children}</Flex>);
OutputFieldWrapper.displayName = 'OutputFieldWrapper';

View File

@@ -0,0 +1,15 @@
import { Input } from '@invoke-ai/ui-library';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/useStringField';
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
export const StringFieldInput = memo(
(props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
const { value, onChange } = useStringField(props);
return <Input className="nodrag nowheel nopan" value={value} onChange={onChange} />;
}
);
StringFieldInput.displayName = 'StringFieldInput';

View File

@@ -0,0 +1,25 @@
import { Textarea } from '@invoke-ai/ui-library';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/useStringField';
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
export const StringFieldTextarea = memo(
(props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
const { value, onChange } = useStringField(props);
return (
<Textarea
className="nodrag nowheel nopan"
value={value}
onChange={onChange}
h="full"
resize="none"
fontSize="sm"
p={2}
/>
);
}
);
StringFieldTextarea.displayName = 'StringFieldTextarea';

View File

@@ -1,17 +1,15 @@
import { Input, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { fieldStringValueChanged } from 'features/nodes/store/nodesSlice';
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useCallback } from 'react';
import type { FieldComponentProps } from './types';
const StringFieldInputComponent = (props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
export const useStringField = (props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
const { nodeId, field, fieldTemplate } = props;
const dispatch = useAppDispatch();
const handleValueChanged = useCallback(
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
dispatch(
fieldStringValueChanged({
@@ -24,11 +22,9 @@ const StringFieldInputComponent = (props: FieldComponentProps<StringFieldInputIn
[dispatch, field.name, nodeId]
);
if (fieldTemplate.ui_component === 'textarea') {
return <Textarea className="nodrag" onChange={handleValueChanged} value={field.value} rows={5} resize="none" />;
}
return <Input className="nodrag" onChange={handleValueChanged} value={field.value} />;
return {
value: field.value,
onChange,
defaultValue: fieldTemplate.default,
};
};
export default memo(StringFieldInputComponent);

View File

@@ -1,5 +1,5 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, FormControl } from '@invoke-ai/ui-library';
import { Combobox } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { fieldBoardValueChanged } from 'features/nodes/store/nodesSlice';
import type { BoardFieldInputInstance, BoardFieldInputTemplate } from 'features/nodes/types/field';
@@ -57,15 +57,15 @@ const BoardFieldInputComponent = (props: FieldComponentProps<BoardFieldInputInst
const noOptionsMessage = useCallback(() => t('boards.noMatching'), [t]);
return (
<FormControl className="nowheel nodrag" isDisabled={!hasBoards}>
<Combobox
value={value}
options={options}
onChange={onChange}
placeholder={t('boards.selectBoard')}
noOptionsMessage={noOptionsMessage}
/>
</FormControl>
<Combobox
className="nowheel nodrag"
value={value}
options={options}
onChange={onChange}
placeholder={t('boards.selectBoard')}
noOptionsMessage={noOptionsMessage}
isDisabled={!hasBoards}
/>
);
};

View File

@@ -27,7 +27,7 @@ const BooleanFieldInputComponent = (
[dispatch, field.name, nodeId]
);
return <Switch className="nodrag" onChange={handleValueChanged} isChecked={field.value}></Switch>;
return <Switch className="nodrag" onChange={handleValueChanged} isChecked={field.value} />;
};
export default memo(BooleanFieldInputComponent);

View File

@@ -1,4 +1,4 @@
import { Box } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { hexToRGBA, rgbaToHex } from 'common/util/colorCodeTransformers';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
@@ -49,7 +49,7 @@ const ColorFieldInputComponent = (props: FieldComponentProps<ColorFieldInputInst
);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Flex flexDir="column" gap={2} w="full">
<HexColorInput
style={{
background: colorTokenToCssVar('base.700'),
@@ -67,7 +67,7 @@ const ColorFieldInputComponent = (props: FieldComponentProps<ColorFieldInputInst
alpha
/>
<RgbaColorPicker className="nodrag" color={color} onChange={handleValueChanged} style={{ width: '100%' }} />
</Box>
</Flex>
);
};

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