Compare commits

...

58 Commits

Author SHA1 Message Date
psychedelicious
3e0a569826 feat(ui): editable heading and text elements 2025-01-31 11:04:24 +11:00
psychedelicious
32327bed5e chore(ui): bump @invoke-ai/ui-library 2025-01-31 11:02:13 +11:00
psychedelicious
468f1bff00 feat(ui): plumbing for editable form elements 2025-01-31 10:18:57 +11:00
psychedelicious
6413753e9b tidy(ui): import reactflow css in main theme provider 2025-01-26 11:31:50 +11:00
psychedelicious
d49ad17257 fix(ui): circular dep 2025-01-26 11:25:32 +11:00
psychedelicious
4ecf8be0be perf(ui): faster InputFieldRenderer
Use non-zod type guards for input field types and fail early when possible
2025-01-26 11:23:07 +11:00
psychedelicious
020544045f chore(ui): lint 2025-01-26 11:14:05 +11:00
psychedelicious
9edd1895bd chore(ui): lint 2025-01-26 11:11:57 +11:00
psychedelicious
9586b4476d fix(ui): node-autoconnect showing invalid connection options 2025-01-26 11:09:46 +11:00
psychedelicious
ac8314139e fix(ui): do not render dashed edges unless animation is enabled 2025-01-26 11:09:46 +11:00
psychedelicious
33bde00f8b tweak(ui): node selection colors 2025-01-26 11:09:46 +11:00
psychedelicious
4ef198fc69 refactor(ui): edge rendering
- Fix issues with positioning of labels
- Optimize styling to be less reliant on JS
2025-01-26 11:09:46 +11:00
psychedelicious
d51529b239 chore(ui): upgrade reactflow to v12 2025-01-26 11:09:46 +11:00
psychedelicious
98cdbedb10 fix(ui): hide nonfunctional delete button on root form element 2025-01-26 11:09:45 +11:00
psychedelicious
99b57acc8b tidy(ui): remove unused mock form builder data 2025-01-26 11:09:45 +11:00
psychedelicious
7c7d8928d0 fix(ui): use redux store for form 2025-01-26 11:09:45 +11:00
psychedelicious
d3b42948d6 fix(ui): start workflow w/ single column as root 2025-01-26 11:09:45 +11:00
psychedelicious
d58ed05bcb fix(ui): allow root element to be drop target 2025-01-26 11:09:45 +11:00
psychedelicious
265cf8ca7e feat(ui): support adding form elements and node fields with dnd 2025-01-26 11:09:45 +11:00
psychedelicious
82a6fcbfdb feat(ui): improved drop target styling 2025-01-26 11:09:45 +11:00
psychedelicious
0e66e539c1 fix(ui): do not allow reparenting to self 2025-01-26 11:09:45 +11:00
psychedelicious
c9579165b9 feat(ui): dnd drop target styling 2025-01-26 11:09:45 +11:00
psychedelicious
db0eeafb57 feat(ui): improved dnd hitbox for edges when center drop is allowed 2025-01-26 11:09:45 +11:00
psychedelicious
e966bf9759 feat(ui): dnd almost fully working (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
71407cf817 feat(ui): dnd mostly working (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
5b6c035c63 feat(ui): dim form element while dragging 2025-01-26 11:09:45 +11:00
psychedelicious
81cdaac2b5 feat(ui): hacking on dnd (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
3329f128f6 feat(ui): iterate on builder (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
578d1bbea4 feat(ui): iterate on builder (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
819346b980 feat(ui): iterate on builder (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
9cabb88560 feat(ui): iterate on builder (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
4485450df6 feat(ui): getPrefixedId supports custom separator 2025-01-26 11:09:45 +11:00
psychedelicious
14281890ce feat(ui): iterate on builder (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
a366a32334 feat(ui): iterate on builder (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
dfc1d67492 feat(ui): iterate on builder (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
f051b0b56c feat(ui): iterate on builder (WIP) 2025-01-26 11:09:45 +11:00
psychedelicious
53fb2509d7 chore(ui): lint 2025-01-26 11:09:45 +11:00
psychedelicious
1a7e134a2c feat(ui): rough out workflow builder data structure 2025-01-26 11:09:45 +11:00
psychedelicious
1665f13000 refactor(ui): split integer, float and string field components in prep for builder 2025-01-26 11:09:45 +11:00
psychedelicious
0dddf4fcf9 revert(ui): rip out linear view config stuff 2025-01-26 11:09:45 +11:00
psychedelicious
e9cb3f6abe refactor(ui): split up float and integer field renderers 2025-01-26 11:09:44 +11:00
psychedelicious
79af1e11d0 feat(ui): rough out workflow builder data structure & dummy data 2025-01-26 11:09:44 +11:00
psychedelicious
914ef1f664 fix(ui): dynamic prompts infinite recursion (wip) 2025-01-26 11:09:44 +11:00
psychedelicious
254d9b52d6 feat(ui): use workflows view context 2025-01-26 11:09:44 +11:00
psychedelicious
b61e07c556 feat(ui): get configurable notes display working 2025-01-26 11:09:44 +11:00
psychedelicious
29570218a7 fix(ui): color field component layout 2025-01-26 11:09:44 +11:00
psychedelicious
e9677940d0 refactor(ui): continued reorg of components & hooks 2025-01-26 11:09:44 +11:00
psychedelicious
1ba9b5407c refactor(ui): continued reorg of components & hooks 2025-01-26 11:09:44 +11:00
psychedelicious
50461b1ddb fix(ui): remove accidental change to zFieldInput schema 2025-01-26 11:09:44 +11:00
psychedelicious
a8b0c1c10c refactor(ui): workflows left panel internal components structure 2025-01-26 11:09:44 +11:00
psychedelicious
5ec173b9bb 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-01-26 11:09:44 +11:00
psychedelicious
861a2378c5 perf(ui): use data attribute for input field wrapper styles 2025-01-26 11:09:44 +11:00
psychedelicious
f1de722beb feat(ui): add ViewContext so components can know where they are being rendered (user-linear view, editor-linear view, or editor-nodes view) 2025-01-26 11:09:44 +11:00
psychedelicious
9ffa1b887b feat(ui): clean up user-linear view styling 2025-01-26 11:09:44 +11:00
psychedelicious
d969958041 feat(ui): show notes icon on user-linear view, replacing info icon 2025-01-26 11:09:44 +11:00
psychedelicious
a842698b7f feat(ui): show notes icon on editor linear view 2025-01-26 11:09:44 +11:00
psychedelicious
b03479107d feat(ui): add notes popover to field title bar 2025-01-26 11:09:44 +11:00
psychedelicious
92d25d304a feat(ui): add notes state to fields 2025-01-26 11:09:44 +11:00
174 changed files with 5065 additions and 2531 deletions

View File

@@ -58,10 +58,11 @@
"@dagrejs/dagre": "^1.1.4",
"@dagrejs/graphlib": "^2.2.4",
"@fontsource-variable/inter": "^5.1.0",
"@invoke-ai/ui-library": "^0.0.44",
"@invoke-ai/ui-library": "^0.0.46",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",
"@xyflow/react": "^12.4.2",
"async-mutex": "^0.5.0",
"chakra-react-select": "^4.9.2",
"cmdk": "^1.0.0",
@@ -96,9 +97,9 @@
"react-icons": "^5.3.0",
"react-redux": "9.1.2",
"react-resizable-panels": "^2.1.4",
"react-textarea-autosize": "^8.5.7",
"react-use": "^17.5.1",
"react-virtuoso": "^4.10.4",
"reactflow": "^11.11.4",
"redux-dynamic-middlewares": "^2.2.0",
"redux-remember": "^5.1.0",
"redux-undo": "^1.1.0",

View File

@@ -24,8 +24,8 @@ 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)
@@ -35,12 +35,15 @@ dependencies:
'@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
@@ -572,7 +575,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
@@ -595,7 +598,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
@@ -605,7 +608,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
@@ -621,7 +624,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
@@ -638,7 +641,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
@@ -663,7 +666,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
@@ -828,7 +831,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'
@@ -841,8 +844,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
@@ -867,7 +870,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
@@ -888,7 +891,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
@@ -901,8 +904,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
@@ -1023,6 +1026,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:
@@ -1033,6 +1054,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
@@ -1084,6 +1115,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:
@@ -1094,12 +1148,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': '*'
@@ -1109,12 +1173,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:
@@ -1133,10 +1197,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
@@ -1678,25 +1754,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
@@ -1704,15 +1780,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
@@ -2170,114 +2241,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:
@@ -3215,137 +3178,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:
@@ -3359,41 +3211,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
@@ -3447,10 +3264,6 @@ packages:
resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==}
dev: true
/@types/geojson@7946.0.14:
resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==}
dev: false
/@types/glob@7.2.0:
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
dependencies:
@@ -3985,6 +3798,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
@@ -4426,7 +4267,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
@@ -4446,8 +4305,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)
@@ -7731,6 +7590,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:
@@ -7739,6 +7618,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==}
@@ -7837,6 +7724,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:
@@ -7876,6 +7785,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:
@@ -7941,25 +7864,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'}
@@ -9072,6 +8976,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'}
@@ -9102,6 +9019,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

@@ -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,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
import { $nodeExecutionStates } from 'features/nodes/hooks/useNodeExecutionState';
import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions';
import { $templates } from 'features/nodes/store/nodesSlice';
import { $needsFit } from 'features/nodes/store/reactFlowInstance';

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,72 +1,79 @@
import { Tooltip } from '@invoke-ai/ui-library';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Tooltip } from '@invoke-ai/ui-library';
import type { HandleType } from '@xyflow/react';
import { Handle, Position } from '@xyflow/react';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import type { ValidationResult } from 'features/nodes/store/util/validateConnection';
import { HANDLE_TOOLTIP_OPEN_DELAY, MODEL_TYPES } from 'features/nodes/types/constants';
import { type FieldInputTemplate, type FieldOutputTemplate, isSingle } from 'features/nodes/types/field';
import type { FieldInputTemplate, FieldOutputTemplate } 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;
type Props = {
handleType: HandleType;
fieldTemplate: FieldInputTemplate | FieldOutputTemplate;
isConnectionInProgress: boolean;
isConnectionStartField: boolean;
validationResult: ValidationResult;
};
const FieldHandle = (props: FieldHandleProps) => {
const { fieldTemplate, handleType, isConnectionInProgress, isConnectionStartField, validationResult } = props;
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 handleStyleBase = {
position: 'absolute',
width: '1rem',
height: '1rem',
zIndex: 1,
background: 'none',
border: 'none',
} satisfies CSSProperties;
const targetHandleStyle = {
...handleStyleBase,
insetInlineStart: '-0.5rem',
} satisfies CSSProperties;
const sourceHandleStyle = {
...handleStyleBase,
insetInlineEnd: '-0.5rem',
} satisfies CSSProperties;
export const FieldHandle = memo((props: Props) => {
const { fieldTemplate, isConnectionInProgress, isConnectionStartField, validationResult, handleType } = 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 fieldTypeName = useFieldTypeName(fieldTemplate.type);
const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
const isModelField = useMemo(() => MODEL_TYPES.some((t) => t === fieldTemplate.type.name), [fieldTemplate.type]);
const tooltip = useMemo(() => {
if (isConnectionInProgress && validationResult.messageTKey) {
@@ -83,12 +90,24 @@ const FieldHandle = (props: FieldHandleProps) => {
>
<Handle
type={handleType}
id={name}
id={fieldTemplate.name}
position={handleType === 'target' ? Position.Left : Position.Right}
style={styles}
/>
style={handleType === 'target' ? targetHandleStyle : sourceHandleStyle}
>
<Box
sx={sx}
data-cardinality={fieldTemplate.type.cardinality}
data-is-batch-field={fieldTemplate.type.batch}
data-is-model-field={isModelField}
data-is-connection-in-progress={isConnectionInProgress}
data-is-connection-start-field={isConnectionStartField}
data-is-connection-valid={validationResult.isValid}
backgroundColor={fieldTemplate.type.cardinality === 'SINGLE' ? fieldColor : 'base.900'}
borderColor={fieldColor}
/>
</Handle>
</Tooltip>
);
};
});
export default memo(FieldHandle);
FieldHandle.displayName = 'FieldHandle';

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,24 @@
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"
/>
);
});
FloatFieldInput.displayName = 'FloatFieldInput ';

View File

@@ -0,0 +1,26 @@
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
/>
);
});
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

@@ -1,13 +1,9 @@
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 { useAppDispatch } from 'app/store/storeHooks';
import { useInputFieldIsExposed } from 'features/nodes/hooks/useInputFieldIsExposed';
import { useInputFieldValue } from 'features/nodes/hooks/useInputFieldValue';
import { workflowExposedFieldAdded, workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiMinusBold, PiPlusBold } from 'react-icons/pi';
@@ -16,19 +12,11 @@ type Props = {
fieldName: string;
};
const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
export const InputFieldAddRemoveLinearViewIconButton = memo(({ 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 value = useInputFieldValue(nodeId, fieldName);
const isExposed = useInputFieldIsExposed(nodeId, fieldName);
const handleExposeField = useCallback(() => {
dispatch(workflowExposedFieldAdded({ nodeId, fieldName, value }));
@@ -63,6 +51,6 @@ const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
/>
);
}
};
});
export default memo(FieldLinearViewToggle);
InputFieldAddRemoveLinearViewIconButton.displayName = 'InputFieldAddRemoveLinearViewIconButton';

View File

@@ -0,0 +1,82 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Circle, Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
import { InputFieldNotesIconButtonEditable } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonEditable';
import { InputFieldResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton';
import { useLinearViewFieldDnd } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiMinusBold } from 'react-icons/pi';
import { InputFieldRenderer } from './InputFieldRenderer';
import { InputFieldTitle } from './InputFieldTitle';
type Props = {
nodeId: string;
fieldName: string;
};
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;
export const InputFieldEditModeLinear = memo(({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
const { t } = useTranslation();
const handleRemoveField = useCallback(() => {
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
}, [dispatch, fieldName, nodeId]);
const ref = useRef<HTMLDivElement>(null);
const [dndListState, isDragging] = useLinearViewFieldDnd(ref, { nodeId, fieldName });
return (
<Box position="relative" w="full">
<Flex
ref={ref}
// This is used to trigger the post-move flash animation
data-field-name={`${nodeId}-${fieldName}`}
data-is-dragging={isDragging}
onMouseEnter={handleMouseOver}
onMouseLeave={handleMouseOut}
sx={sx}
>
<Flex flexDir="column" w="full" gap={1}>
<Flex alignItems="center" gap={2}>
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} />
<Spacer />
{isMouseOverNode && <Circle me={2} size={2} borderRadius="full" bg="invokeBlue.500" />}
<InputFieldNotesIconButtonEditable nodeId={nodeId} fieldName={fieldName} />
<InputFieldResetToInitialValueIconButton nodeId={nodeId} fieldName={fieldName} />
<IconButton
aria-label={t('nodes.removeLinearView')}
tooltip={t('nodes.removeLinearView')}
variant="ghost"
size="xs"
onClick={handleRemoveField}
icon={<PiMinusBold />}
/>
</Flex>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</Flex>
</Flex>
<DndListDropIndicator dndState={dndListState} />
</Box>
);
});
InputFieldEditModeLinear.displayName = 'InputFieldEditModeLinear';

View File

@@ -0,0 +1,145 @@
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { Flex, FormControl } from '@invoke-ai/ui-library';
import { firefoxDndFix } from 'features/dnd/util';
import { FieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldHandle';
import { InputFieldNotesIconButtonEditable } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonEditable';
import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton';
import { buildNodeFieldDndData } from 'features/nodes/components/sidePanel/builder/use-builder-dnd';
import { useInputFieldConnectionState } from 'features/nodes/hooks/useInputFieldConnectionState';
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import type { FieldIdentifier } from 'features/nodes/types/field';
import type { RefObject } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { InputFieldAddRemoveLinearViewIconButton } from './InputFieldAddRemoveLinearViewIconButton';
import { InputFieldRenderer } from './InputFieldRenderer';
import { InputFieldTitle } from './InputFieldTitle';
import { InputFieldWrapper } from './InputFieldWrapper';
interface Props {
nodeId: string;
fieldName: string;
}
export const InputFieldEditModeNodes = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const draggableRef = useRef<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const isInvalid = useInputFieldIsInvalid(nodeId, fieldName);
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
const { isConnectionInProgress, isConnectionStartField, validationResult } = useInputFieldConnectionState(
nodeId,
fieldName
);
const onMouseEnter = useCallback(() => {
setIsHovered(true);
}, []);
const onMouseLeave = useCallback(() => {
setIsHovered(false);
}, []);
const isDragging = useNodeFieldDnd({ nodeId, fieldName }, draggableRef, dragHandleRef);
if (fieldTemplate.input === 'connection' || isConnected) {
return (
<InputFieldWrapper>
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
<InputFieldTitle
nodeId={nodeId}
fieldName={fieldName}
isInvalid={isInvalid}
isDisabled={(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField) || isConnected}
/>
</FormControl>
<FieldHandle
handleType="target"
fieldTemplate={fieldTemplate}
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
validationResult={validationResult}
/>
</InputFieldWrapper>
);
}
return (
<InputFieldWrapper>
<FormControl
ref={draggableRef}
isInvalid={isInvalid}
isDisabled={isConnected}
// Without pointerEvents prop, disabled inputs don't trigger reactflow events. For example, when making a
// connection, the mouse up to end the connection won't fire, leaving the connection in-progress.
pointerEvents={isConnected ? 'none' : 'auto'}
orientation="vertical"
px={2}
opacity={isDragging ? 0.3 : 1}
>
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Flex className="nodrag" ref={dragHandleRef} gap={1}>
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} isInvalid={isInvalid} />
{isHovered && (
<>
<InputFieldNotesIconButtonEditable nodeId={nodeId} fieldName={fieldName} />
<InputFieldResetToDefaultValueIconButton nodeId={nodeId} fieldName={fieldName} />
<InputFieldAddRemoveLinearViewIconButton nodeId={nodeId} fieldName={fieldName} />
</>
)}
</Flex>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</Flex>
</FormControl>
{fieldTemplate.input !== 'direct' && (
<FieldHandle
handleType="target"
fieldTemplate={fieldTemplate}
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
validationResult={validationResult}
/>
)}
</InputFieldWrapper>
);
});
InputFieldEditModeNodes.displayName = 'InputFieldEditModeNodes';
const useNodeFieldDnd = (
fieldIdentifier: FieldIdentifier,
draggableRef: RefObject<HTMLElement>,
dragHandleRef: RefObject<HTMLElement>
) => {
const [isDragging, setIsDragging] = useState(false);
useEffect(() => {
const draggableElement = draggableRef.current;
const dragHandleElement = dragHandleRef.current;
if (!draggableElement || !dragHandleElement) {
return;
}
return combine(
firefoxDndFix(draggableElement),
draggable({
element: draggableElement,
dragHandle: dragHandleElement,
getInitialData: () => buildNodeFieldDndData(fieldIdentifier),
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
},
})
);
}, [dragHandleRef, draggableRef, fieldIdentifier]);
return isDragging;
};

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,25 @@
import type { FormHelperTextProps } from '@invoke-ai/ui-library';
import { FormHelperText } from '@invoke-ai/ui-library';
import { useInputFieldNotes } from 'features/nodes/hooks/useInputFieldNotes';
import { memo } from 'react';
type Props = FormHelperTextProps & {
nodeId: string;
fieldName: string;
};
export const InputFieldNotesHelperText = memo(({ nodeId, fieldName, ...rest }: Props) => {
const notes = useInputFieldNotes(nodeId, fieldName);
if (!notes?.trim()) {
return null;
}
return (
<FormHelperText px={1} {...rest}>
{notes}
</FormHelperText>
);
});
InputFieldNotesHelperText.displayName = 'InputFieldNotesHelperText';

