diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index ef1bbb04503..6b1269100f0 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -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",
diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml
index 4d7a0830d24..cf43a853458 100644
--- a/invokeai/frontend/web/pnpm-lock.yaml
+++ b/invokeai/frontend/web/pnpm-lock.yaml
@@ -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'}
diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx
index 325db314d56..1cdddb76a57 100644
--- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx
+++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx
@@ -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';
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts
index 8d39448ad20..cde399e666a 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts
@@ -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';
diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx
index f9e4e11f085..756ffec0d91 100644
--- a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx
@@ -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'];
};
diff --git a/invokeai/frontend/web/src/common/components/NodeSelectionOverlay.tsx b/invokeai/frontend/web/src/common/components/NodeSelectionOverlay.tsx
deleted file mode 100644
index 31ea415c909..00000000000
--- a/invokeai/frontend/web/src/common/components/NodeSelectionOverlay.tsx
+++ /dev/null
@@ -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 (
-
- );
-};
-
-export default memo(SelectionOverlay);
diff --git a/invokeai/frontend/web/src/common/hooks/useChakraThemeTokens.ts b/invokeai/frontend/web/src/common/hooks/useChakraThemeTokens.ts
deleted file mode 100644
index 93345f6a4cb..00000000000
--- a/invokeai/frontend/web/src/common/hooks/useChakraThemeTokens.ts
+++ /dev/null
@@ -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,
- };
-};
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
index f5a30afcc7e..b774b7ba8be 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
@@ -484,9 +484,10 @@ export function loadImage(src: string): Promise {
export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
export function getPrefixedId(
- prefix: CanvasEntityIdentifier['type'] | CanvasObjectState['type'] | (string & Record)
+ prefix: CanvasEntityIdentifier['type'] | CanvasObjectState['type'] | (string & Record),
+ separator = ':'
): string {
- return `${prefix}:${nanoid()}`;
+ return `${prefix}${separator}${nanoid()}`;
}
export const getEmptyRect = (): Rect => {
diff --git a/invokeai/frontend/web/src/features/dnd/DndListDropIndicator.tsx b/invokeai/frontend/web/src/features/dnd/DndListDropIndicator.tsx
index 3a7152c0f73..600f0d51746 100644
--- a/invokeai/frontend/web/src/features/dnd/DndListDropIndicator.tsx
+++ b/invokeai/frontend/web/src/features/dnd/DndListDropIndicator.tsx
@@ -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
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
index acc2fde7536..de0ab48d7c7 100644
--- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
@@ -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';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx
index 90a1d7427b2..d97408c624c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx
@@ -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 = (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[] = [{ type: 'add', item: node }];
+ const edgeChanges: EdgeChange[] = [];
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({
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx
index aaeea376a19..93093bbcf7d 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx
@@ -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(() => ({ borderRadius }), [borderRadius]);
- const onNodesChange: OnNodesChange = useCallback(
+ const onNodesChange: OnNodesChange = 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 = 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 = 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 = 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 = useCallback(
+ const onReconnectEnd: NonNullable = 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[] = [];
+ const edgeChanges: EdgeChange[] = [];
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[] = [];
+ const edgeChanges: EdgeChange[] = [];
nodes
.filter((n) => n.selected)
.forEach(({ id }) => {
@@ -322,7 +324,7 @@ export const Flow = memo(() => {
});
return (
-
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}
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx
index b55fc6fe70a..fa309559acb 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx
@@ -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 };
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx
index 0d7e7b7d5e4..17c44717f14 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx
@@ -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) => {
+ 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 (
<>
-
- {data?.count && data.count > 1 && (
+
+ {data?.count !== undefined && (
-
-
+
{data.count}
-
+
)}
>
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx
index f9e4deed9e4..bd7484f0beb 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx
@@ -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) => {
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 (
<>
-
+
{label && shouldShowEdgeLabels && (
-
+
{label}
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/buildEdgeSelectors.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/buildEdgeSelectors.ts
new file mode 100644
index 00000000000..9e4aeea5e39
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/buildEdgeSelectors.ts
@@ -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}`;
+ });
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts
index b5801c45edb..e7fa43015b2 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts
@@ -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',
-});
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts
deleted file mode 100644
index 6a783f31581..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts
+++ /dev/null
@@ -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;
- }
- );
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx
index f7587252569..d89e452dfda 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx
@@ -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) => {
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx
index baa7fc262a8..fe3c292ad79 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx
@@ -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) => {
{fieldNames.connectionFields.map((fieldName, i) => (
-
-
-
+
+
+
))}
{outputFieldNames.map((fieldName, i) => (
-
+
+
+
))}
{fieldNames.anyOrDirectFields.map((fieldName) => (
-
-
-
+
+
+
))}
{fieldNames.missingFields.map((fieldName) => (
-
-
-
+
+
+
))}
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx
index b888e8a5162..456f89daa04 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx
@@ -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) => (
{
id={`${nodeId}-collapsed-source`}
isConnectable={false}
position={Position.Right}
- style={collapsedSourceStyles}
+ style={collapsedHandleStyles}
/>
{map(template.outputs, (output) => (
{
- const hasImageOutput = useHasImageOutput(nodeId);
+ const hasImageOutput = useNodeHasImageOutput(nodeId);
const isCacheEnabled = useFeatureStatus('invocationCache');
return (
{
+export const InvocationNodeInfoIcon = memo(({ nodeId }: Props) => {
const needsUpdate = useNodeNeedsUpdate(nodeId);
return (
@@ -20,96 +21,66 @@ const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
);
-};
+});
-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 (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 (
-
- {t('nodes.versionUnknown')}
-
- );
- }
-
- if (!nodeTemplate.version) {
- return (
-
- {t('nodes.version')} {node.data.version} ({t('nodes.unknownTemplate')})
-
- );
- }
-
- if (compare(node.data.version, nodeTemplate.version, '<')) {
- return (
-
- {t('nodes.version')} {node.data.version} ({t('nodes.updateNode')})
-
- );
+ if (label) {
+ return `${label} (${nodeTemplate.title})`;
}
- if (compare(node.data.version, nodeTemplate.version, '>')) {
- return (
-
- {t('nodes.version')} {node.data.version} ({t('nodes.updateApp')})
-
- );
- }
-
- return (
-
- {t('nodes.version')} {node.data.version}
-
- );
- }, [node, nodeTemplate, t]);
-
- if (!isInvocationNode(node)) {
- return {t('nodes.unknownNode')};
- }
+ return nodeTemplate.title;
+ }, [label, nodeTemplate.title]);
return (
{title}
- {nodeTemplate?.nodePack && (
-
- {t('nodes.nodePack')}: {nodeTemplate.nodePack}
-
- )}
+
+ {t('nodes.nodePack')}: {nodeTemplate.nodePack}
+
- {nodeTemplate?.description}
+ {nodeTemplate.description}
- {versionComponent}
- {node.data?.notes && {node.data.notes}}
+
+ {notes && {notes}}
);
});
TooltipContent.displayName = 'TooltipContent';
+
+const Version = ({ nodeVersion, templateVersion }: { nodeVersion: string; templateVersion: string }) => {
+ const { t } = useTranslation();
+
+ if (compare(nodeVersion, templateVersion, '<')) {
+ return (
+
+ {t('nodes.version')} {nodeVersion} ({t('nodes.updateNode')})
+
+ );
+ }
+
+ if (compare(nodeVersion, templateVersion, '>')) {
+ return (
+
+ {t('nodes.version')} {nodeVersion} ({t('nodes.updateApp')})
+
+ );
+ }
+
+ return (
+
+ {t('nodes.version')} {nodeVersion}
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/NotesTextarea.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea.tsx
similarity index 64%
rename from invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/NotesTextarea.tsx
rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea.tsx
index 9f4da396af1..8e316e7841b 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/NotesTextarea.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea.tsx
@@ -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) => {
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
},
[dispatch, nodeId]
);
- if (!isInvocationNode(node)) {
- return null;
- }
return (
{t('nodes.notes')}
-
+
);
-};
+});
-export default memo(NotesTextarea);
+InvocationNodeNotesTextarea.displayName = 'InvocationNodeNotesTextarea';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx
index b58f6fe8ba5..a740a2ee3df 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx
@@ -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;
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx
index ade19d6f897..94534b7028d 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx
@@ -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) => {
+const InvocationNodeWrapper = (props: NodeProps>) => {
const { data, selected } = props;
const { id: nodeId, type, isOpen, label } = data;
const templates = useStore($templates);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/SaveToGalleryCheckbox.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/SaveToGalleryCheckbox.tsx
index 270d137d501..f4eeb2e9dc1 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/SaveToGalleryCheckbox.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/SaveToGalleryCheckbox.tsx
@@ -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) => {
dispatch(
@@ -30,7 +30,7 @@ const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
return (
- {t('nodes.saveToGallery')}
+ {t('nodes.saveToGallery')}
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/UseCacheCheckbox.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/UseCacheCheckbox.tsx
index e8069bf38d5..853d718a859 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/UseCacheCheckbox.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/UseCacheCheckbox.tsx
@@ -23,7 +23,7 @@ const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => {
const { t } = useTranslation();
return (
- {t('invocationCache.useCache')}
+ {t('invocationCache.useCache')}
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx
index da81a56eae5..1cf384dd993 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx
@@ -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 { 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)';
- }
+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;
- if (handleType === 'target') {
- s.insetInlineStart = '-1rem';
- } else {
- s.insetInlineEnd = '-1rem';
- }
+const handleStyleBase = {
+ position: 'absolute',
+ width: '1rem',
+ height: '1rem',
+ zIndex: 1,
+ background: 'none',
+ border: 'none',
+} satisfies CSSProperties;
- if (isConnectionInProgress && !isConnectionStartField && !validationResult.isValid) {
- s.filter = 'opacity(0.4) grayscale(0.7)';
- }
+const targetHandleStyle = {
+ ...handleStyleBase,
+ insetInlineStart: '-0.5rem',
+} satisfies CSSProperties;
- if (isConnectionInProgress && !validationResult.isValid) {
- if (isConnectionStartField) {
- s.cursor = 'grab';
- } else {
- s.cursor = 'not-allowed';
- }
- } else {
- s.cursor = 'crosshair';
- }
+const sourceHandleStyle = {
+ ...handleStyleBase,
+ insetInlineEnd: '-0.5rem',
+} satisfies CSSProperties;
- return s;
- }, [handleType, isConnectionInProgress, isConnectionStartField, type, validationResult.isValid]);
+export const FieldHandle = memo((props: Props) => {
+ const { fieldTemplate, isConnectionInProgress, isConnectionStartField, validationResult, handleType } = props;
+ const { t } = useTranslation();
+ const fieldTypeName = useFieldTypeName(fieldTemplate.type);
+ const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
+ const isModelField = useMemo(() => MODEL_TYPES.some((t) => t === fieldTemplate.type.name), [fieldTemplate.type]);
const tooltip = useMemo(() => {
if (isConnectionInProgress && validationResult.messageTKey) {
@@ -83,12 +90,24 @@ const FieldHandle = (props: FieldHandleProps) => {
>
+ style={handleType === 'target' ? targetHandleStyle : sourceHandleStyle}
+ >
+
+
);
-};
+});
-export default memo(FieldHandle);
+FieldHandle.displayName = 'FieldHandle';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldResetToDefaultValueButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldResetToDefaultValueButton.tsx
deleted file mode 100644
index a3038607447..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldResetToDefaultValueButton.tsx
+++ /dev/null
@@ -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 (
- }
- onClick={onClick}
- isDisabled={isDisabled}
- pointerEvents="auto"
- size="xs"
- />
- );
-};
-
-export default memo(FieldResetToDefaultValueButton);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx
deleted file mode 100644
index a8925f5b3a1..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx
+++ /dev/null
@@ -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 (
-
- {fieldTitle}
- {fieldTemplate && (
-
- {fieldTemplate.description}
-
- )}
- {fieldTypeName && (
-
- {t('parameters.type')}: {fieldTypeName}
-
- )}
- {isInputTemplate && (
-
- {t('common.input')}: {startCase(fieldTemplate.input)}
-
- )}
-
- );
-};
-
-export default memo(FieldTooltipContent);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput.tsx
new file mode 100644
index 00000000000..d97effb5f1a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput.tsx
@@ -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) => {
+ const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
+
+ return (
+
+ );
+});
+
+FloatFieldInput.displayName = 'FloatFieldInput ';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldSlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldSlider.tsx
new file mode 100644
index 00000000000..f57618a671b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldSlider.tsx
@@ -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) => {
+ const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
+
+ return (
+
+ );
+});
+
+FloatFieldSlider.displayName = 'FloatFieldSlider ';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField.ts b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField.ts
new file mode 100644
index 00000000000..15779bf0def
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField.ts
@@ -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) => {
+ 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,
+ };
+};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx
deleted file mode 100644
index 9a05a1c6b53..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
- );
- }
-
- return (
-
-
-
-
-
- {isHovered && }
- {isHovered && }
-
-
-
-
-
- {fieldTemplate.input !== 'direct' && (
-
- )}
-
- );
-};
-
-export default memo(InputField);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddRemoveLinearViewIconButton.tsx
similarity index 60%
rename from invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx
rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddRemoveLinearViewIconButton.tsx
index ff59e029167..9ffe0afa75a 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddRemoveLinearViewIconButton.tsx
@@ -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';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeLinear.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeLinear.tsx
new file mode 100644
index 00000000000..9dc0f61c271
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeLinear.tsx
@@ -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(null);
+ const [dndListState, isDragging] = useLinearViewFieldDnd(ref, { nodeId, fieldName });
+
+ return (
+
+
+
+
+
+
+ {isMouseOverNode && }
+
+
+ }
+ />
+
+
+
+
+
+
+ );
+});
+
+InputFieldEditModeLinear.displayName = 'InputFieldEditModeLinear';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx
new file mode 100644
index 00000000000..8c3d9b82e4b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx
@@ -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(null);
+ const dragHandleRef = useRef(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 (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {isHovered && (
+ <>
+
+
+
+ >
+ )}
+
+
+
+
+
+ {fieldTemplate.input !== 'direct' && (
+
+ )}
+
+ );
+});
+
+InputFieldEditModeNodes.displayName = 'InputFieldEditModeNodes';
+
+const useNodeFieldDnd = (
+ fieldIdentifier: FieldIdentifier,
+ draggableRef: RefObject,
+ dragHandleRef: RefObject
+) => {
+ 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;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx
new file mode 100644
index 00000000000..5f43fc29775
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx
@@ -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 ;
+ }
+
+ return children;
+});
+
+InputFieldGate.displayName = 'InputFieldGate';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesHelperText.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesHelperText.tsx
new file mode 100644
index 00000000000..8cf58f07c59
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesHelperText.tsx
@@ -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 (
+
+ {notes}
+
+ );
+});
+
+InputFieldNotesHelperText.displayName = 'InputFieldNotesHelperText';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonEditable.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonEditable.tsx
new file mode 100644
index 00000000000..a33fc9d4433
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonEditable.tsx
@@ -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) => {
+ dispatch(fieldNotesChanged({ nodeId, fieldName, val: e.target.value }));
+ },
+ [dispatch, fieldName, nodeId]
+ );
+
+ return (
+
+
+ }
+ pointerEvents="auto"
+ size="xs"
+ />
+
+
+
+ {t('nodes.notes')}
+
+
+
+
+ );
+});
+
+InputFieldNotesIconButtonEditable.displayName = 'InputFieldNotesIconButtonEditable';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonReadonly.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonReadonly.tsx
new file mode 100644
index 00000000000..5dcc74382f7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonReadonly.tsx
@@ -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 (
+
+
+ }
+ size="xs"
+ />
+
+
+
+
+ {t('nodes.notes')}
+
+
+ {notes}
+
+
+
+
+ );
+});
+
+InputFieldNotesIconButtonReadonly.displayName = 'InputFieldNotesIconButtonReadonly';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx
index 998fc4ae416..c844853fbe3 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx
@@ -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 ;
+ if (isStringFieldCollectionInputTemplate(template)) {
+ if (!isStringFieldCollectionInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isStringFieldInputInstance(fieldInstance) && isStringFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isStringFieldInputTemplate(template)) {
+ if (!isStringFieldInputInstance(field)) {
+ return null;
+ }
+ if (template.ui_component === 'textarea') {
+ return ;
+ } else {
+ return ;
+ }
}
- if (isBooleanFieldInputInstance(fieldInstance) && isBooleanFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isBooleanFieldInputTemplate(template)) {
+ if (!isBooleanFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isIntegerFieldInputInstance(fieldInstance) && isIntegerFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isIntegerFieldInputTemplate(template)) {
+ if (!isIntegerFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isFloatFieldInputInstance(fieldInstance) && isFloatFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isFloatFieldInputTemplate(template)) {
+ if (!isFloatFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isIntegerFieldCollectionInputInstance(fieldInstance) && isIntegerFieldCollectionInputTemplate(fieldTemplate)) {
- return ;
+ if (isIntegerFieldCollectionInputTemplate(template)) {
+ if (!isIntegerFieldCollectionInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isFloatFieldCollectionInputInstance(fieldInstance) && isFloatFieldCollectionInputTemplate(fieldTemplate)) {
- return ;
+ if (isFloatFieldCollectionInputTemplate(template)) {
+ if (!isFloatFieldCollectionInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isEnumFieldInputInstance(fieldInstance) && isEnumFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isEnumFieldInputTemplate(template)) {
+ if (!isEnumFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isImageFieldCollectionInputInstance(fieldInstance) && isImageFieldCollectionInputTemplate(fieldTemplate)) {
- return ;
+ if (isImageFieldCollectionInputTemplate(template)) {
+ if (!isImageFieldCollectionInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isImageFieldInputInstance(fieldInstance) && isImageFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isImageFieldInputTemplate(template)) {
+ if (!isImageFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isBoardFieldInputInstance(fieldInstance) && isBoardFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isBoardFieldInputTemplate(template)) {
+ if (!isBoardFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isMainModelFieldInputInstance(fieldInstance) && isMainModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isMainModelFieldInputTemplate(template)) {
+ if (!isMainModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isModelIdentifierFieldInputInstance(fieldInstance) && isModelIdentifierFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isModelIdentifierFieldInputTemplate(template)) {
+ if (!isModelIdentifierFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isSDXLRefinerModelFieldInputInstance(fieldInstance) && isSDXLRefinerModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isSDXLRefinerModelFieldInputTemplate(template)) {
+ if (!isSDXLRefinerModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isVAEModelFieldInputInstance(fieldInstance) && isVAEModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isVAEModelFieldInputTemplate(template)) {
+ if (!isVAEModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isT5EncoderModelFieldInputInstance(fieldInstance) && isT5EncoderModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isT5EncoderModelFieldInputTemplate(template)) {
+ if (!isT5EncoderModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isCLIPEmbedModelFieldInputInstance(fieldInstance) && isCLIPEmbedModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isCLIPEmbedModelFieldInputTemplate(template)) {
+ if (!isCLIPEmbedModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isCLIPLEmbedModelFieldInputInstance(fieldInstance) && isCLIPLEmbedModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isCLIPLEmbedModelFieldInputTemplate(template)) {
+ if (!isCLIPLEmbedModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isCLIPGEmbedModelFieldInputInstance(fieldInstance) && isCLIPGEmbedModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isCLIPGEmbedModelFieldInputTemplate(template)) {
+ if (!isCLIPGEmbedModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isControlLoRAModelFieldInputInstance(fieldInstance) && isControlLoRAModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isControlLoRAModelFieldInputTemplate(template)) {
+ if (!isControlLoRAModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isFluxVAEModelFieldInputInstance(fieldInstance) && isFluxVAEModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isFluxVAEModelFieldInputTemplate(template)) {
+ if (!isFluxVAEModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isLoRAModelFieldInputInstance(fieldInstance) && isLoRAModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isLoRAModelFieldInputTemplate(template)) {
+ if (!isLoRAModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isControlNetModelFieldInputInstance(fieldInstance) && isControlNetModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isControlNetModelFieldInputTemplate(template)) {
+ if (!isControlNetModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isIPAdapterModelFieldInputInstance(fieldInstance) && isIPAdapterModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isIPAdapterModelFieldInputTemplate(template)) {
+ if (!isIPAdapterModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isT2IAdapterModelFieldInputInstance(fieldInstance) && isT2IAdapterModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isT2IAdapterModelFieldInputTemplate(template)) {
+ if (!isT2IAdapterModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (
- isSpandrelImageToImageModelFieldInputInstance(fieldInstance) &&
- isSpandrelImageToImageModelFieldInputTemplate(fieldTemplate)
- ) {
- return (
-
- );
+ if (isSpandrelImageToImageModelFieldInputTemplate(template)) {
+ if (!isSpandrelImageToImageModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isColorFieldInputInstance(fieldInstance) && isColorFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isColorFieldInputTemplate(template)) {
+ if (!isColorFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isFluxMainModelFieldInputInstance(fieldInstance) && isFluxMainModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isFluxMainModelFieldInputTemplate(template)) {
+ if (!isFluxMainModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isSD3MainModelFieldInputInstance(fieldInstance) && isSD3MainModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isSD3MainModelFieldInputTemplate(template)) {
+ if (!isSD3MainModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isSDXLMainModelFieldInputInstance(fieldInstance) && isSDXLMainModelFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isSDXLMainModelFieldInputTemplate(template)) {
+ if (!isSDXLMainModelFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isSchedulerFieldInputInstance(fieldInstance) && isSchedulerFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isSchedulerFieldInputTemplate(template)) {
+ if (!isSchedulerFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isFloatGeneratorFieldInputInstance(fieldInstance) && isFloatGeneratorFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isFloatGeneratorFieldInputTemplate(template)) {
+ if (!isFloatGeneratorFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isIntegerGeneratorFieldInputInstance(fieldInstance) && isIntegerGeneratorFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isIntegerGeneratorFieldInputTemplate(template)) {
+ if (!isIntegerGeneratorFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (isStringGeneratorFieldInputInstance(fieldInstance) && isStringGeneratorFieldInputTemplate(fieldTemplate)) {
- return ;
+ if (isStringGeneratorFieldInputTemplate(template)) {
+ if (!isStringGeneratorFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
}
- if (fieldTemplate) {
- // Fallback for when there is no component for the type
- return null;
- }
-};
+ return null;
+});
-export default memo(InputFieldRenderer);
+InputFieldRenderer.displayName = 'InputFieldRenderer';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton.tsx
new file mode 100644
index 00000000000..a147743af7c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton.tsx
@@ -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 (
+ }
+ pointerEvents="auto"
+ size="xs"
+ onClick={resetToDefaultValue}
+ isDisabled={!isValueChanged}
+ />
+ );
+});
+
+InputFieldResetToDefaultValueIconButton.displayName = 'InputFieldResetToDefaultValueIconButton';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton.tsx
new file mode 100644
index 00000000000..a611275867c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton.tsx
@@ -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 (
+ }
+ pointerEvents="auto"
+ size="xs"
+ onClick={resetToInitialLinearViewValue}
+ isDisabled={!isValueChanged}
+ />
+ );
+});
+
+InputFieldResetToInitialValueIconButton.displayName = 'InputFieldResetToInitialValueIconButton';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx
similarity index 73%
rename from invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx
rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx
index d7690669e03..fc4857594ee 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx
@@ -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"
>
: undefined}
+ label={}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
>
@@ -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();
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltip.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltip.tsx
new file mode 100644
index 00000000000..324a83d7e35
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltip.tsx
@@ -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 (
+
+ {fieldTitle}
+
+ {fieldTemplate.description}
+
+
+ {t('parameters.type')}: {fieldTypeName}
+
+
+ {t('common.input')}: {startCase(fieldTemplate.input)}
+
+
+ );
+});
+
+InputFieldTooltip.displayName = 'FieldTooltipContent';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx
new file mode 100644
index 00000000000..615034ea323
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx
@@ -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 (
+
+
+
+ {t('nodes.unknownInput', { name })}
+
+
+
+ );
+});
+
+InputFieldUnknownPlaceholder.displayName = 'InputFieldUnknownPlaceholder';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewMode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewMode.tsx
new file mode 100644
index 00000000000..e4a4bca7ec5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewMode.tsx
@@ -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 (
+
+
+ {label || fieldTemplateTitle}
+
+
+
+
+
+
+
+ );
+});
+
+InputFieldViewMode.displayName = 'InputFieldViewMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx
index 8723538f851..5c5046b0868 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx
@@ -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 (
-
- {children}
-
- );
+export const InputFieldWrapper = memo(({ children }: PropsWithChildren) => {
+ return {children};
});
InputFieldWrapper.displayName = 'InputFieldWrapper';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput.tsx
new file mode 100644
index 00000000000..008f9c1a0d8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput.tsx
@@ -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) => {
+ const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
+
+ return (
+
+ );
+ }
+);
+
+IntegerFieldInput.displayName = 'IntegerFieldInput';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider.tsx
new file mode 100644
index 00000000000..cd8a0a50eb2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider.tsx
@@ -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) => {
+ const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
+
+ return (
+
+ );
+ }
+);
+
+IntegerFieldSlider.displayName = 'IntegerFieldSlider';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField.ts b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField.ts
new file mode 100644
index 00000000000..ccc330feaaf
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField.ts
@@ -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) => {
+ 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,
+ };
+};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx
deleted file mode 100644
index bbbe6deac16..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {t('nodes.unknownInput', { name })}
-
-
-
- );
- }
-
- return children;
-});
-
-InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx
deleted file mode 100644
index a6d36c00389..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx
+++ /dev/null
@@ -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(null);
- const [dndListState, isDragging] = useLinearViewFieldDnd(ref, fieldIdentifier);
-
- return (
-
-
-
-
-
-
- {isMouseOverNode && }
- {isValueChanged && (
- }
- />
- )}
-
- }
- openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
- placement="top"
- >
-
-
-
-
- }
- />
-
-
-
-
-
-
- );
-};
-
-const LinearViewField = ({ fieldIdentifier }: Props) => {
- return (
-
-
-
- );
-};
-
-export default memo(LinearViewField);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx
deleted file mode 100644
index 94e8b627441..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {t('nodes.unknownOutput', {
- name: fieldName,
- })}
-
-
-
- );
- }
-
- return (
-
- }
- openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
- placement="top"
- shouldWrapChildren
- >
-
- {fieldTemplate?.title}
-
-
-
-
- );
-};
-
-export default memo(OutputField);
-
-type OutputFieldWrapperProps = PropsWithChildren<{
- shouldDim: boolean;
-}>;
-
-const OutputFieldWrapper = memo(({ shouldDim, children }: OutputFieldWrapperProps) => (
-
- {children}
-
-));
-
-OutputFieldWrapper.displayName = 'OutputFieldWrapper';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx
new file mode 100644
index 00000000000..2b50e357063
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx
@@ -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 ;
+ }
+
+ return children;
+});
+
+OutputFieldGate.displayName = 'OutputFieldGate';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldNodesEditorView.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldNodesEditorView.tsx
new file mode 100644
index 00000000000..6fac1f148f1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldNodesEditorView.tsx
@@ -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 (
+
+
+
+
+ );
+});
+
+OutputFieldNodesEditorView.displayName = 'OutputFieldNodesEditorView';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle.tsx
new file mode 100644
index 00000000000..0dcf3a501c6
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle.tsx
@@ -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 (
+ }
+ openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
+ placement="top"
+ shouldWrapChildren
+ >
+
+ {fieldTemplate.title}
+
+
+ );
+});
+
+OutputFieldTitle.displayName = 'OutputFieldTitle';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTooltip.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTooltip.tsx
new file mode 100644
index 00000000000..e034720e46e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTooltip.tsx
@@ -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 (
+
+ {fieldTemplate.title}
+
+ {fieldTemplate.description}
+
+
+ {t('parameters.type')}: {fieldTypeName}
+
+
+ );
+});
+
+OutputFieldTooltip.displayName = 'OutputFieldTooltip';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx
new file mode 100644
index 00000000000..9de792b9e56
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx
@@ -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 (
+
+
+
+ {t('nodes.unknownOutput', { name })}
+
+
+
+ );
+});
+
+OutputFieldUnknownPlaceholder.displayName = 'OutputFieldUnknownPlaceholder';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper.tsx
new file mode 100644
index 00000000000..dd45d25368d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper.tsx
@@ -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) => {children});
+
+OutputFieldWrapper.displayName = 'OutputFieldWrapper';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput.tsx
new file mode 100644
index 00000000000..5e57a6cf816
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput.tsx
@@ -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) => {
+ const { value, onChange } = useStringField(props);
+
+ return ;
+ }
+);
+
+StringFieldInput.displayName = 'StringFieldInput';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea.tsx
new file mode 100644
index 00000000000..1e2c0683a82
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea.tsx
@@ -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) => {
+ const { value, onChange } = useStringField(props);
+
+ return (
+
+ );
+ }
+);
+
+StringFieldTextarea.displayName = 'StringFieldTextarea';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/useStringField.ts
similarity index 50%
rename from invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldInputComponent.tsx
rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/useStringField.ts
index d7307f3384b..1affdae5760 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/useStringField.ts
@@ -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) => {
+export const useStringField = (props: FieldComponentProps) => {
const { nodeId, field, fieldTemplate } = props;
const dispatch = useAppDispatch();
- const handleValueChanged = useCallback(
+ const onChange = useCallback(
(e: ChangeEvent) => {
dispatch(
fieldStringValueChanged({
@@ -24,11 +22,9 @@ const StringFieldInputComponent = (props: FieldComponentProps;
- }
-
- return ;
+ return {
+ value: field.value,
+ onChange,
+ defaultValue: fieldTemplate.default,
+ };
};
-
-export default memo(StringFieldInputComponent);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BooleanFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BooleanFieldInputComponent.tsx
index 9d3f4cd13f0..1ca47128015 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BooleanFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BooleanFieldInputComponent.tsx
@@ -27,7 +27,7 @@ const BooleanFieldInputComponent = (
[dispatch, field.name, nodeId]
);
- return ;
+ return ;
};
export default memo(BooleanFieldInputComponent);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ColorFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ColorFieldInputComponent.tsx
index 12f541441a8..fa6ec491ca7 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ColorFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ColorFieldInputComponent.tsx
@@ -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
+
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatFieldCollectionInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatFieldCollectionInputComponent.tsx
new file mode 100644
index 00000000000..0f4fa97827e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatFieldCollectionInputComponent.tsx
@@ -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) => {
+ 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 (
+
+
+ {field.value && field.value.length > 0 && (
+ <>
+
+
+
+ {field.value.map((value, index) => (
+
+ ))}
+
+
+ >
+ )}
+
+ );
+ }
+);
+
+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 (
+ <>
+
+
+ {index + 1}.
+
+
+
+
+
+
+ }
+ aria-label={t('common.delete')}
+ />
+
+ >
+ );
+ }
+);
+FloatListItemContent.displayName = 'FloatListItemContent';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx
index d011a7ea76b..25c376afb9b 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx
@@ -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(
() =>
@@ -132,7 +132,7 @@ const ImageGridItemContent = memo(
}
if (!query.data) {
- return } />;
+ return ;
}
return (
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberFieldCollectionInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerFieldCollectionInputComponent.tsx
similarity index 71%
rename from invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberFieldCollectionInputComponent.tsx
rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerFieldCollectionInputComponent.tsx
index 8f48f01206e..489cf065a54 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberFieldCollectionInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerFieldCollectionInputComponent.tsx
@@ -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
- | FieldComponentProps
- ) => {
+export const IntegerFieldCollectionInputComponent = memo(
+ (props: FieldComponentProps) => {
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 (
{field.value.map((value, index) => (
-
@@ -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';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberFieldInputComponent.tsx
deleted file mode 100644
index e3f33d8a45b..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberFieldInputComponent.tsx
+++ /dev/null
@@ -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 (
-
- );
-};
-
-export default memo(NumberFieldInputComponent);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent.tsx
index 16e04af51cc..ee85fc7d95b 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent.tsx
@@ -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) => {
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorDynamicPromptsCombinatorialSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorDynamicPromptsCombinatorialSettings.tsx
index 7937838a8b1..b11e2b01a28 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorDynamicPromptsCombinatorialSettings.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorDynamicPromptsCombinatorialSettings.tsx
@@ -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 {
+ return;
+ }
+
+ if (!data) {
onChange({ ...state, values: [] });
+ return;
}
- }, [data, isLoading, loadingValues, onChange, state]);
+
+ onChange({ ...state, values: data.prompts });
+ }, [data, isLoading, onChange, state]);
return (
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorDynamicPromptsRandomSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorDynamicPromptsRandomSettings.tsx
index d9aa664ede0..be47fe5a178 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorDynamicPromptsRandomSettings.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorDynamicPromptsRandomSettings.tsx
@@ -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 {
+ return;
+ }
+
+ if (!data) {
onChange({ ...state, values: [] });
+ return;
}
- }, [data, isLoading, loadingValues, onChange, state]);
+
+ onChange({ ...state, values: data.prompts });
+ }, [data, isLoading, onChange, state, view]);
return (
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx
index 76666af3966..9b93c57f07c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx
@@ -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) => {
+const NotesNode = (props: NodeProps>) => {
const { id: nodeId, data, selected } = props;
const { notes, isOpen } = data;
const dispatch = useAppDispatch();
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeCollapseButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeCollapseButton.tsx
index 372854b703d..b3302eb88a7 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeCollapseButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeCollapseButton.tsx
@@ -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;
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx
index 7ae3872864e..aeea13ab222 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx
@@ -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) => {
if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) {
const nodes = selectNodes(store.getState());
- const nodeChanges: NodeChange[] = [];
+ const nodeChanges: NodeChange[] = [];
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}
>
-
-
+
+
{children}
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx
index 56d74c31db9..de0c62722e0 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx
@@ -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();
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx
index 8f37d7a2188..22f61925845 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx
@@ -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);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx
index 4f269d1296a..0d3ca06c8a3 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx
@@ -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);
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/EditModeLeftPanelContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/EditModeLeftPanelContent.tsx
new file mode 100644
index 00000000000..3240629ec25
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/EditModeLeftPanelContent.tsx
@@ -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(null);
+
+ const handleDoubleClickHandle = useCallback(() => {
+ if (!panelGroupRef.current) {
+ return;
+ }
+ panelGroupRef.current.setLayout([50, 50]);
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+EditModeLeftPanelContent.displayName = 'EditModeLeftPanelContent';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx
deleted file mode 100644
index 0b3039cef73..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx
+++ /dev/null
@@ -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(null);
- const workflowListMenu = useWorkflowListMenu();
-
- const handleDoubleClickHandle = useCallback(() => {
- if (!panelGroupRef.current) {
- return;
- }
- panelGroupRef.current.setLayout([50, 50]);
- }, []);
-
- return (
-
-
-
-
- {workflowListMenu.isOpen && (
-
-
-
-
-
- )}
-
-
- {mode === 'view' && }
- {mode === 'edit' && (
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
- );
-};
-
-export default memo(NodeEditorPanelGroup);
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenu.tsx
index 6756cacfa54..3f04690ee07 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenu.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenu.tsx
@@ -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 (
-
+
-
-
- {workflowCategories.map((category) => (
-
- ))}
+
+
+ {workflowCategories.map((category) => (
+
+ ))}
+
+
);
-};
+});
+
+WorkflowListMenu.displayName = 'WorkflowListMenu';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowsTabLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowsTabLeftPanel.tsx
new file mode 100644
index 00000000000..04287d9b5f6
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowsTabLeftPanel.tsx
@@ -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 (
+
+
+
+
+ {workflowListMenu.isOpen && }
+ {mode === 'view' && }
+ {mode === 'edit' && }
+
+
+
+ );
+};
+
+export default memo(WorkflowsTabLeftPanel);
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/AutosizeTextarea.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/AutosizeTextarea.tsx
new file mode 100644
index 00000000000..276dda78e0f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/AutosizeTextarea.tsx
@@ -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 & TextareaProps, typeof ChakraTextareaAutosize>(
+ ({ variant, ...rest }, ref) => {
+ const styles = useStyleConfig('Textarea', { variant });
+ return ;
+ }
+ )
+);
+AutosizeTextarea.displayName = 'AutosizeTextarea';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx
new file mode 100644
index 00000000000..01d83035307
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx
@@ -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 ;
+ }
+
+ // mode === 'edit'
+ return ;
+});
+ContainerElementComponent.displayName = 'ContainerElementComponent';
+
+export const ContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }) => {
+ const depth = useDepthContext();
+ const { id, data } = el;
+ const { children, direction } = data;
+
+ return (
+
+
+
+ {children.map((childId) => (
+
+ ))}
+
+
+
+ );
+});
+ContainerElementComponentViewMode.displayName = 'ContainerElementComponentViewMode';
+
+export const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }) => {
+ const depth = useDepthContext();
+ const { id, data } = el;
+ const { children, direction } = data;
+
+ return (
+
+
+
+
+ {children.map((childId) => (
+
+ ))}
+ {direction === 'row' && children.length < 3 && depth < 2 && }
+ {direction === 'column' && depth < 1 && }
+
+
+
+
+ );
+});
+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 (
+ } 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 (
+ } 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 ;
+ }
+
+ if (isNodeFieldElement(el)) {
+ return ;
+ }
+
+ if (isDividerElement(el)) {
+ return ;
+ }
+
+ if (isHeadingElement(el)) {
+ return ;
+ }
+
+ if (isTextElement(el)) {
+ return ;
+ }
+
+ assert>(false, `Unhandled type for element with id ${id}`);
+});
+FormElementComponent.displayName = 'FormElementComponent';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementComponent.tsx
new file mode 100644
index 00000000000..45276c80653
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementComponent.tsx
@@ -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 ;
+ }
+
+ // mode === 'edit'
+ return ;
+});
+
+DividerElementComponent.displayName = 'DividerElementComponent';
+
+export const DividerElementComponentViewMode = memo(({ el }: { el: DividerElement }) => {
+ const container = useContainerContext();
+ const { id } = el;
+
+ return (
+
+ );
+});
+
+DividerElementComponentViewMode.displayName = 'DividerElementComponentViewMode';
+
+export const DividerElementComponentEditMode = memo(({ el }: { el: DividerElement }) => {
+ const container = useContainerContext();
+ const { id } = el;
+
+ return (
+
+
+
+ );
+});
+
+DividerElementComponentEditMode.displayName = 'DividerElementComponentEditMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DndListDropIndicator.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DndListDropIndicator.tsx
new file mode 100644
index 00000000000..9ce6294fd28
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DndListDropIndicator.tsx
@@ -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 && }
+ */
+ 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 = {
+ horizontal: {
+ height: `${line.thickness}px`,
+ left: 2,
+ right: 2,
+ },
+ vertical: {
+ width: `${line.thickness}px`,
+ top: 2,
+ bottom: 2,
+ },
+};
+
+const edgeToOrientationMap: Record = {
+ top: 'horizontal',
+ bottom: 'horizontal',
+ left: 'vertical',
+ right: 'vertical',
+};
+
+const edgeStyles: Record = {
+ 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 (
+
+ );
+}
+
+export const DndListDropIndicator = ({
+ activeDropRegion,
+ gap,
+}: {
+ activeDropRegion: CenterOrEdge | null;
+ gap?: string;
+}) => {
+ if (!activeDropRegion) {
+ return null;
+ }
+ if (activeDropRegion === 'center') {
+ return null;
+ }
+
+ return (
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeWrapper.tsx
new file mode 100644
index 00000000000..29553241cc7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeWrapper.tsx
@@ -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(null);
+ const dragHandleRef = useRef(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 (
+
+
+
+ {getHeaderLabel(element)} ({element.id})
+
+
+ {element.parentId && (
+ }
+ variant="link"
+ size="sm"
+ alignSelf="stretch"
+ colorScheme="error"
+ />
+ )}
+
+
+ {children}
+
+
+
+ );
+ }
+);
+
+FormElementEditModeWrapper.displayName = 'FormElementEditModeWrapper';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementComponent.tsx
new file mode 100644
index 00000000000..d0f7264fac0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementComponent.tsx
@@ -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 ;
+ }
+
+ // mode === 'edit'
+ return ;
+});
+
+HeadingElementComponent.displayName = 'HeadingElementComponent';
+
+export const HeadingElementComponentViewMode = memo(({ el }: { el: HeadingElement }) => {
+ const { id, data } = el;
+ const { content, level } = data;
+
+ return (
+
+
+ {content || 'Edit to add heading'}
+
+
+ );
+});
+
+HeadingElementComponentViewMode.displayName = 'HeadingElementComponentViewMode';
+
+export const HeadingElementComponentEditMode = memo(({ el }: { el: HeadingElement }) => {
+ const { id } = el;
+
+ return (
+
+
+
+
+
+ );
+});
+
+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(null);
+
+ const onChange = useCallback((e: ChangeEvent) => {
+ 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) => {
+ if (e.key === 'Enter') {
+ onBlur();
+ } else if (e.key === 'Escape') {
+ setLocalContent(content);
+ }
+ },
+ [content, onBlur]
+ );
+
+ return (
+
+ );
+});
+
+EditableHeading.displayName = 'EditableHeading';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementComponent.tsx
new file mode 100644
index 00000000000..6335cf2f2e6
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementComponent.tsx
@@ -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 ;
+ }
+
+ // mode === 'edit'
+ return ;
+});
+
+NodeFieldElementComponent.displayName = 'NodeFieldElementComponent';
+
+export const NodeFieldElementComponentViewMode = memo(({ el }: { el: NodeFieldElement }) => {
+ const { id, data } = el;
+ const { fieldIdentifier } = data;
+
+ return (
+
+
+
+
+
+ );
+});
+
+NodeFieldElementComponentViewMode.displayName = 'NodeFieldElementComponentViewMode';
+
+export const NodeFieldElementComponentEditMode = memo(({ el }: { el: NodeFieldElement }) => {
+ const { id, data } = el;
+ const { fieldIdentifier } = data;
+
+ return (
+
+
+
+
+
+
+
+ );
+});
+
+NodeFieldElementComponentEditMode.displayName = 'NodeFieldElementComponentEditMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementComponent.tsx
new file mode 100644
index 00000000000..51ef9f74958
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementComponent.tsx
@@ -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 ;
+ }
+
+ // mode === 'edit'
+ return ;
+});
+TextElementComponent.displayName = 'TextElementComponent';
+
+export const TextElementComponentViewMode = memo(({ el }: { el: TextElement }) => {
+ const { id, data } = el;
+ const { content, fontSize } = data;
+
+ return (
+
+
+ {content || 'Edit to add text'}
+
+
+ );
+});
+TextElementComponentViewMode.displayName = 'TextElementComponentViewMode';
+
+export const TextElementComponentEditMode = memo(({ el }: { el: TextElement }) => {
+ const { id } = el;
+
+ return (
+
+
+
+
+
+ );
+});
+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(null);
+
+ const onChange = useCallback((e: ChangeEvent) => {
+ 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) => {
+ if (e.key === 'Enter') {
+ onBlur();
+ } else if (e.key === 'Escape') {
+ setLocalContent(content);
+ }
+ },
+ [content, onBlur]
+ );
+
+ return (
+
+ );
+});
+
+EditableText.displayName = 'EditableText';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/WorkflowBuilder.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/WorkflowBuilder.tsx
new file mode 100644
index 00000000000..f76e66a2dad
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/WorkflowBuilder.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ {rootElementId && }
+
+
+
+ );
+});
+
+WorkflowBuilder.displayName = 'WorkflowBuilder';
+
+const ToggleModeButton = memo(() => {
+ const dispatch = useAppDispatch();
+ const mode = useAppSelector(selectWorkflowFormMode);
+
+ const onClick = useCallback(() => {
+ dispatch(formModeToggled());
+ }, [dispatch]);
+
+ return ;
+});
+ToggleModeButton.displayName = 'ToggleModeButton';
+
+const useAddFormElementDnd = (type: Omit, draggableRef: RefObject) => {
+ 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 }) => {
+ const draggableRef = useRef(null);
+ const isDragging = useAddFormElementDnd(type, draggableRef);
+
+ return (
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/center-or-closest-edge.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/center-or-closest-edge.ts
new file mode 100644
index 00000000000..7282c9a4420
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/center-or-closest-edge.ts
@@ -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,
+ 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,
+ {
+ element,
+ input,
+ allowedCenterOrEdge,
+ }: {
+ element: Element;
+ input: Input;
+ allowedCenterOrEdge: CenterOrEdge[];
+ }
+): Record {
+ 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): CenterOrEdge | null {
+ return (userData[uniqueKey] as CenterOrEdge) ?? null;
+}
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/contexts.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/contexts.tsx
new file mode 100644
index 00000000000..2e888dc7cd4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/contexts.tsx
@@ -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(null);
+
+export const ContainerContextProvider = memo(
+ ({ id, direction, children }: PropsWithChildren) => {
+ const ctxValue = useMemo(() => ({ id, direction }), [id, direction]);
+ return {children};
+ }
+);
+ContainerContextProvider.displayName = 'ContainerContextProvider';
+
+export const useContainerContext = () => {
+ const container = useContext(ContainerContext);
+ return container;
+};
+
+const DepthContext = createContext(0);
+
+export const DepthContextProvider = memo(({ depth, children }: PropsWithChildren<{ depth: number }>) => {
+ return {children};
+});
+DepthContextProvider.displayName = 'DepthContextProvider';
+
+export const useDepthContext = () => {
+ const depth = useContext(DepthContext);
+ return depth;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/shared.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/shared.ts
new file mode 100644
index 00000000000..3f7af9c9623
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/shared.ts
@@ -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`;
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-builder-dnd.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-builder-dnd.ts
new file mode 100644
index 00000000000..925761720e1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-builder-dnd.ts
@@ -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): 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): 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): data is NodeFieldDndData => {
+ return uniqueNodeFieldKey in data;
+};
+
+const getElement = (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) => {
+ if (edge === 'left' || edge === 'top') {
+ return index - 1;
+ }
+ return index + 1;
+};
+
+const adjustIndexForNodeFieldDrop = (index: number, edge: Exclude) => {
+ 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,
+ dragHandleRef: RefObject
+) => {
+ const [isDragging, setIsDragging] = useState(false);
+ const [activeDropRegion, setActiveDropRegion] = useState(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;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx
index 3ed2189ff7b..f4796893aa6 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx
@@ -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 (
@@ -74,7 +76,7 @@ const Content = memo((props: ContentProps) => {
-
+ {isInvocationNode && }
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx
index 048276f5c08..9f032c1249c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx
@@ -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) {
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/WorkflowNodeInspectorPanel.tsx
similarity index 92%
rename from invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorPanel.tsx
rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/WorkflowNodeInspectorPanel.tsx
index 9bbf7e6aacd..fa89a8dfc28 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/WorkflowNodeInspectorPanel.tsx
@@ -7,7 +7,7 @@ import InspectorDetailsTab from './InspectorDetailsTab';
import InspectorOutputsTab from './InspectorOutputsTab';
import InspectorTemplateTab from './InspectorTemplateTab';
-const InspectorPanel = () => {
+const WorkflowNodeInspectorPanel = () => {
const { t } = useTranslation();
return (
@@ -38,4 +38,4 @@ const InspectorPanel = () => {
);
};
-export default memo(InspectorPanel);
+export default memo(WorkflowNodeInspectorPanel);
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/ViewModeLeftPanelContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/ViewModeLeftPanelContent.tsx
new file mode 100644
index 00000000000..6ec0815b5fe
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/ViewModeLeftPanelContent.tsx
@@ -0,0 +1,55 @@
+import { Box, Flex } from '@invoke-ai/ui-library';
+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 { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
+import { InputFieldViewMode } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewMode';
+import { ViewContextProvider } from 'features/nodes/contexts/ViewContext';
+import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
+import { t } from 'i18next';
+import { memo } from 'react';
+import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
+
+import { EmptyState } from './EmptyState';
+
+const selectExposedFields = createMemoizedSelector(selectWorkflowSlice, (workflow) => workflow.exposedFields);
+
+export const ViewModeLeftPanelContent = memo(() => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+});
+ViewModeLeftPanelContent.displayName = 'ViewModeLeftPanelContent';
+
+const ViewModeLeftPanelContentInner = memo(() => {
+ const { isLoading } = useGetOpenAPISchemaQuery();
+ const exposedFields = useAppSelector(selectExposedFields);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (exposedFields.length === 0) {
+ return ;
+ }
+
+ return (
+ <>
+ {exposedFields.map(({ nodeId, fieldName }) => (
+
+
+
+ ))}
+ >
+ );
+});
+ViewModeLeftPanelContentInner.displayName = ' ViewModeLeftPanelContentInner';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx
deleted file mode 100644
index 482de6693e5..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
-import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent';
-import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
-import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
-import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
-import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
-import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
-import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
-import { t } from 'i18next';
-import { memo } from 'react';
-import { PiArrowCounterClockwiseBold, PiInfoBold } from 'react-icons/pi';
-
-type Props = {
- nodeId: string;
- fieldName: string;
-};
-
-const WorkflowFieldInternal = ({ nodeId, fieldName }: Props) => {
- const label = useFieldLabel(nodeId, fieldName);
- const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, 'inputs');
- const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName);
-
- return (
-
-
- {label || fieldTemplateTitle}
-
-
- {isValueChanged && (
- }
- />
- )}
- }
- openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
- placement="top"
- >
-
-
-
-
-
-
-
- );
-};
-
-const WorkflowField = ({ nodeId, fieldName }: Props) => {
- return (
-
-
-
- );
-};
-
-export default memo(WorkflowField);
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowViewMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowViewMode.tsx
deleted file mode 100644
index deadafb3b43..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowViewMode.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Box, Flex } from '@invoke-ai/ui-library';
-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 { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
-import { t } from 'i18next';
-import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
-
-import { EmptyState } from './EmptyState';
-import WorkflowField from './WorkflowField';
-
-const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
- return {
- fields: workflow.exposedFields,
- name: workflow.name,
- };
-});
-
-export const WorkflowViewMode = () => {
- const { isLoading } = useGetOpenAPISchemaQuery();
- const { fields } = useAppSelector(selector);
- return (
-
-
-
- {isLoading ? (
-
- ) : fields.length ? (
- fields.map(({ nodeId, fieldName }) => (
-
- ))
- ) : (
-
- )}
-
-
-
- );
-};
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx
index 2547b9a7749..7270bd72645 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx
@@ -1,7 +1,7 @@
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
import { reorderWithEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge';
-import { Box, Flex } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
@@ -10,7 +10,8 @@ import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { deepClone } from 'common/util/deepClone';
import { singleWorkflowFieldDndSource } from 'features/dnd/dnd';
import { triggerPostMoveFlash } from 'features/dnd/util';
-import LinearViewFieldInternal from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField';
+import { InputFieldEditModeLinear } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeLinear';
+import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { isEqual } from 'lodash-es';
@@ -23,13 +24,11 @@ const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => workf
const WorkflowLinearTab = () => {
return (
-
-
-
-
-
-
-
+
+
+
+
+
);
};
@@ -142,11 +141,10 @@ const FieldListInnerContent = memo(({ fields }: { fields: FieldIdentifier[] }) =
return (
<>
- {fields.map((fieldIdentifier) => (
-
+ {fields.map(({ nodeId, fieldName }) => (
+
+
+
))}
>
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx
index bdc384d40d0..1bfe0f7eac9 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx
@@ -1,23 +1,28 @@
import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
+import { WorkflowBuilder } from 'features/nodes/components/sidePanel/builder/WorkflowBuilder';
+import WorkflowLinearTab from 'features/nodes/components/sidePanel/workflow/WorkflowLinearTab';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import WorkflowGeneralTab from './WorkflowGeneralTab';
import WorkflowJSONTab from './WorkflowJSONTab';
-import WorkflowLinearTab from './WorkflowLinearTab';
-const WorkflowPanel = () => {
+const WorkflowFieldsLinearViewPanel = () => {
const { t } = useTranslation();
return (
+ {t('common.builder')}
{t('common.linear')}
{t('common.details')}
JSON
+
+
+
@@ -33,4 +38,4 @@ const WorkflowPanel = () => {
);
};
-export default memo(WorkflowPanel);
+export default memo(WorkflowFieldsLinearViewPanel);
diff --git a/invokeai/frontend/web/src/features/nodes/contexts/ViewContext.tsx b/invokeai/frontend/web/src/features/nodes/contexts/ViewContext.tsx
new file mode 100644
index 00000000000..ae11ef45e29
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/contexts/ViewContext.tsx
@@ -0,0 +1,21 @@
+import type { PropsWithChildren } from 'react';
+import { createContext, memo, useContext } from 'react';
+import { assert } from 'tsafe';
+
+type View = 'view-mode-linear' | 'edit-mode-linear' | 'edit-mode-nodes';
+
+const ViewContext = createContext(null);
+
+type Props = PropsWithChildren<{ view: View }>;
+
+export const ViewContextProvider = memo((props: Props) => {
+ return {props.children};
+});
+
+ViewContextProvider.displayName = 'ViewContextProvider';
+
+export const useViewContext = () => {
+ const context = useContext(ViewContext);
+ assert(context !== null, 'useViewContext must be used within a ViewContextProvider');
+ return context;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBatchGroupId.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBatchGroupId.ts
index 9ae03fa39ad..a41f4ca04ec 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useBatchGroupId.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useBatchGroupId.ts
@@ -1,19 +1,24 @@
-import { useNode } from 'features/nodes/hooks/useNode';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { isBatchNode, isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useBatchGroupId = (nodeId: string) => {
- const node = useNode(nodeId);
+ const selector = useMemo(() => {
+ return createSelector(selectNodesSlice, (nodes) => {
+ const node = selectNode(nodes, nodeId);
+ if (!isInvocationNode(node)) {
+ return;
+ }
+ if (!isBatchNode(node)) {
+ return;
+ }
+ return node.data.inputs['batch_group_id']?.value as string;
+ });
+ }, [nodeId]);
- const batchGroupId = useMemo(() => {
- if (!isInvocationNode(node)) {
- return;
- }
- if (!isBatchNode(node)) {
- return;
- }
- return node.data.inputs['batch_group_id']?.value as string;
- }, [node]);
+ const batchGroupId = useAppSelector(selector);
return batchGroupId;
};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts
index 4e96c219f86..10f49292350 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts
@@ -1,4 +1,5 @@
import { useStore } from '@nanostores/react';
+import { useReactFlow } from '@xyflow/react';
import { $templates } from 'features/nodes/store/nodesSlice';
import { NODE_WIDTH } from 'features/nodes/types/constants';
import type { AnyNode, InvocationTemplate } from 'features/nodes/types/invocation';
@@ -6,7 +7,6 @@ import { buildCurrentImageNode } from 'features/nodes/util/node/buildCurrentImag
import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode';
import { buildNotesNode } from 'features/nodes/util/node/buildNotesNode';
import { useCallback } from 'react';
-import { useReactFlow } from 'reactflow';
export const useBuildNode = () => {
const templates = useStore($templates);
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts
index dffd1447303..e650cde2cdc 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts
@@ -1,4 +1,6 @@
import { useStore } from '@nanostores/react';
+import type { EdgeChange, OnConnect, OnConnectEnd, OnConnectStart } from '@xyflow/react';
+import { useUpdateNodeInternals } from '@xyflow/react';
import { useAppStore } from 'app/store/storeHooks';
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import {
@@ -12,9 +14,8 @@ import {
import { selectNodes, selectNodesSlice } from 'features/nodes/store/selectors';
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
+import type { AnyEdge } from 'features/nodes/types/invocation';
import { useCallback, useMemo } from 'react';
-import type { EdgeChange, OnConnect, OnConnectEnd, OnConnectStart } from 'reactflow';
-import { useUpdateNodeInternals } from 'reactflow';
import { assert } from 'tsafe';
export const useConnection = () => {
@@ -94,7 +95,7 @@ export const useConnection = () => {
);
if (connection) {
const newEdge = connectionToEdge(connection);
- const edgeChanges: EdgeChange[] = [{ type: 'add', item: newEdge }];
+ const edgeChanges: EdgeChange[] = [{ type: 'add', item: newEdge }];
const nodesToUpdate = [newEdge.source, newEdge.target];
if (edgePendingUpdate) {
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts
deleted file mode 100644
index 4ed2c358c5a..00000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { useStore } from '@nanostores/react';
-import { createSelector } from '@reduxjs/toolkit';
-import { useAppSelector } from 'app/store/storeHooks';
-import { $edgePendingUpdate, $pendingConnection, $templates } from 'features/nodes/store/nodesSlice';
-import { selectNodesSlice } from 'features/nodes/store/selectors';
-import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector';
-import { useMemo } from 'react';
-
-type UseConnectionStateProps = {
- nodeId: string;
- fieldName: string;
- kind: 'inputs' | 'outputs';
-};
-
-export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => {
- const pendingConnection = useStore($pendingConnection);
- const templates = useStore($templates);
- const edgePendingUpdate = useStore($edgePendingUpdate);
-
- const selectIsConnected = useMemo(
- () =>
- createSelector(selectNodesSlice, (nodes) =>
- Boolean(
- nodes.edges.filter((edge) => {
- return (
- (kind === 'inputs' ? edge.target : edge.source) === nodeId &&
- (kind === 'inputs' ? edge.targetHandle : edge.sourceHandle) === fieldName
- );
- }).length
- )
- ),
- [fieldName, kind, nodeId]
- );
-
- const selectValidationResult = useMemo(
- () => makeConnectionErrorSelector(templates, nodeId, fieldName, kind === 'inputs' ? 'target' : 'source'),
- [templates, nodeId, fieldName, kind]
- );
-
- const isConnected = useAppSelector(selectIsConnected);
- const isConnectionInProgress = useMemo(() => Boolean(pendingConnection), [pendingConnection]);
- const isConnectionStartField = useMemo(() => {
- if (!pendingConnection) {
- return false;
- }
- return (
- pendingConnection.nodeId === nodeId &&
- pendingConnection.handleId === fieldName &&
- pendingConnection.fieldTemplate.fieldKind === { inputs: 'input', outputs: 'output' }[kind]
- );
- }, [fieldName, kind, nodeId, pendingConnection]);
- const validationResult = useAppSelector((s) => selectValidationResult(s, pendingConnection, edgePendingUpdate));
-
- const shouldDim = useMemo(
- () => Boolean(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField),
- [validationResult, isConnectionInProgress, isConnectionStartField]
- );
-
- return {
- isConnected,
- isConnectionInProgress,
- isConnectionStartField,
- validationResult,
- shouldDim,
- };
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOriginalValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOriginalValue.ts
deleted file mode 100644
index a9ebc991e22..00000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOriginalValue.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { createSelector } from '@reduxjs/toolkit';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
-import { fieldValueReset } from 'features/nodes/store/nodesSlice';
-import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
-import { isEqual } from 'lodash-es';
-import { useCallback, useMemo } from 'react';
-
-export const useFieldOriginalValue = (nodeId: string, fieldName: string) => {
- const dispatch = useAppDispatch();
- const selectOriginalExposedFieldValues = useMemo(
- () =>
- createSelector(
- selectWorkflowSlice,
- (workflow) =>
- workflow.originalExposedFieldValues.find((v) => v.nodeId === nodeId && v.fieldName === fieldName)?.value
- ),
- [nodeId, fieldName]
- );
- const originalValue = useAppSelector(selectOriginalExposedFieldValues);
- const value = useFieldValue(nodeId, fieldName);
- const isValueChanged = useMemo(() => !isEqual(value, originalValue), [value, originalValue]);
- const onReset = useCallback(() => {
- dispatch(fieldValueReset({ nodeId, fieldName, value: originalValue }));
- }, [dispatch, fieldName, nodeId, originalValue]);
-
- return { originalValue, isValueChanged, onReset };
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts
deleted file mode 100644
index 585ef3fe1cb..00000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
-import type { FieldOutputTemplate } from 'features/nodes/types/field';
-import { useMemo } from 'react';
-
-export const useFieldOutputTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate | null => {
- const template = useNodeTemplate(nodeId);
- const fieldTemplate = useMemo(() => template.outputs[fieldName] ?? null, [fieldName, template.outputs]);
- return fieldTemplate;
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts
deleted file mode 100644
index 1e1c7a4dd84..00000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useStore } from '@nanostores/react';
-import { createSelector } from '@reduxjs/toolkit';
-import { useAppSelector } from 'app/store/storeHooks';
-import { $templates } from 'features/nodes/store/nodesSlice';
-import { selectInvocationNodeType, selectNodesSlice } from 'features/nodes/store/selectors';
-import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field';
-import { useMemo } from 'react';
-import { assert } from 'tsafe';
-
-export const useFieldTemplate = (
- nodeId: string,
- fieldName: string,
- kind: 'inputs' | 'outputs'
-): FieldInputTemplate | FieldOutputTemplate => {
- const templates = useStore($templates);
- const selectNodeType = useMemo(
- () => createSelector(selectNodesSlice, (nodes) => selectInvocationNodeType(nodes, nodeId)),
- [nodeId]
- );
- const nodeType = useAppSelector(selectNodeType);
- const fieldTemplate = useMemo(() => {
- const template = templates[nodeType];
- assert(template, `Template for node type ${nodeType} not found`);
- if (kind === 'inputs') {
- const fieldTemplate = template.inputs[fieldName];
- assert(fieldTemplate, `Field template for field ${fieldName} not found`);
- return fieldTemplate;
- } else {
- const fieldTemplate = template.outputs[fieldName];
- assert(fieldTemplate, `Field template for field ${fieldName} not found`);
- return fieldTemplate;
- }
- }, [fieldName, kind, nodeType, templates]);
-
- return fieldTemplate;
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts
deleted file mode 100644
index 5484044e9a1..00000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
-import { useMemo } from 'react';
-
-export const useFieldTemplateTitle = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): string | null => {
- const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
- const fieldTemplateTitle = useMemo(() => fieldTemplate.title, [fieldTemplate]);
- return fieldTemplateTitle;
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldConnectionState.ts
new file mode 100644
index 00000000000..96908478585
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldConnectionState.ts
@@ -0,0 +1,35 @@
+import { useStore } from '@nanostores/react';
+import { useAppSelector } from 'app/store/storeHooks';
+import { $edgePendingUpdate, $pendingConnection, $templates } from 'features/nodes/store/nodesSlice';
+import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector';
+import { useMemo } from 'react';
+
+export const useInputFieldConnectionState = (nodeId: string, fieldName: string) => {
+ const pendingConnection = useStore($pendingConnection);
+ const templates = useStore($templates);
+ const edgePendingUpdate = useStore($edgePendingUpdate);
+
+ const selectValidationResult = useMemo(
+ () => makeConnectionErrorSelector(templates, nodeId, fieldName, 'target'),
+ [templates, nodeId, fieldName]
+ );
+
+ const isConnectionInProgress = useMemo(() => Boolean(pendingConnection), [pendingConnection]);
+ const isConnectionStartField = useMemo(() => {
+ if (!pendingConnection) {
+ return false;
+ }
+ return (
+ pendingConnection.nodeId === nodeId &&
+ pendingConnection.handleId === fieldName &&
+ pendingConnection.fieldTemplate.fieldKind === 'input'
+ );
+ }, [fieldName, nodeId, pendingConnection]);
+ const validationResult = useAppSelector((s) => selectValidationResult(s, pendingConnection, edgePendingUpdate));
+
+ return {
+ isConnectionInProgress,
+ isConnectionStartField,
+ validationResult,
+ };
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldDefaultValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldDefaultValue.ts
new file mode 100644
index 00000000000..0f7f3856dc4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldDefaultValue.ts
@@ -0,0 +1,23 @@
+import { useAppDispatch } from 'app/store/storeHooks';
+import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
+import { useInputFieldValue } from 'features/nodes/hooks/useInputFieldValue';
+import { fieldValueReset } from 'features/nodes/store/nodesSlice';
+import { isEqual } from 'lodash-es';
+import { useCallback, useMemo } from 'react';
+
+export const useInputFieldDefaultValue = (nodeId: string, fieldName: string) => {
+ const dispatch = useAppDispatch();
+
+ const value = useInputFieldValue(nodeId, fieldName);
+ const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
+
+ const isValueChanged = useMemo(() => {
+ return !isEqual(value, fieldTemplate.default);
+ }, [value, fieldTemplate.default]);
+
+ const resetToDefaultValue = useCallback(() => {
+ dispatch(fieldValueReset({ nodeId, fieldName, value: fieldTemplate.default }));
+ }, [dispatch, fieldName, fieldTemplate.default, nodeId]);
+
+ return { defaultValue: fieldTemplate.default, isValueChanged, resetToDefaultValue };
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInitialLinearViewValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInitialLinearViewValue.ts
new file mode 100644
index 00000000000..e31dacfa26d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInitialLinearViewValue.ts
@@ -0,0 +1,28 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useInputFieldValue } from 'features/nodes/hooks/useInputFieldValue';
+import { fieldValueReset } from 'features/nodes/store/nodesSlice';
+import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
+import { isEqual } from 'lodash-es';
+import { useCallback, useMemo } from 'react';
+
+export const useInputFieldInitialLinearViewValue = (nodeId: string, fieldName: string) => {
+ const dispatch = useAppDispatch();
+ const selectInitialLinearViewValue = useMemo(
+ () =>
+ createSelector(
+ selectWorkflowSlice,
+ (workflow) =>
+ workflow.originalExposedFieldValues.find((v) => v.nodeId === nodeId && v.fieldName === fieldName)?.value
+ ),
+ [nodeId, fieldName]
+ );
+ const initialLinearViewValue = useAppSelector(selectInitialLinearViewValue);
+ const value = useInputFieldValue(nodeId, fieldName);
+ const isValueChanged = useMemo(() => !isEqual(value, initialLinearViewValue), [value, initialLinearViewValue]);
+ const resetToInitialLinearViewValue = useCallback(() => {
+ dispatch(fieldValueReset({ nodeId, fieldName, value: initialLinearViewValue }));
+ }, [dispatch, fieldName, nodeId, initialLinearViewValue]);
+
+ return { initialLinearViewValue, isValueChanged, resetToInitialLinearViewValue };
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstance.ts
similarity index 55%
rename from invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstance.ts
index 37bc108a95e..470832f7ddb 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstance.ts
@@ -3,17 +3,20 @@ import { useAppSelector } from 'app/store/storeHooks';
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
import type { FieldInputInstance } from 'features/nodes/types/field';
import { useMemo } from 'react';
+import { assert } from 'tsafe';
-export const useFieldInputInstance = (nodeId: string, fieldName: string): FieldInputInstance | null => {
+export const useInputFieldInstance = (nodeId: string, fieldName: string): FieldInputInstance => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
- return selectFieldInputInstance(nodes, nodeId, fieldName);
+ const instance = selectFieldInputInstance(nodes, nodeId, fieldName);
+ assert(instance, `Instance for input field ${fieldName} not found`);
+ return instance;
}),
[fieldName, nodeId]
);
- const fieldData = useAppSelector(selector);
+ const instance = useAppSelector(selector);
- return fieldData;
+ return instance;
};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstanceExists.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstanceExists.ts
new file mode 100644
index 00000000000..bc9702705c4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstanceExists.ts
@@ -0,0 +1,20 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+
+export const useInputFieldInstanceExists = (nodeId: string, fieldName: string) => {
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodesSlice) => {
+ const node = selectInvocationNode(nodesSlice, nodeId);
+ const instance = node.data.inputs[fieldName];
+ return Boolean(instance);
+ }),
+ [fieldName, nodeId]
+ );
+
+ const exists = useAppSelector(selector);
+
+ return exists;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsConnected.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsConnected.ts
new file mode 100644
index 00000000000..bda4622db8b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsConnected.ts
@@ -0,0 +1,21 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectNodesSlice } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+
+export const useInputFieldIsConnected = (nodeId: string, fieldName: string) => {
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodes) => {
+ const firstConnectedEdge = nodes.edges.find((edge) => {
+ return edge.target === nodeId && edge.targetHandle === fieldName;
+ });
+ return firstConnectedEdge !== undefined;
+ }),
+ [fieldName, nodeId]
+ );
+
+ const isConnected = useAppSelector(selector);
+
+ return isConnected;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsExposed.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsExposed.ts
new file mode 100644
index 00000000000..4245cdde711
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsExposed.ts
@@ -0,0 +1,17 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
+import { useMemo } from 'react';
+
+export const useInputFieldIsExposed = (nodeId: string, fieldName: string) => {
+ const selectIsExposed = useMemo(
+ () =>
+ createSelector(selectWorkflowSlice, (workflow) => {
+ return Boolean(workflow.exposedFields.find((f) => f.nodeId === nodeId && f.fieldName === fieldName));
+ }),
+ [fieldName, nodeId]
+ );
+
+ const isExposed = useAppSelector(selectIsExposed);
+ return isExposed;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldIsInvalid.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts
similarity index 81%
rename from invokeai/frontend/web/src/features/nodes/hooks/useFieldIsInvalid.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts
index 2f97296087a..a6e52260e13 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldIsInvalid.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts
@@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
-import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
-import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
+import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
+import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
import {
isFloatFieldCollectionInputInstance,
@@ -20,9 +20,9 @@ import {
} from 'features/nodes/types/fieldValidators';
import { useMemo } from 'react';
-export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
- const template = useFieldInputTemplate(nodeId, fieldName);
- const connectionState = useConnectionState({ nodeId, fieldName, kind: 'inputs' });
+export const useInputFieldIsInvalid = (nodeId: string, fieldName: string) => {
+ const template = useInputFieldTemplate(nodeId, fieldName);
+ const isConnected = useInputFieldIsConnected(nodeId, fieldName);
const selectIsInvalid = useMemo(() => {
return createSelector(selectNodesSlice, (nodes) => {
@@ -35,11 +35,11 @@ export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
// 'connection' input fields have no data validation - only connection validation
if (template.input === 'connection') {
- return template.required && !connectionState.isConnected;
+ return template.required && !isConnected;
}
// 'any' input fields are valid if they are connected
- if (template.input === 'any' && connectionState.isConnected) {
+ if (template.input === 'any' && isConnected) {
return false;
}
@@ -77,7 +77,7 @@ export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
// Field looks OK
return false;
});
- }, [connectionState.isConnected, fieldName, nodeId, template]);
+ }, [nodeId, fieldName, template, isConnected]);
const isInvalid = useAppSelector(selectIsInvalid);
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldLabel.ts
similarity index 84%
rename from invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useInputFieldLabel.ts
index d7a7fe48f23..57197090077 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldLabel.ts
@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
-export const useFieldLabel = (nodeId: string, fieldName: string): string | null => {
+export const useInputFieldLabel = (nodeId: string, fieldName: string): string | null => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldName.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldName.ts
new file mode 100644
index 00000000000..573edd89f99
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldName.ts
@@ -0,0 +1,27 @@
+import { useStore } from '@nanostores/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { $templates } from 'features/nodes/store/nodesSlice';
+import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+
+export const useInputFieldName = (nodeId: string, fieldName: string) => {
+ const templates = useStore($templates);
+
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodesSlice) => {
+ const node = selectInvocationNode(nodesSlice, nodeId);
+ const instance = node.data.inputs[fieldName];
+ const nodeTemplate = templates[node.data.type];
+ const fieldTemplate = nodeTemplate?.inputs[fieldName];
+ const name = instance?.label || fieldTemplate?.title || fieldName;
+ return name;
+ }),
+ [fieldName, nodeId, templates]
+ );
+
+ const name = useAppSelector(selector);
+
+ return name;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNamesByStatus.ts
similarity index 95%
rename from invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNamesByStatus.ts
index 19849fb296f..4dba5a776ab 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNamesByStatus.ts
@@ -20,7 +20,7 @@ const isAnyOrDirectInputField = (field: FieldInputTemplate) => {
);
};
-export const useFieldNames = (nodeId: string) => {
+export const useInputFieldNamesByStatus = (nodeId: string) => {
const template = useNodeTemplate(nodeId);
const node = useNodeData(nodeId);
const fieldNames = useMemo(() => {
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNotes.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNotes.ts
new file mode 100644
index 00000000000..80ff92ebf4b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNotes.ts
@@ -0,0 +1,22 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectNodesSlice } from 'features/nodes/store/selectors';
+import { isInvocationNode } from 'features/nodes/types/invocation';
+import { useMemo } from 'react';
+
+export const useInputFieldNotes = (nodeId: string, fieldName: string) => {
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodes) => {
+ const node = nodes.nodes.find((node) => node.id === nodeId);
+ if (!isInvocationNode(node)) {
+ return;
+ }
+ return node?.data.inputs[fieldName]?.notes;
+ }),
+ [fieldName, nodeId]
+ );
+
+ const notes = useAppSelector(selector);
+ return notes;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplate.ts
similarity index 76%
rename from invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplate.ts
index 729319e0dd8..34afde1e495 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplate.ts
@@ -3,11 +3,11 @@ import type { FieldInputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
import { assert } from 'tsafe';
-export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate => {
+export const useInputFieldTemplate = (nodeId: string, fieldName: string): FieldInputTemplate => {
const template = useNodeTemplate(nodeId);
const fieldTemplate = useMemo(() => {
const _fieldTemplate = template.inputs[fieldName];
- assert(_fieldTemplate, `Field template for field ${fieldName} not found`);
+ assert(_fieldTemplate, `Template for input field ${fieldName} not found.`);
return _fieldTemplate;
}, [fieldName, template.inputs]);
return fieldTemplate;
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateExists.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateExists.ts
new file mode 100644
index 00000000000..b14fa1d6e40
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateExists.ts
@@ -0,0 +1,25 @@
+import { useStore } from '@nanostores/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { $templates } from 'features/nodes/store/nodesSlice';
+import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+
+export const useInputFieldTemplateExists = (nodeId: string, fieldName: string) => {
+ const templates = useStore($templates);
+
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodesSlice) => {
+ const node = selectInvocationNode(nodesSlice, nodeId);
+ const nodeTemplate = templates[node.data.type];
+ const fieldTemplate = nodeTemplate?.inputs[fieldName];
+ return Boolean(fieldTemplate);
+ }),
+ [fieldName, nodeId, templates]
+ );
+
+ const exists = useAppSelector(selector);
+
+ return exists;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateTitle.ts
new file mode 100644
index 00000000000..319c219faf4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateTitle.ts
@@ -0,0 +1,15 @@
+import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
+import { useMemo } from 'react';
+import { assert } from 'tsafe';
+
+export const useInputFieldTemplateTitle = (nodeId: string, fieldName: string): string => {
+ const template = useNodeTemplate(nodeId);
+
+ const title = useMemo(() => {
+ const fieldTemplate = template.inputs[fieldName];
+ assert(fieldTemplate, `Template for input field ${fieldName} not found.`);
+ return fieldTemplate.title;
+ }, [fieldName, template.inputs]);
+
+ return title;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldValue.ts
similarity index 90%
rename from invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useInputFieldValue.ts
index 7dfe27bcc56..d1b83b22178 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldValue.ts
@@ -4,7 +4,7 @@ import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
-export const useFieldValue = (nodeId: string, fieldName: string) => {
+export const useInputFieldValue = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts
index e1121e08dc5..e4337018bc7 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts
@@ -1,24 +1,19 @@
-// TODO: enable this at some point
import { useStore } from '@nanostores/react';
+import type { IsValidConnection } from '@xyflow/react';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { $edgePendingUpdate, $templates } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { validateConnection } from 'features/nodes/store/util/validateConnection';
import { selectShouldShouldValidateGraph } from 'features/nodes/store/workflowSettingsSlice';
+import type { AnyEdge } from 'features/nodes/types/invocation';
import { useCallback } from 'react';
-import type { Connection } from 'reactflow';
-/**
- * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts`
- * TODO: Figure out how to do this without duplicating all the logic
- */
-
-export const useIsValidConnection = () => {
+export const useIsValidConnection = (): IsValidConnection => {
const store = useAppStore();
const templates = useStore($templates);
const shouldValidateGraph = useAppSelector(selectShouldShouldValidateGraph);
- const isValidConnection = useCallback(
- ({ source, sourceHandle, target, targetHandle }: Connection): boolean => {
+ const isValidConnection = useCallback>(
+ ({ source, sourceHandle, target, targetHandle }) => {
// Connection must have valid targets
if (!(source && sourceHandle && target && targetHandle)) {
return false;
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNode.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNode.ts
deleted file mode 100644
index eb49e4080e2..00000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useNode.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
-import { useAppSelector } from 'app/store/storeHooks';
-import { selectNode, selectNodesSlice } from 'features/nodes/store/selectors';
-import { useMemo } from 'react';
-import type { Node } from 'reactflow';
-
-export const useNode = (nodeId: string): Node => {
- const selector = useMemo(
- () =>
- createMemoizedSelector(selectNodesSlice, (nodes) => {
- return selectNode(nodes, nodeId);
- }),
- [nodeId]
- );
-
- const node = useAppSelector(selector);
-
- return node;
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeCopyPaste.ts
similarity index 95%
rename from invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useNodeCopyPaste.ts
index 3f561eafca7..3974857ea11 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeCopyPaste.ts
@@ -1,3 +1,4 @@
+import type { EdgeChange, NodeChange } from '@xyflow/react';
import { logger } from 'app/logging/logger';
import { getStore } from 'app/store/nanostores/store';
import { deepClone } from 'common/util/deepClone';
@@ -13,9 +14,9 @@ import {
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
import { validateConnection } from 'features/nodes/store/util/validateConnection';
+import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
import { t } from 'i18next';
import { isEqual, isNil, uniqWith } from 'lodash-es';
-import type { EdgeChange, NodeChange } from 'reactflow';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
@@ -80,8 +81,8 @@ const _pasteSelection = (withEdgesToCopiedNodes?: boolean) => {
edge.id = uuidv4();
});
- const nodeChanges: NodeChange[] = [];
- const edgeChanges: EdgeChange[] = [];
+ const nodeChanges: NodeChange[] = [];
+ const edgeChanges: EdgeChange[] = [];
// Deselect existing nodes
nodes.forEach(({ id, selected }) => {
if (selected) {
@@ -166,6 +167,6 @@ const pasteSelectionWithEdges = () => {
const api = { copySelection, pasteSelection, pasteSelectionWithEdges };
-export const useCopyPaste = () => {
+export const useNodeCopyPaste = () => {
return api;
};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeExecutionState.ts
similarity index 97%
rename from invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useNodeExecutionState.ts
index a8755e67c4a..30ea97a9593 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeExecutionState.ts
@@ -19,7 +19,7 @@ const initialNodeExecutionState: Omit = {
outputs: [],
};
-export const useExecutionState = (nodeId?: string) => {
+export const useNodeExecutionState = (nodeId?: string) => {
const executionStates = useStore($nodeExecutionStates, nodeId ? { keys: [nodeId] } : undefined);
const executionState = useMemo(() => (nodeId ? executionStates[nodeId] : undefined), [executionStates, nodeId]);
return executionState;
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts
similarity index 89%
rename from invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts
index 1078b18cc67..5855f5542b1 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts
@@ -2,7 +2,7 @@ import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { some } from 'lodash-es';
import { useMemo } from 'react';
-export const useHasImageOutput = (nodeId: string): boolean => {
+export const useNodeHasImageOutput = (nodeId: string): boolean => {
const template = useNodeTemplate(nodeId);
const hasImageOutput = useMemo(
() =>
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsIntermediate.ts
similarity index 87%
rename from invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useNodeIsIntermediate.ts
index d46d7148c00..d7b6f8a10ec 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsIntermediate.ts
@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
-export const useIsIntermediate = (nodeId: string): boolean => {
+export const useNodeIsIntermediate = (nodeId: string): boolean => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsInvocationNode.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsInvocationNode.ts
new file mode 100644
index 00000000000..c3acbf5763d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsInvocationNode.ts
@@ -0,0 +1,20 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectNode, selectNodesSlice } from 'features/nodes/store/selectors';
+import { isInvocationNode as _isInvocationNode } from 'features/nodes/types/invocation';
+import { useMemo } from 'react';
+
+export const useNodeIsInvocationNode = (nodeId: string): boolean => {
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodes) => {
+ const node = selectNode(nodes, nodeId);
+ return _isInvocationNode(node);
+ }),
+ [nodeId]
+ );
+
+ const isInvocationNode = useAppSelector(selector);
+
+ return isInvocationNode;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeNotes.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNotes.ts
new file mode 100644
index 00000000000..5c68cc546b7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNotes.ts
@@ -0,0 +1,19 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+
+export const useInvocationNodeNotes = (nodeId: string): string => {
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodes) => {
+ const node = selectInvocationNode(nodes, nodeId);
+ return node.data.notes;
+ }),
+ [nodeId]
+ );
+
+ const notes = useAppSelector(selector);
+
+ return notes;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts
new file mode 100644
index 00000000000..06098a0fe58
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts
@@ -0,0 +1,18 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+
+export const useNodeVersion = (nodeId: string) => {
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodesSlice) => {
+ const node = selectInvocationNode(nodesSlice, nodeId);
+ return node.data.version;
+ }),
+ [nodeId]
+ );
+
+ const version = useAppSelector(selector);
+ return version;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldConnectionState.ts
new file mode 100644
index 00000000000..4bd6606467c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldConnectionState.ts
@@ -0,0 +1,35 @@
+import { useStore } from '@nanostores/react';
+import { useAppSelector } from 'app/store/storeHooks';
+import { $edgePendingUpdate, $pendingConnection, $templates } from 'features/nodes/store/nodesSlice';
+import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector';
+import { useMemo } from 'react';
+
+export const useOutputFieldConnectionState = (nodeId: string, fieldName: string) => {
+ const pendingConnection = useStore($pendingConnection);
+ const templates = useStore($templates);
+ const edgePendingUpdate = useStore($edgePendingUpdate);
+
+ const selectValidationResult = useMemo(
+ () => makeConnectionErrorSelector(templates, nodeId, fieldName, 'source'),
+ [templates, nodeId, fieldName]
+ );
+
+ const isConnectionInProgress = useMemo(() => Boolean(pendingConnection), [pendingConnection]);
+ const isConnectionStartField = useMemo(() => {
+ if (!pendingConnection) {
+ return false;
+ }
+ return (
+ pendingConnection.nodeId === nodeId &&
+ pendingConnection.handleId === fieldName &&
+ pendingConnection.fieldTemplate.fieldKind === 'output'
+ );
+ }, [fieldName, nodeId, pendingConnection]);
+ const validationResult = useAppSelector((s) => selectValidationResult(s, pendingConnection, edgePendingUpdate));
+
+ return {
+ isConnectionInProgress,
+ isConnectionStartField,
+ validationResult,
+ };
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldIsConnected.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldIsConnected.ts
new file mode 100644
index 00000000000..c6c5f214ab8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldIsConnected.ts
@@ -0,0 +1,21 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectNodesSlice } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+
+export const useOutputFieldIsConnected = (nodeId: string, fieldName: string) => {
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodes) => {
+ const firstConnectedEdge = nodes.edges.find((edge) => {
+ return edge.source === nodeId && edge.sourceHandle === fieldName;
+ });
+ return firstConnectedEdge !== undefined;
+ }),
+ [fieldName, nodeId]
+ );
+
+ const isConnected = useAppSelector(selector);
+
+ return isConnected;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldName.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldName.ts
new file mode 100644
index 00000000000..8b237484e23
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldName.ts
@@ -0,0 +1,26 @@
+import { useStore } from '@nanostores/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { $templates } from 'features/nodes/store/nodesSlice';
+import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+
+export const useOutputFieldName = (nodeId: string, fieldName: string) => {
+ const templates = useStore($templates);
+
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodesSlice) => {
+ const node = selectInvocationNode(nodesSlice, nodeId);
+ const nodeTemplate = templates[node.data.type];
+ const fieldTemplate = nodeTemplate?.outputs[fieldName];
+ const name = fieldTemplate?.title || fieldName;
+ return name;
+ }),
+ [fieldName, nodeId, templates]
+ );
+
+ const name = useAppSelector(selector);
+
+ return name;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplate.ts
new file mode 100644
index 00000000000..d30b4a8bb5f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplate.ts
@@ -0,0 +1,14 @@
+import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
+import type { FieldOutputTemplate } from 'features/nodes/types/field';
+import { useMemo } from 'react';
+import { assert } from 'tsafe';
+
+export const useOutputFieldTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate => {
+ const template = useNodeTemplate(nodeId);
+ const fieldTemplate = useMemo(() => {
+ const _fieldTemplate = template.outputs[fieldName];
+ assert(_fieldTemplate, `Template for output field ${fieldName} not found`);
+ return _fieldTemplate;
+ }, [fieldName, template.outputs]);
+ return fieldTemplate;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplateExists.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplateExists.ts
new file mode 100644
index 00000000000..37962efb465
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplateExists.ts
@@ -0,0 +1,25 @@
+import { useStore } from '@nanostores/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { $templates } from 'features/nodes/store/nodesSlice';
+import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+
+export const useOutputFieldTemplateExists = (nodeId: string, fieldName: string) => {
+ const templates = useStore($templates);
+
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodesSlice) => {
+ const node = selectInvocationNode(nodesSlice, nodeId);
+ const nodeTemplate = templates[node.data.type];
+ const fieldTemplate = nodeTemplate?.outputs[fieldName];
+ return Boolean(fieldTemplate);
+ }),
+ [fieldName, nodeId, templates]
+ );
+
+ const exists = useAppSelector(selector);
+
+ return exists;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts b/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts
index 7f531c3dba6..892f0d73e16 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts
@@ -2,13 +2,10 @@ import { type FieldType, isCollection, isSingleOrCollection } from 'features/nod
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-export const useFieldTypeName = (fieldType?: FieldType): string => {
+export const useFieldTypeName = (fieldType: FieldType): string => {
const { t } = useTranslation();
const name = useMemo(() => {
- if (!fieldType) {
- return '';
- }
const { name } = fieldType;
if (isCollection(fieldType)) {
return t('nodes.collectionFieldType', { name });
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts
index 6e00d374f63..e118f8901d8 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts
@@ -1,10 +1,10 @@
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useMemo } from 'react';
-import { useHasImageOutput } from './useHasImageOutput';
+import { useNodeHasImageOutput } from './useNodeHasImageOutput';
export const useWithFooter = (nodeId: string) => {
- const hasImageOutput = useHasImageOutput(nodeId);
+ const hasImageOutput = useNodeHasImageOutput(nodeId);
const isCacheEnabled = useFeatureStatus('invocationCache');
const withFooter = useMemo(() => hasImageOutput || isCacheEnabled, [hasImageOutput, isCacheEnabled]);
return withFooter;
diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
index 9bf859dc01a..924ff76cc8d 100644
--- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
@@ -1,5 +1,7 @@
import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
+import type { EdgeChange, NodeChange, Viewport, XYPosition } from '@xyflow/react';
+import { applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from '@xyflow/react';
import type { PersistConfig } from 'app/store/store';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { workflowLoaded } from 'features/nodes/store/actions';
@@ -72,12 +74,10 @@ import {
zT5EncoderModelFieldValue,
zVAEModelFieldValue,
} from 'features/nodes/types/field';
-import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation';
+import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
import { atom, computed } from 'nanostores';
import type { MouseEvent } from 'react';
-import type { Edge, EdgeChange, NodeChange, Viewport, XYPosition } from 'reactflow';
-import { applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow';
import type { UndoableOptions } from 'redux-undo';
import type { z } from 'zod';
@@ -95,33 +95,40 @@ type FieldValueAction = PayloadAction<{
value: T;
}>;
+const getField = (nodeId: string, fieldName: string, state: NodesState) => {
+ const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
+ const node = state.nodes?.[nodeIndex];
+ if (!isInvocationNode(node)) {
+ return;
+ }
+ return node.data?.inputs[fieldName];
+};
+
const fieldValueReducer = (
state: NodesState,
action: FieldValueAction,
schema: z.ZodTypeAny
) => {
const { nodeId, fieldName, value } = action.payload;
- const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
- const node = state.nodes?.[nodeIndex];
- if (!isInvocationNode(node)) {
+ const field = getField(nodeId, fieldName, state);
+ if (!field) {
return;
}
- const input = node.data?.inputs[fieldName];
const result = schema.safeParse(value);
- if (!input || nodeIndex < 0 || !result.success) {
+ if (!result.success) {
return;
}
- input.value = result.data;
+ field.value = result.data;
};
export const nodesSlice = createSlice({
name: 'nodes',
initialState: initialNodesState,
reducers: {
- nodesChanged: (state, action: PayloadAction) => {
- state.nodes = applyNodeChanges(action.payload, state.nodes);
+ nodesChanged: (state, action: PayloadAction[]>) => {
+ state.nodes = applyNodeChanges(action.payload, state.nodes);
// Remove edges that are no longer valid, due to a removed or otherwise changed node
- const edgeChanges: EdgeChange[] = [];
+ const edgeChanges: EdgeChange[] = [];
state.edges.forEach((e) => {
const sourceExists = state.nodes.some((n) => n.id === e.source);
const targetExists = state.nodes.some((n) => n.id === e.target);
@@ -129,10 +136,10 @@ export const nodesSlice = createSlice({
edgeChanges.push({ type: 'remove', id: e.id });
}
});
- state.edges = applyEdgeChanges(edgeChanges, state.edges);
+ state.edges = applyEdgeChanges(edgeChanges, state.edges);
},
- edgesChanged: (state, action: PayloadAction) => {
- const changes: EdgeChange[] = [];
+ edgesChanged: (state, action: PayloadAction[]>) => {
+ const changes: EdgeChange[] = [];
// We may need to massage the edge changes or otherwise handle them
action.payload.forEach((change) => {
if (change.type === 'remove' || change.type === 'select') {
@@ -244,7 +251,7 @@ export const nodesSlice = createSlice({
(node) => isInvocationNode(node) && node.data.isOpen === false
);
- const collapsedEdgesToCreate: Edge<{ count: number }>[] = [];
+ const collapsedEdgesToCreate: AnyEdge[] = [];
// hide all edges
connectedEdges.forEach((edge) => {
@@ -264,7 +271,7 @@ export const nodesSlice = createSlice({
target: edge.target,
type: 'collapsed',
data: { count: 1 },
- updatable: false,
+ reconnectable: false,
selected: edge.selected,
});
}
@@ -285,14 +292,14 @@ export const nodesSlice = createSlice({
target: edge.target,
type: 'collapsed',
data: { count: 1 },
- updatable: false,
+ reconnectable: false,
selected: edge.selected,
});
}
}
});
if (collapsedEdgesToCreate.length) {
- state.edges = applyEdgeChanges(
+ state.edges = applyEdgeChanges(
collapsedEdgesToCreate.map((edge) => ({ type: 'add', item: edge })),
state.edges
);
@@ -325,11 +332,17 @@ export const nodesSlice = createSlice({
fieldStringCollectionValueChanged: (state, action: FieldValueAction) => {
fieldValueReducer(state, action, zStringFieldCollectionValue);
},
- fieldNumberValueChanged: (state, action: FieldValueAction) => {
- fieldValueReducer(state, action, zIntegerFieldValue.or(zFloatFieldValue));
+ fieldIntegerValueChanged: (state, action: FieldValueAction) => {
+ fieldValueReducer(state, action, zIntegerFieldValue);
+ },
+ fieldFloatValueChanged: (state, action: FieldValueAction) => {
+ fieldValueReducer(state, action, zFloatFieldValue);
},
- fieldNumberCollectionValueChanged: (state, action: FieldValueAction) => {
- fieldValueReducer(state, action, zIntegerFieldCollectionValue.or(zFloatFieldCollectionValue));
+ fieldFloatCollectionValueChanged: (state, action: FieldValueAction) => {
+ fieldValueReducer(state, action, zFloatFieldCollectionValue);
+ },
+ fieldIntegerCollectionValueChanged: (state, action: FieldValueAction) => {
+ fieldValueReducer(state, action, zIntegerFieldCollectionValue);
},
fieldBooleanValueChanged: (state, action: FieldValueAction) => {
fieldValueReducer(state, action, zBooleanFieldValue);
@@ -409,6 +422,14 @@ export const nodesSlice = createSlice({
fieldStringGeneratorValueChanged: (state, action: FieldValueAction) => {
fieldValueReducer(state, action, zStringGeneratorFieldValue);
},
+ fieldNotesChanged: (state, action: PayloadAction<{ nodeId: string; fieldName: string; val?: string }>) => {
+ const { nodeId, fieldName, val } = action.payload;
+ const field = getField(nodeId, fieldName, state);
+ if (!field) {
+ return;
+ }
+ field.notes = val;
+ },
notesNodeValueChanged: (state, action: PayloadAction<{ nodeId: string; value: string }>) => {
const { nodeId, value } = action.payload;
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
@@ -428,13 +449,28 @@ export const nodesSlice = createSlice({
extraReducers: (builder) => {
builder.addCase(workflowLoaded, (state, action) => {
const { nodes, edges } = action.payload;
- state.nodes = applyNodeChanges(
- nodes.map((node) => ({
- type: 'add',
- item: { ...node, ...SHARED_NODE_PROPERTIES },
- })),
- []
- );
+
+ const changes: NodeChange[] = [];
+ for (const node of nodes) {
+ if (node.type === 'notes') {
+ changes.push({
+ type: 'add',
+ item: {
+ ...SHARED_NODE_PROPERTIES,
+ ...node,
+ },
+ });
+ } else if (node.type === 'invocation') {
+ changes.push({
+ type: 'add',
+ item: {
+ ...SHARED_NODE_PROPERTIES,
+ ...node,
+ },
+ });
+ }
+ }
+ state.nodes = applyNodeChanges(changes, []);
state.edges = applyEdgeChanges(
edges.map((edge) => ({ type: 'add', item: edge })),
[]
@@ -460,8 +496,10 @@ export const {
fieldLoRAModelValueChanged,
fieldModelIdentifierValueChanged,
fieldMainModelValueChanged,
- fieldNumberValueChanged,
- fieldNumberCollectionValueChanged,
+ fieldIntegerValueChanged,
+ fieldFloatValueChanged,
+ fieldFloatCollectionValueChanged,
+ fieldIntegerCollectionValueChanged,
fieldRefinerModelValueChanged,
fieldSchedulerValueChanged,
fieldStringValueChanged,
@@ -476,6 +514,7 @@ export const {
fieldFloatGeneratorValueChanged,
fieldIntegerGeneratorValueChanged,
fieldStringGeneratorValueChanged,
+ fieldNotesChanged,
nodeEditorReset,
nodeIsIntermediateChanged,
nodeIsOpenChanged,
@@ -492,10 +531,10 @@ export const $cursorPos = atom(null);
export const $templates = atom({});
export const $hasTemplates = computed($templates, (templates) => Object.keys(templates).length > 0);
export const $copiedNodes = atom([]);
-export const $copiedEdges = atom([]);
-export const $edgesToCopiedNodes = atom([]);
+export const $copiedEdges = atom([]);
+export const $edgesToCopiedNodes = atom([]);
export const $pendingConnection = atom(null);
-export const $edgePendingUpdate = atom(null);
+export const $edgePendingUpdate = atom(null);
export const $didUpdateEdge = atom(false);
export const $lastEdgeUpdateMouseEvent = atom(null);
@@ -576,8 +615,10 @@ export const isAnyNodeOrEdgeMutation = isAnyOf(
fieldLabelChanged,
fieldLoRAModelValueChanged,
fieldMainModelValueChanged,
- fieldNumberValueChanged,
- fieldNumberCollectionValueChanged,
+ fieldIntegerValueChanged,
+ fieldIntegerCollectionValueChanged,
+ fieldFloatValueChanged,
+ fieldFloatCollectionValueChanged,
fieldRefinerModelValueChanged,
fieldSchedulerValueChanged,
fieldStringValueChanged,
diff --git a/invokeai/frontend/web/src/features/nodes/store/reactFlowInstance.ts b/invokeai/frontend/web/src/features/nodes/store/reactFlowInstance.ts
index cfbadc66697..db76ff1f912 100644
--- a/invokeai/frontend/web/src/features/nodes/store/reactFlowInstance.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/reactFlowInstance.ts
@@ -1,5 +1,6 @@
+import type { ReactFlowInstance } from '@xyflow/react';
+import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
import { atom } from 'nanostores';
-import type { ReactFlowInstance } from 'reactflow';
-export const $flow = atom(null);
+export const $flow = atom | null>(null);
export const $needsFit = atom(true);
diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts
index 2cc7df3deea..ca10f21a592 100644
--- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts
@@ -3,12 +3,11 @@ import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { NodesState } from 'features/nodes/store/types';
import type { FieldInputInstance } from 'features/nodes/types/field';
-import type { InvocationNode, InvocationNodeData } from 'features/nodes/types/invocation';
+import type { AnyNode, InvocationNode, InvocationNodeData } from 'features/nodes/types/invocation';
import { isInvocationNode } from 'features/nodes/types/invocation';
-import type { Node } from 'reactflow';
import { assert } from 'tsafe';
-export const selectNode = (nodesSlice: NodesState, nodeId: string): Node => {
+export const selectNode = (nodesSlice: NodesState, nodeId: string): AnyNode => {
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
assert(node !== undefined, `Node ${nodeId} not found`);
return node;
diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts
index 5684d0f2b33..bc961bf5d56 100644
--- a/invokeai/frontend/web/src/features/nodes/store/types.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/types.ts
@@ -1,17 +1,12 @@
+import type { HandleType } from '@xyflow/react';
import type {
FieldIdentifier,
FieldInputTemplate,
FieldOutputTemplate,
StatefulFieldValue,
} from 'features/nodes/types/field';
-import type {
- AnyNode,
- InvocationNodeEdge,
- InvocationTemplate,
- NodeExecutionState,
-} from 'features/nodes/types/invocation';
+import type { AnyEdge, AnyNode, InvocationTemplate, NodeExecutionState } from 'features/nodes/types/invocation';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
-import type { HandleType } from 'reactflow';
import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
export type Templates = Record;
@@ -27,7 +22,7 @@ export type PendingConnection = {
export type NodesState = {
_version: 1;
nodes: AnyNode[];
- edges: InvocationNodeEdge[];
+ edges: AnyEdge[];
};
export type WorkflowMode = 'edit' | 'view';
@@ -39,6 +34,7 @@ export type WorkflowsState = Omit & {
_version: 1;
isTouched: boolean;
mode: WorkflowMode;
+ formMode: WorkflowMode;
originalExposedFieldValues: FieldIdentifierWithValue[];
searchTerm: string;
orderBy?: WorkflowRecordOrderBy;
diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts b/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts
index bd110a50a1f..6974a6ec543 100644
--- a/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts
@@ -1,4 +1,4 @@
-import type { Node } from 'reactflow';
+import type { Node } from '@xyflow/react';
export const findUnoccupiedPosition = (nodes: Node[], x: number, y: number) => {
let newX = x;
diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts
index e6c117d91e9..9fb2795ae81 100644
--- a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts
@@ -1,6 +1,6 @@
import type { Templates } from 'features/nodes/store/types';
import type { FieldType } from 'features/nodes/types/field';
-import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation';
+import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
/**
* Given a collect node, return the type of the items it collects. The graph is traversed to find the first node and
@@ -15,7 +15,7 @@ import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocatio
export const getCollectItemType = (
templates: Templates,
nodes: AnyNode[],
- edges: InvocationNodeEdge[],
+ edges: AnyEdge[],
nodeId: string
): FieldType | null => {
const firstEdgeToCollect = edges.find((edge) => edge.target === nodeId && edge.targetHandle === 'item');
diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts
index adc51341d7d..2a88e2078a1 100644
--- a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts
@@ -1,9 +1,9 @@
+import type { Connection } from '@xyflow/react';
import type { Templates } from 'features/nodes/store/types';
import { validateConnection } from 'features/nodes/store/util/validateConnection';
import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field';
-import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation';
+import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
import { map } from 'lodash-es';
-import type { Connection, Edge } from 'reactflow';
/**
*
@@ -23,9 +23,9 @@ export const getFirstValidConnection = (
target: string,
targetHandle: string | null,
nodes: AnyNode[],
- edges: InvocationNodeEdge[],
+ edges: AnyEdge[],
templates: Templates,
- edgePendingUpdate: Edge | null
+ edgePendingUpdate: AnyEdge | null
): Connection | null => {
if (source === target) {
return null;
@@ -81,9 +81,9 @@ export const getTargetCandidateFields = (
sourceHandle: string,
target: string,
nodes: AnyNode[],
- edges: Edge[],
+ edges: AnyEdge[],
templates: Templates,
- edgePendingUpdate: Edge | null
+ edgePendingUpdate: AnyEdge | null
): FieldInputTemplate[] => {
const sourceNode = nodes.find((n) => n.id === source);
const targetNode = nodes.find((n) => n.id === target);
@@ -117,9 +117,9 @@ export const getSourceCandidateFields = (
targetHandle: string,
source: string,
nodes: AnyNode[],
- edges: Edge[],
+ edges: AnyEdge[],
templates: Templates,
- edgePendingUpdate: Edge | null
+ edgePendingUpdate: AnyEdge | null
): FieldOutputTemplate[] => {
const targetNode = nodes.find((n) => n.id === target);
const sourceNode = nodes.find((n) => n.id === source);
diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.ts b/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.ts
index c1a4e51f0cc..9b0b99e48de 100644
--- a/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.ts
@@ -1,5 +1,5 @@
import graphlib from '@dagrejs/graphlib';
-import type { Edge, Node } from 'reactflow';
+import type { Edge, Node } from '@xyflow/react';
/**
* Check if adding an edge between the source and target nodes would create a cycle in the graph.
diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts
index 823aa923219..2b6eb070e5e 100644
--- a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts
@@ -1,9 +1,10 @@
+import type { HandleType } from '@xyflow/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import type { RootState } from 'app/store/store';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { NodesState, PendingConnection, Templates } from 'features/nodes/store/types';
import { buildRejectResult, validateConnection } from 'features/nodes/store/util/validateConnection';
-import type { Edge, HandleType } from 'reactflow';
+import type { AnyEdge } from 'features/nodes/types/invocation';
/**
* Creates a selector that validates a pending connection.
@@ -26,9 +27,9 @@ export const makeConnectionErrorSelector = (
return createMemoizedSelector(
selectNodesSlice,
(state: RootState, pendingConnection: PendingConnection | null) => pendingConnection,
- (state: RootState, pendingConnection: PendingConnection | null, edgePendingUpdate: Edge | null) =>
+ (state: RootState, pendingConnection: PendingConnection | null, edgePendingUpdate: AnyEdge | null) =>
edgePendingUpdate,
- (nodesSlice: NodesState, pendingConnection: PendingConnection | null, edgePendingUpdate: Edge | null) => {
+ (nodesSlice: NodesState, pendingConnection: PendingConnection | null, edgePendingUpdate: AnyEdge | null) => {
const { nodes, edges } = nodesSlice;
if (!pendingConnection) {
diff --git a/invokeai/frontend/web/src/features/nodes/store/util/reactFlowUtil.ts b/invokeai/frontend/web/src/features/nodes/store/util/reactFlowUtil.ts
index 89be7951a2a..67264bf41ba 100644
--- a/invokeai/frontend/web/src/features/nodes/store/util/reactFlowUtil.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/util/reactFlowUtil.ts
@@ -1,4 +1,5 @@
-import type { Connection, Edge } from 'reactflow';
+import type { Connection } from '@xyflow/react';
+import type { AnyEdge } from 'features/nodes/types/invocation';
import { assert } from 'tsafe';
/**
@@ -19,7 +20,7 @@ const getEdgeId = (connection: Connection): string => {
* @returns The edge
* @throws If the connection is invalid (e.g. missing source, sourcehandle, target, or targetHandle)
*/
-export const connectionToEdge = (connection: Connection): Edge => {
+export const connectionToEdge = (connection: Connection): AnyEdge => {
const { source, sourceHandle, target, targetHandle } = connection;
assert(source && sourceHandle && target && targetHandle, 'Invalid connection');
return {
diff --git a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts
index 9a33c063578..77eff30b0f4 100644
--- a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts
@@ -1,10 +1,9 @@
import type { Templates } from 'features/nodes/store/types';
-import type { InvocationTemplate } from 'features/nodes/types/invocation';
+import type { AnyEdge, InvocationTemplate } from 'features/nodes/types/invocation';
import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode';
import type { OpenAPIV3_1 } from 'openapi-types';
-import type { Edge } from 'reactflow';
-export const buildEdge = (source: string, sourceHandle: string, target: string, targetHandle: string): Edge => ({
+export const buildEdge = (source: string, sourceHandle: string, target: string, targetHandle: string): AnyEdge => ({
source,
sourceHandle,
target,
diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts
index d717968d808..a5b9598d167 100644
--- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts
@@ -1,10 +1,10 @@
+import type { Connection as NullableConnection } from '@xyflow/react';
import type { Templates } from 'features/nodes/store/types';
import { areTypesEqual } from 'features/nodes/store/util/areTypesEqual';
import { getCollectItemType } from 'features/nodes/store/util/getCollectItemType';
import { getHasCycles } from 'features/nodes/store/util/getHasCycles';
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
-import type { AnyNode } from 'features/nodes/types/invocation';
-import type { Connection as NullableConnection, Edge } from 'reactflow';
+import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
import type { SetNonNullable } from 'type-fest';
type Connection = SetNonNullable;
@@ -22,15 +22,15 @@ export type ValidationResult =
type ValidateConnectionFunc = (
connection: Connection,
nodes: AnyNode[],
- edges: Edge[],
+ edges: AnyEdge[],
templates: Templates,
- ignoreEdge: Edge | null,
+ ignoreEdge: AnyEdge | null,
strict?: boolean
) => ValidationResult;
const getEqualityPredicate =
(c: Connection) =>
- (e: Edge): boolean => {
+ (e: AnyEdge): boolean => {
return (
e.target === c.target &&
e.targetHandle === c.targetHandle &&
@@ -41,7 +41,7 @@ const getEqualityPredicate =
const getTargetEqualityPredicate =
(c: Connection) =>
- (e: Edge): boolean => {
+ (e: AnyEdge): boolean => {
return e.target === c.target && e.targetHandle === c.targetHandle;
};
diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts
index 135cfcc562e..8d0ef927cd9 100644
--- a/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts
@@ -1,8 +1,8 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
+import { SelectionMode } from '@xyflow/react';
import type { PersistConfig, RootState } from 'app/store/store';
import type { Selector } from 'react-redux';
-import { SelectionMode } from 'reactflow';
export type WorkflowSettingsState = {
_version: 1;
diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
index b1663b2a11d..2a29c5df791 100644
--- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
@@ -1,6 +1,7 @@
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
+import { useAppSelector } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
import { workflowLoaded } from 'features/nodes/store/actions';
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged } from 'features/nodes/store/nodesSlice';
@@ -11,23 +12,66 @@ import type {
} from 'features/nodes/store/types';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
-import type { WorkflowCategory, WorkflowV3 } from 'features/nodes/types/workflow';
+import type {
+ ContainerElement,
+ FormElement,
+ HeadingElement,
+ NodeFieldElement,
+ TextElement,
+ WorkflowCategory,
+ WorkflowV3,
+} from 'features/nodes/types/workflow';
+import {
+ buildContainer,
+ isContainerElement,
+ isHeadingElement,
+ isNodeFieldElement,
+ isTextElement,
+} from 'features/nodes/types/workflow';
import { isEqual, omit, uniqBy } from 'lodash-es';
+import { useMemo } from 'react';
import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
import { selectNodesSlice } from './selectors';
-const blankWorkflow: Omit = {
- name: '',
- author: '',
- description: '',
- version: '',
- contact: '',
- tags: '',
- notes: '',
- exposedFields: [],
- meta: { version: '3.0.0', category: 'user' },
- id: undefined,
+type FormElementDataChangedAction = PayloadAction<{
+ id: string;
+ changes: Partial;
+}>;
+
+const formElementDataChangedReducer = (
+ state: WorkflowState,
+ action: FormElementDataChangedAction,
+ guard: (element: FormElement) => element is T
+) => {
+ const { id, changes } = action.payload;
+ const element = state.form?.elements[id];
+ if (!element || !guard(element)) {
+ return;
+ }
+ element.data = { ...element.data, ...changes } as T['data'];
+};
+
+const getBlankWorkflow = (): Omit => {
+ const rootElement = buildContainer('column', []);
+ return {
+ name: '',
+ author: '',
+ description: '',
+ version: '',
+ contact: '',
+ tags: '',
+ notes: '',
+ exposedFields: [],
+ meta: { version: '3.0.0', category: 'user' },
+ id: undefined,
+ form: {
+ elements: {
+ [rootElement.id]: rootElement,
+ },
+ rootElementId: rootElement.id,
+ },
+ };
};
const initialWorkflowState: WorkflowState = {
@@ -39,7 +83,8 @@ const initialWorkflowState: WorkflowState = {
orderBy: undefined, // initial value is decided in component
orderDirection: 'DESC',
categorySections: {},
- ...blankWorkflow,
+ formMode: 'view',
+ ...getBlankWorkflow(),
};
export const workflowSlice = createSlice({
@@ -123,6 +168,60 @@ export const workflowSlice = createSlice({
workflowSaved: (state) => {
state.isTouched = false;
},
+ formLoaded: (state, action: PayloadAction<{ elements: Record; rootElementId: string }>) => {
+ state.form = action.payload;
+ },
+ formCreated: (state, action: PayloadAction<{ container: ContainerElement }>) => {
+ const { container } = action.payload;
+ state.form = {
+ elements: {
+ [container.id]: container,
+ },
+ rootElementId: container.id,
+ };
+ },
+ formElementAdded: (state, action: PayloadAction<{ element: FormElement; containerId: string; index?: number }>) => {
+ if (!state.form) {
+ // Cannot add an element if the form has not been created
+ return;
+ }
+ const { element, containerId, index } = action.payload;
+ addElement({ formState: state.form, element, containerId, index });
+ },
+ formElementRemoved: (state, action: PayloadAction<{ id: string }>) => {
+ if (!state.form) {
+ // Cannot remove an element if the form has not been created
+ return;
+ }
+ const { id } = action.payload;
+ recursivelyRemoveElement({ id, formState: state.form });
+ },
+ formElementMoved: (state, action: PayloadAction<{ id: string; containerId: string; index?: number }>) => {
+ if (!state.form) {
+ // Cannot remove an element if the form has not been created
+ return;
+ }
+ const { id, containerId, index } = action.payload;
+ moveElement({ formState: state.form, id, containerId, index });
+ },
+ formElementHeadingDataChanged: (state, action: FormElementDataChangedAction) => {
+ formElementDataChangedReducer(state, action, isHeadingElement);
+ },
+ formElementTextDataChanged: (state, action: FormElementDataChangedAction) => {
+ formElementDataChangedReducer(state, action, isTextElement);
+ },
+ formElementNodeFieldDataChanged: (state, action: FormElementDataChangedAction) => {
+ formElementDataChangedReducer(state, action, isNodeFieldElement);
+ },
+ formElementContainerDataChanged: (state, action: FormElementDataChangedAction) => {
+ formElementDataChangedReducer(state, action, isContainerElement);
+ },
+ formReset: (state) => {
+ state.form = undefined;
+ },
+ formModeToggled: (state) => {
+ state.formMode = state.formMode === 'edit' ? 'view' : 'edit';
+ },
},
extraReducers: (builder) => {
builder.addCase(workflowLoaded, (state, action) => {
@@ -236,6 +335,17 @@ export const {
workflowOrderByChanged,
workflowOrderDirectionChanged,
categorySectionsChanged,
+ formLoaded,
+ formCreated,
+ formElementAdded,
+ formElementRemoved,
+ formElementMoved,
+ formElementHeadingDataChanged,
+ formElementTextDataChanged,
+ formElementNodeFieldDataChanged,
+ formElementContainerDataChanged,
+ formReset,
+ formModeToggled,
} = workflowSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -260,6 +370,7 @@ const createWorkflowSelector = (selector: Selector) =>
export const selectWorkflowName = createWorkflowSelector((workflow) => workflow.name);
export const selectWorkflowId = createWorkflowSelector((workflow) => workflow.id);
export const selectWorkflowMode = createWorkflowSelector((workflow) => workflow.mode);
+export const selectWorkflowFormMode = createWorkflowSelector((workflow) => workflow.formMode);
export const selectWorkflowIsTouched = createWorkflowSelector((workflow) => workflow.isTouched);
export const selectWorkflowSearchTerm = createWorkflowSelector((workflow) => workflow.searchTerm);
export const selectWorkflowOrderBy = createWorkflowSelector((workflow) => workflow.orderBy);
@@ -272,3 +383,110 @@ export const selectCleanEditor = createSelector([selectNodesSlice, selectWorkflo
const savedWorkflow = !!workflow.id;
return noNodes && !isTouched && !savedWorkflow;
});
+
+export const selectRootElementId = createWorkflowSelector((workflow) => workflow.form?.rootElementId);
+const buildSelectElement = (id: string) => createWorkflowSelector((workflow) => workflow.form?.elements[id]);
+export const useElement = (id: string): FormElement | undefined => {
+ const selector = useMemo(() => buildSelectElement(id), [id]);
+ const element = useAppSelector(selector);
+ return element;
+};
+
+const recursivelyRemoveElement = (args: {
+ id: string;
+ containerId?: string;
+ formState: NonNullable;
+}): boolean => {
+ const { id, containerId, formState } = args;
+ const { elements, rootElementId } = formState;
+ const container = elements[containerId || rootElementId];
+
+ if (!container || !isContainerElement(container)) {
+ return false;
+ }
+
+ const index = container.data.children.indexOf(id);
+ if (index !== -1) {
+ container.data.children.splice(index, 1);
+ delete elements[id];
+ return true;
+ }
+
+ for (const childId of container.data.children) {
+ if (recursivelyRemoveElement({ id, containerId: childId, formState })) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+const addElement = (args: {
+ formState: NonNullable;
+ element: FormElement;
+ containerId: string;
+ index?: number;
+}) => {
+ const { formState, element, containerId, index } = args;
+ const { elements } = formState;
+ const container = elements[containerId];
+ if (!container || !isContainerElement(container)) {
+ return;
+ }
+
+ element.parentId = containerId;
+ elements[element.id] = element;
+
+ if (index === undefined) {
+ container.data.children.push(element.id);
+ } else {
+ container.data.children.splice(index, 0, element.id);
+ }
+};
+
+const moveElement = (args: {
+ formState: NonNullable;
+ id: string;
+ containerId: string;
+ index?: number;
+}) => {
+ const { formState, id, containerId, index } = args;
+ const { elements } = formState;
+
+ const element = elements[id];
+ if (!element) {
+ return;
+ }
+ const newContainer = elements[containerId];
+ if (!newContainer || !isContainerElement(newContainer)) {
+ return;
+ }
+
+ if (newContainer.data.children.includes(id)) {
+ // Moving within the same container - remove the element from its current position and insert it at the new position
+ const currentIndex = newContainer.data.children.indexOf(id);
+ if (currentIndex === -1) {
+ return;
+ }
+ newContainer.data.children.splice(currentIndex, 1);
+ if (index === undefined) {
+ newContainer.data.children.push(id);
+ } else {
+ newContainer.data.children.splice(index, 0, id);
+ }
+ } else if (element.parentId !== undefined) {
+ const oldContainer = elements[element.parentId];
+ if (!oldContainer || !isContainerElement(oldContainer)) {
+ return;
+ }
+ oldContainer.data.children = oldContainer.data.children.filter((childId) => childId !== id);
+ if (index === undefined) {
+ newContainer.data.children.push(id);
+ } else {
+ newContainer.data.children.splice(index, 0, id);
+ }
+ element.parentId = containerId;
+ } else {
+ // Should never happen
+ }
+};
diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts
index cff7f8ac6bc..9b7e7e6bc6c 100644
--- a/invokeai/frontend/web/src/features/nodes/types/constants.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts
@@ -1,4 +1,4 @@
-import type { Node } from 'reactflow';
+import type { AnyNode } from 'features/nodes/types/invocation';
/**
* How long to wait before showing a tooltip when hovering a field handle.
@@ -19,7 +19,7 @@ export const DRAG_HANDLE_CLASSNAME = 'node-drag-handle';
/**
* reactflow-specifc properties shared between all node types.
*/
-export const SHARED_NODE_PROPERTIES: Partial = {
+export const SHARED_NODE_PROPERTIES: Partial = {
dragHandle: `.${DRAG_HANDLE_CLASSNAME}`,
};
diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts
index 6c516ca0867..ccfa41a5f71 100644
--- a/invokeai/frontend/web/src/features/nodes/types/field.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/field.ts
@@ -1,4 +1,3 @@
-import { buildTypeGuard } from 'features/parameters/types/parameterSchemas';
import { isNil, trim } from 'lodash-es';
import MersenneTwister from 'mtwist';
import { assert } from 'tsafe';
@@ -33,11 +32,12 @@ import { zBoardField, zColorField, zImageField, zModelIdentifierField, zSchedule
/** */
// #region Base schemas & misc
-const zFieldInput = z.enum(['connection', 'direct', 'any', 'batch']);
+const zFieldInput = z.enum(['connection', 'direct', 'any']);
const zFieldUIComponent = z.enum(['none', 'textarea', 'slider']);
const zFieldInputInstanceBase = z.object({
name: z.string().trim().min(1),
label: z.string().nullish(),
+ notes: z.string().nullish(),
});
const zFieldTemplateBase = z.object({
name: z.string().min(1),
@@ -88,7 +88,10 @@ const zIntegerCollectionFieldType = zFieldTypeBase.extend({
cardinality: z.literal(COLLECTION),
originalType: zStatelessFieldType.optional(),
});
-export const isIntegerCollectionFieldType = buildTypeGuard(zIntegerCollectionFieldType);
+export const isIntegerCollectionFieldType = (
+ fieldType: FieldType
+): fieldType is z.infer =>
+ fieldType.name === 'IntegerField' && fieldType.cardinality === COLLECTION;
const zFloatFieldType = zFieldTypeBase.extend({
name: z.literal('FloatField'),
@@ -99,7 +102,10 @@ const zFloatCollectionFieldType = zFieldTypeBase.extend({
cardinality: z.literal(COLLECTION),
originalType: zStatelessFieldType.optional(),
});
-export const isFloatCollectionFieldType = buildTypeGuard(zFloatCollectionFieldType);
+export const isFloatCollectionFieldType = (
+ fieldType: FieldType
+): fieldType is z.infer =>
+ fieldType.name === 'FloatField' && fieldType.cardinality === COLLECTION;
const zStringFieldType = zFieldTypeBase.extend({
name: z.literal('StringField'),
@@ -110,7 +116,10 @@ const zStringCollectionFieldType = zFieldTypeBase.extend({
cardinality: z.literal(COLLECTION),
originalType: zStatelessFieldType.optional(),
});
-export const isStringCollectionFieldType = buildTypeGuard(zStringCollectionFieldType);
+export const isStringCollectionFieldType = (
+ fieldType: FieldType
+): fieldType is z.infer =>
+ fieldType.name === 'StringField' && fieldType.cardinality === COLLECTION;
const zBooleanFieldType = zFieldTypeBase.extend({
name: z.literal('BooleanField'),
@@ -129,7 +138,11 @@ const zImageCollectionFieldType = zFieldTypeBase.extend({
cardinality: z.literal(COLLECTION),
originalType: zStatelessFieldType.optional(),
});
-export const isImageCollectionFieldType = buildTypeGuard(zImageCollectionFieldType);
+export const isImageCollectionFieldType = (
+ fieldType: FieldType
+): fieldType is z.infer =>
+ fieldType.name === 'ImageField' && fieldType.cardinality === COLLECTION;
+
const zBoardFieldType = zFieldTypeBase.extend({
name: z.literal('BoardField'),
originalType: zStatelessFieldType.optional(),
@@ -271,6 +284,22 @@ export const isSingleOrCollection = (fieldType: FieldType): boolean =>
fieldType.cardinality === zCardinality.enum.SINGLE_OR_COLLECTION;
// #endregion
+const buildInstanceTypeGuard = (schema: T) => {
+ return (val: unknown): val is z.infer => schema.safeParse(val).success;
+};
+
+const buildTemplateTypeGuard =
+ (name: string, cardinality?: 'SINGLE' | 'COLLECTION' | 'SINGLE_OR_COLLECTION') =>
+ (template: FieldInputTemplate): template is T => {
+ if (template.type.name !== name) {
+ return false;
+ }
+ if (cardinality) {
+ return template.type.cardinality === cardinality;
+ }
+ return true;
+ };
+
// #region IntegerField
export const zIntegerFieldValue = z.number().int();
@@ -293,8 +322,8 @@ const zIntegerFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type IntegerFieldValue = z.infer;
export type IntegerFieldInputInstance = z.infer;
export type IntegerFieldInputTemplate = z.infer;
-export const isIntegerFieldInputInstance = buildTypeGuard(zIntegerFieldInputInstance);
-export const isIntegerFieldInputTemplate = buildTypeGuard(zIntegerFieldInputTemplate);
+export const isIntegerFieldInputInstance = buildInstanceTypeGuard(zIntegerFieldInputInstance);
+export const isIntegerFieldInputTemplate = buildTemplateTypeGuard('IntegerField', 'SINGLE');
// #endregion
// #region IntegerField Collection
@@ -331,12 +360,14 @@ const zIntegerFieldCollectionOutputTemplate = zFieldOutputTemplateBase.extend({
export type IntegerFieldCollectionValue = z.infer;
export type IntegerFieldCollectionInputInstance = z.infer;
export type IntegerFieldCollectionInputTemplate = z.infer;
-export const isIntegerFieldCollectionInputInstance = buildTypeGuard(zIntegerFieldCollectionInputInstance);
-export const isIntegerFieldCollectionInputTemplate = buildTypeGuard(zIntegerFieldCollectionInputTemplate);
+export const isIntegerFieldCollectionInputInstance = buildInstanceTypeGuard(zIntegerFieldCollectionInputInstance);
+export const isIntegerFieldCollectionInputTemplate = buildTemplateTypeGuard(
+ 'IntegerField',
+ 'COLLECTION'
+);
// #endregion
// #region FloatField
-
export const zFloatFieldValue = z.number();
const zFloatFieldInputInstance = zFieldInputInstanceBase.extend({
value: zFloatFieldValue,
@@ -357,8 +388,8 @@ const zFloatFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type FloatFieldValue = z.infer;
export type FloatFieldInputInstance = z.infer;
export type FloatFieldInputTemplate = z.infer;
-export const isFloatFieldInputInstance = buildTypeGuard(zFloatFieldInputInstance);
-export const isFloatFieldInputTemplate = buildTypeGuard(zFloatFieldInputTemplate);
+export const isFloatFieldInputInstance = buildInstanceTypeGuard(zFloatFieldInputInstance);
+export const isFloatFieldInputTemplate = buildTemplateTypeGuard('FloatField', 'SINGLE');
// #endregion
// #region FloatField Collection
@@ -388,19 +419,20 @@ const zFloatFieldCollectionInputTemplate = zFieldInputTemplateBase
},
{ message: 'maxItems must be greater than or equal to minItems' }
);
-
const zFloatFieldCollectionOutputTemplate = zFieldOutputTemplateBase.extend({
type: zFloatCollectionFieldType,
});
export type FloatFieldCollectionValue = z.infer;
export type FloatFieldCollectionInputInstance = z.infer;
export type FloatFieldCollectionInputTemplate = z.infer;
-export const isFloatFieldCollectionInputInstance = buildTypeGuard(zFloatFieldCollectionInputInstance);
-export const isFloatFieldCollectionInputTemplate = buildTypeGuard(zFloatFieldCollectionInputTemplate);
+export const isFloatFieldCollectionInputInstance = buildInstanceTypeGuard(zFloatFieldCollectionInputInstance);
+export const isFloatFieldCollectionInputTemplate = buildTemplateTypeGuard(
+ 'FloatField',
+ 'COLLECTION'
+);
// #endregion
// #region StringField
-
export const zStringFieldValue = z.string();
const zStringFieldInputInstance = zFieldInputInstanceBase.extend({
value: zStringFieldValue,
@@ -425,6 +457,12 @@ const zStringFieldInputTemplate = zFieldInputTemplateBase
const zStringFieldOutputTemplate = zFieldOutputTemplateBase.extend({
type: zStringFieldType,
});
+export type StringFieldValue = z.infer;
+export type StringFieldInputInstance = z.infer;
+export type StringFieldInputTemplate = z.infer;
+export const isStringFieldInputInstance = buildInstanceTypeGuard(zStringFieldInputInstance);
+export const isStringFieldInputTemplate = buildTemplateTypeGuard('StringField', 'SINGLE');
+// #endregion
// #region StringField Collection
export const zStringFieldCollectionValue = z.array(zStringFieldValue).optional();
@@ -466,19 +504,14 @@ const zStringFieldCollectionOutputTemplate = zFieldOutputTemplateBase.extend({
export type StringFieldCollectionValue = z.infer;
export type StringFieldCollectionInputInstance = z.infer;
export type StringFieldCollectionInputTemplate = z.infer;
-export const isStringFieldCollectionInputInstance = buildTypeGuard(zStringFieldCollectionInputInstance);
-export const isStringFieldCollectionInputTemplate = buildTypeGuard(zStringFieldCollectionInputTemplate);
-// #endregion
-
-export type StringFieldValue = z.infer;
-export type StringFieldInputInstance = z.infer;
-export type StringFieldInputTemplate = z.infer;
-export const isStringFieldInputInstance = buildTypeGuard(zStringFieldInputInstance);
-export const isStringFieldInputTemplate = buildTypeGuard(zStringFieldInputTemplate);
+export const isStringFieldCollectionInputInstance = buildInstanceTypeGuard(zStringFieldCollectionInputInstance);
+export const isStringFieldCollectionInputTemplate = buildTemplateTypeGuard(
+ 'StringField',
+ 'COLLECTION'
+);
// #endregion
// #region BooleanField
-
export const zBooleanFieldValue = z.boolean();
const zBooleanFieldInputInstance = zFieldInputInstanceBase.extend({
value: zBooleanFieldValue,
@@ -494,12 +527,11 @@ const zBooleanFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type BooleanFieldValue = z.infer;
export type BooleanFieldInputInstance = z.infer;
export type BooleanFieldInputTemplate = z.infer;
-export const isBooleanFieldInputInstance = buildTypeGuard(zBooleanFieldInputInstance);
-export const isBooleanFieldInputTemplate = buildTypeGuard(zBooleanFieldInputTemplate);
+export const isBooleanFieldInputInstance = buildInstanceTypeGuard(zBooleanFieldInputInstance);
+export const isBooleanFieldInputTemplate = buildTemplateTypeGuard('BooleanField');
// #endregion
// #region EnumField
-
export const zEnumFieldValue = z.string();
const zEnumFieldInputInstance = zFieldInputInstanceBase.extend({
value: zEnumFieldValue,
@@ -517,8 +549,8 @@ const zEnumFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type EnumFieldValue = z.infer;
export type EnumFieldInputInstance = z.infer;
export type EnumFieldInputTemplate = z.infer;
-export const isEnumFieldInputInstance = buildTypeGuard(zEnumFieldInputInstance);
-export const isEnumFieldInputTemplate = buildTypeGuard(zEnumFieldInputTemplate);
+export const isEnumFieldInputInstance = buildInstanceTypeGuard(zEnumFieldInputInstance);
+export const isEnumFieldInputTemplate = buildTemplateTypeGuard('EnumField');
// #endregion
// #region ImageField
@@ -537,8 +569,8 @@ const zImageFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type ImageFieldValue = z.infer;
export type ImageFieldInputInstance = z.infer;
export type ImageFieldInputTemplate = z.infer;
-export const isImageFieldInputInstance = buildTypeGuard(zImageFieldInputInstance);
-export const isImageFieldInputTemplate = buildTypeGuard(zImageFieldInputTemplate);
+export const isImageFieldInputInstance = buildInstanceTypeGuard(zImageFieldInputInstance);
+export const isImageFieldInputTemplate = buildTemplateTypeGuard('ImageField', 'SINGLE');
// #endregion
// #region ImageField Collection
@@ -570,12 +602,14 @@ const zImageFieldCollectionOutputTemplate = zFieldOutputTemplateBase.extend({
export type ImageFieldCollectionValue = z.infer;
export type ImageFieldCollectionInputInstance = z.infer;
export type ImageFieldCollectionInputTemplate = z.infer;
-export const isImageFieldCollectionInputInstance = buildTypeGuard(zImageFieldCollectionInputInstance);
-export const isImageFieldCollectionInputTemplate = buildTypeGuard(zImageFieldCollectionInputTemplate);
+export const isImageFieldCollectionInputInstance = buildInstanceTypeGuard(zImageFieldCollectionInputInstance);
+export const isImageFieldCollectionInputTemplate = buildTemplateTypeGuard(
+ 'ImageField',
+ 'COLLECTION'
+);
// #endregion
// #region BoardField
-
export const zBoardFieldValue = zBoardField.optional();
const zBoardFieldInputInstance = zFieldInputInstanceBase.extend({
value: zBoardFieldValue,
@@ -591,12 +625,11 @@ const zBoardFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type BoardFieldValue = z.infer;
export type BoardFieldInputInstance = z.infer;
export type BoardFieldInputTemplate = z.infer;
-export const isBoardFieldInputInstance = buildTypeGuard(zBoardFieldInputInstance);
-export const isBoardFieldInputTemplate = buildTypeGuard(zBoardFieldInputTemplate);
+export const isBoardFieldInputInstance = buildInstanceTypeGuard(zBoardFieldInputInstance);
+export const isBoardFieldInputTemplate = buildTemplateTypeGuard('BoardField');
// #endregion
// #region ColorField
-
export const zColorFieldValue = zColorField.optional();
const zColorFieldInputInstance = zFieldInputInstanceBase.extend({
value: zColorFieldValue,
@@ -612,12 +645,11 @@ const zColorFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type ColorFieldValue = z.infer;
export type ColorFieldInputInstance = z.infer;
export type ColorFieldInputTemplate = z.infer;
-export const isColorFieldInputInstance = buildTypeGuard(zColorFieldInputInstance);
-export const isColorFieldInputTemplate = buildTypeGuard(zColorFieldInputTemplate);
+export const isColorFieldInputInstance = buildInstanceTypeGuard(zColorFieldInputInstance);
+export const isColorFieldInputTemplate = buildTemplateTypeGuard('ColorField');
// #endregion
// #region MainModelField
-
export const zMainModelFieldValue = zModelIdentifierField.optional();
const zMainModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zMainModelFieldValue,
@@ -633,8 +665,8 @@ const zMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type MainModelFieldValue = z.infer;
export type MainModelFieldInputInstance = z.infer;
export type MainModelFieldInputTemplate = z.infer;
-export const isMainModelFieldInputInstance = buildTypeGuard(zMainModelFieldInputInstance);
-export const isMainModelFieldInputTemplate = buildTypeGuard(zMainModelFieldInputTemplate);
+export const isMainModelFieldInputInstance = buildInstanceTypeGuard(zMainModelFieldInputInstance);
+export const isMainModelFieldInputTemplate = buildTemplateTypeGuard('MainModelField');
// #endregion
// #region ModelIdentifierField
@@ -653,12 +685,12 @@ const zModelIdentifierFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type ModelIdentifierFieldValue = z.infer;
export type ModelIdentifierFieldInputInstance = z.infer;
export type ModelIdentifierFieldInputTemplate = z.infer;
-export const isModelIdentifierFieldInputInstance = buildTypeGuard(zModelIdentifierFieldInputInstance);
-export const isModelIdentifierFieldInputTemplate = buildTypeGuard(zModelIdentifierFieldInputTemplate);
+export const isModelIdentifierFieldInputInstance = buildInstanceTypeGuard(zModelIdentifierFieldInputInstance);
+export const isModelIdentifierFieldInputTemplate =
+ buildTemplateTypeGuard('ModelIdentifierField');
// #endregion
// #region SDXLMainModelField
-
const zSDXLMainModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL models only.
const zSDXLMainModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zSDXLMainModelFieldValue,
@@ -673,12 +705,12 @@ const zSDXLMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
});
export type SDXLMainModelFieldInputInstance = z.infer;
export type SDXLMainModelFieldInputTemplate = z.infer;
-export const isSDXLMainModelFieldInputInstance = buildTypeGuard(zSDXLMainModelFieldInputInstance);
-export const isSDXLMainModelFieldInputTemplate = buildTypeGuard(zSDXLMainModelFieldInputTemplate);
+export const isSDXLMainModelFieldInputInstance = buildInstanceTypeGuard(zSDXLMainModelFieldInputInstance);
+export const isSDXLMainModelFieldInputTemplate =
+ buildTemplateTypeGuard('SDXLMainModelField');
// #endregion
// #region SD3MainModelField
-
const zSD3MainModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL models only.
const zSD3MainModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zSD3MainModelFieldValue,
@@ -693,13 +725,12 @@ const zSD3MainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
});
export type SD3MainModelFieldInputInstance = z.infer;
export type SD3MainModelFieldInputTemplate = z.infer;
-export const isSD3MainModelFieldInputInstance = buildTypeGuard(zSD3MainModelFieldInputInstance);
-export const isSD3MainModelFieldInputTemplate = buildTypeGuard(zSD3MainModelFieldInputTemplate);
-
+export const isSD3MainModelFieldInputInstance = buildInstanceTypeGuard(zSD3MainModelFieldInputInstance);
+export const isSD3MainModelFieldInputTemplate =
+ buildTemplateTypeGuard('SD3MainModelField');
// #endregion
// #region FluxMainModelField
-
const zFluxMainModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL models only.
const zFluxMainModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zFluxMainModelFieldValue,
@@ -714,13 +745,12 @@ const zFluxMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
});
export type FluxMainModelFieldInputInstance = z.infer;
export type FluxMainModelFieldInputTemplate = z.infer;
-export const isFluxMainModelFieldInputInstance = buildTypeGuard(zFluxMainModelFieldInputInstance);
-export const isFluxMainModelFieldInputTemplate = buildTypeGuard(zFluxMainModelFieldInputTemplate);
-
+export const isFluxMainModelFieldInputInstance = buildInstanceTypeGuard(zFluxMainModelFieldInputInstance);
+export const isFluxMainModelFieldInputTemplate =
+ buildTemplateTypeGuard('FluxMainModelField');
// #endregion
// #region SDXLRefinerModelField
-
/** @alias */ // tells knip to ignore this duplicate export
export const zSDXLRefinerModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL Refiner models only.
const zSDXLRefinerModelFieldInputInstance = zFieldInputInstanceBase.extend({
@@ -737,8 +767,9 @@ const zSDXLRefinerModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type SDXLRefinerModelFieldValue = z.infer;
export type SDXLRefinerModelFieldInputInstance = z.infer;
export type SDXLRefinerModelFieldInputTemplate = z.infer;
-export const isSDXLRefinerModelFieldInputInstance = buildTypeGuard(zSDXLRefinerModelFieldInputInstance);
-export const isSDXLRefinerModelFieldInputTemplate = buildTypeGuard(zSDXLRefinerModelFieldInputTemplate);
+export const isSDXLRefinerModelFieldInputInstance = buildInstanceTypeGuard(zSDXLRefinerModelFieldInputInstance);
+export const isSDXLRefinerModelFieldInputTemplate =
+ buildTemplateTypeGuard('SDXLRefinerModelField');
// #endregion
// #region VAEModelField
@@ -758,12 +789,11 @@ const zVAEModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type VAEModelFieldValue = z.infer;
export type VAEModelFieldInputInstance = z.infer;
export type VAEModelFieldInputTemplate = z.infer;
-export const isVAEModelFieldInputInstance = buildTypeGuard(zVAEModelFieldInputInstance);
-export const isVAEModelFieldInputTemplate = buildTypeGuard(zVAEModelFieldInputTemplate);
+export const isVAEModelFieldInputInstance = buildInstanceTypeGuard(zVAEModelFieldInputInstance);
+export const isVAEModelFieldInputTemplate = buildTemplateTypeGuard('VAEModelField');
// #endregion
// #region LoRAModelField
-
export const zLoRAModelFieldValue = zModelIdentifierField.optional();
const zLoRAModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zLoRAModelFieldValue,
@@ -779,12 +809,11 @@ const zLoRAModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type LoRAModelFieldValue = z.infer;
export type LoRAModelFieldInputInstance = z.infer;
export type LoRAModelFieldInputTemplate = z.infer;
-export const isLoRAModelFieldInputInstance = buildTypeGuard(zLoRAModelFieldInputInstance);
-export const isLoRAModelFieldInputTemplate = buildTypeGuard(zLoRAModelFieldInputTemplate);
+export const isLoRAModelFieldInputInstance = buildInstanceTypeGuard(zLoRAModelFieldInputInstance);
+export const isLoRAModelFieldInputTemplate = buildTemplateTypeGuard('LoRAModelField');
// #endregion
// #region ControlNetModelField
-
export const zControlNetModelFieldValue = zModelIdentifierField.optional();
const zControlNetModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zControlNetModelFieldValue,
@@ -800,12 +829,12 @@ const zControlNetModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type ControlNetModelFieldValue = z.infer;
export type ControlNetModelFieldInputInstance = z.infer;
export type ControlNetModelFieldInputTemplate = z.infer;
-export const isControlNetModelFieldInputInstance = buildTypeGuard(zControlNetModelFieldInputInstance);
-export const isControlNetModelFieldInputTemplate = buildTypeGuard(zControlNetModelFieldInputTemplate);
+export const isControlNetModelFieldInputInstance = buildInstanceTypeGuard(zControlNetModelFieldInputInstance);
+export const isControlNetModelFieldInputTemplate =
+ buildTemplateTypeGuard('ControlNetModelField');
// #endregion
// #region IPAdapterModelField
-
export const zIPAdapterModelFieldValue = zModelIdentifierField.optional();
const zIPAdapterModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zIPAdapterModelFieldValue,
@@ -821,12 +850,12 @@ const zIPAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type IPAdapterModelFieldValue = z.infer;
export type IPAdapterModelFieldInputInstance = z.infer;
export type IPAdapterModelFieldInputTemplate = z.infer;
-export const isIPAdapterModelFieldInputInstance = buildTypeGuard(zIPAdapterModelFieldInputInstance);
-export const isIPAdapterModelFieldInputTemplate = buildTypeGuard(zIPAdapterModelFieldInputTemplate);
+export const isIPAdapterModelFieldInputInstance = buildInstanceTypeGuard(zIPAdapterModelFieldInputInstance);
+export const isIPAdapterModelFieldInputTemplate =
+ buildTemplateTypeGuard('IPAdapterModelField');
// #endregion
// #region T2IAdapterField
-
export const zT2IAdapterModelFieldValue = zModelIdentifierField.optional();
const zT2IAdapterModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zT2IAdapterModelFieldValue,
@@ -842,12 +871,12 @@ const zT2IAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type T2IAdapterModelFieldValue = z.infer;
export type T2IAdapterModelFieldInputInstance = z.infer;
export type T2IAdapterModelFieldInputTemplate = z.infer;
-export const isT2IAdapterModelFieldInputInstance = buildTypeGuard(zT2IAdapterModelFieldInputInstance);
-export const isT2IAdapterModelFieldInputTemplate = buildTypeGuard(zT2IAdapterModelFieldInputTemplate);
+export const isT2IAdapterModelFieldInputInstance = buildInstanceTypeGuard(zT2IAdapterModelFieldInputInstance);
+export const isT2IAdapterModelFieldInputTemplate =
+ buildTemplateTypeGuard('T2IAdapterModelField');
// #endregion
// #region SpandrelModelToModelField
-
export const zSpandrelImageToImageModelFieldValue = zModelIdentifierField.optional();
const zSpandrelImageToImageModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zSpandrelImageToImageModelFieldValue,
@@ -863,12 +892,11 @@ const zSpandrelImageToImageModelFieldOutputTemplate = zFieldOutputTemplateBase.e
export type SpandrelImageToImageModelFieldValue = z.infer;
export type SpandrelImageToImageModelFieldInputInstance = z.infer;
export type SpandrelImageToImageModelFieldInputTemplate = z.infer;
-export const isSpandrelImageToImageModelFieldInputInstance = buildTypeGuard(
+export const isSpandrelImageToImageModelFieldInputInstance = buildInstanceTypeGuard(
zSpandrelImageToImageModelFieldInputInstance
);
-export const isSpandrelImageToImageModelFieldInputTemplate = buildTypeGuard(
- zSpandrelImageToImageModelFieldInputTemplate
-);
+export const isSpandrelImageToImageModelFieldInputTemplate =
+ buildTemplateTypeGuard('SpandrelImageToImageModelField');
// #endregion
// #region T5EncoderModelField
@@ -882,18 +910,15 @@ const zT5EncoderModelFieldInputTemplate = zFieldInputTemplateBase.extend({
originalType: zFieldType.optional(),
default: zT5EncoderModelFieldValue,
});
-
export type T5EncoderModelFieldValue = z.infer;
-
export type T5EncoderModelFieldInputInstance = z.infer;
export type T5EncoderModelFieldInputTemplate = z.infer;
-export const isT5EncoderModelFieldInputInstance = buildTypeGuard(zT5EncoderModelFieldInputInstance);
-export const isT5EncoderModelFieldInputTemplate = buildTypeGuard(zT5EncoderModelFieldInputTemplate);
-
+export const isT5EncoderModelFieldInputInstance = buildInstanceTypeGuard(zT5EncoderModelFieldInputInstance);
+export const isT5EncoderModelFieldInputTemplate =
+ buildTemplateTypeGuard('T5EncoderModelField');
// #endregion
// #region FluxVAEModelField
-
export const zFluxVAEModelFieldValue = zModelIdentifierField.optional();
const zFluxVAEModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zFluxVAEModelFieldValue,
@@ -903,18 +928,15 @@ const zFluxVAEModelFieldInputTemplate = zFieldInputTemplateBase.extend({
originalType: zFieldType.optional(),
default: zFluxVAEModelFieldValue,
});
-
export type FluxVAEModelFieldValue = z.infer;
-
export type FluxVAEModelFieldInputInstance = z.infer;
export type FluxVAEModelFieldInputTemplate = z.infer;
-export const isFluxVAEModelFieldInputInstance = buildTypeGuard(zFluxVAEModelFieldInputInstance);
-export const isFluxVAEModelFieldInputTemplate = buildTypeGuard(zFluxVAEModelFieldInputTemplate);
-
+export const isFluxVAEModelFieldInputInstance = buildInstanceTypeGuard(zFluxVAEModelFieldInputInstance);
+export const isFluxVAEModelFieldInputTemplate =
+ buildTemplateTypeGuard('FluxVAEModelField');
// #endregion
// #region CLIPEmbedModelField
-
export const zCLIPEmbedModelFieldValue = zModelIdentifierField.optional();
const zCLIPEmbedModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zCLIPEmbedModelFieldValue,
@@ -924,18 +946,15 @@ const zCLIPEmbedModelFieldInputTemplate = zFieldInputTemplateBase.extend({
originalType: zFieldType.optional(),
default: zCLIPEmbedModelFieldValue,
});
-
export type CLIPEmbedModelFieldValue = z.infer;
-
export type CLIPEmbedModelFieldInputInstance = z.infer;
export type CLIPEmbedModelFieldInputTemplate = z.infer;
-export const isCLIPEmbedModelFieldInputInstance = buildTypeGuard(zCLIPEmbedModelFieldInputInstance);
-export const isCLIPEmbedModelFieldInputTemplate = buildTypeGuard(zCLIPEmbedModelFieldInputTemplate);
-
+export const isCLIPEmbedModelFieldInputInstance = buildInstanceTypeGuard(zCLIPEmbedModelFieldInputInstance);
+export const isCLIPEmbedModelFieldInputTemplate =
+ buildTemplateTypeGuard('CLIPEmbedModelField');
// #endregion
// #region CLIPLEmbedModelField
-
export const zCLIPLEmbedModelFieldValue = zModelIdentifierField.optional();
const zCLIPLEmbedModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zCLIPLEmbedModelFieldValue,
@@ -945,18 +964,15 @@ const zCLIPLEmbedModelFieldInputTemplate = zFieldInputTemplateBase.extend({
originalType: zFieldType.optional(),
default: zCLIPLEmbedModelFieldValue,
});
-
export type CLIPLEmbedModelFieldValue = z.infer;
-
export type CLIPLEmbedModelFieldInputInstance = z.infer;
export type CLIPLEmbedModelFieldInputTemplate = z.infer;
-export const isCLIPLEmbedModelFieldInputInstance = buildTypeGuard(zCLIPLEmbedModelFieldInputInstance);
-export const isCLIPLEmbedModelFieldInputTemplate = buildTypeGuard(zCLIPLEmbedModelFieldInputTemplate);
-
+export const isCLIPLEmbedModelFieldInputInstance = buildInstanceTypeGuard(zCLIPLEmbedModelFieldInputInstance);
+export const isCLIPLEmbedModelFieldInputTemplate =
+ buildTemplateTypeGuard('CLIPLEmbedModelField');
// #endregion
// #region CLIPGEmbedModelField
-
export const zCLIPGEmbedModelFieldValue = zModelIdentifierField.optional();
const zCLIPGEmbedModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zCLIPGEmbedModelFieldValue,
@@ -966,18 +982,15 @@ const zCLIPGEmbedModelFieldInputTemplate = zFieldInputTemplateBase.extend({
originalType: zFieldType.optional(),
default: zCLIPGEmbedModelFieldValue,
});
-
export type CLIPGEmbedModelFieldValue = z.infer;
-
export type CLIPGEmbedModelFieldInputInstance = z.infer;
export type CLIPGEmbedModelFieldInputTemplate = z.infer;
-export const isCLIPGEmbedModelFieldInputInstance = buildTypeGuard(zCLIPGEmbedModelFieldInputInstance);
-export const isCLIPGEmbedModelFieldInputTemplate = buildTypeGuard(zCLIPGEmbedModelFieldInputTemplate);
-
+export const isCLIPGEmbedModelFieldInputInstance = buildInstanceTypeGuard(zCLIPGEmbedModelFieldInputInstance);
+export const isCLIPGEmbedModelFieldInputTemplate =
+ buildTemplateTypeGuard('CLIPGEmbedModelField');
// #endregion
// #region ControlLoRAModelField
-
export const zControlLoRAModelFieldValue = zModelIdentifierField.optional();
const zControlLoRAModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zControlLoRAModelFieldValue,
@@ -987,18 +1000,15 @@ const zControlLoRAModelFieldInputTemplate = zFieldInputTemplateBase.extend({
originalType: zFieldType.optional(),
default: zControlLoRAModelFieldValue,
});
-
export type ControlLoRAModelFieldValue = z.infer;
-
export type ControlLoRAModelFieldInputInstance = z.infer;
export type ControlLoRAModelFieldInputTemplate = z.infer;
-export const isControlLoRAModelFieldInputInstance = buildTypeGuard(zControlLoRAModelFieldInputInstance);
-export const isControlLoRAModelFieldInputTemplate = buildTypeGuard(zControlLoRAModelFieldInputTemplate);
-
+export const isControlLoRAModelFieldInputInstance = buildInstanceTypeGuard(zControlLoRAModelFieldInputInstance);
+export const isControlLoRAModelFieldInputTemplate =
+ buildTemplateTypeGuard('ControlLoRAModelField');
// #endregion
// #region SchedulerField
-
export const zSchedulerFieldValue = zSchedulerField.optional();
const zSchedulerFieldInputInstance = zFieldInputInstanceBase.extend({
value: zSchedulerFieldValue,
@@ -1014,8 +1024,8 @@ const zSchedulerFieldOutputTemplate = zFieldOutputTemplateBase.extend({
export type SchedulerFieldValue = z.infer;
export type SchedulerFieldInputInstance = z.infer;
export type SchedulerFieldInputTemplate = z.infer