Compare commits

...

165 Commits

Author SHA1 Message Date
Mary Hipp
59bd6b935d wip missing fields prototype 2025-02-19 16:00:33 -05:00
Mary Hipp
4bba7de070 fix omnipresent pencil 2025-02-19 09:52:37 -05:00
psychedelicious
e1f2b232c8 feat(ui): color picker improvements
- Support transparency w/ color picker. To do this, we need to hide the bg layer before sampling. In testing, this has a negligible performance impact.
- Add an RGBA value readout next to the color picker ring.
2025-02-18 15:38:50 +11:00
psychedelicious
2c5b0195fc fix(ui): straight lines drawn with shift-click get cut off when canvas moved between clicks
Need to opt-out of the clipping logic when using shift-click to not cut off the line.
2025-02-18 15:38:50 +11:00
psychedelicious
56792b2d2c fix(ui): mask layers not showing up until you zoom
Unfortunately I couldn't reliably reproduce the issue, so I'm not 100% sure this fixes it. But I think there is a race condition that results in `updateCompositingRectSize` erroneously seeing the layer has no objects and skipping the update.

To address this, the compositing rect fill/size/pos are all now force-updated when the fill/objects are changed. Theoretically it should be impossible for the issue to occur now.
2025-02-18 15:38:50 +11:00
psychedelicious
d71e8b4980 fix(ui): cursor visibility
- Fix an issue where the cursor disappeared when selecting a non-renderable entity. For example, when selecting a reference image layer and certain tools, the cursor would disappear.
- Ensure color picker works no matter what layer types are selected.

The logic for showing/hiding the cursor needed to be rearranged a bit for this fix.
2025-02-18 15:38:50 +11:00
Mary Hipp
ca50f8193c add AppFeature for retryQueueItem in case we want to easily disable 2025-02-18 09:14:03 +11:00
psychedelicious
7ee636b68b feat(ui): add retry buttons to queue tab
- Add the new HTTP endpoint to the queue client
- Add buttons to the queue items to retry them
2025-02-18 09:14:03 +11:00
psychedelicious
926f69677a chore(ui): typegen 2025-02-18 09:14:03 +11:00
psychedelicious
675ac348de feat(app): add retry queue item functionality
Retrying a queue item means cloning it, resetting all execution-related state. Retried queue items reference the item they were retried from by id. This relationship is not enforced by any DB constraints.

- Add `retried_from_item_id` to `session_queue` table in DB in a migration.
- Add `retry_items_by_id` method to session queue service. Accepts a list of queue item IDs and clones them (minus execution state). Returns a list of retried items. Items that are not in a canceled or failed state are skipped.
- Add `retry_items_by_id` HTTP endpoint that maps 1-to-1 to the queue service method.
- Add `queue_items_retried` event, which includes the list of retried items.
2025-02-18 09:14:03 +11:00
psychedelicious
62e5b9da18 docs(ui): add comments for recent perf optimizations 2025-02-17 09:28:13 +11:00
psychedelicious
65eabde297 per(ui): move field desc content to own component 2025-02-17 09:28:13 +11:00
psychedelicious
6bebd2bfc8 chore(ui): lint 2025-02-17 09:28:13 +11:00
psychedelicious
cd785ba64b perf(ui): optimize field handle/title/etc rendering 2025-02-17 09:28:13 +11:00
psychedelicious
726b4637db perf(ui): optimize workflow editor inspector panel rendering 2025-02-17 09:28:13 +11:00
psychedelicious
b50241fe6a perf(ui): make field description popver rendering lazy 2025-02-17 09:28:13 +11:00
psychedelicious
5b8735db3b perf(ui): optimize node update checking 2025-02-17 09:28:13 +11:00
psychedelicious
ce286363d0 perf(ui): optimize checking if a field value is changed by wrapping in single selector 2025-02-17 09:28:13 +11:00
psychedelicious
2fa47cf270 perf(ui): use lazy rendering for builder element settings popovers 2025-02-17 09:28:13 +11:00
psychedelicious
3446486f40 perf(ui): do not use memoized selector for control adapter state 2025-02-17 09:28:13 +11:00
psychedelicious
a0cdcdef57 perf(ui): debounce invoke readiness calculations 2025-02-17 09:28:13 +11:00
psychedelicious
abbb3609c8 fix(ui): race condition that causes non-user-facing error when handling canvas filter cancelations
The abortController could be null by the time we attempt to abort it
2025-02-17 09:28:13 +11:00
psychedelicious
700ad78f87 Revert "perf(ui): connection line issue on chrome"
This reverts commit 9d482e5fe621c2dbbde18ed17301a12b0e7f2580.
2025-02-17 09:28:13 +11:00
psychedelicious
cfb08f326e perf(ui): fix issue w/ add node cmdk component (more fixed) 2025-02-17 09:28:13 +11:00
psychedelicious
aae4fa3cca perf(ui): reduce animations which slow down reactflow 2025-02-17 09:28:13 +11:00
psychedelicious
109adc5a93 perf(ui): fix issue w/ add node cmdk component 2025-02-17 09:28:13 +11:00
psychedelicious
acb7ef8837 perf(ui): slightly more efficient gallery pagination componsts 2025-02-17 09:28:13 +11:00
psychedelicious
3c5e829c72 feat(ui): use new more efficient RTK upsert methods 2025-02-17 09:28:13 +11:00
psychedelicious
10d9e75391 fix(ui): rtk upgrade TS issues 2025-02-17 09:28:13 +11:00
psychedelicious
b6a892a673 chore(ui): bump @reduxjs/toolkit to latest 2025-02-17 09:28:13 +11:00
psychedelicious
479d5cc362 perf(ui): isolate a lot of root-level hooks in a memoized component 2025-02-17 09:28:13 +11:00
psychedelicious
01e4fd100f perf(ui): optimized invocation node component structure 2025-02-17 09:28:13 +11:00
psychedelicious
8ecf9fb7e3 perf(ui): connection line issue on chrome 2025-02-17 09:28:13 +11:00
psychedelicious
436d5ee0c6 chore(ui): lint 2025-02-17 09:28:13 +11:00
psychedelicious
0671fec844 perf(ui): workflow editor misc
- Optimize component and hook structure for input fields to reduce rerenders of component tree
- Remove memoization on some selectors where it serves no purpose (bc the object will have a stable identity until it changes, at which point we need to re-render anyways)
- Shift the connection error selector logic around to rely more on the stable identity of pending connection objects
2025-02-17 09:28:13 +11:00
Eugene Brodsky
4dbde53f9b fix(docker): use the node22 image for the frontend build 2025-02-15 17:21:34 -05:00
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
248 changed files with 7931 additions and 4274 deletions

View File

@@ -58,7 +58,7 @@ RUN --mount=type=cache,target=/home/ubuntu/.cache/uv,uid=1000,gid=1000 \
#### Build the Web UI ------------------------------------
FROM node:20-slim AS web-builder
FROM docker.io/node:22-slim AS web-builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack use pnpm@8.x

View File

@@ -16,6 +16,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
ClearResult,
EnqueueBatchResult,
PruneResult,
RetryItemsResult,
SessionQueueCountsByDestination,
SessionQueueItem,
SessionQueueItemDTO,
@@ -135,6 +136,19 @@ async def cancel_by_destination(
)
@session_queue_router.put(
"/{queue_id}/retry_items_by_id",
operation_id="retry_items_by_id",
responses={200: {"model": RetryItemsResult}},
)
async def retry_items_by_id(
queue_id: str = Path(description="The queue id to perform this operation on"),
item_ids: list[int] = Body(description="The queue item ids to retry"),
) -> RetryItemsResult:
"""Immediately cancels all queue items with the given origin"""
return ApiDependencies.invoker.services.session_queue.retry_items_by_id(queue_id=queue_id, item_ids=item_ids)
@session_queue_router.put(
"/{queue_id}/clear",
operation_id="clear",

View File

@@ -28,6 +28,7 @@ from invokeai.app.services.events.events_common import (
ModelLoadCompleteEvent,
ModelLoadStartedEvent,
QueueClearedEvent,
QueueItemsRetriedEvent,
QueueItemStatusChangedEvent,
)
@@ -39,6 +40,7 @@ if TYPE_CHECKING:
from invokeai.app.services.session_queue.session_queue_common import (
BatchStatus,
EnqueueBatchResult,
RetryItemsResult,
SessionQueueItem,
SessionQueueStatus,
)
@@ -99,6 +101,10 @@ class EventServiceBase:
"""Emitted when a batch is enqueued"""
self.dispatch(BatchEnqueuedEvent.build(enqueue_result))
def emit_queue_items_retried(self, retry_result: "RetryItemsResult") -> None:
"""Emitted when a list of queue items are retried"""
self.dispatch(QueueItemsRetriedEvent.build(retry_result))
def emit_queue_cleared(self, queue_id: str) -> None:
"""Emitted when a queue is cleared"""
self.dispatch(QueueClearedEvent.build(queue_id))

View File

@@ -10,6 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
QUEUE_ITEM_STATUS,
BatchStatus,
EnqueueBatchResult,
RetryItemsResult,
SessionQueueItem,
SessionQueueStatus,
)
@@ -290,6 +291,22 @@ class BatchEnqueuedEvent(QueueEventBase):
)
@payload_schema.register
class QueueItemsRetriedEvent(QueueEventBase):
"""Event model for queue_items_retried"""
__event_name__ = "queue_items_retried"
retried_item_ids: list[int] = Field(description="The IDs of the queue items that were retried")
@classmethod
def build(cls, retry_result: RetryItemsResult) -> "QueueItemsRetriedEvent":
return cls(
queue_id=retry_result.queue_id,
retried_item_ids=retry_result.retried_item_ids,
)
@payload_schema.register
class QueueClearedEvent(QueueEventBase):
"""Event model for queue_cleared"""

View File

@@ -14,6 +14,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
IsEmptyResult,
IsFullResult,
PruneResult,
RetryItemsResult,
SessionQueueCountsByDestination,
SessionQueueItem,
SessionQueueItemDTO,
@@ -139,3 +140,8 @@ class SessionQueueBase(ABC):
def set_queue_item_session(self, item_id: int, session: GraphExecutionState) -> SessionQueueItem:
"""Sets the session for a session queue item. Use this to update the session state."""
pass
@abstractmethod
def retry_items_by_id(self, queue_id: str, item_ids: list[int]) -> RetryItemsResult:
"""Retries the given queue items"""
pass

View File