View File

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

View File

@@ -0,0 +1,45 @@
import { Box, Flex, IconButton, Popover, PopoverContent, PopoverTrigger, Text } from '@invoke-ai/ui-library';
import { useInputFieldNotes } from 'features/nodes/hooks/useInputFieldNotes';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiNoteBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldNotesIconButtonReadonly = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const notes = useInputFieldNotes(nodeId, fieldName);
if (!notes?.trim()) {
return null;
}
return (
<Popover>
<PopoverTrigger>
<IconButton
variant="ghost"
tooltip={t('nodes.notes')}
aria-label={t('nodes.notes')}
icon={<PiNoteBold />}
size="xs"
/>
</PopoverTrigger>
<PopoverContent p={2} w={256}>
<Flex flexDir="column" gap={2}>
<Text color="base.300" fontWeight="semibold" fontSize="sm">
{t('nodes.notes')}
</Text>
<Box borderWidth={1} borderRadius="base" p={2} w="full" h="full">
<Text fontSize="sm">{notes}</Text>
</Box>
</Flex>
</PopoverContent>
</Popover>
);
});
InputFieldNotesIconButtonReadonly.displayName = 'InputFieldNotesIconButtonReadonly';

View File

@@ -1,12 +1,17 @@
import { FloatFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput';
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 { 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,
@@ -94,174 +99,265 @@ 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 = {
nodeId: string;
fieldName: string;
};
const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
const fieldInstance = useFieldInputInstance(nodeId, fieldName);
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
export const InputFieldRenderer = memo(({ nodeId, fieldName }: InputFieldProps) => {
const field = useInputFieldInstance(nodeId, fieldName);
const template = useInputFieldTemplate(nodeId, fieldName);
if (isStringFieldCollectionInputInstance(fieldInstance) && isStringFieldCollectionInputTemplate(fieldTemplate)) {
return <StringFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isStringFieldCollectionInputTemplate(template)) {
if (!isStringFieldCollectionInputInstance(field)) {
return null;
}
return <StringFieldCollectionInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
}
if (isStringFieldInputInstance(fieldInstance) && isStringFieldInputTemplate(fieldTemplate)) {
return <StringFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
if (isStringFieldInputTemplate(template)) {
if (!isStringFieldInputInstance(field)) {
return null;
}
if (template.ui_component === 'textarea') {
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
} else {
return <StringFieldInput 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;
}
return <IntegerFieldInput 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;
}
return <FloatFieldInput 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,30 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useInputFieldInitialLinearViewValue } from 'features/nodes/hooks/useInputFieldInitialLinearViewValue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldResetToInitialValueIconButton = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const { isValueChanged, resetToInitialLinearViewValue } = useInputFieldInitialLinearViewValue(nodeId, fieldName);
return (
<IconButton
variant="ghost"
tooltip={t('nodes.resetToDefaultValue')}
aria-label={t('nodes.resetToDefaultValue')}
icon={<PiArrowCounterClockwiseBold />}
pointerEvents="auto"
size="xs"
onClick={resetToInitialLinearViewValue}
isDisabled={!isValueChanged}
/>
);
});
InputFieldResetToInitialValueIconButton.displayName = 'InputFieldResetToInitialValueIconButton';

View File

@@ -1,37 +1,54 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import {
Editable,
EditableInput,
EditablePreview,
Flex,
forwardRef,
Tooltip,
useEditableControls,
} from '@invoke-ai/ui-library';
import { Editable, EditableInput, EditablePreview, Flex, 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 { InputFieldTooltip } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltip';
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 type { MouseEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import FieldTooltipContent from './FieldTooltipContent';
const editablePreviewStyles: 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,
},
};
const editableInputStyles: SystemStyleObject = {
p: 0,
w: 'full',
fontWeight: 'semibold',
color: 'base.100',
_focusVisible: {
p: 0,
textAlign: 'left',
boxShadow: 'none',
},
};
interface Props {
nodeId: string;
fieldName: string;
kind: 'inputs' | 'outputs';
isInvalid?: boolean;
withTooltip?: boolean;
shouldDim?: boolean;
isDisabled?: 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);
export const InputFieldTitle = memo((props: Props) => {
const { nodeId, fieldName, isInvalid, isDisabled } = props;
const label = useInputFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -62,7 +79,6 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
onChange={handleChange}
onSubmit={handleSubmit}
as={Flex}
ref={ref}
position="relative"
overflow="hidden"
alignItems="center"
@@ -71,15 +87,14 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
w="full"
>
<Tooltip
label={withTooltip ? <FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="inputs" /> : undefined}
label={<InputFieldTooltip nodeId={nodeId} fieldName={fieldName} />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
>
<EditablePreview
fontWeight="semibold"
sx={editablePreviewStyles}
noOfLines={1}
color={isInvalid ? 'error.300' : 'base.300'}
opacity={shouldDim ? 0.5 : 1}
data-is-invalid={isInvalid}
data-is-disabled={isDisabled}
/>
</Tooltip>
<EditableInput className="nodrag" sx={editableInputStyles} />
@@ -88,26 +103,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
);
});
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);
InputFieldTitle.displayName = 'InputFieldTitle';
const EditableControls = memo(() => {
const { isEditing, getEditButtonProps } = useEditableControls();

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 InputFieldTooltip = 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>
);
});
InputFieldTooltip.displayName = 'FieldTooltipContent';

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