@@ -234,6 +234,9 @@ class SessionQueueItemWithoutGraph(BaseModel):
field_values: Optional[list[NodeFieldValue]] = Field(
default=None, description="The field values that were used for this queue item"
)
retried_from_item_id: Optional[int] = Field(
default=None, description="The item_id of the queue item that this item was retried from"
)
@classmethod
def queue_item_dto_from_dict(cls, queue_item_dict: dict) -> "SessionQueueItemDTO":
@@ -344,6 +347,11 @@ class EnqueueBatchResult(BaseModel):
priority: int = Field(description="The priority of the enqueued batch")
class RetryItemsResult(BaseModel):
queue_id: str = Field(description="The ID of the queue")
retried_item_ids: list[int] = Field(description="The IDs of the queue items that were retried")
class ClearResult(BaseModel):
"""Result of clearing the session queue"""
@@ -481,6 +489,7 @@ class SessionQueueValueToInsert(NamedTuple):
workflow: Optional[str] # workflow json
origin: str | None
destination: str | None
retried_from_item_id: int | None = None
ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert]
@@ -493,16 +502,16 @@ def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new
session.id = uuid_string()
values_to_insert.append(
SessionQueueValueToInsert(
queue_id, # queue_id
session.model_dump_json(warnings=False, exclude_none=True), # session (json)
session.id, # session_id
batch.batch_id, # batch_id
queue_id=queue_id,
session=session.model_dump_json(warnings=False, exclude_none=True), # as json
session_id=session.id,
batch_id=batch.batch_id,
# must use pydantic_encoder bc field_values is a list of models
json.dumps(field_values, default=to_jsonable_python) if field_values else None, # field_values (json)
priority, # priority
json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json)
batch.origin, # origin
batch.destination, # destination
field_values=json.dumps(field_values, default=to_jsonable_python) if field_values else None, # as json
priority=priority,
workflow=json.dumps(workflow, default=to_jsonable_python) if workflow else None, # as json
origin=batch.origin,
destination=batch.destination,
)
)
return values_to_insert

View File