@@ -0,0 +1,31 @@
import { Box, FormControl, FormLabel, Spacer } from '@invoke-ai/ui-library';
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
import { InputFieldResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton';
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
import { memo } from 'react';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldViewMode = memo(({ nodeId, fieldName }: Props) => {
const label = useInputFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
return (
<FormControl w="full" gap={2} flexDir="column">
<FormLabel fontSize="sm" display="flex" w="full" m={0} gap={2} ps={1}>
{label || fieldTemplateTitle}
<Spacer />
<InputFieldResetToInitialValueIconButton nodeId={nodeId} fieldName={fieldName} />
</FormLabel>
<Box w="full" h="full">
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</Box>
</FormControl>
);
});
InputFieldViewMode.displayName = 'InputFieldViewMode';

View File

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

View File

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

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"
w="full"
marks
withThumbTooltip
/>
);
}
);
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

@@ -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,41 @@
import { FieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldHandle';
import { OutputFieldTitle } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle';
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
import { useOutputFieldConnectionState } from 'features/nodes/hooks/useOutputFieldConnectionState';
import { useOutputFieldIsConnected } from 'features/nodes/hooks/useOutputFieldIsConnected';
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
}>;
export const OutputFieldNodesEditorView = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
const isConnected = useOutputFieldIsConnected(nodeId, fieldName);
const { isConnectionInProgress, isConnectionStartField, validationResult } = useOutputFieldConnectionState(
nodeId,
fieldName
);
return (
<OutputFieldWrapper>
<OutputFieldTitle
nodeId={nodeId}
fieldName={fieldName}
isDisabled={(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField) || isConnected}
/>
<FieldHandle
handleType="source"
fieldTemplate={fieldTemplate}
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
validationResult={validationResult}
/>
</OutputFieldWrapper>
);
});
OutputFieldNodesEditorView.displayName = 'OutputFieldNodesEditorView';

View File

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

View File