@@ -1,7 +1,10 @@
import json
import sqlite3
import threading
from typing import Optional, Union, cast
from pydantic_core import to_jsonable_python
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.session_queue.session_queue_base import SessionQueueBase
from invokeai.app.services.session_queue.session_queue_common import (
@@ -18,11 +21,13 @@ from invokeai.app.services.session_queue.session_queue_common import (
IsEmptyResult,
IsFullResult,
PruneResult,
RetryItemsResult,
SessionQueueCountsByDestination,
SessionQueueItem,
SessionQueueItemDTO,
SessionQueueItemNotFoundError,
SessionQueueStatus,
SessionQueueValueToInsert,
calc_session_count,
prepare_values_to_insert,
)
@@ -130,8 +135,8 @@ class SqliteSessionQueue(SessionQueueBase):
self.__cursor.executemany(
"""--sql
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination, retried_from_item_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
values_to_insert,
)
@@ -761,3 +766,71 @@ class SqliteSessionQueue(SessionQueueBase):
canceled=counts.get("canceled", 0),
total=total,
)
def retry_items_by_id(self, queue_id: str, item_ids: list[int]) -> RetryItemsResult:
"""Retries the given queue items"""
try:
self.__lock.acquire()
values_to_insert: list[SessionQueueValueToInsert] = []
retried_item_ids: list[int] = []
for item_id in item_ids:
queue_item = self.get_queue_item(item_id)
if queue_item.status not in ("failed", "canceled"):
continue
retried_item_ids.append(item_id)
field_values_json = (
json.dumps(queue_item.field_values, default=to_jsonable_python) if queue_item.field_values else None
)
workflow_json = (
json.dumps(queue_item.workflow, default=to_jsonable_python) if queue_item.workflow else None
)
cloned_session = GraphExecutionState(graph=queue_item.session.graph)
cloned_session_json = cloned_session.model_dump_json(warnings=False, exclude_none=True)
retried_from_item_id = (
queue_item.retried_from_item_id
if queue_item.retried_from_item_id is not None
else queue_item.item_id
)
value_to_insert = SessionQueueValueToInsert(
queue_id=queue_item.queue_id,
batch_id=queue_item.batch_id,
destination=queue_item.destination,
field_values=field_values_json,
origin=queue_item.origin,
priority=queue_item.priority,
workflow=workflow_json,
session=cloned_session_json,
session_id=cloned_session.id,
retried_from_item_id=retried_from_item_id,
)
values_to_insert.append(value_to_insert)
# TODO(psyche): Handle max queue size?
self.__cursor.executemany(
"""--sql
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination, retried_from_item_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
values_to_insert,
)
self.__conn.commit()
except Exception:
self.__conn.rollback()
raise
finally:
self.__lock.release()
retry_result = RetryItemsResult(
queue_id=queue_id,
retried_item_ids=retried_item_ids,
)
self.__invoker.services.events.emit_queue_items_retried(retry_result)
return retry_result

View File

@@ -18,6 +18,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_12 import
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_13 import build_migration_13
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_14 import build_migration_14
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_15 import build_migration_15
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_16 import build_migration_16
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
@@ -53,6 +54,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator.register_migration(build_migration_13())
migrator.register_migration(build_migration_14())
migrator.register_migration(build_migration_15())
migrator.register_migration(build_migration_16())
migrator.run_migrations()
return db

View File

@@ -0,0 +1,31 @@
import sqlite3
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
class Migration16Callback:
def __call__(self, cursor: sqlite3.Cursor) -> None:
self._add_retried_from_item_id_col(cursor)
def _add_retried_from_item_id_col(self, cursor: sqlite3.Cursor) -> None:
"""
- Adds `retried_from_item_id` column to the session queue table.
"""
cursor.execute("ALTER TABLE session_queue ADD COLUMN retried_from_item_id INTEGER;")
def build_migration_16() -> Migration:
"""
Build the migration from database version 15 to 16.
This migration does the following:
- Adds `retried_from_item_id` column to the session queue table.
"""
migration_16 = Migration(
from_version=15,
to_version=16,
callback=Migration16Callback(),
)
return migration_16

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",
"@reduxjs/toolkit": "2.5.1",
"@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",

View File

@@ -24,23 +24,26 @@ dependencies:
specifier: ^5.1.0
version: 5.1.0
'@invoke-ai/ui-library':
specifier: ^0.0.44
version: 0.0.44(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
specifier: ^0.0.46
version: 0.0.46(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
'@nanostores/react':
specifier: ^0.7.3
version: 0.7.3(nanostores@0.11.3)(react@18.3.1)
'@reduxjs/toolkit':
specifier: 2.2.3
version: 2.2.3(react-redux@9.1.2)(react@18.3.1)
specifier: 2.5.1
version: 2.5.1(react-redux@9.1.2)(react@18.3.1)
'@roarr/browser-log-writer':
specifier: ^1.3.0
version: 1.3.0
'@xyflow/react':
specifier: ^12.4.2
version: 12.4.2(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
async-mutex:
specifier: ^0.5.0
version: 0.5.0
chakra-react-select:
specifier: ^4.9.2
version: 4.9.2(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
version: 4.9.2(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.14.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
cmdk:
specifier: ^1.0.0
version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
@@ -137,15 +140,15 @@ dependencies:
react-resizable-panels:
specifier: ^2.1.4
version: 2.1.4(react-dom@18.3.1)(react@18.3.1)
react-textarea-autosize:
specifier: ^8.5.7
version: 8.5.7(@types/react@18.3.11)(react@18.3.1)
react-use:
specifier: ^17.5.1
version: 17.5.1(react-dom@18.3.1)(react@18.3.1)
react-virtuoso:
specifier: ^4.10.4
version: 4.10.4(react-dom@18.3.1)(react@18.3.1)
reactflow:
specifier: ^11.11.4
version: 11.11.4(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
redux-dynamic-middlewares:
specifier: ^2.2.0
version: 2.2.0
@@ -573,7 +576,7 @@ packages:
'@chakra-ui/react-types': 2.0.7(react@18.3.1)
'@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1)
'@chakra-ui/system': 2.6.2(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(react@18.3.1)
react: 18.3.1
dev: false
@@ -596,7 +599,7 @@ packages:
react: '>=18'
dependencies:
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1)
'@chakra-ui/system': 2.6.2(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(react@18.3.1)
react: 18.3.1
dev: false
@@ -606,7 +609,7 @@ packages:
'@chakra-ui/react': '>=2.0.0'
react: '>=18'
dependencies:
'@chakra-ui/react': 2.10.4(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/react': 2.10.4(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
dev: false
@@ -622,7 +625,7 @@ packages:
'@chakra-ui/react-children-utils': 2.0.6(react@18.3.1)
'@chakra-ui/react-context': 2.1.0(react@18.3.1)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1)
'@chakra-ui/system': 2.6.2(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(react@18.3.1)
react: 18.3.1
dev: false
@@ -639,7 +642,7 @@ packages:
'@chakra-ui/breakpoint-utils': 2.0.8
'@chakra-ui/react-env': 3.1.0(react@18.3.1)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1)
'@chakra-ui/system': 2.6.2(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(react@18.3.1)
react: 18.3.1
dev: false
@@ -664,7 +667,7 @@ packages:
'@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1)
'@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1)
'@chakra-ui/system': 2.6.2(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(react@18.3.1)
'@chakra-ui/transition': 2.1.0(framer-motion@11.10.0)(react@18.3.1)
framer-motion: 11.10.0(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
@@ -829,7 +832,7 @@ packages:
react: 18.3.1
dev: false
/@chakra-ui/react@2.10.4(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1):
/@chakra-ui/react@2.10.4(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-XyRWnuZ1Uw7Mlj5pKUGO5/WhnIHP/EOrpy6lGZC1yWlkd0eIfIpYMZ1ALTZx4KPEdbBaes48dgiMT2ROCqLhkA==}
peerDependencies:
'@emotion/react': '>=11'
@@ -842,8 +845,8 @@ packages:
'@chakra-ui/styled-system': 2.12.1(react@18.3.1)
'@chakra-ui/theme': 3.4.7(@chakra-ui/styled-system@2.12.1)(react@18.3.1)
'@chakra-ui/utils': 2.2.3(react@18.3.1)
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.11)(react@18.3.1)
'@emotion/react': 11.14.0(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0)(@types/react@18.3.11)(react@18.3.1)
'@popperjs/core': 2.11.8
'@zag-js/focus-visible': 0.31.1
aria-hidden: 1.2.4
@@ -868,7 +871,7 @@ packages:
react: '>=18'
dependencies:
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1)
'@chakra-ui/system': 2.6.2(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(react@18.3.1)
react: 18.3.1
dev: false
@@ -889,7 +892,7 @@ packages:
lodash.mergewith: 4.6.2
dev: false
/@chakra-ui/system@2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1):
/@chakra-ui/system@2.6.2(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(react@18.3.1):
resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==}
peerDependencies:
'@emotion/react': ^11.0.0
@@ -902,8 +905,8 @@ packages:
'@chakra-ui/styled-system': 2.9.2
'@chakra-ui/theme-utils': 2.0.21
'@chakra-ui/utils': 2.0.15
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.11)(react@18.3.1)
'@emotion/react': 11.14.0(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0)(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-fast-compare: 3.2.2
dev: false
@@ -1024,6 +1027,24 @@ packages:
- supports-color
dev: false
/@emotion/babel-plugin@11.13.5:
resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
dependencies:
'@babel/helper-module-imports': 7.25.7
'@babel/runtime': 7.25.7
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
'@emotion/serialize': 1.3.3
babel-plugin-macros: 3.1.0
convert-source-map: 1.9.0
escape-string-regexp: 4.0.0
find-root: 1.1.0
source-map: 0.5.7
stylis: 4.2.0
transitivePeerDependencies:
- supports-color
dev: false
/@emotion/cache@11.13.1:
resolution: {integrity: sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==}
dependencies:
@@ -1034,6 +1055,16 @@ packages:
stylis: 4.2.0
dev: false
/@emotion/cache@11.14.0:
resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==}
dependencies:
'@emotion/memoize': 0.9.0
'@emotion/sheet': 1.4.0
'@emotion/utils': 1.4.2
'@emotion/weak-memoize': 0.4.0
stylis: 4.2.0
dev: false
/@emotion/hash@0.9.2:
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
dev: false
@@ -1085,6 +1116,29 @@ packages:
- supports-color
dev: false
/@emotion/react@11.14.0(@types/react@18.3.11)(react@18.3.1):
resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==}
peerDependencies:
'@types/react': '*'
react: '>=16.8.0'
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.7
'@emotion/babel-plugin': 11.13.5
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1)
'@emotion/utils': 1.4.2
'@emotion/weak-memoize': 0.4.0
'@types/react': 18.3.11
hoist-non-react-statics: 3.3.2
react: 18.3.1
transitivePeerDependencies:
- supports-color
dev: false
/@emotion/serialize@1.3.2:
resolution: {integrity: sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==}
dependencies:
@@ -1095,12 +1149,22 @@ packages:
csstype: 3.1.3
dev: false
/@emotion/serialize@1.3.3:
resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==}
dependencies:
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
'@emotion/unitless': 0.10.0
'@emotion/utils': 1.4.2
csstype: 3.1.3
dev: false
/@emotion/sheet@1.4.0:
resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==}
dev: false
/@emotion/styled@11.13.0(@emotion/react@11.13.3)(@types/react@18.3.11)(react@18.3.1):
resolution: {integrity: sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==}
/@emotion/styled@11.14.0(@emotion/react@11.14.0)(@types/react@18.3.11)(react@18.3.1):
resolution: {integrity: sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==}
peerDependencies:
'@emotion/react': ^11.0.0-rc.0
'@types/react': '*'
@@ -1110,12 +1174,12 @@ packages:
optional: true
dependencies:
'@babel/runtime': 7.25.7
'@emotion/babel-plugin': 11.12.0
'@emotion/babel-plugin': 11.13.5
'@emotion/is-prop-valid': 1.3.1
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@emotion/serialize': 1.3.2
'@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1)
'@emotion/utils': 1.4.1
'@emotion/react': 11.14.0(@types/react@18.3.11)(react@18.3.1)
'@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1)
'@emotion/utils': 1.4.2
'@types/react': 18.3.11
react: 18.3.1
transitivePeerDependencies:
@@ -1134,10 +1198,22 @@ packages:
react: 18.3.1
dev: false
/@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1):
resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==}
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.3.1
dev: false
/@emotion/utils@1.4.1:
resolution: {integrity: sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==}
dev: false
/@emotion/utils@1.4.2:
resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==}
dev: false
/@emotion/weak-memoize@0.4.0:
resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==}
dev: false
@@ -1904,25 +1980,25 @@ packages:
prettier: 3.3.3
dev: true
/@invoke-ai/ui-library@0.0.44(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-PDseHmdr8oi8cmrpx3UwIYHn4NduAJX2R0pM0pyM54xrCMPMgYiCbC/eOs8Gt4fBc2ziiPZ9UGoW4evnE3YJsg==}
/@invoke-ai/ui-library@0.0.46(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-3YBuWWhRbTUHi0RZKeyvDEvweoyZmeBdUGJIhemjdAgGx6l98rAMeCs8IQH+SYjSAIhiGRGf45fQ33PDK8Jkmw==}
peerDependencies:
'@fontsource-variable/inter': ^5.0.16
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
'@chakra-ui/anatomy': 2.2.2
'@chakra-ui/anatomy': 2.3.5
'@chakra-ui/icons': 2.2.4(@chakra-ui/react@2.10.4)(react@18.3.1)
'@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1)
'@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/react': 2.10.4(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/styled-system': 2.9.2
'@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2)
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.11)(react@18.3.1)
'@chakra-ui/react': 2.10.4(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/styled-system': 2.12.1(react@18.3.1)
'@chakra-ui/theme-tools': 2.2.7(@chakra-ui/styled-system@2.12.1)(react@18.3.1)
'@emotion/react': 11.14.0(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0)(@types/react@18.3.11)(react@18.3.1)
'@fontsource-variable/inter': 5.1.0
'@nanostores/react': 0.7.3(nanostores@0.11.3)(react@18.3.1)
chakra-react-select: 4.9.2(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
chakra-react-select: 4.10.1(@chakra-ui/react@2.10.4)(@emotion/react@11.14.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1)
lodash-es: 4.17.21
nanostores: 0.11.3
@@ -1930,15 +2006,10 @@ packages:
overlayscrollbars-react: 0.5.6(overlayscrollbars@2.10.0)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-i18next: 15.0.2(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
react-icons: 5.3.0(react@18.3.1)
react-select: 5.8.0(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
react-i18next: 15.4.0(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
react-icons: 5.4.0(react@18.3.1)
react-select: 5.10.0(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
transitivePeerDependencies:
- '@chakra-ui/form-control'
- '@chakra-ui/icon'
- '@chakra-ui/media-query'
- '@chakra-ui/menu'
- '@chakra-ui/spinner'
- '@chakra-ui/system'
- '@types/react'
- i18next
@@ -2395,114 +2466,6 @@ packages:
react: 18.3.1
dev: false
/@reactflow/background@11.3.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.5(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/controls@11.2.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.5(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/core@11.11.4(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@types/d3': 7.4.3
'@types/d3-drag': 3.0.7
'@types/d3-selection': 3.0.10
'@types/d3-zoom': 3.0.8
classcat: 5.0.5
d3-drag: 3.0.0
d3-selection: 3.0.0
d3-zoom: 3.0.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.5(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/minimap@11.7.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
'@types/d3-selection': 3.0.10
'@types/d3-zoom': 3.0.8
classcat: 5.0.5
d3-selection: 3.0.0
d3-zoom: 3.0.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.5(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/node-resizer@2.2.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
classcat: 5.0.5
d3-drag: 3.0.0
d3-selection: 3.0.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.5(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/node-toolbar@1.3.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.5(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@redocly/ajv@8.11.2:
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
dependencies:
@@ -2536,10 +2499,10 @@ packages:
- supports-color
dev: true
/@reduxjs/toolkit@2.2.3(react-redux@9.1.2)(react@18.3.1):
resolution: {integrity: sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==}
/@reduxjs/toolkit@2.5.1(react-redux@9.1.2)(react@18.3.1):
resolution: {integrity: sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
peerDependenciesMeta:
react:
@@ -3673,137 +3636,26 @@ packages:
'@types/node': 20.16.10
dev: true
/@types/d3-array@3.2.1:
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
dev: false
/@types/d3-axis@3.0.6:
resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
dependencies:
'@types/d3-selection': 3.0.10
dev: false
/@types/d3-brush@3.0.6:
resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
dependencies:
'@types/d3-selection': 3.0.10
dev: false
/@types/d3-chord@3.0.6:
resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
dev: false
/@types/d3-color@3.1.3:
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
dev: false
/@types/d3-contour@3.0.6:
resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
dependencies:
'@types/d3-array': 3.2.1
'@types/geojson': 7946.0.14
dev: false
/@types/d3-delaunay@6.0.4:
resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
dev: false
/@types/d3-dispatch@3.0.6:
resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==}
dev: false
/@types/d3-drag@3.0.7:
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
dependencies:
'@types/d3-selection': 3.0.10
dev: false
/@types/d3-dsv@3.0.7:
resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
dev: false
/@types/d3-ease@3.0.2:
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
dev: false
/@types/d3-fetch@3.0.7:
resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
dependencies:
'@types/d3-dsv': 3.0.7
dev: false
/@types/d3-force@3.0.10:
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
dev: false
/@types/d3-format@3.0.4:
resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
dev: false
/@types/d3-geo@3.1.0:
resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
dependencies:
'@types/geojson': 7946.0.14
dev: false
/@types/d3-hierarchy@3.1.7:
resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
dev: false
/@types/d3-interpolate@3.0.4:
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
dependencies:
'@types/d3-color': 3.1.3
dev: false
/@types/d3-path@3.1.0:
resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==}
dev: false
/@types/d3-polygon@3.0.2:
resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
dev: false
/@types/d3-quadtree@3.0.6:
resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
dev: false
/@types/d3-random@3.0.3:
resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
dev: false
/@types/d3-scale-chromatic@3.0.3:
resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==}
dev: false
/@types/d3-scale@4.0.8:
resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
dependencies:
'@types/d3-time': 3.0.3
dev: false
/@types/d3-selection@3.0.10:
resolution: {integrity: sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==}
dev: false
/@types/d3-shape@3.1.6:
resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==}
dependencies:
'@types/d3-path': 3.1.0
dev: false
/@types/d3-time-format@4.0.3:
resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
dev: false
/@types/d3-time@3.0.3:
resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==}
dev: false
/@types/d3-timer@3.0.2:
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
dev: false
/@types/d3-transition@3.0.8:
resolution: {integrity: sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==}
dependencies:
@@ -3817,41 +3669,6 @@ packages:
'@types/d3-selection': 3.0.10
dev: false
/@types/d3@7.4.3:
resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
dependencies:
'@types/d3-array': 3.2.1
'@types/d3-axis': 3.0.6
'@types/d3-brush': 3.0.6
'@types/d3-chord': 3.0.6
'@types/d3-color': 3.1.3
'@types/d3-contour': 3.0.6
'@types/d3-delaunay': 6.0.4
'@types/d3-dispatch': 3.0.6
'@types/d3-drag': 3.0.7
'@types/d3-dsv': 3.0.7
'@types/d3-ease': 3.0.2
'@types/d3-fetch': 3.0.7
'@types/d3-force': 3.0.10
'@types/d3-format': 3.0.4
'@types/d3-geo': 3.1.0
'@types/d3-hierarchy': 3.1.7
'@types/d3-interpolate': 3.0.4
'@types/d3-path': 3.1.0
'@types/d3-polygon': 3.0.2
'@types/d3-quadtree': 3.0.6
'@types/d3-random': 3.0.3
'@types/d3-scale': 4.0.8
'@types/d3-scale-chromatic': 3.0.3
'@types/d3-selection': 3.0.10
'@types/d3-shape': 3.1.6
'@types/d3-time': 3.0.3
'@types/d3-time-format': 4.0.3
'@types/d3-timer': 3.0.2
'@types/d3-transition': 3.0.8
'@types/d3-zoom': 3.0.8
dev: false
/@types/dateformat@5.0.2:
resolution: {integrity: sha512-M95hNBMa/hnwErH+a+VOD/sYgTmo15OTYTM2Hr52/e0OdOuY+Crag+kd3/ioZrhg0WGbl9Sm3hR7UU+MH6rfOw==}
dev: true
@@ -3901,10 +3718,6 @@ packages:
'@types/serve-static': 1.15.7
dev: true
/@types/geojson@7946.0.14:
resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==}
dev: false
/@types/hast@3.0.4:
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
dependencies:
@@ -3936,7 +3749,7 @@ packages:
/@types/lodash.mergewith@4.6.7:
resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==}
dependencies:
'@types/lodash': 4.17.10
'@types/lodash': 4.17.15
dev: false
/@types/lodash.mergewith@4.6.9:
@@ -3948,6 +3761,10 @@ packages:
/@types/lodash@4.17.10:
resolution: {integrity: sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==}
/@types/lodash@4.17.15:
resolution: {integrity: sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==}
dev: false
/@types/mdx@2.0.13:
resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
dev: true
@@ -4450,6 +4267,34 @@ packages:
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
dev: false
/@xyflow/react@12.4.2(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-AFJKVc/fCPtgSOnRst3xdYJwiEcUN9lDY7EO/YiRvFHYCJGgfzg+jpvZjkTOnBLGyrMJre9378pRxAc3fsR06A==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@xyflow/system': 0.0.50
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.5(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@xyflow/system@0.0.50:
resolution: {integrity: sha512-HVUZd4LlY88XAaldFh2nwVxDOcdIBxGpQ5txzwfJPf+CAjj2BfYug1fHs2p4yS7YO8H6A3EFJQovBE8YuHkAdg==}
dependencies:
'@types/d3-drag': 3.0.7
'@types/d3-selection': 3.0.10
'@types/d3-transition': 3.0.8
'@types/d3-zoom': 3.0.8
d3-drag: 3.0.0
d3-selection: 3.0.0
d3-zoom: 3.0.0
dev: false
/@zag-js/dom-query@0.31.1:
resolution: {integrity: sha512-oiuohEXAXhBxpzzNm9k2VHGEOLC1SXlXSbRPcfBZ9so5NRQUA++zCE7cyQJqGLTZR0t3itFLlZqDbYEXRrefwg==}
dev: false
@@ -4941,7 +4786,25 @@ packages:
pathval: 2.0.0
dev: true
/chakra-react-select@4.9.2(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
/chakra-react-select@4.10.1(@chakra-ui/react@2.10.4)(@emotion/react@11.14.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-0d7lubrmcm7molVYNYWEYi7o71W8wn/WruINon+m23XQLYvJ+bZlYVawDdWYdJjX8O1nzJlTDo4b7CB6zTsr4A==}
peerDependencies:
'@chakra-ui/react': 2.x
'@emotion/react': ^11.8.1
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
'@chakra-ui/react': 2.10.4(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@emotion/react': 11.14.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-select: 5.8.0(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- supports-color
dev: false
/chakra-react-select@4.9.2(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.14.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-uhvKAJ1I2lbIwdn+wx0YvxX5rtQVI0gXL0apx0CXm3blIxk7qf6YuCh2TnGuGKst8gj8jUFZyhYZiGlcvgbBRQ==}
peerDependencies:
'@chakra-ui/form-control': ^2.0.0
@@ -4961,8 +4824,8 @@ packages:
'@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1)
'@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.10.0)(react@18.3.1)
'@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1)
'@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1)
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@chakra-ui/system': 2.6.2(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(react@18.3.1)
'@emotion/react': 11.14.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-select: 5.8.0(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
@@ -8271,6 +8134,26 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/react-i18next@15.4.0(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.25.7
html-parse-stringify: 3.0.1
i18next: 23.15.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/react-icons@5.3.0(react@18.3.1):
resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==}
peerDependencies:
@@ -8279,6 +8162,14 @@ packages:
react: 18.3.1
dev: false
/react-icons@5.4.0(react@18.3.1):
resolution: {integrity: sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==}
peerDependencies:
react: '*'
dependencies:
react: 18.3.1
dev: false
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -8377,6 +8268,28 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/react-select@5.10.0(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-k96gw+i6N3ExgDwPIg0lUPmexl1ygPe6u5BdQFNBhkpbwroIgCNXdubtIzHfThYXYYTubwOBafoMnn7ruEP1xA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
dependencies:
'@babel/runtime': 7.25.7
'@emotion/cache': 11.14.0
'@emotion/react': 11.14.0(@types/react@18.3.11)(react@18.3.1)
'@floating-ui/dom': 1.6.11
'@types/react-transition-group': 4.4.11
memoize-one: 6.0.0
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-transition-group: 4.4.5(react-dom@18.3.1)(react@18.3.1)
use-isomorphic-layout-effect: 1.2.0(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- supports-color
dev: false
/react-select@5.8.0(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==}
peerDependencies:
@@ -8416,6 +8329,20 @@ packages:
tslib: 2.7.0
dev: false
/react-textarea-autosize@8.5.7(@types/react@18.3.11)(react@18.3.1):
resolution: {integrity: sha512-2MqJ3p0Jh69yt9ktFIaZmORHXw4c4bxSIhCeWiFwmJ9EYKgLmuNII3e9c9b2UO+ijl4StnpZdqpxNIhTdHvqtQ==}
engines: {node: '>=10'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
dependencies:
'@babel/runtime': 7.25.7
react: 18.3.1
use-composed-ref: 1.4.0(@types/react@18.3.11)(react@18.3.1)
use-latest: 1.3.0(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
dev: false
/react-transition-group@4.4.5(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@@ -8481,25 +8408,6 @@ packages:
dependencies:
loose-envify: 1.4.0
/reactflow@11.11.4(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/background': 11.3.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
'@reactflow/controls': 11.2.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
'@reactflow/core': 11.11.4(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
'@reactflow/minimap': 11.7.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
'@reactflow/node-resizer': 2.2.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
'@reactflow/node-toolbar': 1.3.14(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -9646,6 +9554,19 @@ packages:
tslib: 2.7.0
dev: false
/use-composed-ref@1.4.0(@types/react@18.3.11)(react@18.3.1):
resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.11
react: 18.3.1
dev: false
/use-debounce@10.0.3(react@18.3.1):
resolution: {integrity: sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg==}
engines: {node: '>= 16.0.0'}
@@ -9676,6 +9597,33 @@ packages:
react: 18.3.1
dev: false
/use-isomorphic-layout-effect@1.2.0(@types/react@18.3.11)(react@18.3.1):
resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.11
react: 18.3.1
dev: false
/use-latest@1.3.0(@types/react@18.3.11)(react@18.3.1):
resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.11
react: 18.3.1
use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.11)(react@18.3.1)
dev: false
/use-sidecar@1.1.2(@types/react@18.3.11)(react@18.3.1):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}

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",
@@ -225,6 +228,8 @@
"cancelTooltip": "Cancel Current Item",
"cancelSucceeded": "Item Canceled",
"cancelFailed": "Problem Canceling Item",
"retrySucceeded": "Item Retried",
"retryFailed": "Problem Retrying Item",
"confirm": "Confirm",
"prune": "Prune",
"pruneTooltip": "Prune {{item_count}} Completed Items",
@@ -236,6 +241,7 @@
"clearFailed": "Problem Clearing Queue",
"cancelBatch": "Cancel Batch",
"cancelItem": "Cancel Item",
"retryItem": "Retry Item",
"cancelBatchSucceeded": "Batch Canceled",
"cancelBatchFailed": "Problem Canceling Batch",
"clearQueueAlertDialog": "Clearing the queue immediately cancels any processing items and clears the queue entirely. Pending filters will be canceled.",
@@ -931,6 +937,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",
@@ -938,6 +945,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",
@@ -952,6 +960,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",
@@ -1694,7 +1703,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",

View File

@@ -27,6 +27,7 @@ import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterM
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal';
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { useReadinessWatcher } from 'features/queue/store/readiness';
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
@@ -53,49 +54,20 @@ interface Props {
}
const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
const language = useAppSelector(selectLanguage);
const logger = useLogger('system');
const dispatch = useAppDispatch();
const clearStorage = useClearStorage();
// singleton!
useSocketIO();
useGlobalModifiersInit();
useGlobalHotkeys();
useGetOpenAPISchemaQuery();
useSyncLoggingConfig();
const handleReset = useCallback(() => {
clearStorage();
location.reload();
return false;
}, [clearStorage]);
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
useEffect(() => {
if (size(config)) {
logger.info({ config }, 'Received config');
dispatch(configChanged(config));
}
}, [dispatch, config, logger]);
useEffect(() => {
dispatch(appStarted());
}, [dispatch]);
useStudioInitAction(studioInitAction);
useStarterModelsToast();
useSyncQueueStatus();
useFocusRegionWatcher();
return (
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
<Box id="invoke-app-wrapper" w="100dvw" h="100dvh" position="relative" overflow="hidden">
<AppContent />
</Box>
<HookIsolator config={config} studioInitAction={studioInitAction} />
<DeleteImageModal />
<ChangeBoardModal />
<DynamicPromptsModal />
@@ -122,3 +94,43 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
};
export default memo(App);
// Running these hooks in a separate component ensures we do not inadvertently rerender the entire app when they change.
const HookIsolator = memo(
({ config, studioInitAction }: { config: PartialAppConfig; studioInitAction?: StudioInitAction }) => {
const language = useAppSelector(selectLanguage);
const logger = useLogger('system');
const dispatch = useAppDispatch();
// singleton!
useReadinessWatcher();
useSocketIO();
useGlobalModifiersInit();
useGlobalHotkeys();
useGetOpenAPISchemaQuery();
useSyncLoggingConfig();
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
useEffect(() => {
if (size(config)) {
logger.info({ config }, 'Received config');
dispatch(configChanged(config));
}
}, [dispatch, config, logger]);
useEffect(() => {
dispatch(appStarted());
}, [dispatch]);
useStudioInitAction(studioInitAction);
useStarterModelsToast();
useSyncQueueStatus();
useFocusRegionWatcher();
return null;
}
);
HookIsolator.displayName = 'HookIsolator';

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

@@ -1,6 +1,4 @@
import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit';
import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors';
import type { RootState } from 'app/store/store';
import { isEqual } from 'lodash-es';
/**
@@ -14,11 +12,9 @@ export const createMemoizedSelector = createSelectorCreator({
argsMemoize: lruMemoize,
});
export const getSelectorsOptions: GetSelectorsOptions = {
export const getSelectorsOptions = {
createSelector: createDraftSafeSelectorCreator({
memoize: lruMemoize,
argsMemoize: lruMemoize,
}),
};
export const createMemoizedAppSelector = createMemoizedSelector.withTypes<RootState>();

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

@@ -26,7 +26,8 @@ export type AppFeature =
| 'modelCache'
| 'bulkDownload'
| 'starterModels'
| 'hfToken';
| 'hfToken'
| 'retryQueueItem';
/**
* A disable-able Stable Diffusion feature
*/

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,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,5 +1,5 @@
import { Flex, IconButton } from '@invoke-ai/ui-library';
import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector';
import { createSelector } from '@reduxjs/toolkit';
import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
@@ -34,7 +34,7 @@ import type {
} from 'services/api/types';
const buildSelectControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) =>
createMemoizedAppSelector(selectCanvasSlice, (canvas) => {
createSelector(selectCanvasSlice, (canvas) => {
const layer = selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerControlAdapter');
return layer.controlAdapter;
});

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

@@ -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

@@ -59,11 +59,11 @@ export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase<
this.syncOpacity();
}
if (!prevState || this.state.fill !== prevState.fill) {
// On first render, we must force the update
this.renderer.updateCompositingRectFill(!prevState);
// On first render, or when the fill changes, we must force the update
this.renderer.updateCompositingRectFill(true);
}
if (!prevState) {
// On first render, we must force the updates
if (!prevState || this.state.objects !== prevState.objects) {
// On first render, or when the objects change, we must force the update
this.renderer.updateCompositingRectSize(true);
this.renderer.updateCompositingRectPosition(true);
}

View File

@@ -59,11 +59,11 @@ export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase
this.syncOpacity();
}
if (!prevState || this.state.fill !== prevState.fill) {
// On first render, we must force the update
this.renderer.updateCompositingRectFill(!prevState);
// On first render, or when the fill changes, we must force the update
this.renderer.updateCompositingRectFill(true);
}
if (!prevState) {
// On first render, we must force the updates
if (!prevState || this.state.objects !== prevState.objects) {
// On first render, or when the objects change, we must force the update
this.renderer.updateCompositingRectSize(true);
this.renderer.updateCompositingRectPosition(true);
}

View File

@@ -284,8 +284,8 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
this.log.error({ error: serializeError(filterResult.error) }, 'Error filtering');
this.$isProcessing.set(false);
// Clean up the abort controller as needed
if (!this.abortController.signal.aborted) {
this.abortController.abort();
if (!controller.signal.aborted) {
controller.abort();
}
this.abortController = null;
return;
@@ -324,8 +324,8 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
this.$isProcessing.set(false);
// Clean up the abort controller as needed
if (!this.abortController.signal.aborted) {
this.abortController.abort();
if (!controller.signal.aborted) {
controller.abort();
}
this.abortController = null;

View File

@@ -277,8 +277,11 @@ export class CanvasBrushToolModule extends CanvasModuleBase {
let points: number[];
let isShiftDraw = false;
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
isShiftDraw = true;
points = [
lastLinePoint.x,
lastLinePoint.y,
@@ -298,15 +301,18 @@ export class CanvasBrushToolModule extends CanvasModuleBase {
points,
strokeWidth: settings.brushWidth,
color: this.manager.stateApi.getCurrentColor(),
clip: this.parent.getClip(selectedEntity.state),
// When shift is held, the line may extend beyond the clip region. No clip for these lines.
clip: isShiftDraw ? null : this.parent.getClip(selectedEntity.state),
});
} else {
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line');
let points: number[];
let isShiftDraw = false;
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
isShiftDraw = true;
points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y];
} else {
// Create a new line with the current point
@@ -319,7 +325,8 @@ export class CanvasBrushToolModule extends CanvasModuleBase {
points,
strokeWidth: settings.brushWidth,
color: this.manager.stateApi.getCurrentColor(),
clip: this.parent.getClip(selectedEntity.state),
// When shift is held, the line may extend beyond the clip region. No clip for these lines.
clip: isShiftDraw ? null : this.parent.getClip(selectedEntity.state),
});
}
};

View File

@@ -3,7 +3,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import type { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule';
import { getColorAtCoordinate, getPrefixedId } from 'features/controlLayers/konva/util';
import type { RgbColor } from 'features/controlLayers/store/types';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { RGBA_BLACK } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
@@ -52,6 +52,39 @@ type CanvasColorPickerToolModuleConfig = {
* The color of the crosshair line borders.
*/
CROSSHAIR_BORDER_COLOR: string;
/**
* The color of the RGBA value text.
*/
TEXT_COLOR: string;
/**
* The padding of the RGBA value text within the background rect.
*/
TEXT_PADDING: number;
/**
* The font size of the RGBA value text.
*/
TEXT_FONT_SIZE: number;
/**
* The color of the RGBA value text background rect.
*/
TEXT_BG_COLOR: string;
/**
* The width of the RGBA value text background rect.
*/
TEXT_BG_WIDTH: number;
/**
* The height of the RGBA value text background rect.
*/
TEXT_BG_HEIGHT: number;
/**
* The corner radius of the RGBA value text background rect.
*/
TEXT_BG_CORNER_RADIUS: number;
/**
* The x offset of the RGBA value text background rect from the color picker ring.
*/
TEXT_BG_X_OFFSET: number;
};
const DEFAULT_CONFIG: CanvasColorPickerToolModuleConfig = {
@@ -65,6 +98,14 @@ const DEFAULT_CONFIG: CanvasColorPickerToolModuleConfig = {
CROSSHAIR_LINE_LENGTH: 10,
CROSSHAIR_LINE_COLOR: 'rgba(0,0,0,1)',
CROSSHAIR_BORDER_COLOR: 'rgba(255,255,255,0.8)',
TEXT_COLOR: 'rgba(255,255,255,1)',
TEXT_BG_COLOR: 'rgba(0,0,0,0.8)',
TEXT_BG_HEIGHT: 62,
TEXT_BG_WIDTH: 62,
TEXT_BG_CORNER_RADIUS: 7,
TEXT_PADDING: 8,
TEXT_FONT_SIZE: 12,
TEXT_BG_X_OFFSET: 7,
};
/**
@@ -83,7 +124,7 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
/**
* The color currently under the cursor. Only has a value when the color picker tool is active.
*/
$colorUnderCursor = atom<RgbColor>(RGBA_BLACK);
$colorUnderCursor = atom<RgbaColor>(RGBA_BLACK);
/**
* The Konva objects that make up the color picker tool preview:
@@ -105,6 +146,9 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
crosshairSouthOuter: Konva.Line;
crosshairWestInner: Konva.Line;
crosshairWestOuter: Konva.Line;
rgbaTextGroup: Konva.Group;
rgbaText: Konva.Text;
rgbaTextBackground: Konva.Rect;
};
constructor(parent: CanvasToolModule) {
@@ -202,8 +246,28 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
stroke: this.config.CROSSHAIR_BORDER_COLOR,
perfectDrawEnabled: false,
}),
rgbaTextGroup: new Konva.Group({
listening: false,
name: `${this.type}:color_picker_text_group`,
}),
rgbaText: new Konva.Text({
listening: false,
name: `${this.type}:color_picker_text`,
fill: this.config.TEXT_COLOR,
fontFamily: 'monospace',
align: 'left',
fontStyle: 'bold',
verticalAlign: 'middle',
}),
rgbaTextBackground: new Konva.Rect({
listening: false,
name: `${this.type}:color_picker_text_background`,
fill: this.config.TEXT_BG_COLOR,
}),
};
this.konva.rgbaTextGroup.add(this.konva.rgbaTextBackground, this.konva.rgbaText);
this.konva.group.add(
this.konva.ringCandidateColor,
this.konva.ringCurrentColor,
@@ -216,7 +280,8 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
this.konva.crosshairSouthOuter,
this.konva.crosshairSouthInner,
this.konva.crosshairWestOuter,
this.konva.crosshairWestInner
this.konva.crosshairWestInner,
this.konva.rgbaTextGroup
);
}
@@ -233,11 +298,6 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
return;
}
if (!this.parent.getCanDraw()) {
this.setVisibility(false);
return;
}
const cursorPos = this.parent.$cursorPos.get();
if (!cursorPos) {
@@ -283,6 +343,24 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
outerRadius: colorPickerOuterRadius + twoPixels,
});
const textBgWidth = this.manager.stage.unscale(this.config.TEXT_BG_WIDTH);
const textBgHeight = this.manager.stage.unscale(this.config.TEXT_BG_HEIGHT);
this.konva.rgbaTextBackground.setAttrs({
width: textBgWidth,
height: textBgHeight,
cornerRadius: this.manager.stage.unscale(this.config.TEXT_BG_CORNER_RADIUS),
});
this.konva.rgbaText.setAttrs({
padding: this.manager.stage.unscale(this.config.TEXT_PADDING),
fontSize: this.manager.stage.unscale(this.config.TEXT_FONT_SIZE),
text: `R: ${colorUnderCursor.r}\nG: ${colorUnderCursor.g}\nB: ${colorUnderCursor.b}\nA: ${colorUnderCursor.a}`,
});
this.konva.rgbaTextGroup.setAttrs({
x: x + this.manager.stage.unscale(this.config.RING_OUTER_RADIUS + this.config.TEXT_BG_X_OFFSET),
y: y - textBgHeight / 2,
});
const size = this.manager.stage.unscale(this.config.CROSSHAIR_LINE_LENGTH);
const space = this.manager.stage.unscale(this.config.CROSSHAIR_INNER_RADIUS);
const innerThickness = this.manager.stage.unscale(this.config.CROSSHAIR_LINE_THICKNESS);
@@ -329,11 +407,8 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
onStagePointerUp = (_e: KonvaEventObject<PointerEvent>) => {
const color = this.$colorUnderCursor.get();
if (color) {
const settings = this.manager.stateApi.getSettings();
// This will update the color but not the alpha value
this.manager.stateApi.setColor({ ...settings.color, ...color });
}
const settings = this.manager.stateApi.getSettings();
this.manager.stateApi.setColor({ ...settings.color, ...color });
};
onStagePointerMove = (_e: KonvaEventObject<PointerEvent>) => {
@@ -346,7 +421,11 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
return;
}
// Hide the background layer so we can get the color under the cursor without the grid interfering
this.manager.background.konva.layer.visible(false);
const color = getColorAtCoordinate(this.manager.stage.konva.stage, cursorPos.absolute);
this.manager.background.konva.layer.visible(true);
if (color) {
this.$colorUnderCursor.set(color);
}

View File

@@ -22,6 +22,7 @@ import type {
Coordinate,
Tool,
} from 'features/controlLayers/store/types';
import { isRenderableEntityType } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import { atom } from 'nanostores';
@@ -177,24 +178,26 @@ export class CanvasToolModule extends CanvasModuleBase {
stage.setCursor('not-allowed');
} else if (tool === 'bbox') {
this.tools.bbox.syncCursorStyle();
} else if (this.manager.stateApi.getRenderedEntityCount() === 0) {
stage.setCursor('not-allowed');
} else if (selectedEntityAdapter?.$isDisabled.get()) {
stage.setCursor('not-allowed');
} else if (selectedEntityAdapter?.$isEntityTypeHidden.get()) {
stage.setCursor('not-allowed');
} else if (selectedEntityAdapter?.$isLocked.get()) {
stage.setCursor('not-allowed');
} else if (tool === 'brush') {
this.tools.brush.syncCursorStyle();
} else if (tool === 'eraser') {
this.tools.eraser.syncCursorStyle();
} else if (tool === 'colorPicker') {
this.tools.colorPicker.syncCursorStyle();
} else if (tool === 'move') {
this.tools.move.syncCursorStyle();
} else if (tool === 'rect') {
this.tools.rect.syncCursorStyle();
} else if (selectedEntityAdapter && isRenderableEntityType(selectedEntityAdapter.entityIdentifier.type)) {
if (selectedEntityAdapter.$isDisabled.get()) {
stage.setCursor('not-allowed');
} else if (selectedEntityAdapter.$isEntityTypeHidden.get()) {
stage.setCursor('not-allowed');
} else if (selectedEntityAdapter.$isLocked.get()) {
stage.setCursor('not-allowed');
} else if (tool === 'brush') {
this.tools.brush.syncCursorStyle();
} else if (tool === 'eraser') {
this.tools.eraser.syncCursorStyle();
} else if (tool === 'move') {
this.tools.move.syncCursorStyle();
} else if (tool === 'rect') {
this.tools.rect.syncCursorStyle();
}
} else if (this.manager.stateApi.getRenderedEntityCount() === 0) {
stage.setCursor('not-allowed');
} else {
stage.setCursor('not-allowed');
}
@@ -387,15 +390,17 @@ export class CanvasToolModule extends CanvasModuleBase {
try {
this.$lastPointerType.set(e.evt.pointerType);
if (!this.getCanDraw()) {
return;
}
const tool = this.$tool.get();
if (tool === 'colorPicker') {
this.tools.colorPicker.onStagePointerUp(e);
} else if (tool === 'brush') {
}
if (!this.getCanDraw()) {
return;
}
if (tool === 'brush') {
this.tools.brush.onStagePointerUp(e);
} else if (tool === 'eraser') {
this.tools.eraser.onStagePointerUp(e);
@@ -416,15 +421,17 @@ export class CanvasToolModule extends CanvasModuleBase {
this.$lastPointerType.set(e.evt.pointerType);
this.syncCursorPositions();
if (!this.getCanDraw()) {
return;
}
const tool = this.$tool.get();
if (tool === 'colorPicker') {
this.tools.colorPicker.onStagePointerMove(e);
} else if (tool === 'brush') {
}
if (!this.getCanDraw()) {
return;
}
if (tool === 'brush') {
await this.tools.brush.onStagePointerMove(e);
} else if (tool === 'eraser') {
await this.tools.eraser.onStagePointerMove(e);

View File

@@ -7,6 +7,7 @@ import type {
Coordinate,
CoordinateWithPressure,
Rect,
RgbaColor,
} from 'features/controlLayers/store/types';
import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
@@ -15,7 +16,6 @@ import { clamp } from 'lodash-es';
import { customAlphabet } from 'nanoid';
import type { StrokeOptions } from 'perfect-freehand';
import getStroke from 'perfect-freehand';
import type { RgbColor } from 'react-colorful';
import { assert } from 'tsafe';
/**
@@ -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 => {
@@ -723,7 +724,7 @@ export const getPointerType = (e: KonvaEventObject<PointerEvent>): 'mouse' | 'pe
* @param coord The coordinate to get the color at. This must be the _absolute_ coordinate on the stage.
* @returns The color under the coordinate, or null if there was a problem getting the color.
*/
export const getColorAtCoordinate = (stage: Konva.Stage, coord: Coordinate): RgbColor | null => {
export const getColorAtCoordinate = (stage: Konva.Stage, coord: Coordinate): RgbaColor | null => {
const ctx = stage
.toCanvas({ x: coord.x, y: coord.y, width: 1, height: 1, imageSmoothingEnabled: false })
.getContext('2d');
@@ -732,13 +733,13 @@ export const getColorAtCoordinate = (stage: Konva.Stage, coord: Coordinate): Rgb
return null;
}
const [r, g, b, _a] = ctx.getImageData(0, 0, 1, 1).data;
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
if (r === undefined || g === undefined || b === undefined) {
if (r === undefined || g === undefined || b === undefined || a === undefined) {
return null;
}
return { r, g, b };
return { r, g, b, a };
};
export const roundRect = (rect: Rect): Rect => {

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} />
{isHovering.isTrue && (
<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

@@ -6,7 +6,6 @@ import { createSelector } from '@reduxjs/toolkit';
import { galleryImageClicked } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
import { useBoolean } from 'common/hooks/useBoolean';
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
import type { DndDragPreviewMultipleImageState } from 'features/dnd/DndDragPreviewMultipleImage';
import { createMultipleImageDragPreview, setMultipleImageDragPreview } from 'features/dnd/DndDragPreviewMultipleImage';
@@ -19,6 +18,7 @@ import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid
import { SizedSkeletonLoader } from 'features/gallery/components/ImageGrid/SizedSkeletonLoader';
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { atom } from 'nanostores';
import type { MouseEventHandler } from 'react';
import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
@@ -178,7 +178,20 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
);
}, [imageDTO, element, store, dndId]);
const isHovered = useBoolean(false);
// Perf optimization:
// The gallery image component can be heavy and re-render often. We want to track hovering state without causing
// unnecessary re-renders. To do this, we use a local atom - which has a stable reference - in the image component -
// and then pass the atom to the hover icons component, which subscribes to the atom and re-renders when the atom
// changes.
const $isHovered = useMemo(() => atom(false), []);
const onMouseOver = useCallback(() => {
$isHovered.set(true);
}, [$isHovered]);
const onMouseOut = useCallback(() => {
$isHovered.set(false);
}, [$isHovered]);
const onClick = useCallback<MouseEventHandler<HTMLDivElement>>(
(e) => {
@@ -217,8 +230,8 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
<Flex
role="button"
className="gallery-image"
onMouseOver={isHovered.setTrue}
onMouseOut={isHovered.setFalse}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
onClick={onClick}
onDoubleClick={onDoubleClick}
data-selected={isSelected}
@@ -234,7 +247,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
maxH="full"
borderRadius="base"
/>
<GalleryImageHoverIcons imageDTO={imageDTO} isHovered={isHovered.isTrue} />
<GalleryImageHoverIcons imageDTO={imageDTO} $isHovered={$isHovered} />
</Flex>
</Box>
{dragPreviewState?.type === 'multiple-image' ? createMultipleImageDragPreview(dragPreviewState) : null}

View File

@@ -1,19 +1,22 @@
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { GalleryImageDeleteIconButton } from 'features/gallery/components/ImageGrid/GalleryImageDeleteIconButton';
import { GalleryImageOpenInViewerIconButton } from 'features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton';
import { GalleryImageSizeBadge } from 'features/gallery/components/ImageGrid/GalleryImageSizeBadge';
import { GalleryImageStarIconButton } from 'features/gallery/components/ImageGrid/GalleryImageStarIconButton';
import { selectAlwaysShouldImageSizeBadge } from 'features/gallery/store/gallerySelectors';
import type { Atom } from 'nanostores';
import { memo } from 'react';
import type { ImageDTO } from 'services/api/types';
type Props = {
imageDTO: ImageDTO;
isHovered: boolean;
$isHovered: Atom<boolean>;
};
export const GalleryImageHoverIcons = memo(({ imageDTO, isHovered }: Props) => {
export const GalleryImageHoverIcons = memo(({ imageDTO, $isHovered }: Props) => {
const alwaysShowImageSizeBadge = useAppSelector(selectAlwaysShouldImageSizeBadge);
const isHovered = useStore($isHovered);
return (
<>

View File

@@ -17,79 +17,19 @@ import { useTranslation } from 'react-i18next';
export const JumpTo = memo(() => {
const { t } = useTranslation();
const { goToPage, currentPage, pages } = useGalleryPagination();
const [newPage, setNewPage] = useState(currentPage);
const { isOpen, onToggle, onClose } = useDisclosure();
const ref = useRef<HTMLInputElement>(null);
const onOpen = useCallback(() => {
setNewPage(currentPage);
setTimeout(() => {
const input = ref.current?.querySelector('input');
input?.focus();
input?.select();
}, 0);
}, [currentPage]);
const onChangeJumpTo = useCallback((v: number) => {
setNewPage(v - 1);
}, []);
const onClickGo = useCallback(() => {
goToPage(newPage);
onClose();
}, [newPage, goToPage, onClose]);
useHotkeys(
'enter',
() => {
onClickGo();
},
{ enabled: isOpen, enableOnFormTags: ['input'] },
[isOpen, onClickGo]
);
useHotkeys(
'esc',
() => {
setNewPage(currentPage);
onClose();
},
{ enabled: isOpen, enableOnFormTags: ['input'] },
[isOpen, onClose]
);
useEffect(() => {
setNewPage(currentPage);
}, [currentPage]);
const disclosure = useDisclosure();
return (
<Popover isOpen={isOpen} onClose={onClose} onOpen={onOpen} isLazy lazyBehavior="unmount">
<Popover isOpen={disclosure.isOpen} onClose={disclosure.onClose} isLazy lazyBehavior="unmount">
<PopoverTrigger>
<Button aria-label={t('gallery.jump')} size="sm" onClick={onToggle} variant="outline">
<Button aria-label={t('gallery.jump')} size="sm" onClick={disclosure.onToggle} variant="outline">
{t('gallery.jump')}
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<Flex gap={2} alignItems="center">
<FormControl>
<CompositeNumberInput
ref={ref}
size="sm"
maxW="60px"
value={newPage + 1}
min={1}
max={pages}
step={1}
onChange={onChangeJumpTo}
/>
</FormControl>
<Button h="full" size="sm" onClick={onClickGo}>
{t('gallery.go')}
</Button>
</Flex>
<JumpToContent disclosure={disclosure} />
</PopoverBody>
</PopoverContent>
</Popover>
@@ -97,3 +37,68 @@ export const JumpTo = memo(() => {
});
JumpTo.displayName = 'JumpTo';
const JumpToContent = memo(({ disclosure }: { disclosure: ReturnType<typeof useDisclosure> }) => {
const { t } = useTranslation();
const { goToPage, currentPage, pages } = useGalleryPagination();
const [newPage, setNewPage] = useState(currentPage);
const ref = useRef<HTMLInputElement>(null);
const onChangeJumpTo = useCallback((v: number) => {
setNewPage(v - 1);
}, []);
const onClickGo = useCallback(() => {
goToPage(newPage);
disclosure.onClose();
}, [goToPage, newPage, disclosure]);
useHotkeys(
'enter',
() => {
onClickGo();
},
{ enabled: disclosure.isOpen, enableOnFormTags: ['input'] },
[disclosure.isOpen, onClickGo]
);
useHotkeys(
'esc',
() => {
setNewPage(currentPage);
disclosure.onClose();
},
{ enabled: disclosure.isOpen, enableOnFormTags: ['input'] },
[disclosure.isOpen, disclosure.onClose]
);
useEffect(() => {
setTimeout(() => {
const input = ref.current?.querySelector('input');
input?.focus();
input?.select();
}, 0);
setNewPage(currentPage);
}, [currentPage]);
return (
<Flex gap={2} alignItems="center">
<FormControl>
<CompositeNumberInput
ref={ref}
size="sm"
maxW="60px"
value={newPage + 1}
min={1}
max={pages}
step={1}
onChange={onChangeJumpTo}
/>
</FormControl>
<Button h="full" size="sm" onClick={onClickGo}>
{t('gallery.go')}
</Button>
</Flex>
);
});
JumpToContent.displayName = 'JumpToContent';

View File

@@ -115,19 +115,13 @@ export const useGalleryPagination = () => {
},
[throttledOnOffsetChanged, limit]
);
const goToFirst = useCallback(() => {
throttledOnOffsetChanged({ offset: 0 });
}, [throttledOnOffsetChanged]);
const goToLast = useCallback(() => {
throttledOnOffsetChanged({ offset: (pages - 1) * (limit || 0) });
}, [throttledOnOffsetChanged, pages, limit]);
// handle when total/pages decrease and user is on high page number (ie bulk removing or deleting)
useEffect(() => {
if (pages && currentPage + 1 > pages) {
goToLast();
throttledOnOffsetChanged({ offset: (pages - 1) * (limit || 0) });
}
}, [currentPage, pages, goToLast]);
}, [currentPage, pages, throttledOnOffsetChanged, limit]);
const pageButtons = useMemo(() => {
if (pages > 7) {
@@ -135,35 +129,16 @@ export const useGalleryPagination = () => {
}
return range(1, pages);
}, [currentPage, pages]);
const isFirstEnabled = useMemo(() => currentPage > 0, [currentPage]);
const isLastEnabled = useMemo(() => currentPage < pages - 1, [currentPage, pages]);
const rangeDisplay = useMemo(() => {
const startItem = currentPage * (limit || 0) + 1;
const endItem = Math.min((currentPage + 1) * (limit || 0), total);
return `${startItem}-${endItem} of ${total}`;
}, [total, currentPage, limit]);
const numberOnPage = useMemo(() => {
return Math.min((currentPage + 1) * (limit || 0), total);
}, [currentPage, limit, total]);
return {
count,
total,
currentPage,
pages,
isNextEnabled,
isPrevEnabled,
goNext,
goPrev,
goToPage,
goToFirst,
goToLast,
goNext,
isPrevEnabled,
isNextEnabled,
pageButtons,
isFirstEnabled,
isLastEnabled,
rangeDisplay,
numberOnPage,
goToPage,
currentPage,
total,
pages,
};
};

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,25 +12,27 @@ 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';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
import {
$addNodeCmdk,
$cursorPos,
$edgePendingUpdate,
$pendingConnection,
$templates,
edgesChanged,
nodesChanged,
useAddNodeCmdk,
} from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
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';
@@ -38,34 +40,12 @@ import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memoize } from 'lodash-es';
import { computed } from 'nanostores';
import type { ChangeEvent } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useCallback, 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';
const useThrottle = <T,>(value: T, limit: number) => {
const [throttledValue, setThrottledValue] = useState(value);
const lastRan = useRef(Date.now());
useEffect(() => {
const handler = setTimeout(
function () {
if (Date.now() - lastRan.current >= limit) {
setThrottledValue(value);
lastRan.current = Date.now();
}
},
limit - (Date.now() - lastRan.current)
);
return () => {
clearTimeout(handler);
};
}, [value, limit]);
return throttledValue;
};
import { objectEntries } from 'tsafe';
import { useDebounce } from 'use-debounce';
const useAddNode = () => {
const { t } = useTranslation();
@@ -95,8 +75,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 });
@@ -162,19 +142,26 @@ const cmdkRootSx: SystemStyleObject = {
export const AddNodeCmdk = memo(() => {
const { t } = useTranslation();
const addNodeCmdk = useAddNodeCmdk();
const inputRef = useRef<HTMLInputElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const addNode = useAddNode();
const tab = useAppSelector(selectActiveTab);
const throttledSearchTerm = useThrottle(searchTerm, 100);
// Filtering the list is expensive - debounce the search term to avoid stutters
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);
const isOpen = useStore($addNodeCmdk);
const open = useCallback(() => {
$addNodeCmdk.set(true);
}, []);
const close = useCallback(() => {
$addNodeCmdk.set(false);
}, []);
useRegisteredHotkeys({
id: 'addNode',
category: 'workflows',
callback: addNodeCmdk.setTrue,
callback: open,
options: { enabled: tab === 'workflows', preventDefault: true },
dependencies: [addNodeCmdk.setTrue, tab],
dependencies: [open, tab],
});
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
@@ -182,10 +169,10 @@ export const AddNodeCmdk = memo(() => {
}, []);
const onClose = useCallback(() => {
addNodeCmdk.setFalse();
close();
setSearchTerm('');
$pendingConnection.set(null);
}, [addNodeCmdk]);
}, [close]);
const onSelect = useCallback(
(value: string) => {
@@ -196,14 +183,7 @@ export const AddNodeCmdk = memo(() => {
);
return (
<Modal
isOpen={addNodeCmdk.isTrue}
onClose={onClose}
useInert={false}
initialFocusRef={inputRef}
size="xl"
isCentered
>
<Modal isOpen={isOpen} onClose={onClose} useInert={false} initialFocusRef={inputRef} size="xl" isCentered>
<ModalOverlay />
<ModalContent h="512" maxH="70%">
<ModalBody p={2} h="full" sx={cmdkRootSx}>
@@ -224,7 +204,7 @@ export const AddNodeCmdk = memo(() => {
/>
</CommandEmpty>
<CommandList>
<NodeCommandList searchTerm={throttledSearchTerm} onSelect={onSelect} />
<NodeCommandList searchTerm={debouncedSearchTerm} onSelect={onSelect} />
</CommandList>
</ScrollableContent>
</Box>
@@ -381,11 +361,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,83 +1,121 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
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 {
useInputFieldNamesAnyOrDirect,
useInputFieldNamesConnection,
useInputFieldNamesMissing,
} 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';
type Props = {
nodeId: string;
isOpen: boolean;
label: string;
type: string;
selected: boolean;
};
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
const fieldNames = useFieldNames(nodeId);
const sx: SystemStyleObject = {
flexDirection: 'column',
w: 'full',
h: 'full',
py: 2,
gap: 1,
borderBottomRadius: 'base',
'&[data-with-footer="true"]': {
borderBottomRadius: 0,
},
};
const InvocationNode = ({ nodeId, isOpen }: Props) => {
const withFooter = useWithFooter(nodeId);
const outputFieldNames = useOutputFieldNames(nodeId);
return (
<NodeWrapper nodeId={nodeId} selected={selected}>
<InvocationNodeHeader nodeId={nodeId} isOpen={isOpen} label={label} selected={selected} type={type} />
<>
<InvocationNodeHeader nodeId={nodeId} isOpen={isOpen} />
{isOpen && (
<>
<Flex
layerStyle="nodeBody"
flexDirection="column"
w="full"
h="full"
py={2}
gap={1}
borderBottomRadius={withFooter ? 0 : 'base'}
>
<Flex layerStyle="nodeBody" sx={sx} data-with-footer={withFooter}>
<Flex flexDir="column" px={2} w="full" h="full">
<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>
</GridItem>
))}
{outputFieldNames.map((fieldName, i) => (
<GridItem gridColumnStart={2} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.output-field`}>
<OutputField nodeId={nodeId} fieldName={fieldName} />
</GridItem>
))}
<ConnectionFields nodeId={nodeId} />
<OutputFields nodeId={nodeId} />
</Grid>
{fieldNames.anyOrDirectFields.map((fieldName) => (
<InvocationInputFieldCheck
key={`${nodeId}.${fieldName}.input-field`}
nodeId={nodeId}
fieldName={fieldName}
>
<InputField nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck>
))}
{fieldNames.missingFields.map((fieldName) => (
<InvocationInputFieldCheck
key={`${nodeId}.${fieldName}.input-field`}
nodeId={nodeId}
fieldName={fieldName}
>
<InputField nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck>
))}
<AnyOrDirectFields nodeId={nodeId} />
<MissingFields nodeId={nodeId} />
</Flex>
</Flex>
{withFooter && <InvocationNodeFooter nodeId={nodeId} />}
</>
)}
</NodeWrapper>
</>
);
};
export default memo(InvocationNode);
const ConnectionFields = memo(({ nodeId }: { nodeId: string }) => {
const fieldNames = useInputFieldNamesConnection(nodeId);
return (
<>
{fieldNames.map((fieldName, i) => (
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
<InputFieldGate nodeId={nodeId} fieldName={fieldName}>
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
</GridItem>
))}
</>
);
});
ConnectionFields.displayName = 'ConnectionFields';
const AnyOrDirectFields = memo(({ nodeId }: { nodeId: string }) => {
const fieldNames = useInputFieldNamesAnyOrDirect(nodeId);
return (
<>
{fieldNames.map((fieldName) => (
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
))}
</>
);
});
AnyOrDirectFields.displayName = 'AnyOrDirectFields';
const MissingFields = memo(({ nodeId }: { nodeId: string }) => {
const fieldNames = useInputFieldNamesMissing(nodeId);
return (
<>
{fieldNames.map((fieldName) => (
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
))}
</>
);
});
MissingFields.displayName = 'MissingFields';
const OutputFields = memo(({ nodeId }: { nodeId: string }) => {
const fieldNames = useOutputFieldNames(nodeId);
return (
<>
{fieldNames.map((fieldName, i) => (
<GridItem gridColumnStart={2} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.output-field`}>
<OutputFieldGate nodeId={nodeId} fieldName={fieldName}>
<OutputFieldNodesEditorView nodeId={nodeId} fieldName={fieldName} />
</OutputFieldGate>
</GridItem>
))}
</>
);
});
OutputFields.displayName = 'OutputFields';

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

@@ -1,3 +1,4 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
import NodeTitle from 'features/nodes/components/flow/nodes/common/NodeTitle';
@@ -5,29 +6,30 @@ 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 = {
nodeId: string;
isOpen: boolean;
label: string;
type: string;
selected: boolean;
};
const sx: SystemStyleObject = {
borderTopRadius: 'base',
alignItems: 'center',
justifyContent: 'space-between',
h: 8,
textAlign: 'center',
color: 'base.200',
borderBottomRadius: 'base',
'&[data-is-open="true"]': {
borderBottomRadius: 0,
},
};
const InvocationNodeHeader = ({ nodeId, isOpen }: Props) => {
return (
<Flex
layerStyle="nodeHeader"
borderTopRadius="base"
borderBottomRadius={isOpen ? 0 : 'base'}
alignItems="center"
justifyContent="space-between"
h={8}
textAlign="center"
color="base.200"
>
<Flex layerStyle="nodeHeader" sx={sx} data-is-open={isOpen}>
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
<InvocationNodeClassificationIcon nodeId={nodeId} />
<NodeTitle nodeId={nodeId} />

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,6 +1,5 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { useNodePack } from 'features/nodes/hooks/useNodePack';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { memo } from 'react';
@@ -11,14 +10,13 @@ type Props = {
isOpen: boolean;
label: string;
type: string;
selected: boolean;
};
const InvocationNodeUnknownFallback = ({ nodeId, isOpen, label, type, selected }: Props) => {
const InvocationNodeUnknownFallback = ({ nodeId, isOpen, label, type }: Props) => {
const { t } = useTranslation();
const nodePack = useNodePack(nodeId);
return (
<NodeWrapper nodeId={nodeId} selected={selected}>
<>
<Flex
className={DRAG_HANDLE_CLASSNAME}
layerStyle="nodeHeader"
@@ -64,7 +62,7 @@ const InvocationNodeUnknownFallback = ({ nodeId, isOpen, label, type, selected }
</Flex>
</Flex>
)}
</NodeWrapper>
</>
);
};

View File

@@ -1,16 +1,17 @@
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import type { Node, NodeProps } from '@xyflow/react';
import { useAppSelector } from 'app/store/storeHooks';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
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);
@@ -27,11 +28,17 @@ const InvocationNodeWrapper = (props: NodeProps<InvocationNodeData>) => {
if (!hasTemplate) {
return (
<InvocationNodeUnknownFallback nodeId={nodeId} isOpen={isOpen} label={label} type={type} selected={selected} />
<NodeWrapper nodeId={nodeId} selected={selected}>
<InvocationNodeUnknownFallback nodeId={nodeId} isOpen={isOpen} label={label} type={type} />
</NodeWrapper>
);
}
return <InvocationNode nodeId={nodeId} isOpen={isOpen} label={label} type={type} selected={selected} />;
return (
<NodeWrapper nodeId={nodeId} selected={selected}>
<InvocationNode nodeId={nodeId} isOpen={isOpen} />
</NodeWrapper>
);
};
export default memo(InvocationNodeWrapper);

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,73 @@
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 { t } = useTranslation();
return (
<Popover isLazy lazyBehavior="unmount">
<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}>
<Content nodeId={nodeId} fieldName={fieldName} />
</PopoverContent>
</Popover>
);
});
InputFieldDescriptionPopover.displayName = 'InputFieldDescriptionPopover';
const Content = 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 (
<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>
);
});
Content.displayName = 'Content';

View File

@@ -0,0 +1,130 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
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 { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import type { FieldInputTemplate } from 'features/nodes/types/field';
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 isInvalid = useInputFieldIsInvalid(nodeId, fieldName);
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
if (fieldTemplate.input === 'connection' || isConnected) {
return (
<ConnectedOrConnectionField
nodeId={nodeId}
fieldName={fieldName}
isInvalid={isInvalid}
isConnected={isConnected}
fieldTemplate={fieldTemplate}
/>
);
}
return (
<DirectField
nodeId={nodeId}
fieldName={fieldName}
isInvalid={isInvalid}
isConnected={isConnected}
fieldTemplate={fieldTemplate}
/>
);
});
InputFieldEditModeNodes.displayName = 'InputFieldEditModeNodes';
type CommonProps = {
nodeId: string;
fieldName: string;
isInvalid: boolean;
isConnected: boolean;
fieldTemplate: FieldInputTemplate;
};
const ConnectedOrConnectionField = memo(({ nodeId, fieldName, isInvalid, isConnected }: CommonProps) => {
return (
<InputFieldWrapper>
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} isInvalid={isInvalid} />
</FormControl>
<InputFieldHandle nodeId={nodeId} fieldName={fieldName} />
</InputFieldWrapper>
);
});
ConnectedOrConnectionField.displayName = 'ConnectedOrConnectionField';
const directFieldSx: SystemStyleObject = {
orientation: 'vertical',
px: 2,
'&[data-is-dragging="true"]': {
opacity: 0.3,
},
// 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: 'auto',
'&[data-is-connected="true"]': {
pointerEvents: 'none',
},
};
const DirectField = memo(({ nodeId, fieldName, isInvalid, isConnected, fieldTemplate }: CommonProps) => {
const draggableRef = useRef<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const onMouseEnter = useCallback(() => {
setIsHovered(true);
}, []);
const onMouseLeave = useCallback(() => {
setIsHovered(false);
}, []);
const isDragging = useNodeFieldDnd({ nodeId, fieldName }, fieldTemplate, draggableRef, dragHandleRef);
return (
<InputFieldWrapper>
<FormControl
ref={draggableRef}
isInvalid={isInvalid}
isDisabled={isConnected}
sx={directFieldSx}
data-is-connected={isConnected}
data-is-dragging={isDragging}
>
<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 nodeId={nodeId} fieldName={fieldName} />}
</InputFieldWrapper>
);
});
DirectField.displayName = 'DirectField';

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,159 @@
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 {
useConnectionErrorTKey,
useIsConnectionInProgress,
useIsConnectionStartField,
} from 'features/nodes/hooks/useFieldConnectionState';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
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 Props = {
nodeId: string;
fieldName: string;
};
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(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
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 isConnectionInProgress = useIsConnectionInProgress();
if (isConnectionInProgress) {
return (
<ConnectionInProgressHandle
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
fieldTypeName={fieldTypeName}
fieldColor={fieldColor}
isModelField={isModelField}
/>
);
}
return (
<IdleHandle
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
fieldTypeName={fieldTypeName}
fieldColor={fieldColor}
isModelField={isModelField}
/>
);
});
InputFieldHandle.displayName = 'InputFieldHandle';
type HandleCommonProps = {
nodeId: string;
fieldName: string;
fieldTemplate: FieldInputTemplate;
fieldTypeName: string;
fieldColor: string;
isModelField: boolean;
};
const IdleHandle = memo(({ fieldTemplate, fieldTypeName, fieldColor, isModelField }: HandleCommonProps) => {
return (
<Tooltip label={fieldTypeName} 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={false}
data-is-connection-start-field={false}
data-is-connection-valid={false}
backgroundColor={fieldTemplate.type.cardinality === 'SINGLE' ? fieldColor : 'base.900'}
borderColor={fieldColor}
/>
</Handle>
</Tooltip>
);
});
IdleHandle.displayName = 'IdleHandle';
const ConnectionInProgressHandle = memo(
({ nodeId, fieldName, fieldTemplate, fieldTypeName, fieldColor, isModelField }: HandleCommonProps) => {
const { t } = useTranslation();
const isConnectionStartField = useIsConnectionStartField(nodeId, fieldName, 'target');
const connectionError = useConnectionErrorTKey(nodeId, fieldName, 'target');
const tooltip = useMemo(() => {
if (connectionError !== null) {
return t(connectionError);
}
return fieldTypeName;
}, [fieldTypeName, t, connectionError]);
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={true}
data-is-connection-start-field={isConnectionStartField}
data-is-connection-valid={connectionError === null}
backgroundColor={fieldTemplate.type.cardinality === 'SINGLE' ? fieldColor : 'base.900'}
borderColor={fieldColor}
/>
</Handle>
</Tooltip>
);
}
);
ConnectionInProgressHandle.displayName = 'ConnectionInProgressHandle';

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,295 @@ 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} />;
// When deciding which component to render, first we check the type of the template, which is more efficient than the
// instance type check. The instance type check uses zod and is slower.
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,92 @@
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 {
useConnectionErrorTKey,
useIsConnectionInProgress,
useIsConnectionStartField,
} from 'features/nodes/hooks/useFieldConnectionState';
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
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;
}
export const InputFieldTitle = memo((props: Props) => {
const { nodeId, fieldName, isInvalid } = props;
const inputRef = useRef<HTMLInputElement>(null);
const label = useInputFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
const { t } = useTranslation();
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
const isConnectionStartField = useIsConnectionStartField(nodeId, fieldName, 'target');
const isConnectionInProgress = useIsConnectionInProgress();
const connectionError = useConnectionErrorTKey(nodeId, fieldName, 'target');
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={
(isConnectionInProgress && connectionError !== null && !isConnectionStartField) || isConnected
}
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,19 @@
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',
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,159 @@
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 {
useConnectionErrorTKey,
useIsConnectionInProgress,
useIsConnectionStartField,
} from 'features/nodes/hooks/useFieldConnectionState';
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
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 = {
nodeId: string;
fieldName: string;
};
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(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
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 isConnectionInProgress = useIsConnectionInProgress();
if (isConnectionInProgress) {
return (
<ConnectionInProgressHandle
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
fieldTypeName={fieldTypeName}
fieldColor={fieldColor}
isModelField={isModelField}
/>
);
}
return (
<IdleHandle
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
fieldTypeName={fieldTypeName}
fieldColor={fieldColor}
isModelField={isModelField}
/>
);
});
OutputFieldHandle.displayName = 'OutputFieldHandle';
type HandleCommonProps = {
nodeId: string;
fieldName: string;
fieldTemplate: FieldOutputTemplate;
fieldTypeName: string;
fieldColor: string;
isModelField: boolean;
};
const IdleHandle = memo(({ fieldTemplate, fieldTypeName, fieldColor, isModelField }: HandleCommonProps) => {
return (
<Tooltip label={fieldTypeName} placement="start" 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={false}
data-is-connection-start-field={false}
data-is-connection-valid={false}
backgroundColor={fieldTemplate.type.cardinality === 'SINGLE' ? fieldColor : 'base.900'}
borderColor={fieldColor}
/>
</Handle>
</Tooltip>
);
});
IdleHandle.displayName = 'IdleHandle';
const ConnectionInProgressHandle = memo(
({ nodeId, fieldName, fieldTemplate, fieldTypeName, fieldColor, isModelField }: HandleCommonProps) => {
const { t } = useTranslation();
const isConnectionStartField = useIsConnectionStartField(nodeId, fieldName, 'target');
const connectionErrorTKey = useConnectionErrorTKey(nodeId, fieldName, 'target');
const tooltip = useMemo(() => {
if (connectionErrorTKey !== null) {
return t(connectionErrorTKey);
}
return fieldTypeName;
}, [fieldTypeName, t, connectionErrorTKey]);
return (
<Tooltip label={tooltip} placement="start" 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={true}
data-is-connection-start-field={isConnectionStartField}
data-is-connection-valid={connectionErrorTKey === null}
backgroundColor={fieldTemplate.type.cardinality === 'SINGLE' ? fieldColor : 'base.900'}
borderColor={fieldColor}
/>
</Handle>
</Tooltip>
);
}
);
ConnectionInProgressHandle.displayName = 'ConnectionInProgressHandle';

View File

@@ -0,0 +1,21 @@
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 type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
}>;
export const OutputFieldNodesEditorView = memo(({ nodeId, fieldName }: Props) => {
return (
<OutputFieldWrapper>
<OutputFieldTitle nodeId={nodeId} fieldName={fieldName} />
<OutputFieldHandle nodeId={nodeId} fieldName={fieldName} />
</OutputFieldWrapper>
);
});
OutputFieldNodesEditorView.displayName = 'OutputFieldNodesEditorView';

View File

@@ -0,0 +1,54 @@
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 {
useConnectionErrorTKey,
useIsConnectionInProgress,
useIsConnectionStartField,
} from 'features/nodes/hooks/useFieldConnectionState';
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
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 = {
nodeId: string;
fieldName: string;
};
export const OutputFieldTitle = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
const isConnectionStartField = useIsConnectionStartField(nodeId, fieldName, 'source');
const isConnectionInProgress = useIsConnectionInProgress();
const connectionErrorTKey = useConnectionErrorTKey(nodeId, fieldName, 'source');
return (
<Tooltip
label={<OutputFieldTooltipContent nodeId={nodeId} fieldName={fieldName} />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
>
<Text
data-is-disabled={
(isConnectionInProgress && connectionErrorTKey !== null && !isConnectionStartField) || isConnected
}
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,16 @@
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',
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);

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