@@ -0,0 +1,29 @@
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 OutputFieldTooltip = 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>
);
});
OutputFieldTooltip.displayName = 'OutputFieldTooltip';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,221 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import {
Button,
CompositeNumberInput,
Divider,
Flex,
FormLabel,
Grid,
GridItem,
IconButton,
} from '@invoke-ai/ui-library';
import { NUMPY_RAND_MAX } from 'app/constants';
import { useAppStore } from 'app/store/nanostores/store';
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { fieldFloatCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import type { FloatFieldCollectionInputInstance, FloatFieldCollectionInputTemplate } from 'features/nodes/types/field';
import { isNil } from 'lodash-es';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import type { FieldComponentProps } from './types';
const overlayscrollbarsOptions = getOverlayScrollbarsParams().options;
const sx = {
borderWidth: 1,
'&[data-error=true]': {
borderColor: 'error.500',
borderStyle: 'solid',
},
} satisfies SystemStyleObject;
export const FloatFieldCollectionInputComponent = memo(
(props: FieldComponentProps<FloatFieldCollectionInputInstance, FloatFieldCollectionInputTemplate>) => {
const { nodeId, field, fieldTemplate } = props;
const store = useAppStore();
const { t } = useTranslation();
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
const onChangeValue = useCallback(
(value: FloatFieldCollectionInputInstance['value']) => {
store.dispatch(fieldFloatCollectionValueChanged({ nodeId, fieldName: field.name, value }));
},
[field.name, nodeId, store]
);
const onRemoveNumber = useCallback(
(index: number) => {
const newValue = field.value ? [...field.value] : [];
newValue.splice(index, 1);
onChangeValue(newValue);
},
[field.value, onChangeValue]
);
const onChangeNumber = useCallback(
(index: number, value: number) => {
const newValue = field.value ? [...field.value] : [];
newValue[index] = value;
onChangeValue(newValue);
},
[field.value, onChangeValue]
);
const onAddNumber = useCallback(() => {
const newValue = field.value ? [...field.value, 0] : [0];
onChangeValue(newValue);
}, [field.value, onChangeValue]);
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 (
<Flex
className="nodrag"
position="relative"
w="full"
h="auto"
maxH={64}
alignItems="stretch"
justifyContent="center"
p={1}
sx={sx}
data-error={isInvalid}
borderRadius="base"
flexDir="column"
gap={1}
>
<Button onClick={onAddNumber} variant="ghost">
{t('nodes.addItem')}
</Button>
{field.value && field.value.length > 0 && (
<>
<Divider />
<OverlayScrollbarsComponent
className="nowheel"
defer
style={overlayScrollbarsStyles}
options={overlayscrollbarsOptions}
>
<Grid gap={1} gridTemplateColumns="auto 1fr auto" alignItems="center">
{field.value.map((value, index) => (
<FloatListItemContent
key={index}
value={value}
index={index}
min={min}
max={max}
step={step}
fineStep={fineStep}
onRemoveNumber={onRemoveNumber}
onChangeNumber={onChangeNumber}
/>
))}
</Grid>
</OverlayScrollbarsComponent>
</>
)}
</Flex>
);
}
);
FloatFieldCollectionInputComponent.displayName = 'FloatFieldCollectionInputComponent';
type FloatListItemContentProps = {
value: number;
index: number;
min: number;
max: number;
step: number;
fineStep: number;
onRemoveNumber: (index: number) => void;
onChangeNumber: (index: number, value: number) => void;
};
const FloatListItemContent = memo(
({ value, index, min, max, step, fineStep, onRemoveNumber, onChangeNumber }: FloatListItemContentProps) => {
const { t } = useTranslation();
const onClickRemove = useCallback(() => {
onRemoveNumber(index);
}, [index, onRemoveNumber]);
const onChange = useCallback(
(value: number) => {
onChangeNumber(index, value);
},
[index, onChangeNumber]
);
return (
<>
<GridItem>
<FormLabel ps={1} m={0}>
{index + 1}.
</FormLabel>
</GridItem>
<GridItem>
<CompositeNumberInput
onChange={onChange}
value={value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
flexGrow={1}
/>
</GridItem>
<GridItem>
<IconButton
tabIndex={-1}
size="sm"
variant="link"
alignSelf="stretch"
onClick={onClickRemove}
icon={<PiXBold />}
aria-label={t('common.delete')}
/>
</GridItem>
</>
);
}
);
FloatListItemContent.displayName = 'FloatListItemContent';

View File

@@ -10,7 +10,7 @@ import { addImagesToNodeImageFieldCollectionDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { DndImage } from 'features/dnd/DndImage';
import { DndImageIcon } from 'features/dnd/DndImageIcon';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import type { ImageField } from 'features/nodes/types/common';
import type { ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
@@ -39,7 +39,7 @@ export const ImageFieldCollectionInputComponent = memo(
const { nodeId, field } = props;
const store = useAppStore();
const isInvalid = useFieldIsInvalid(nodeId, field.name);
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
const dndTargetData = useMemo<AddImagesToNodeImageFieldCollection>(
() =>
@@ -132,7 +132,7 @@ const ImageGridItemContent = memo(
}
if (!query.data) {
return <IAINoContentFallback icon={<PiExclamationMarkBold />} />;
return <IAINoContentFallback icon={PiExclamationMarkBold} />;
}
return (

View File

@@ -12,11 +12,9 @@ import {
import { NUMPY_RAND_MAX } from 'app/constants';
import { useAppStore } from 'app/store/nanostores/store';
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { fieldNumberCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { fieldIntegerCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import type {
FloatFieldCollectionInputInstance,
FloatFieldCollectionInputTemplate,
IntegerFieldCollectionInputInstance,
IntegerFieldCollectionInputTemplate,
} from 'features/nodes/types/field';
@@ -38,41 +36,43 @@ const sx = {
},
} satisfies SystemStyleObject;
export const NumberFieldCollectionInputComponent = memo(
(
props:
| FieldComponentProps<IntegerFieldCollectionInputInstance, IntegerFieldCollectionInputTemplate>
| FieldComponentProps<FloatFieldCollectionInputInstance, FloatFieldCollectionInputTemplate>
) => {
export const IntegerFieldCollectionInputComponent = memo(
(props: FieldComponentProps<IntegerFieldCollectionInputInstance, IntegerFieldCollectionInputTemplate>) => {
const { nodeId, field, fieldTemplate } = props;
const store = useAppStore();
const { t } = useTranslation();
const isInvalid = useFieldIsInvalid(nodeId, field.name);
const isIntegerField = useMemo(() => fieldTemplate.type.name === 'IntegerField', [fieldTemplate.type]);
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
const onChangeValue = useCallback(
(value: IntegerFieldCollectionInputInstance['value']) => {
store.dispatch(fieldIntegerCollectionValueChanged({ nodeId, fieldName: field.name, value }));
},
[field.name, nodeId, store]
);
const onRemoveNumber = useCallback(
(index: number) => {
const newValue = field.value ? [...field.value] : [];
newValue.splice(index, 1);
store.dispatch(fieldNumberCollectionValueChanged({ nodeId, fieldName: field.name, value: newValue }));
onChangeValue(newValue);
},
[field.name, field.value, nodeId, store]
[field.value, onChangeValue]
);
const onChangeNumber = useCallback(
(index: number, value: number) => {
const newValue = field.value ? [...field.value] : [];
newValue[index] = value;
store.dispatch(fieldNumberCollectionValueChanged({ nodeId, fieldName: field.name, value: newValue }));
onChangeValue(newValue);
},
[field.name, field.value, nodeId, store]
[field.value, onChangeValue]
);
const onAddNumber = useCallback(() => {
const newValue = field.value ? [...field.value, 0] : [0];
store.dispatch(fieldNumberCollectionValueChanged({ nodeId, fieldName: field.name, value: newValue }));
}, [field.name, field.value, nodeId, store]);
onChangeValue(newValue);
}, [field.value, onChangeValue]);
const min = useMemo(() => {
let min = -NUMPY_RAND_MAX;
@@ -98,17 +98,17 @@ export const NumberFieldCollectionInputComponent = memo(
const step = useMemo(() => {
if (isNil(fieldTemplate.multipleOf)) {
return isIntegerField ? 1 : 0.1;
return 1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf, isIntegerField]);
}, [fieldTemplate.multipleOf]);
const fineStep = useMemo(() => {
if (isNil(fieldTemplate.multipleOf)) {
return isIntegerField ? 1 : 0.01;
return 1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf, isIntegerField]);
}, [fieldTemplate.multipleOf]);
return (
<Flex
@@ -140,7 +140,7 @@ export const NumberFieldCollectionInputComponent = memo(
>
<Grid gap={1} gridTemplateColumns="auto 1fr auto" alignItems="center">
{field.value.map((value, index) => (
<NumberListItemContent
<IntegerListItemContent
key={index}
value={value}
index={index}
@@ -148,7 +148,6 @@ export const NumberFieldCollectionInputComponent = memo(
max={max}
step={step}
fineStep={fineStep}
isIntegerField={isIntegerField}
onRemoveNumber={onRemoveNumber}
onChangeNumber={onChangeNumber}
/>
@@ -162,12 +161,11 @@ export const NumberFieldCollectionInputComponent = memo(
}
);
NumberFieldCollectionInputComponent.displayName = 'NumberFieldCollectionInputComponent';
IntegerFieldCollectionInputComponent.displayName = 'IntegerFieldCollectionInputComponent';
type NumberListItemContentProps = {
type IntegerListItemContentProps = {
value: number;
index: number;
isIntegerField: boolean;
min: number;
max: number;
step: number;
@@ -176,28 +174,18 @@ type NumberListItemContentProps = {
onChangeNumber: (index: number, value: number) => void;
};
const NumberListItemContent = memo(
({
value,
index,
isIntegerField,
min,
max,
step,
fineStep,
onRemoveNumber,
onChangeNumber,
}: NumberListItemContentProps) => {
const IntegerListItemContent = memo(
({ value, index, min, max, step, fineStep, onRemoveNumber, onChangeNumber }: IntegerListItemContentProps) => {
const { t } = useTranslation();
const onClickRemove = useCallback(() => {
onRemoveNumber(index);
}, [index, onRemoveNumber]);
const onChange = useCallback(
(v: number) => {
onChangeNumber(index, isIntegerField ? Math.floor(Number(v)) : Number(v));
(value: number) => {
onChangeNumber(index, Math.floor(value));
},
[index, isIntegerField, onChangeNumber]
[index, onChangeNumber]
);
return (
@@ -234,4 +222,4 @@ const NumberListItemContent = memo(
);
}
);
NumberListItemContent.displayName = 'NumberListItemContent';
IntegerListItemContent.displayName = 'IntegerListItemContent';

View File

@@ -1,89 +0,0 @@
import { CompositeNumberInput } from '@invoke-ai/ui-library';
import { NUMPY_RAND_MAX } from 'app/constants';
import { useAppDispatch } from 'app/store/storeHooks';
import { fieldNumberValueChanged } from 'features/nodes/store/nodesSlice';
import type {
FloatFieldInputInstance,
FloatFieldInputTemplate,
IntegerFieldInputInstance,
IntegerFieldInputTemplate,
} from 'features/nodes/types/field';
import { isNil } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import type { FieldComponentProps } from './types';
const NumberFieldInputComponent = (
props: FieldComponentProps<
IntegerFieldInputInstance | FloatFieldInputInstance,
IntegerFieldInputTemplate | FloatFieldInputTemplate
>
) => {
const { nodeId, field, fieldTemplate } = props;
const dispatch = useAppDispatch();
const isIntegerField = useMemo(() => fieldTemplate.type.name === 'IntegerField', [fieldTemplate.type]);
const handleValueChanged = useCallback(
(v: number) => {
dispatch(
fieldNumberValueChanged({
nodeId,
fieldName: field.name,
value: isIntegerField ? Math.floor(Number(v)) : Number(v),
})
);
},
[dispatch, field.name, isIntegerField, 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 isIntegerField ? 1 : 0.1;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf, isIntegerField]);
const fineStep = useMemo(() => {
if (isNil(fieldTemplate.multipleOf)) {
return isIntegerField ? 1 : 0.01;
}
return fieldTemplate.multipleOf;
}, [fieldTemplate.multipleOf, isIntegerField]);
return (
<CompositeNumberInput
defaultValue={fieldTemplate.default}
onChange={handleValueChanged}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className="nodrag"
/>
);
};
export default memo(NumberFieldInputComponent);

View File

@@ -2,7 +2,7 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, Divider, Flex, FormLabel, Grid, GridItem, IconButton, Input } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { fieldStringCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import type {
StringFieldCollectionInputInstance,
@@ -32,7 +32,7 @@ export const StringFieldCollectionInputComponent = memo(
const { t } = useTranslation();
const store = useAppStore();
const isInvalid = useFieldIsInvalid(nodeId, field.name);
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
const onRemoveString = useCallback(
(index: number) => {

View File

@@ -29,8 +29,7 @@ export const StringGeneratorDynamicPromptsCombinatorialSettings = memo(
);
const arg = useMemo(() => {
const { input, maxPrompts } = state;
return { prompt: input, max_prompts: maxPrompts, combinatorial: true };
return { prompt: state.input, max_prompts: state.maxPrompts, combinatorial: true };
}, [state]);
const [debouncedArg] = useDebounce(arg, 300);
@@ -38,13 +37,16 @@ export const StringGeneratorDynamicPromptsCombinatorialSettings = memo(
useEffect(() => {
if (isLoading) {
onChange({ ...state, values: loadingValues });
} else if (data) {
onChange({ ...state, values: data.prompts });
} else {
onChange({ ...state, values: [] });
return;
}
}, [data, isLoading, loadingValues, onChange, state]);
if (!data) {
onChange({ ...state, values: [] });
return;
}
onChange({ ...state, values: data.prompts });
}, [data, isLoading, onChange, state]);
return (
<Flex gap={2} flexDir="column">

View File

@@ -1,5 +1,6 @@
import { Checkbox, CompositeNumberInput, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { GeneratorTextareaWithFileUpload } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/GeneratorTextareaWithFileUpload';
import { useViewContext } from 'features/nodes/contexts/ViewContext';
import type { StringGeneratorDynamicPromptsRandom } from 'features/nodes/types/field';
import { isNil, random } from 'lodash-es';
import { memo, useCallback, useEffect, useMemo } from 'react';
@@ -14,6 +15,7 @@ type StringGeneratorDynamicPromptsRandomSettingsProps = {
export const StringGeneratorDynamicPromptsRandomSettings = memo(
({ state, onChange }: StringGeneratorDynamicPromptsRandomSettingsProps) => {
const { t } = useTranslation();
const view = useViewContext();
const loadingValues = useMemo(() => [`<${t('nodes.generatorLoading')}>`], [t]);
const onChangeInput = useCallback(
@@ -39,22 +41,25 @@ export const StringGeneratorDynamicPromptsRandomSettings = memo(
);
const arg = useMemo(() => {
const { input, count, seed } = state;
return { prompt: input, max_prompts: count, combinatorial: false, seed: seed ?? random() };
}, [state]);
return { prompt: state.input, max_prompts: state.count, combinatorial: false, seed: state.seed ?? random() };
}, [state.count, state.input, state.seed]);
const [debouncedArg] = useDebounce(arg, 300);
const { data, isLoading } = useDynamicPromptsQuery(debouncedArg);
useEffect(() => {
if (isLoading) {
onChange({ ...state, values: loadingValues });
} else if (data) {
onChange({ ...state, values: data.prompts });
} else {
onChange({ ...state, values: [] });
return;
}
}, [data, isLoading, loadingValues, onChange, state]);
if (!data) {
onChange({ ...state, values: [] });
return;
}
onChange({ ...state, values: data.prompts });
}, [data, isLoading, onChange, state, view]);
return (
<Flex gap={2} flexDir="column">

View File

@@ -1,4 +1,5 @@
import { Box, Flex, Textarea } from '@invoke-ai/ui-library';
import type { Node, NodeProps } from '@xyflow/react';
import { useAppDispatch } from 'app/store/storeHooks';
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
import NodeTitle from 'features/nodes/components/flow/nodes/common/NodeTitle';
@@ -7,9 +8,8 @@ import { notesNodeValueChanged } from 'features/nodes/store/nodesSlice';
import type { NotesNodeData } from 'features/nodes/types/invocation';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import type { NodeProps } from 'reactflow';
const NotesNode = (props: NodeProps<NotesNodeData>) => {
const NotesNode = (props: NodeProps<Node<NotesNodeData>>) => {
const { id: nodeId, data, selected } = props;
const { notes, isOpen } = data;
const dispatch = useAppDispatch();

View File

@@ -1,9 +1,9 @@
import { Icon, IconButton } from '@invoke-ai/ui-library';
import { useUpdateNodeInternals } from '@xyflow/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { nodeIsOpenChanged } from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react';
import { PiCaretUpBold } from 'react-icons/pi';
import { useUpdateNodeInternals } from 'reactflow';
interface Props {
nodeId: string;

View File

@@ -1,17 +1,17 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
import type { ChakraProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library';
import type { NodeChange } from '@xyflow/react';
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { nodesChanged } from 'features/nodes/store/nodesSlice';
import { selectNodes } from 'features/nodes/store/selectors';
import { selectNodeOpacity } from 'features/nodes/store/workflowSettingsSlice';
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from 'features/nodes/types/constants';
import type { AnyNode } from 'features/nodes/types/invocation';
import { zNodeStatus } from 'features/nodes/types/invocation';
import type { MouseEvent, PropsWithChildren } from 'react';
import { memo, useCallback } from 'react';
import type { NodeChange } from 'reactflow';
type NodeWrapperProps = PropsWithChildren & {
nodeId: string;
@@ -19,20 +19,75 @@ type NodeWrapperProps = PropsWithChildren & {
width?: ChakraProps['w'];
};
const containerSx: SystemStyleObject = {
h: 'full',
position: 'relative',
borderRadius: 'base',
transitionProperty: 'common',
transitionDuration: '0.1s',
cursor: 'grab',
};
const shadowsSx: SystemStyleObject = {
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'base',
pointerEvents: 'none',
zIndex: -1,
shadow: 'var(--invoke-shadows-xl), var(--invoke-shadows-base), var(--invoke-shadows-base)',
};
const inProgressSx: SystemStyleObject = {
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'md',
pointerEvents: 'none',
transitionProperty: 'common',
transitionDuration: '0.1s',
opacity: 0.7,
zIndex: -1,
visibility: 'hidden',
shadow: '0 0 0 2px var(--invoke-colors-yellow-400), 0 0 20px 2px var(--invoke-colors-orange-700)',
'&[data-is-in-progress="true"]': {
visibility: 'visible',
},
};
const selectionOverlaySx: SystemStyleObject = {
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'base',
transitionProperty: 'common',
transitionDuration: '0.1s',
pointerEvents: 'none',
visibility: 'hidden',
opacity: 0.5,
'&[data-is-selected="true"], &[data-is-hovered="true"]': { visibility: 'visible' },
'&[data-is-selected="true"]': { shadow: '0 0 0 3px var(--invoke-colors-blue-300)' },
'&[data-is-hovered="true"]': { shadow: '0 0 0 2px var(--invoke-colors-blue-300)' },
'&[data-is-selected="true"][data-is-hovered="true"]': {
opacity: 1,
shadow: '0 0 0 3px var(--invoke-colors-blue-300)',
},
};
const NodeWrapper = (props: NodeWrapperProps) => {
const { nodeId, width, children, selected } = props;
const store = useAppStore();
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
const executionState = useExecutionState(nodeId);
const executionState = useNodeExecutionState(nodeId);
const isInProgress = executionState?.status === zNodeStatus.enum.IN_PROGRESS;
const [nodeInProgress, shadowsXl, shadowsBase] = useToken('shadows', [
'nodeInProgress',
'shadows.xl',
'shadows.base',
]);
const dispatch = useAppDispatch();
const opacity = useAppSelector(selectNodeOpacity);
@@ -42,7 +97,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
(e: MouseEvent<HTMLDivElement>) => {
if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) {
const nodes = selectNodes(store.getState());
const nodeChanges: NodeChange[] = [];
const nodeChanges: NodeChange<AnyNode>[] = [];
nodes.forEach(({ id, selected }) => {
if (selected !== (id === nodeId)) {
nodeChanges.push({ type: 'select', id, selected: id === nodeId });
@@ -63,42 +118,14 @@ const NodeWrapper = (props: NodeWrapperProps) => {
onMouseEnter={handleMouseOver}
onMouseLeave={handleMouseOut}
className={DRAG_HANDLE_CLASSNAME}
h="full"
position="relative"
borderRadius="base"
w={width ? width : NODE_WIDTH}
transitionProperty="common"
transitionDuration="0.1s"
cursor="grab"
sx={containerSx}
width={width || NODE_WIDTH}
opacity={opacity}
>
<Box
position="absolute"
top={0}
insetInlineEnd={0}
bottom={0}
insetInlineStart={0}
borderRadius="base"
pointerEvents="none"
shadow={`${shadowsXl}, ${shadowsBase}, ${shadowsBase}`}
zIndex={-1}
/>
<Box
position="absolute"
top={0}
insetInlineEnd={0}
bottom={0}
insetInlineStart={0}
borderRadius="md"
pointerEvents="none"
transitionProperty="common"
transitionDuration="0.1s"
opacity={0.7}
shadow={isInProgress ? nodeInProgress : undefined}
zIndex={-1}
/>
<Box sx={shadowsSx} />
<Box sx={inProgressSx} data-is-in-progress={isInProgress} />
{children}
<NodeSelectionOverlay isSelected={selected} isHovered={isMouseOverNode} />
<Box sx={selectionOverlaySx} data-is-selected={selected} data-is-hovered={isMouseOverNode} />
</Box>
);
};

View File

@@ -1,4 +1,5 @@
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { useReactFlow } from '@xyflow/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
selectShouldShowMinimapPanel,
@@ -12,7 +13,6 @@ import {
PiMagnifyingGlassPlusBold,
PiMapPinBold,
} from 'react-icons/pi';
import { useReactFlow } from 'reactflow';
const ViewportControls = () => {
const { t } = useTranslation();

View File

@@ -1,9 +1,9 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { chakra, Flex } from '@invoke-ai/ui-library';
import { MiniMap } from '@xyflow/react';
import { useAppSelector } from 'app/store/storeHooks';
import { selectShouldShowMinimapPanel } from 'features/nodes/store/workflowSettingsSlice';
import { memo } from 'react';
import { MiniMap } from 'reactflow';
const ChakraMiniMap = chakra(MiniMap);

View File

@@ -15,6 +15,7 @@ import {
ModalOverlay,
Switch,
} from '@invoke-ai/ui-library';
import { SelectionMode } from '@xyflow/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton';
@@ -35,7 +36,6 @@ import {
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectionMode } from 'reactflow';
const formLabelProps: FormLabelProps = { flexGrow: 1 };
export const [useWorkflowEditorSettingsModal] = buildUseBoolean(false);

View File

@@ -0,0 +1,50 @@
import { Box } from '@invoke-ai/ui-library';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { ViewContextProvider } from 'features/nodes/contexts/ViewContext';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import type { CSSProperties } from 'react';
import { memo, useCallback, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
import WorkflowNodeInspectorPanel from './inspector/WorkflowNodeInspectorPanel';
import WorkflowFieldsLinearViewPanel from './workflow/WorkflowPanel';
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
export const EditModeLeftPanelContent = memo(() => {
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const handleDoubleClickHandle = useCallback(() => {
if (!panelGroupRef.current) {
return;
}
panelGroupRef.current.setLayout([50, 50]);
}, []);
return (
<ViewContextProvider view="edit-mode-linear">
<Box position="relative" w="full" h="full">
<ScrollableContent>
<PanelGroup
ref={panelGroupRef}
id="workflow-panel-group"
autoSaveId="workflow-panel-group"
direction="vertical"
style={panelGroupStyles}
>
<Panel id="workflow" collapsible minSize={25}>
<WorkflowFieldsLinearViewPanel />
</Panel>
<ResizeHandle onDoubleClick={handleDoubleClickHandle} />
<Panel id="inspector" collapsible minSize={25}>
<WorkflowNodeInspectorPanel />
</Panel>
</PanelGroup>
</ScrollableContent>
</Box>
</ViewContextProvider>
);
});
EditModeLeftPanelContent.displayName = 'EditModeLeftPanelContent';

View File

@@ -1,79 +0,0 @@
import 'reactflow/dist/style.css';
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu';
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo, useCallback, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
import InspectorPanel from './inspector/InspectorPanel';
import { WorkflowViewMode } from './viewMode/WorkflowViewMode';
import WorkflowPanel from './workflow/WorkflowPanel';
import { WorkflowListMenu } from './WorkflowListMenu/WorkflowListMenu';
import { WorkflowListMenuTrigger } from './WorkflowListMenu/WorkflowListMenuTrigger';
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
const overlayScrollbarsStyles: CSSProperties = {
height: '100%',
width: '100%',
};
const NodeEditorPanelGroup = () => {
const mode = useAppSelector(selectWorkflowMode);
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const workflowListMenu = useWorkflowListMenu();
const handleDoubleClickHandle = useCallback(() => {
if (!panelGroupRef.current) {
return;
}
panelGroupRef.current.setLayout([50, 50]);
}, []);
return (
<Flex w="full" h="full" gap={2} flexDir="column">
<WorkflowListMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
{workflowListMenu.isOpen && (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<WorkflowListMenu />
</Flex>
</OverlayScrollbarsComponent>
)}
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
{mode === 'view' && <WorkflowViewMode />}
{mode === 'edit' && (
<PanelGroup
ref={panelGroupRef}
id="workflow-panel-group"
autoSaveId="workflow-panel-group"
direction="vertical"
style={panelGroupStyles}
>
<Panel id="workflow" collapsible minSize={25}>
<WorkflowPanel />
</Panel>
<ResizeHandle onDoubleClick={handleDoubleClickHandle} />
<Panel id="inspector" collapsible minSize={25}>
<InspectorPanel />
</Panel>
</PanelGroup>
)}
</OverlayScrollbarsComponent>
</Box>
</Flex>
</Flex>
);
};
export default memo(NodeEditorPanelGroup);

View File

@@ -1,28 +1,34 @@
import { Flex } from '@invoke-ai/ui-library';
import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import UploadWorkflowButton from 'features/workflowLibrary/components/UploadWorkflowButton';
import { memo } from 'react';
import { WorkflowList } from './WorkflowList';
import WorkflowSearch from './WorkflowSearch';
import { WorkflowSortControl } from './WorkflowSortControl';
export const WorkflowListMenu = () => {
export const WorkflowListMenu = memo(() => {
const workflowCategories = useStore($workflowCategories);
return (
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
<Flex w="full" h="full" flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
<WorkflowSearch />
<WorkflowSortControl />
<UploadWorkflowButton />
</Flex>
{workflowCategories.map((category) => (
<WorkflowList key={category} category={category} />
))}
<Box position="relative" w="full" h="full">
<ScrollableContent>
{workflowCategories.map((category) => (
<WorkflowList key={category} category={category} />
))}
</ScrollableContent>
</Box>
</Flex>
);
};
});
WorkflowListMenu.displayName = 'WorkflowListMenu';

View File

@@ -0,0 +1,30 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { EditModeLeftPanelContent } from 'features/nodes/components/sidePanel/EditModeLeftPanelContent';
import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu';
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
import { memo } from 'react';
import { ViewModeLeftPanelContent } from './viewMode/ViewModeLeftPanelContent';
import { WorkflowListMenu } from './WorkflowListMenu/WorkflowListMenu';
import { WorkflowListMenuTrigger } from './WorkflowListMenu/WorkflowListMenuTrigger';
const WorkflowsTabLeftPanel = () => {
const mode = useAppSelector(selectWorkflowMode);
const workflowListMenu = useWorkflowListMenu();
return (
<Flex w="full" h="full" gap={2} flexDir="column">
<WorkflowListMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
{workflowListMenu.isOpen && <WorkflowListMenu />}
{mode === 'view' && <ViewModeLeftPanelContent />}
{mode === 'edit' && <EditModeLeftPanelContent />}
</Box>
</Flex>
</Flex>
);
};
export default memo(WorkflowsTabLeftPanel);

View File

@@ -0,0 +1,16 @@
import type { TextareaProps } from '@invoke-ai/ui-library';
import { chakra, forwardRef, typedMemo, useStyleConfig } from '@invoke-ai/ui-library';
import type { ComponentProps } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
const ChakraTextareaAutosize = chakra(TextareaAutosize);
export const AutosizeTextarea = typedMemo(
forwardRef<ComponentProps<typeof ChakraTextareaAutosize> & TextareaProps, typeof ChakraTextareaAutosize>(
({ variant, ...rest }, ref) => {
const styles = useStyleConfig('Textarea', { variant });
return <ChakraTextareaAutosize __css={styles} ref={ref} {...rest} />;
}
)
);
AutosizeTextarea.displayName = 'AutosizeTextarea';

View File

@@ -0,0 +1,151 @@
import { Flex, IconButton, type SystemStyleObject } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
ContainerContextProvider,
DepthContextProvider,
useDepthContext,
} from 'features/nodes/components/sidePanel/builder/contexts';
import { DividerElementComponent } from 'features/nodes/components/sidePanel/builder/DividerElementComponent';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { HeadingElementComponent } from 'features/nodes/components/sidePanel/builder/HeadingElementComponent';
import { NodeFieldElementComponent } from 'features/nodes/components/sidePanel/builder/NodeFieldElementComponent';
import { TextElementComponent } from 'features/nodes/components/sidePanel/builder/TextElementComponent';
import { formElementAdded, selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import type { ContainerElement } from 'features/nodes/types/workflow';
import {
buildContainer,
CONTAINER_CLASS_NAME,
isContainerElement,
isDividerElement,
isHeadingElement,
isNodeFieldElement,
isTextElement,
} from 'features/nodes/types/workflow';
import { memo, useCallback } from 'react';
import { PiPlusBold } from 'react-icons/pi';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const sx: SystemStyleObject = {
gap: 4,
flex: '1 1 0',
'&[data-container-direction="column"]': {
flexDir: 'column',
},
'&[data-container-direction="row"]': {
flexDir: 'row',
},
};
export const ContainerElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isContainerElement(el)) {
return null;
}
if (mode === 'view') {
return <ContainerElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <ContainerElementComponentEditMode el={el} />;
});
ContainerElementComponent.displayName = 'ContainerElementComponent';
export const ContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }) => {
const depth = useDepthContext();
const { id, data } = el;
const { children, direction } = data;
return (
<DepthContextProvider depth={depth + 1}>
<ContainerContextProvider id={id} direction={direction}>
<Flex id={id} className={CONTAINER_CLASS_NAME} sx={sx} data-container-direction={direction}>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
</Flex>
</ContainerContextProvider>
</DepthContextProvider>
);
});
ContainerElementComponentViewMode.displayName = 'ContainerElementComponentViewMode';
export const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }) => {
const depth = useDepthContext();
const { id, data } = el;
const { children, direction } = data;
return (
<FormElementEditModeWrapper element={el}>
<DepthContextProvider depth={depth + 1}>
<ContainerContextProvider id={id} direction={direction}>
<Flex id={id} className={CONTAINER_CLASS_NAME} sx={sx} data-container-direction={direction}>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
{direction === 'row' && children.length < 3 && depth < 2 && <AddColumnButton el={el} />}
{direction === 'column' && depth < 1 && <AddRowButton el={el} />}
</Flex>
</ContainerContextProvider>
</DepthContextProvider>
</FormElementEditModeWrapper>
);
});
ContainerElementComponentEditMode.displayName = 'ContainerElementComponentEditMode';
const AddColumnButton = ({ el }: { el: ContainerElement }) => {
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
const element = buildContainer('column', [], el.id);
dispatch(formElementAdded({ element, containerId: el.id }));
}, [dispatch, el.id]);
return (
<IconButton onClick={onClick} aria-label="add column" icon={<PiPlusBold />} h="unset" variant="ghost" size="sm" />
);
};
const AddRowButton = ({ el }: { el: ContainerElement }) => {
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
const element = buildContainer('row', [], el.id);
dispatch(formElementAdded({ element, containerId: el.id }));
}, [dispatch, el.id]);
return (
<IconButton onClick={onClick} aria-label="add row" icon={<PiPlusBold />} w="unset" variant="ghost" size="sm" />
);
};
// TODO(psyche): Can we move this into a separate file and avoid circular dependencies between it and ContainerElementComponent?
export const FormElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
if (!el) {
return null;
}
if (isContainerElement(el)) {
return <ContainerElementComponent key={id} id={id} />;
}
if (isNodeFieldElement(el)) {
return <NodeFieldElementComponent key={id} id={id} />;
}
if (isDividerElement(el)) {
return <DividerElementComponent key={id} id={id} />;
}
if (isHeadingElement(el)) {
return <HeadingElementComponent key={id} id={id} />;
}
if (isTextElement(el)) {
return <TextElementComponent key={id} id={id} />;
}
assert<Equals<typeof el, never>>(false, `Unhandled type for element with id ${id}`);
});
FormElementComponent.displayName = 'FormElementComponent';

View File

@@ -0,0 +1,74 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import type { DividerElement } from 'features/nodes/types/workflow';
import { DIVIDER_CLASS_NAME, isDividerElement } from 'features/nodes/types/workflow';
import { memo } from 'react';
const sx: SystemStyleObject = {
bg: 'base.700',
flexShrink: 0,
'&[data-orientation="horizontal"]': {
width: '100%',
height: '1px',
},
'&[data-orientation="vertical"]': {
height: '100%',
width: '1px',
},
};
export const DividerElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isDividerElement(el)) {
return;
}
if (mode === 'view') {
return <DividerElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <DividerElementComponentEditMode el={el} />;
});
DividerElementComponent.displayName = 'DividerElementComponent';
export const DividerElementComponentViewMode = memo(({ el }: { el: DividerElement }) => {
const container = useContainerContext();
const { id } = el;
return (
<Flex
id={id}
className={DIVIDER_CLASS_NAME}
sx={sx}
data-orientation={container?.direction === 'column' ? 'horizontal' : 'vertical'}
/>
);
});
DividerElementComponentViewMode.displayName = 'DividerElementComponentViewMode';
export const DividerElementComponentEditMode = memo(({ el }: { el: DividerElement }) => {
const container = useContainerContext();
const { id } = el;
return (
<FormElementEditModeWrapper element={el}>
<Flex
id={id}
className={DIVIDER_CLASS_NAME}
sx={sx}
data-orientation={container?.direction === 'column' ? 'horizontal' : 'vertical'}
/>
</FormElementEditModeWrapper>
);
});
DividerElementComponentEditMode.displayName = 'DividerElementComponentEditMode';

View File

@@ -0,0 +1,127 @@
// Adapted from https://github.com/alexreardon/pdnd-react-tailwind/blob/main/src/drop-indicator.tsx
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/types';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box } from '@invoke-ai/ui-library';
import type { CenterOrEdge } from 'features/nodes/components/sidePanel/builder/center-or-closest-edge';
/**
* Design decisions for the drop indicator's main line
*/
const line = {
thickness: 2,
backgroundColor: 'base.500',
};
type DropIndicatorProps = {
/**
* The `edge` to draw a drop indicator on.
*
* `edge` is required as for the best possible performance
* outcome you should only render this component when it needs to do something
*
* @example {closestEdge && <DropIndicator edge={closestEdge} />}
*/
edge: Edge;
/**
* `gap` allows you to position the drop indicator further away from the drop target.
* `gap` should be the distance between your drop targets
* a drop indicator will be rendered halfway between the drop targets
* (the drop indicator will be offset by half of the `gap`)
*
* `gap` should be a valid CSS length.
* @example "8px"
* @example "var(--gap)"
*/
gap?: string;
};
const lineStyles: SystemStyleObject = {
display: 'block',
position: 'absolute',
zIndex: 1,
borderRadius: 'full',
// Blocking pointer events to prevent the line from triggering drag events
// Dragging over the line should count as dragging over the element behind it
pointerEvents: 'none',
background: line.backgroundColor,
};
type Orientation = 'horizontal' | 'vertical';
const orientationStyles: Record<Orientation, SystemStyleObject> = {
horizontal: {
height: `${line.thickness}px`,
left: 2,
right: 2,
},
vertical: {
width: `${line.thickness}px`,
top: 2,
bottom: 2,
},
};
const edgeToOrientationMap: Record<Edge, Orientation> = {
top: 'horizontal',
bottom: 'horizontal',
left: 'vertical',
right: 'vertical',
};
const edgeStyles: Record<Edge, SystemStyleObject> = {
top: {
top: 'var(--local-line-offset)',
},
right: {
right: 'var(--local-line-offset)',
},
bottom: {
bottom: 'var(--local-line-offset)',
},
left: {
left: 'var(--local-line-offset)',
},
};
/**
* __Drop indicator__
*
* A drop indicator is used to communicate the intended resting place of the draggable item. The orientation of the drop indicator should always match the direction of the content flow.
*/
function DndDropIndicatorInternal({ edge, gap = '0px' }: DropIndicatorProps) {
/**
* To clearly communicate the resting place of a draggable item during a drag operation,
* the drop indicator should be positioned half way between draggable items.
*/
const lineOffset = `calc(-0.5 * (${gap} + ${line.thickness}px))`;
const orientation = edgeToOrientationMap[edge];
return (
<Box
sx={{ ...lineStyles, ...orientationStyles[orientation], ...edgeStyles[edge], '--local-line-offset': lineOffset }}
/>
);
}
export const DndListDropIndicator = ({
activeDropRegion,
gap,
}: {
activeDropRegion: CenterOrEdge | null;
gap?: string;
}) => {
if (!activeDropRegion) {
return null;
}
if (activeDropRegion === 'center') {
return null;
}
return (
<DndDropIndicatorInternal
edge={activeDropRegion}
// This is the gap between items in the list, used to calculate the position of the drop indicator
gap={gap || 'var(--invoke-space-2)'}
/>
);
};

View File

@@ -0,0 +1,103 @@
import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { DndListDropIndicator } from 'features/nodes/components/sidePanel/builder/DndListDropIndicator';
import { EDIT_MODE_WRAPPER_CLASS_NAME, getEditModeWrapperId } from 'features/nodes/components/sidePanel/builder/shared';
import { useDraggableFormElement } from 'features/nodes/components/sidePanel/builder/use-builder-dnd';
import { formElementRemoved } from 'features/nodes/store/workflowSlice';
import { type FormElement, isContainerElement } from 'features/nodes/types/workflow';
import { startCase } from 'lodash-es';
import { memo, useCallback, useRef } from 'react';
import { PiXBold } from 'react-icons/pi';
const getHeaderLabel = (el: FormElement) => {
if (isContainerElement(el)) {
if (el.data.direction === 'column') {
return 'Column';
}
return 'Row';
}
return startCase(el.type);
};
const wrapperSx: SystemStyleObject = {
position: 'relative',
flexDir: 'column',
boxShadow: '0 0 0 1px var(--invoke-colors-base-750)',
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'flex-start',
w: 'full',
h: 'full',
'&[data-is-dragging="true"]': {
opacity: 0.3,
},
'&[data-active-drop-region="center"]': {
opacity: 1,
bg: 'base.700',
},
};
const headerSx: SystemStyleObject = {
w: 'full',
ps: 2,
h: 8,
borderTopRadius: 'inherit',
borderColor: 'inherit',
alignItems: 'center',
cursor: 'grab',
bg: 'base.700',
'&[data-depth="0"]': { bg: 'base.800' },
'&[data-depth="1"]': { bg: 'base.800' },
'&[data-depth="2"]': { bg: 'base.750' },
};
export const FormElementEditModeWrapper = memo(
({ element, children, ...rest }: { element: FormElement } & FlexProps) => {
const draggableRef = useRef<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const [activeDropRegion, isDragging] = useDraggableFormElement(element.id, draggableRef, dragHandleRef);
const depth = useDepthContext();
const dispatch = useAppDispatch();
const removeElement = useCallback(() => {
dispatch(formElementRemoved({ id: element.id }));
}, [dispatch, element.id]);
return (
<Flex
id={getEditModeWrapperId(element.id)}
ref={draggableRef}
sx={wrapperSx}
className={EDIT_MODE_WRAPPER_CLASS_NAME}
data-is-dragging={isDragging}
data-active-drop-region={activeDropRegion}
{...rest}
>
<Flex ref={dragHandleRef} sx={headerSx} data-depth={depth}>
<Text fontWeight="semibold" noOfLines={1} wordBreak="break-all">
{getHeaderLabel(element)} ({element.id})
</Text>
<Spacer />
{element.parentId && (
<IconButton
aria-label="delete"
onClick={removeElement}
icon={<PiXBold />}
variant="link"
size="sm"
alignSelf="stretch"
colorScheme="error"
/>
)}
</Flex>
<Flex w="full" p={4} alignItems="center" gap={4}>
{children}
</Flex>
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
</Flex>
);
}
);
FormElementEditModeWrapper.displayName = 'FormElementEditModeWrapper';

View File

@@ -0,0 +1,119 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { formElementHeadingDataChanged, selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import type { HeadingElement } from 'features/nodes/types/workflow';
import { HEADING_CLASS_NAME, isHeadingElement } from 'features/nodes/types/workflow';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
const LEVEL_TO_FONT_SIZE = {
1: '4xl',
2: '3xl',
3: '2xl',
4: 'xl',
5: 'lg',
} as const;
export const HeadingElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isHeadingElement(el)) {
return null;
}
if (mode === 'view') {
return <HeadingElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <HeadingElementComponentEditMode el={el} />;
});
HeadingElementComponent.displayName = 'HeadingElementComponent';
export const HeadingElementComponentViewMode = memo(({ el }: { el: HeadingElement }) => {
const { id, data } = el;
const { content, level } = data;
return (
<Flex id={id} className={HEADING_CLASS_NAME}>
<Text fontWeight="bold" fontSize={LEVEL_TO_FONT_SIZE[level]}>
{content || 'Edit to add heading'}
</Text>
</Flex>
);
});
HeadingElementComponentViewMode.displayName = 'HeadingElementComponentViewMode';
export const HeadingElementComponentEditMode = memo(({ el }: { el: HeadingElement }) => {
const { id } = el;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={HEADING_CLASS_NAME} w="full">
<EditableHeading el={el} />
</Flex>
</FormElementEditModeWrapper>
);
});
HeadingElementComponentEditMode.displayName = 'HeadingElementComponentEditMode';
export const EditableHeading = memo(({ el }: { el: HeadingElement }) => {
const dispatch = useAppDispatch();
const { id, data } = el;
const { content, level } = data;
const [localContent, setLocalContent] = useState(content);
const ref = useRef<HTMLTextAreaElement>(null);
const onChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setLocalContent(e.target.value);
}, []);
const onBlur = useCallback(() => {
const trimmedContent = localContent.trim();
if (trimmedContent === content) {
return;
}
setLocalContent(trimmedContent);
dispatch(formElementHeadingDataChanged({ id, changes: { content: trimmedContent } }));
}, [localContent, content, id, dispatch]);
const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
onBlur();
} else if (e.key === 'Escape') {
setLocalContent(content);
}
},
[content, onBlur]
);
return (
<AutosizeTextarea
ref={ref}
placeholder="Heading"
value={localContent}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
variant="outline"
overflowWrap="anywhere"
w="full"
minRows={1}
maxRows={10}
resize="none"
p={2}
fontWeight="bold"
fontSize={LEVEL_TO_FONT_SIZE[level]}
/>
);
});
EditableHeading.displayName = 'EditableHeading';

View File

@@ -0,0 +1,59 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { InputFieldViewMode } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewMode';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { isNodeFieldElement, NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
import { memo } from 'react';
export const NodeFieldElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isNodeFieldElement(el)) {
return null;
}
if (mode === 'view') {
return <NodeFieldElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <NodeFieldElementComponentEditMode el={el} />;
});
NodeFieldElementComponent.displayName = 'NodeFieldElementComponent';
export const NodeFieldElementComponentViewMode = memo(({ el }: { el: NodeFieldElement }) => {
const { id, data } = el;
const { fieldIdentifier } = data;
return (
<Flex id={id} className={NODE_FIELD_CLASS_NAME}>
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
<InputFieldViewMode nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
</InputFieldGate>
</Flex>
);
});
NodeFieldElementComponentViewMode.displayName = 'NodeFieldElementComponentViewMode';
export const NodeFieldElementComponentEditMode = memo(({ el }: { el: NodeFieldElement }) => {
const { id, data } = el;
const { fieldIdentifier } = data;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={NODE_FIELD_CLASS_NAME} w="full">
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
<InputFieldViewMode nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
</InputFieldGate>
</Flex>
</FormElementEditModeWrapper>
);
});
NodeFieldElementComponentEditMode.displayName = 'NodeFieldElementComponentEditMode';

View File

@@ -0,0 +1,106 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { formElementTextDataChanged, selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import type { TextElement } from 'features/nodes/types/workflow';
import { isTextElement, TEXT_CLASS_NAME } from 'features/nodes/types/workflow';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
export const TextElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isTextElement(el)) {
return null;
}
if (mode === 'view') {
return <TextElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <TextElementComponentEditMode el={el} />;
});
TextElementComponent.displayName = 'TextElementComponent';
export const TextElementComponentViewMode = memo(({ el }: { el: TextElement }) => {
const { id, data } = el;
const { content, fontSize } = data;
return (
<Flex id={id} className={TEXT_CLASS_NAME}>
<Text fontSize={fontSize} overflowWrap="anywhere">
{content || 'Edit to add text'}
</Text>
</Flex>
);
});
TextElementComponentViewMode.displayName = 'TextElementComponentViewMode';
export const TextElementComponentEditMode = memo(({ el }: { el: TextElement }) => {
const { id } = el;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={TEXT_CLASS_NAME} w="full">
<EditableText el={el} />
</Flex>
</FormElementEditModeWrapper>
);
});
TextElementComponentEditMode.displayName = 'TextElementComponentEditMode';
export const EditableText = memo(({ el }: { el: TextElement }) => {
const dispatch = useAppDispatch();
const { id, data } = el;
const { content, fontSize } = data;
const [localContent, setLocalContent] = useState(content);
const ref = useRef<HTMLTextAreaElement>(null);
const onChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setLocalContent(e.target.value);
}, []);
const onBlur = useCallback(() => {
const trimmedContent = localContent.trim();
if (trimmedContent === content) {
return;
}
setLocalContent(trimmedContent);
dispatch(formElementTextDataChanged({ id, changes: { content: trimmedContent } }));
}, [localContent, content, id, dispatch]);
const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
onBlur();
} else if (e.key === 'Escape') {
setLocalContent(content);
}
},
[content, onBlur]
);
return (
<AutosizeTextarea
ref={ref}
placeholder="Text"
value={localContent}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
fontSize={fontSize}
variant="outline"
overflowWrap="anywhere"
w="full"
minRows={1}
maxRows={10}
resize="none"
p={2}
/>
);
});
EditableText.displayName = 'EditableText';

View File

@@ -0,0 +1,109 @@
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { firefoxDndFix } from 'features/dnd/util';
import { FormElementComponent } from 'features/nodes/components/sidePanel/builder/ContainerElementComponent';
import {
buildAddFormElementDndData,
useMonitorForFormElementDnd,
} from 'features/nodes/components/sidePanel/builder/use-builder-dnd';
import { formModeToggled, selectRootElementId, selectWorkflowFormMode } from 'features/nodes/store/workflowSlice';
import type { FormElement } from 'features/nodes/types/workflow';
import { buildContainer, buildDivider, buildHeading, buildText } from 'features/nodes/types/workflow';
import type { RefObject } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { assert } from 'tsafe';
export const WorkflowBuilder = memo(() => {
const mode = useAppSelector(selectWorkflowFormMode);
const rootElementId = useAppSelector(selectRootElementId);
useMonitorForFormElementDnd();
return (
<ScrollableContent>
<Flex w="full" justifyContent="center">
<Flex flexDir="column" w={mode === 'view' ? '512px' : 'min-content'} minW="512px" gap={2}>
<ButtonGroup isAttached={false} justifyContent="center">
<ToggleModeButton />
<AddFormElementDndButton type="container" />
<AddFormElementDndButton type="divider" />
<AddFormElementDndButton type="heading" />
<AddFormElementDndButton type="text" />
</ButtonGroup>
{rootElementId && <FormElementComponent id={rootElementId} />}
</Flex>
</Flex>
</ScrollableContent>
);
});
WorkflowBuilder.displayName = 'WorkflowBuilder';
const ToggleModeButton = memo(() => {
const dispatch = useAppDispatch();
const mode = useAppSelector(selectWorkflowFormMode);
const onClick = useCallback(() => {
dispatch(formModeToggled());
}, [dispatch]);
return <Button onClick={onClick}>{mode === 'view' ? 'Edit' : 'View'}</Button>;
});
ToggleModeButton.displayName = 'ToggleModeButton';
const useAddFormElementDnd = (type: Omit<FormElement['type'], 'node-field'>, draggableRef: RefObject<HTMLElement>) => {
const [isDragging, setIsDragging] = useState(false);
useEffect(() => {
const draggableElement = draggableRef.current;
if (!draggableElement) {
return;
}
return combine(
firefoxDndFix(draggableElement),
draggable({
element: draggableElement,
getInitialData: () => {
if (type === 'container') {
const element = buildContainer('row', []);
return buildAddFormElementDndData(element);
}
if (type === 'divider') {
const element = buildDivider();
return buildAddFormElementDndData(element);
}
if (type === 'heading') {
const element = buildHeading('default heading', 1);
return buildAddFormElementDndData(element);
}
if (type === 'text') {
const element = buildText('default text', 'sm');
return buildAddFormElementDndData(element);
}
assert(false);
},
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
},
})
);
}, [draggableRef, type]);
return isDragging;
};
const AddFormElementDndButton = ({ type }: { type: Omit<FormElement['type'], 'node-field'> }) => {
const draggableRef = useRef<HTMLButtonElement>(null);
const isDragging = useAddFormElementDnd(type, draggableRef);
return (
<Button ref={draggableRef} variant="ghost" pointerEvents="auto" opacity={isDragging ? 0.3 : 1}>
{type}
</Button>
);
};

View File

@@ -0,0 +1,119 @@
// Adapted from https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/src/closest-edge.ts
// This adaptation adds 'center' as a possible target
import type { Input, Position } from '@atlaskit/pragmatic-drag-and-drop/types';
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/types';
export type CenterOrEdge = 'center' | Edge;
// re-exporting type to make it easy to use
// When the DOM element is small, the closest-edge algorithm can result in a very small hitbox for the center
// region, making it difficult for the user to hit the center. To mitigate this, when the center is allowed,
// we use an absolute edge hitbox size of 10px or 1/4 of the element's size, whichever is smaller.
const getDistanceToCenterOrEdge: {
[TKey in CenterOrEdge]: (
rect: Pick<DOMRect, 'top' | 'right' | 'bottom' | 'left' | 'width' | 'height'>,
client: Position,
isCenterAllowed: boolean
) => number;
} = {
top: (rect, client, isCenterAllowed) => {
const distanceFromTop = Math.abs(client.y - rect.top);
if (!isCenterAllowed) {
return distanceFromTop;
}
const hitboxHeight = Math.min(rect.height / 4, 10);
if (distanceFromTop <= hitboxHeight) {
return 0;
}
return Infinity;
},
right: (rect, client, isCenterAllowed) => {
const distanceFromRight = Math.abs(rect.right - client.x);
if (!isCenterAllowed) {
return distanceFromRight;
}
const hitboxWidth = Math.min(rect.width / 4, 10);
if (distanceFromRight <= hitboxWidth) {
return 0;
}
return Infinity;
},
bottom: (rect, client, isCenterAllowed) => {
const distanceFromBottom = Math.abs(rect.bottom - client.y);
if (!isCenterAllowed) {
return distanceFromBottom;
}
const hitboxHeight = Math.min(rect.height / 4, 10);
if (distanceFromBottom <= hitboxHeight) {
return 0;
}
return Infinity;
},
left: (rect, client, isCenterAllowed) => {
const distanceFromLeft = Math.abs(client.x - rect.left);
if (!isCenterAllowed) {
return distanceFromLeft;
}
const hitboxWidth = Math.min(rect.width / 4, 10);
if (distanceFromLeft <= hitboxWidth) {
return 0;
}
return Infinity;
},
center: (rect, client, _) => {
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
return Math.sqrt((client.x - centerX) ** 2 + (client.y - centerY) ** 2);
},
};
// using a symbol so we can guarantee a key with a unique value
const uniqueKey = Symbol('centerWithClosestEdge');
/**
* Adds a unique `Symbol` to the `userData` object. Use with `extractClosestEdge()` for type safe lookups.
*/
export function attachClosestCenterOrEdge(
userData: Record<string | symbol, unknown>,
{
element,
input,
allowedCenterOrEdge,
}: {
element: Element;
input: Input;
allowedCenterOrEdge: CenterOrEdge[];
}
): Record<string | symbol, unknown> {
const client: Position = {
x: input.clientX,
y: input.clientY,
};
// I tried caching the result of `getBoundingClientRect()` for a single
// frame in order to improve performance.
// However, on measurement I saw no improvement. So no longer caching
const rect: DOMRect = element.getBoundingClientRect();
const isCenterAllowed = allowedCenterOrEdge.includes('center');
const entries = allowedCenterOrEdge.map((edge) => {
return { edge, value: getDistanceToCenterOrEdge[edge](rect, client, isCenterAllowed) };
});
// edge can be `null` when `allowedCenterOrEdge` is []
const addClosestCenterOrEdge: CenterOrEdge | null = entries.sort((a, b) => a.value - b.value)[0]?.edge ?? null;
return {
...userData,
[uniqueKey]: addClosestCenterOrEdge,
};
}
/**
* Returns the value added by `attachClosestEdge()` to the `userData` object. It will return `null` if there is no value.
*/
export function extractClosestCenterOrEdge(userData: Record<string | symbol, unknown>): CenterOrEdge | null {
return (userData[uniqueKey] as CenterOrEdge) ?? null;
}

View File

@@ -0,0 +1,35 @@
import type { ContainerElement, ElementId } from 'features/nodes/types/workflow';
import type { PropsWithChildren } from 'react';
import { createContext, memo, useContext, useMemo } from 'react';
type ContainerContextValue = {
id: ElementId;
direction: ContainerElement['data']['direction'];
};
const ContainerContext = createContext<ContainerContextValue | null>(null);
export const ContainerContextProvider = memo(
({ id, direction, children }: PropsWithChildren<ContainerContextValue>) => {
const ctxValue = useMemo(() => ({ id, direction }), [id, direction]);
return <ContainerContext.Provider value={ctxValue}>{children}</ContainerContext.Provider>;
}
);
ContainerContextProvider.displayName = 'ContainerContextProvider';
export const useContainerContext = () => {
const container = useContext(ContainerContext);
return container;
};
const DepthContext = createContext<number>(0);
export const DepthContextProvider = memo(({ depth, children }: PropsWithChildren<{ depth: number }>) => {
return <DepthContext.Provider value={depth}>{children}</DepthContext.Provider>;
});
DepthContextProvider.displayName = 'DepthContextProvider';
export const useDepthContext = () => {
const depth = useContext(DepthContext);
return depth;
};

View File

@@ -0,0 +1,5 @@
import { getPrefixedId } from 'features/controlLayers/konva/util';
export const EDIT_MODE_WRAPPER_CLASS_NAME = getPrefixedId('edit-mode-wrapper', '-');
export const getEditModeWrapperId = (id: string) => `${id}-edit-mode-wrapper`;

View File

@@ -0,0 +1,373 @@
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import {
draggable,
dropTargetForElements,
monitorForElements,
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { getStore } from 'app/store/nanostores/store';
import { useAppDispatch } from 'app/store/storeHooks';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { firefoxDndFix, triggerPostMoveFlash } from 'features/dnd/util';
import type { CenterOrEdge } from 'features/nodes/components/sidePanel/builder/center-or-closest-edge';
import {
attachClosestCenterOrEdge,
extractClosestCenterOrEdge,
} from 'features/nodes/components/sidePanel/builder/center-or-closest-edge';
import { getEditModeWrapperId } from 'features/nodes/components/sidePanel/builder/shared';
import { formElementAdded, formElementMoved } from 'features/nodes/store/workflowSlice';
import type { FieldIdentifier } from 'features/nodes/types/field';
import type { ElementId, FormElement } from 'features/nodes/types/workflow';
import { buildNodeField, isContainerElement } from 'features/nodes/types/workflow';
import type { RefObject } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { flushSync } from 'react-dom';
import { assert } from 'tsafe';
const uniqueMoveFormElementKey = Symbol('move-form-element');
type MoveFormElementDndData = {
[uniqueMoveFormElementKey]: true;
element: FormElement;
};
const buildMoveFormElementDndData = (element: FormElement): MoveFormElementDndData => ({
[uniqueMoveFormElementKey]: true,
element,
});
const isMoveFormElementDndData = (data: Record<string | symbol, unknown>): data is MoveFormElementDndData => {
return uniqueMoveFormElementKey in data;
};
const uniqueAddFormElementKey = Symbol('add-form-element');
type AddFormElementDndData = {
[uniqueAddFormElementKey]: true;
element: FormElement;
};
export const buildAddFormElementDndData = (element: FormElement): AddFormElementDndData => ({
[uniqueAddFormElementKey]: true,
element,
});
const isAddFormElementDndData = (data: Record<string | symbol, unknown>): data is AddFormElementDndData => {
return uniqueAddFormElementKey in data;
};
const uniqueNodeFieldKey = Symbol('node-field');
type NodeFieldDndData = {
[uniqueNodeFieldKey]: true;
fieldIdentifier: FieldIdentifier;
};
export const buildNodeFieldDndData = (fieldIdentifier: FieldIdentifier): NodeFieldDndData => ({
[uniqueNodeFieldKey]: true,
fieldIdentifier,
});
const isNodeFieldDndData = (data: Record<string | symbol, unknown>): data is NodeFieldDndData => {
return uniqueNodeFieldKey in data;
};
const getElement = <T extends FormElement>(id: ElementId, guard?: (el: FormElement) => el is T): T => {
const el = getStore().getState().workflow.form?.elements[id];
assert(el);
if (guard) {
assert(guard(el));
return el;
} else {
return el as T;
}
};
const adjustIndexForFormElementMoveDrop = (index: number, edge: Exclude<CenterOrEdge, 'center'>) => {
if (edge === 'left' || edge === 'top') {
return index - 1;
}
return index + 1;
};
const adjustIndexForNodeFieldDrop = (index: number, edge: Exclude<CenterOrEdge, 'center'>) => {
if (edge === 'left' || edge === 'top') {
return index;
}
return index + 1;
};
const flashElement = (elementId: ElementId) => {
const element = document.querySelector(`#${getEditModeWrapperId(elementId)}`);
if (element instanceof HTMLElement) {
triggerPostMoveFlash(element, colorTokenToCssVar('base.700'));
}
};
export const useMonitorForFormElementDnd = () => {
const dispatch = useAppDispatch();
const handleMoveFormElementDrop = useCallback(
(sourceData: MoveFormElementDndData, targetData: MoveFormElementDndData) => {
if (sourceData.element.id === targetData.element.id) {
return;
}
const closestCenterOrEdge = extractClosestCenterOrEdge(targetData);
if (closestCenterOrEdge === 'center') {
// Move the element to the target container - should we double-check that the target is a container?
flushSync(() => {
dispatch(formElementMoved({ id: sourceData.element.id, containerId: targetData.element.id }));
});
// Flash the element that was moved
flashElement(sourceData.element.id);
} else if (closestCenterOrEdge) {
// Move the element to the target's parent container at the correct index
const { parentId } = targetData.element;
assert(parentId !== undefined, 'Target element should have a parent');
const isReparenting = parentId !== sourceData.element.parentId;
const parentContainer = getElement(parentId, isContainerElement);
const targetIndex = parentContainer.data.children.findIndex((elementId) => elementId === targetData.element.id);
let index: number | undefined = undefined;
if (!isReparenting) {
const sourceIndex = parentContainer.data.children.findIndex(
(elementId) => elementId === sourceData.element.id
);
if (
sourceIndex === targetIndex ||
sourceIndex === adjustIndexForFormElementMoveDrop(targetIndex, closestCenterOrEdge)
) {
return;
}
index = targetIndex;
} else {
index = adjustIndexForFormElementMoveDrop(targetIndex, closestCenterOrEdge);
}
flushSync(() => {
dispatch(
formElementMoved({
id: sourceData.element.id,
containerId: parentId,
index,
})
);
});
// Flash the element that was moved
flashElement(sourceData.element.id);
} else {
// No container, cannot do anything
return;
}
},
[dispatch]
);
const handleAddFormElementDrop = useCallback(
(sourceData: AddFormElementDndData, targetData: MoveFormElementDndData) => {
const closestCenterOrEdge = extractClosestCenterOrEdge(targetData);
if (closestCenterOrEdge === 'center') {
// Move the element to the target container - should we double-check that the target is a container?
const { element } = sourceData;
flushSync(() => {
dispatch(formElementAdded({ element, containerId: targetData.element.id }));
});
flashElement(element.id);
} else if (closestCenterOrEdge) {
// Move the element to the target's parent container at the correct index
const { parentId } = targetData.element;
assert(parentId !== undefined, 'Target element should have a parent');
const { element } = sourceData;
const parentContainer = getElement(parentId, isContainerElement);
const targetIndex = parentContainer.data.children.findIndex((elementId) => elementId === targetData.element.id);
const index = adjustIndexForNodeFieldDrop(targetIndex, closestCenterOrEdge);
flushSync(() => {
dispatch(
formElementAdded({
element,
containerId: parentId,
index,
})
);
});
flashElement(element.id);
} else {
// No container, cannot do anything
return;
}
},
[dispatch]
);
const handleNodeFieldDrop = useCallback(
(sourceData: NodeFieldDndData, targetData: MoveFormElementDndData) => {
const closestCenterOrEdge = extractClosestCenterOrEdge(targetData);
const { nodeId, fieldName } = sourceData.fieldIdentifier;
if (closestCenterOrEdge === 'center') {
// Move the element to the target container - should we double-check that the target is a container?
const element = buildNodeField(nodeId, fieldName, targetData.element.id);
flushSync(() => {
dispatch(formElementAdded({ element, containerId: targetData.element.id }));
});
flashElement(element.id);
} else if (closestCenterOrEdge) {
// Move the element to the target's parent container at the correct index
const { parentId } = targetData.element;
assert(parentId !== undefined, 'Target element should have a parent');
const element = buildNodeField(nodeId, fieldName, parentId);
const parentContainer = getElement(parentId, isContainerElement);
const targetIndex = parentContainer.data.children.findIndex((elementId) => elementId === targetData.element.id);
const index = adjustIndexForNodeFieldDrop(targetIndex, closestCenterOrEdge);
flushSync(() => {
dispatch(
formElementAdded({
element,
containerId: parentId,
index,
})
);
});
flashElement(element.id);
} else {
// No container, cannot do anything
return;
}
},
[dispatch]
);
useEffect(() => {
return monitorForElements({
canMonitor: ({ source }) =>
isMoveFormElementDndData(source.data) ||
isNodeFieldDndData(source.data) ||
isAddFormElementDndData(source.data),
onDrop: ({ location, source }) => {
const target = location.current.dropTargets[0];
if (!target) {
return;
}
const sourceData = source.data;
const targetData = target.data;
if (isMoveFormElementDndData(targetData) && isMoveFormElementDndData(sourceData)) {
handleMoveFormElementDrop(sourceData, targetData);
return;
}
if (isMoveFormElementDndData(targetData) && isAddFormElementDndData(sourceData)) {
handleAddFormElementDrop(sourceData, targetData);
return;
}
if (isMoveFormElementDndData(targetData) && isNodeFieldDndData(sourceData)) {
handleNodeFieldDrop(sourceData, targetData);
return;
}
},
});
}, [handleAddFormElementDrop, handleMoveFormElementDrop, handleNodeFieldDrop]);
};
export const useDraggableFormElement = (
elementId: ElementId,
draggableRef: RefObject<HTMLElement>,
dragHandleRef: RefObject<HTMLElement>
) => {
const [isDragging, setIsDragging] = useState(false);
const [activeDropRegion, setActiveDropRegion] = useState<CenterOrEdge | null>(null);
useEffect(() => {
const draggableElement = draggableRef.current;
const dragHandleElement = dragHandleRef.current;
if (!draggableElement || !dragHandleElement) {
return;
}
const _element = getElement(elementId);
return combine(
firefoxDndFix(draggableElement),
draggable({
// The root element is not draggable
canDrag: () => Boolean(_element.parentId),
element: draggableElement,
dragHandle: dragHandleElement,
getInitialData: () => buildMoveFormElementDndData(getElement(elementId)),
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
},
}),
dropTargetForElements({
element: draggableElement,
canDrop: ({ source }) =>
isMoveFormElementDndData(source.data) ||
isNodeFieldDndData(source.data) ||
isAddFormElementDndData(source.data),
getData: ({ input }) => {
const element = getElement(elementId);
const container = element.parentId ? getElement(element.parentId, isContainerElement) : null;
const data = buildMoveFormElementDndData(element);
const allowedCenterOrEdge: CenterOrEdge[] = [];
if (isContainerElement(element)) {
allowedCenterOrEdge.push('center');
}
if (container?.data.direction === 'row') {
allowedCenterOrEdge.push('left', 'right');
}
if (container?.data.direction === 'column') {
allowedCenterOrEdge.push('top', 'bottom');
}
return attachClosestCenterOrEdge(data, {
element: draggableElement,
input,
allowedCenterOrEdge,
});
},
getIsSticky: () => true,
onDrag: ({ self, location, source }) => {
const innermostDropTargetElement = location.current.dropTargets.at(0)?.element;
// If the innermost target is not this draggable element, bail. We only want to react when dragging over _this_ element.
if (!innermostDropTargetElement || innermostDropTargetElement !== draggableElement) {
setActiveDropRegion(null);
return;
}
const closestCenterOrEdge = extractClosestCenterOrEdge(self.data);
// Don't allow reparanting to the same container
if (closestCenterOrEdge === 'center' && source.element === draggableElement) {
setActiveDropRegion(null);
return;
}
// Only need to update react state if nothing has changed.
// Prevents re-rendering.
setActiveDropRegion(closestCenterOrEdge);
},
onDragLeave: () => {
setActiveDropRegion(null);
},
onDrop: () => {
setActiveDropRegion(null);
},
})
);
}, [dragHandleRef, draggableRef, elementId]);
return [activeDropRegion, isDragging] as const;
};

View File

@@ -4,7 +4,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
import { InvocationNodeNotesTextarea } from 'features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea';
import { useNodeIsInvocationNode } from 'features/nodes/hooks/useNodeIsInvocationNode';
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
@@ -55,6 +56,7 @@ type ContentProps = {
const Content = memo((props: ContentProps) => {
const { t } = useTranslation();
const needsUpdate = useNodeNeedsUpdate(props.nodeId);
const isInvocationNode = useNodeIsInvocationNode(props.nodeId);
return (
<Box position="relative" w="full" h="full">
<ScrollableContent>
@@ -74,7 +76,7 @@ const Content = memo((props: ContentProps) => {
</Text>
</FormControl>
</HStack>
<NotesTextarea nodeId={props.nodeId} />
{isInvocationNode && <InvocationNodeNotesTextarea nodeId={props.nodeId} />}
</Flex>
</ScrollableContent>
</Box>

View File

@@ -5,7 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
@@ -35,7 +35,7 @@ const InspectorOutputsTab = () => {
[templates]
);
const data = useAppSelector(selector);
const nes = useExecutionState(data?.nodeId);
const nes = useNodeExecutionState(data?.nodeId);
const { t } = useTranslation();
if (!data || !nes) {

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