mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-15 06:18:03 -05:00
Compare commits
58 Commits
v5.9.1
...
psyche/fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e0a569826 | ||
|
|
32327bed5e | ||
|
|
468f1bff00 | ||
|
|
6413753e9b | ||
|
|
d49ad17257 | ||
|
|
4ecf8be0be | ||
|
|
020544045f | ||
|
|
9edd1895bd | ||
|
|
9586b4476d | ||
|
|
ac8314139e | ||
|
|
33bde00f8b | ||
|
|
4ef198fc69 | ||
|
|
d51529b239 | ||
|
|
98cdbedb10 | ||
|
|
99b57acc8b | ||
|
|
7c7d8928d0 | ||
|
|
d3b42948d6 | ||
|
|
d58ed05bcb | ||
|
|
265cf8ca7e | ||
|
|
82a6fcbfdb | ||
|
|
0e66e539c1 | ||
|
|
c9579165b9 | ||
|
|
db0eeafb57 | ||
|
|
e966bf9759 | ||
|
|
71407cf817 | ||
|
|
5b6c035c63 | ||
|
|
81cdaac2b5 | ||
|
|
3329f128f6 | ||
|
|
578d1bbea4 | ||
|
|
819346b980 | ||
|
|
9cabb88560 | ||
|
|
4485450df6 | ||
|
|
14281890ce | ||
|
|
a366a32334 | ||
|
|
dfc1d67492 | ||
|
|
f051b0b56c | ||
|
|
53fb2509d7 | ||
|
|
1a7e134a2c | ||
|
|
1665f13000 | ||
|
|
0dddf4fcf9 | ||
|
|
e9cb3f6abe | ||
|
|
79af1e11d0 | ||
|
|
914ef1f664 | ||
|
|
254d9b52d6 | ||
|
|
b61e07c556 | ||
|
|
29570218a7 | ||
|
|
e9677940d0 | ||
|
|
1ba9b5407c | ||
|
|
50461b1ddb | ||
|
|
a8b0c1c10c | ||
|
|
5ec173b9bb | ||
|
|
861a2378c5 | ||
|
|
f1de722beb | ||
|
|
9ffa1b887b | ||
|
|
d969958041 | ||
|
|
a842698b7f | ||
|
|
b03479107d | ||
|
|
92d25d304a |
@@ -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",
|
||||
|
||||
590
invokeai/frontend/web/pnpm-lock.yaml
generated
590
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -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'}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
type Props = {
|
||||
isSelected: boolean;
|
||||
isHovered: boolean;
|
||||
};
|
||||
const SelectionOverlay = ({ isSelected, isHovered }: Props) => {
|
||||
const shadow = useMemo(() => {
|
||||
if (isSelected && isHovered) {
|
||||
return 'nodeHoveredSelected';
|
||||
}
|
||||
if (isSelected) {
|
||||
return 'nodeSelected';
|
||||
}
|
||||
if (isHovered) {
|
||||
return 'nodeHovered';
|
||||
}
|
||||
return undefined;
|
||||
}, [isHovered, isSelected]);
|
||||
return (
|
||||
<Box
|
||||
className="selection-box"
|
||||
position="absolute"
|
||||
top={0}
|
||||
insetInlineEnd={0}
|
||||
bottom={0}
|
||||
insetInlineStart={0}
|
||||
borderRadius="base"
|
||||
opacity={isSelected || isHovered ? 1 : 0.5}
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
pointerEvents="none"
|
||||
shadow={shadow}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SelectionOverlay);
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -484,9 +484,10 @@ export function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
|
||||
|
||||
export function getPrefixedId(
|
||||
prefix: CanvasEntityIdentifier['type'] | CanvasObjectState['type'] | (string & Record<never, never>)
|
||||
prefix: CanvasEntityIdentifier['type'] | CanvasObjectState['type'] | (string & Record<never, never>),
|
||||
separator = ':'
|
||||
): string {
|
||||
return `${prefix}:${nanoid()}`;
|
||||
return `${prefix}${separator}${nanoid()}`;
|
||||
}
|
||||
|
||||
export const getEmptyRect = (): Rect => {
|
||||
|
||||
@@ -9,7 +9,8 @@ import type { DndListTargetState } from 'features/dnd/types';
|
||||
*/
|
||||
const line = {
|
||||
thickness: 2,
|
||||
backgroundColor: 'base.500',
|
||||
backgroundColor: 'red',
|
||||
// backgroundColor: 'base.500',
|
||||
};
|
||||
|
||||
type DropIndicatorProps = {
|
||||
@@ -104,7 +105,7 @@ function DndDropIndicatorInternal({ edge, gap = '0px' }: DropIndicatorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export const DndListDropIndicator = ({ dndState }: { dndState: DndListTargetState }) => {
|
||||
export const DndListDropIndicator = ({ dndState, gap }: { dndState: DndListTargetState; gap?: string }) => {
|
||||
if (dndState.type !== 'is-dragging-over') {
|
||||
return null;
|
||||
}
|
||||
@@ -117,7 +118,7 @@ export const DndListDropIndicator = ({ dndState }: { dndState: DndListTargetStat
|
||||
<DndDropIndicatorInternal
|
||||
edge={dndState.closestEdge}
|
||||
// This is the gap between items in the list, used to calculate the position of the drop indicator
|
||||
gap="var(--invoke-space-2)"
|
||||
gap={gap || 'var(--invoke-space-2)'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Text,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { EdgeChange, NodeChange } from '@xyflow/react';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
@@ -31,6 +32,7 @@ import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupied
|
||||
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
|
||||
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
|
||||
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
|
||||
import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -41,8 +43,8 @@ import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCircuitryBold, PiFlaskBold, PiHammerBold, PiLightningFill } from 'react-icons/pi';
|
||||
import type { EdgeChange, NodeChange } from 'reactflow';
|
||||
import type { S } from 'services/api/types';
|
||||
import { objectEntries } from 'tsafe';
|
||||
|
||||
const useThrottle = <T,>(value: T, limit: number) => {
|
||||
const [throttledValue, setThrottledValue] = useState(value);
|
||||
@@ -95,8 +97,8 @@ const useAddNode = () => {
|
||||
node.selected = true;
|
||||
|
||||
// Deselect all other nodes and edges
|
||||
const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
|
||||
const edgeChanges: EdgeChange[] = [];
|
||||
const nodeChanges: NodeChange<AnyNode>[] = [{ type: 'add', item: node }];
|
||||
const edgeChanges: EdgeChange<AnyEdge>[] = [];
|
||||
nodes.forEach(({ id, selected }) => {
|
||||
if (selected) {
|
||||
nodeChanges.push({ type: 'select', id, selected: false });
|
||||
@@ -381,11 +383,11 @@ const NodeCommandList = memo(({ searchTerm, onSelect }: { searchTerm: string; on
|
||||
if (filter(template, searchTerm)) {
|
||||
const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
|
||||
|
||||
for (const field of Object.values(candidateFields)) {
|
||||
for (const [_fieldName, fieldTemplate] of objectEntries(candidateFields)) {
|
||||
const sourceType =
|
||||
pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
|
||||
pendingConnection.handleType === 'source' ? pendingConnection.fieldTemplate.type : fieldTemplate.type;
|
||||
const targetType =
|
||||
pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
|
||||
pendingConnection.handleType === 'target' ? pendingConnection.fieldTemplate.type : fieldTemplate.type;
|
||||
|
||||
if (validateConnectionTypes(sourceType, targetType)) {
|
||||
_items.push({
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type {
|
||||
EdgeChange,
|
||||
HandleType,
|
||||
NodeChange,
|
||||
OnEdgesChange,
|
||||
OnInit,
|
||||
OnMoveEnd,
|
||||
OnNodesChange,
|
||||
OnReconnect,
|
||||
ProOptions,
|
||||
ReactFlowProps,
|
||||
ReactFlowState,
|
||||
} from '@xyflow/react';
|
||||
import { Background, ReactFlow, useStore as useReactFlowStore, useUpdateNodeInternals } from '@xyflow/react';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useConnection } from 'features/nodes/hooks/useConnection';
|
||||
import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste';
|
||||
import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
|
||||
import { useNodeCopyPaste } from 'features/nodes/hooks/useNodeCopyPaste';
|
||||
import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
|
||||
import {
|
||||
$addNodeCmdk,
|
||||
@@ -30,23 +44,11 @@ import {
|
||||
} from 'features/nodes/store/selectors';
|
||||
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
|
||||
import { selectSelectionMode, selectShouldSnapToGrid } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import type { CSSProperties, MouseEvent } from 'react';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import type {
|
||||
EdgeChange,
|
||||
NodeChange,
|
||||
OnEdgesChange,
|
||||
OnEdgeUpdateFunc,
|
||||
OnInit,
|
||||
OnMoveEnd,
|
||||
OnNodesChange,
|
||||
ProOptions,
|
||||
ReactFlowProps,
|
||||
ReactFlowState,
|
||||
} from 'reactflow';
|
||||
import { Background, ReactFlow, useStore as useReactFlowStore, useUpdateNodeInternals } from 'reactflow';
|
||||
|
||||
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
|
||||
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
|
||||
@@ -58,13 +60,13 @@ import NotesNode from './nodes/Notes/NotesNode';
|
||||
const edgeTypes = {
|
||||
collapsed: InvocationCollapsedEdge,
|
||||
default: InvocationDefaultEdge,
|
||||
};
|
||||
} as const;
|
||||
|
||||
const nodeTypes = {
|
||||
invocation: InvocationNodeWrapper,
|
||||
current_image: CurrentImageNode,
|
||||
notes: NotesNode,
|
||||
};
|
||||
} as const;
|
||||
|
||||
// TODO: can we support reactflow? if not, we could style the attribution so it matches the app
|
||||
const proOptions: ProOptions = { hideAttribution: true };
|
||||
@@ -97,7 +99,7 @@ export const Flow = memo(() => {
|
||||
const [borderRadius] = useToken('radii', ['base']);
|
||||
const flowStyles = useMemo<CSSProperties>(() => ({ borderRadius }), [borderRadius]);
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
const onNodesChange: OnNodesChange<AnyNode> = useCallback(
|
||||
(nodeChanges) => {
|
||||
dispatch(nodesChanged(nodeChanges));
|
||||
const flow = $flow.get();
|
||||
@@ -112,7 +114,7 @@ export const Flow = memo(() => {
|
||||
[dispatch, needsFit]
|
||||
);
|
||||
|
||||
const onEdgesChange: OnEdgesChange = useCallback(
|
||||
const onEdgesChange: OnEdgesChange<AnyEdge> = useCallback(
|
||||
(changes) => {
|
||||
if (changes.length > 0) {
|
||||
dispatch(edgesChanged(changes));
|
||||
@@ -130,7 +132,7 @@ export const Flow = memo(() => {
|
||||
onCloseGlobal();
|
||||
}, [onCloseGlobal]);
|
||||
|
||||
const onInit: OnInit = useCallback((flow) => {
|
||||
const onInit: OnInit<AnyNode, AnyEdge> = useCallback((flow) => {
|
||||
$flow.set(flow);
|
||||
flow.fitView();
|
||||
}, []);
|
||||
@@ -158,13 +160,13 @@ export const Flow = memo(() => {
|
||||
* where the edge is deleted if you click it accidentally).
|
||||
*/
|
||||
|
||||
const onEdgeUpdateStart: NonNullable<ReactFlowProps['onEdgeUpdateStart']> = useCallback((e, edge, _handleType) => {
|
||||
const onReconnectStart = useCallback((event: MouseEvent, edge: AnyEdge, _handleType: HandleType) => {
|
||||
$edgePendingUpdate.set(edge);
|
||||
$didUpdateEdge.set(false);
|
||||
$lastEdgeUpdateMouseEvent.set(e);
|
||||
$lastEdgeUpdateMouseEvent.set(event);
|
||||
}, []);
|
||||
|
||||
const onEdgeUpdate: OnEdgeUpdateFunc = useCallback(
|
||||
const onReconnect: OnReconnect = useCallback(
|
||||
(oldEdge, newConnection) => {
|
||||
// This event is fired when an edge update is successful
|
||||
$didUpdateEdge.set(true);
|
||||
@@ -183,7 +185,7 @@ export const Flow = memo(() => {
|
||||
[dispatch, updateNodeInternals]
|
||||
);
|
||||
|
||||
const onEdgeUpdateEnd: NonNullable<ReactFlowProps['onEdgeUpdateEnd']> = useCallback(
|
||||
const onReconnectEnd: NonNullable<ReactFlowProps['onReconnectEnd']> = useCallback(
|
||||
(e, edge, _handleType) => {
|
||||
const didUpdateEdge = $didUpdateEdge.get();
|
||||
// Fall back to a reasonable default event
|
||||
@@ -208,7 +210,7 @@ export const Flow = memo(() => {
|
||||
|
||||
// #endregion
|
||||
|
||||
const { copySelection, pasteSelection, pasteSelectionWithEdges } = useCopyPaste();
|
||||
const { copySelection, pasteSelection, pasteSelectionWithEdges } = useNodeCopyPaste();
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'copySelection',
|
||||
@@ -220,8 +222,8 @@ export const Flow = memo(() => {
|
||||
|
||||
const selectAll = useCallback(() => {
|
||||
const { nodes, edges } = selectNodesSlice(store.getState());
|
||||
const nodeChanges: NodeChange[] = [];
|
||||
const edgeChanges: EdgeChange[] = [];
|
||||
const nodeChanges: NodeChange<AnyNode>[] = [];
|
||||
const edgeChanges: EdgeChange<AnyEdge>[] = [];
|
||||
nodes.forEach(({ id, selected }) => {
|
||||
if (!selected) {
|
||||
nodeChanges.push({ type: 'select', id, selected: true });
|
||||
@@ -294,8 +296,8 @@ export const Flow = memo(() => {
|
||||
|
||||
const deleteSelection = useCallback(() => {
|
||||
const { nodes, edges } = selectNodesSlice(store.getState());
|
||||
const nodeChanges: NodeChange[] = [];
|
||||
const edgeChanges: EdgeChange[] = [];
|
||||
const nodeChanges: NodeChange<AnyNode>[] = [];
|
||||
const edgeChanges: EdgeChange<AnyEdge>[] = [];
|
||||
nodes
|
||||
.filter((n) => n.selected)
|
||||
.forEach(({ id }) => {
|
||||
@@ -322,7 +324,7 @@ export const Flow = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
<ReactFlow<AnyNode, AnyEdge>
|
||||
id="workflow-editor"
|
||||
ref={flowWrapper}
|
||||
defaultViewport={viewport}
|
||||
@@ -334,9 +336,9 @@ export const Flow = memo(() => {
|
||||
onMouseMove={onMouseMove}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
onReconnect={onReconnect}
|
||||
onReconnectStart={onReconnectStart}
|
||||
onReconnectEnd={onReconnectEnd}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnect={onConnect}
|
||||
onConnectEnd={onConnectEnd}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -1,13 +1,39 @@
|
||||
import { Badge, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Badge, Box, chakra } from '@invoke-ai/ui-library';
|
||||
import type { EdgeProps } from '@xyflow/react';
|
||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@xyflow/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
|
||||
import { getEdgeStyles } from 'features/nodes/components/flow/edges/util/getEdgeColor';
|
||||
import { makeEdgeSelector } from 'features/nodes/components/flow/edges/util/makeEdgeSelector';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { buildSelectAreConnectedNodesSelected } from 'features/nodes/components/flow/edges/util/buildEdgeSelectors';
|
||||
import { selectShouldAnimateEdges } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import type { CollapsedInvocationNodeEdge } from 'features/nodes/types/invocation';
|
||||
import { memo, useMemo } from 'react';
|
||||
import type { EdgeProps } from 'reactflow';
|
||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow';
|
||||
|
||||
const ChakraBaseEdge = chakra(BaseEdge);
|
||||
|
||||
const baseEdgeSx: SystemStyleObject = {
|
||||
strokeWidth: '3px !important',
|
||||
stroke: 'base.500 !important',
|
||||
opacity: '0.5 !important',
|
||||
strokeDasharray: 'none',
|
||||
'&[data-selected="true"]': {
|
||||
opacity: '1 !important',
|
||||
},
|
||||
'&[data-selected="true"], &[data-are-connected-nodes-selected="true"]': {
|
||||
strokeDasharray: '5 !important',
|
||||
},
|
||||
'&[data-should-animate-edges="true"]': {
|
||||
animation: 'dashdraw 0.5s linear infinite !important',
|
||||
},
|
||||
};
|
||||
|
||||
const badgeSx: SystemStyleObject = {
|
||||
bg: 'base.500',
|
||||
opacity: 0.5,
|
||||
shadow: 'base',
|
||||
'&[data-selected="true"]': {
|
||||
opacity: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const InvocationCollapsedEdge = ({
|
||||
sourceX,
|
||||
@@ -20,17 +46,15 @@ const InvocationCollapsedEdge = ({
|
||||
data,
|
||||
selected = false,
|
||||
source,
|
||||
sourceHandleId,
|
||||
target,
|
||||
targetHandleId,
|
||||
}: EdgeProps<{ count: number }>) => {
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(
|
||||
() => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId),
|
||||
[templates, source, sourceHandleId, target, targetHandleId]
|
||||
}: EdgeProps<CollapsedInvocationNodeEdge>) => {
|
||||
const shouldAnimateEdges = useAppSelector(selectShouldAnimateEdges);
|
||||
const selectAreConnectedNodesSelected = useMemo(
|
||||
() => buildSelectAreConnectedNodesSelected(source, target),
|
||||
[source, target]
|
||||
);
|
||||
|
||||
const { shouldAnimateEdges, areConnectedNodesSelected } = useAppSelector(selector);
|
||||
const areConnectedNodesSelected = useAppSelector(selectAreConnectedNodesSelected);
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
@@ -41,31 +65,29 @@ const InvocationCollapsedEdge = ({
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
const { base500 } = useChakraThemeTokens();
|
||||
|
||||
const edgeStyles = useMemo(
|
||||
() => getEdgeStyles(base500, selected, shouldAnimateEdges, areConnectedNodesSelected),
|
||||
[areConnectedNodesSelected, base500, selected, shouldAnimateEdges]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge path={edgePath} markerEnd={markerEnd} style={edgeStyles} />
|
||||
{data?.count && data.count > 1 && (
|
||||
<ChakraBaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
sx={baseEdgeSx}
|
||||
data-selected={selected}
|
||||
data-are-connected-nodes-selected={areConnectedNodesSelected}
|
||||
data-should-animate-edges={shouldAnimateEdges}
|
||||
/>
|
||||
{data?.count !== undefined && (
|
||||
<EdgeLabelRenderer>
|
||||
<Flex
|
||||
data-testid="asdfasdfasdf"
|
||||
<Box
|
||||
position="absolute"
|
||||
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
|
||||
className="nodrag nopan"
|
||||
// Unfortunately edge labels do not get the same zIndex treatment as edges do, so we need to manage this ourselves
|
||||
className="edge-label-renderer__custom-edge nodrag nopan" // Unfortunately edge labels do not get the same zIndex treatment as edges do, so we need to manage this ourselves
|
||||
// See: https://github.com/xyflow/xyflow/issues/3658
|
||||
zIndex={1001}
|
||||
>
|
||||
<Badge variant="solid" bg="base.500" opacity={selected ? 0.8 : 0.5} boxShadow="base">
|
||||
<Badge variant="solid" sx={badgeSx} data-selected={selected}>
|
||||
{data.count}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</Box>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,14 +1,61 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { chakra, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { EdgeProps } from '@xyflow/react';
|
||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@xyflow/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { getEdgeStyles } from 'features/nodes/components/flow/edges/util/getEdgeColor';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectShouldShowEdgeLabels } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { selectShouldAnimateEdges, selectShouldShowEdgeLabels } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import type { DefaultInvocationNodeEdge } from 'features/nodes/types/invocation';
|
||||
import { memo, useMemo } from 'react';
|
||||
import type { EdgeProps } from 'reactflow';
|
||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow';
|
||||
|
||||
import { makeEdgeSelector } from './util/makeEdgeSelector';
|
||||
import {
|
||||
buildSelectAreConnectedNodesSelected,
|
||||
buildSelectEdgeColor,
|
||||
buildSelectEdgeLabel,
|
||||
} from './util/buildEdgeSelectors';
|
||||
|
||||
const ChakraBaseEdge = chakra(BaseEdge);
|
||||
|
||||
const baseEdgeSx: SystemStyleObject = {
|
||||
strokeWidth: '3px !important',
|
||||
opacity: '0.5 !important',
|
||||
strokeDasharray: 'none',
|
||||
'&[data-selected="true"]': {
|
||||
opacity: '1 !important',
|
||||
},
|
||||
'&[data-should-animate-edges="true"]': {
|
||||
animation: 'dashdraw 0.5s linear infinite !important',
|
||||
'&[data-selected="true"], &[data-are-connected-nodes-selected="true"]': {
|
||||
strokeDasharray: '5 !important',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const edgeLabelWrapperSx: SystemStyleObject = {
|
||||
pointerEvents: 'all',
|
||||
position: 'absolute',
|
||||
bg: 'base.800',
|
||||
borderRadius: 'base',
|
||||
borderWidth: 1,
|
||||
opacity: 0.5,
|
||||
borderColor: 'transparent',
|
||||
py: 1,
|
||||
px: 3,
|
||||
shadow: 'md',
|
||||
'&[data-selected="true"]': {
|
||||
opacity: 1,
|
||||
borderColor: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const edgeLabelTextSx: SystemStyleObject = {
|
||||
fontWeight: 'semibold',
|
||||
color: 'base.300',
|
||||
'&[data-selected="true"]': {
|
||||
color: 'base.100',
|
||||
},
|
||||
};
|
||||
|
||||
const InvocationDefaultEdge = ({
|
||||
sourceX,
|
||||
@@ -23,15 +70,26 @@ const InvocationDefaultEdge = ({
|
||||
target,
|
||||
sourceHandleId,
|
||||
targetHandleId,
|
||||
}: EdgeProps) => {
|
||||
}: EdgeProps<DefaultInvocationNodeEdge>) => {
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(
|
||||
() => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId),
|
||||
const shouldAnimateEdges = useAppSelector(selectShouldAnimateEdges);
|
||||
const shouldShowEdgeLabels = useAppSelector(selectShouldShowEdgeLabels);
|
||||
|
||||
const selectAreConnectedNodesSelected = useMemo(
|
||||
() => buildSelectAreConnectedNodesSelected(source, target),
|
||||
[source, target]
|
||||
);
|
||||
const selectStrokeColor = useMemo(
|
||||
() => buildSelectEdgeColor(templates, source, sourceHandleId, target, targetHandleId),
|
||||
[templates, source, sourceHandleId, target, targetHandleId]
|
||||
);
|
||||
|
||||
const { shouldAnimateEdges, areConnectedNodesSelected, stroke, label } = useAppSelector(selector);
|
||||
const shouldShowEdgeLabels = useAppSelector(selectShouldShowEdgeLabels);
|
||||
const selectEdgeLabel = useMemo(
|
||||
() => buildSelectEdgeLabel(templates, source, sourceHandleId, target, targetHandleId),
|
||||
[templates, source, sourceHandleId, target, targetHandleId]
|
||||
);
|
||||
const areConnectedNodesSelected = useAppSelector(selectAreConnectedNodesSelected);
|
||||
const stroke = useAppSelector(selectStrokeColor);
|
||||
const label = useAppSelector(selectEdgeLabel);
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
@@ -42,31 +100,26 @@ const InvocationDefaultEdge = ({
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
const edgeStyles = useMemo(
|
||||
() => getEdgeStyles(stroke, selected, shouldAnimateEdges, areConnectedNodesSelected),
|
||||
[areConnectedNodesSelected, stroke, selected, shouldAnimateEdges]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge path={edgePath} markerEnd={markerEnd} style={edgeStyles} />
|
||||
<ChakraBaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
sx={baseEdgeSx}
|
||||
stroke={`${stroke} !important`}
|
||||
data-selected={selected}
|
||||
data-are-connected-nodes-selected={areConnectedNodesSelected}
|
||||
data-should-animate-edges={shouldAnimateEdges}
|
||||
/>
|
||||
{label && shouldShowEdgeLabels && (
|
||||
<EdgeLabelRenderer>
|
||||
<Flex
|
||||
className="nodrag nopan"
|
||||
pointerEvents="all"
|
||||
position="absolute"
|
||||
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
borderWidth={1}
|
||||
borderColor={selected ? 'undefined' : 'transparent'}
|
||||
opacity={selected ? 1 : 0.5}
|
||||
py={1}
|
||||
px={3}
|
||||
shadow="md"
|
||||
data-selected={selected}
|
||||
sx={edgeLabelWrapperSx}
|
||||
>
|
||||
<Text size="sm" fontWeight="semibold" color={selected ? 'base.100' : 'base.300'}>
|
||||
<Text size="sm" sx={edgeLabelTextSx} data-selected={selected}>
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -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}`;
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
|
||||
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
|
||||
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||
import { useFieldNames } from 'features/nodes/hooks/useFieldNames';
|
||||
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
|
||||
import { OutputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate';
|
||||
import { OutputFieldNodesEditorView } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldNodesEditorView';
|
||||
import { useInputFieldNamesByStatus } from 'features/nodes/hooks/useInputFieldNamesByStatus';
|
||||
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
|
||||
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
|
||||
import { memo } from 'react';
|
||||
|
||||
import InputField from './fields/InputField';
|
||||
import OutputField from './fields/OutputField';
|
||||
import { InputFieldEditModeNodes } from './fields/InputFieldEditModeNodes';
|
||||
import InvocationNodeFooter from './InvocationNodeFooter';
|
||||
import InvocationNodeHeader from './InvocationNodeHeader';
|
||||
|
||||
@@ -20,7 +21,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
||||
const fieldNames = useFieldNames(nodeId);
|
||||
const fieldNames = useInputFieldNamesByStatus(nodeId);
|
||||
const withFooter = useWithFooter(nodeId);
|
||||
const outputFieldNames = useOutputFieldNames(nodeId);
|
||||
|
||||
@@ -42,34 +43,28 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
||||
<Grid gridTemplateColumns="1fr auto" gridAutoRows="1fr">
|
||||
{fieldNames.connectionFields.map((fieldName, i) => (
|
||||
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
|
||||
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||
</InvocationInputFieldCheck>
|
||||
<InputFieldGate nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
|
||||
</InputFieldGate>
|
||||
</GridItem>
|
||||
))}
|
||||
{outputFieldNames.map((fieldName, i) => (
|
||||
<GridItem gridColumnStart={2} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.output-field`}>
|
||||
<OutputField nodeId={nodeId} fieldName={fieldName} />
|
||||
<OutputFieldGate nodeId={nodeId} fieldName={fieldName}>
|
||||
<OutputFieldNodesEditorView nodeId={nodeId} fieldName={fieldName} />
|
||||
</OutputFieldGate>
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
{fieldNames.anyOrDirectFields.map((fieldName) => (
|
||||
<InvocationInputFieldCheck
|
||||
key={`${nodeId}.${fieldName}.input-field`}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
>
|
||||
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||
</InvocationInputFieldCheck>
|
||||
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
|
||||
</InputFieldGate>
|
||||
))}
|
||||
{fieldNames.missingFields.map((fieldName) => (
|
||||
<InvocationInputFieldCheck
|
||||
key={`${nodeId}.${fieldName}.input-field`}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
>
|
||||
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||
</InvocationInputFieldCheck>
|
||||
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
|
||||
</InputFieldGate>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -1,40 +1,25 @@
|
||||
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||
import { map } from 'lodash-es';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
const hiddenHandleStyles: CSSProperties = { visibility: 'hidden' };
|
||||
const collapsedHandleStyles: CSSProperties = {
|
||||
borderWidth: 0,
|
||||
borderRadius: '3px',
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
backgroundColor: 'var(--invoke-colors-base-600)',
|
||||
zIndex: -1,
|
||||
};
|
||||
|
||||
const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
|
||||
const template = useNodeTemplate(nodeId);
|
||||
const { base600 } = useChakraThemeTokens();
|
||||
|
||||
const dummyHandleStyles: CSSProperties = useMemo(
|
||||
() => ({
|
||||
borderWidth: 0,
|
||||
borderRadius: '3px',
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
backgroundColor: base600,
|
||||
zIndex: -1,
|
||||
}),
|
||||
[base600]
|
||||
);
|
||||
|
||||
const collapsedTargetStyles: CSSProperties = useMemo(
|
||||
() => ({ ...dummyHandleStyles, left: '-0.5rem' }),
|
||||
[dummyHandleStyles]
|
||||
);
|
||||
const collapsedSourceStyles: CSSProperties = useMemo(
|
||||
() => ({ ...dummyHandleStyles, right: '-0.5rem' }),
|
||||
[dummyHandleStyles]
|
||||
);
|
||||
|
||||
if (!template) {
|
||||
return null;
|
||||
@@ -47,7 +32,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
|
||||
id={`${nodeId}-collapsed-target`}
|
||||
isConnectable={false}
|
||||
position={Position.Left}
|
||||
style={collapsedTargetStyles}
|
||||
style={collapsedHandleStyles}
|
||||
/>
|
||||
{map(template.inputs, (input) => (
|
||||
<Handle
|
||||
@@ -64,7 +49,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
|
||||
id={`${nodeId}-collapsed-source`}
|
||||
isConnectable={false}
|
||||
position={Position.Right}
|
||||
style={collapsedSourceStyles}
|
||||
style={collapsedHandleStyles}
|
||||
/>
|
||||
{map(template.outputs, (output) => (
|
||||
<Handle
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
|
||||
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
|
||||
import { useNodeHasImageOutput } from 'features/nodes/hooks/useNodeHasImageOutput';
|
||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
const props: ChakraProps = { w: 'unset' };
|
||||
|
||||
const InvocationNodeFooter = ({ nodeId }: Props) => {
|
||||
const hasImageOutput = useHasImageOutput(nodeId);
|
||||
const hasImageOutput = useNodeHasImageOutput(nodeId);
|
||||
const isCacheEnabled = useFeatureStatus('invocationCache');
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -5,7 +5,7 @@ import InvocationNodeClassificationIcon from 'features/nodes/components/flow/nod
|
||||
import { memo } from 'react';
|
||||
|
||||
import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles';
|
||||
import InvocationNodeInfoIcon from './InvocationNodeInfoIcon';
|
||||
import { InvocationNodeInfoIcon } from './InvocationNodeInfoIcon';
|
||||
import InvocationNodeStatusIndicator from './InvocationNodeStatusIndicator';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { compare } from 'compare-versions';
|
||||
import { useNode } from 'features/nodes/hooks/useNode';
|
||||
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
|
||||
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
|
||||
import { useInvocationNodeNotes } from 'features/nodes/hooks/useNodeNotes';
|
||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { useNodeVersion } from 'features/nodes/hooks/useNodeVersion';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiInfoBold } from 'react-icons/pi';
|
||||
@@ -12,7 +13,7 @@ interface Props {
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
|
||||
export const InvocationNodeInfoIcon = memo(({ nodeId }: Props) => {
|
||||
const needsUpdate = useNodeNeedsUpdate(nodeId);
|
||||
|
||||
return (
|
||||
@@ -20,96 +21,66 @@ const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
|
||||
<Icon as={PiInfoBold} display="block" boxSize={4} w={8} color={needsUpdate ? 'error.400' : 'base.400'} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(InvocationNodeInfoIcon);
|
||||
InvocationNodeInfoIcon.displayName = 'InvocationNodeInfoIcon';
|
||||
|
||||
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const node = useNode(nodeId);
|
||||
const notes = useInvocationNodeNotes(nodeId);
|
||||
const label = useNodeLabel(nodeId);
|
||||
const version = useNodeVersion(nodeId);
|
||||
const nodeTemplate = useNodeTemplate(nodeId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (node.data?.label && nodeTemplate?.title) {
|
||||
return `${node.data.label} (${nodeTemplate.title})`;
|
||||
if (label) {
|
||||
return `${label} (${nodeTemplate.title})`;
|
||||
}
|
||||
|
||||
if (node.data?.label && !nodeTemplate) {
|
||||
return node.data.label;
|
||||
}
|
||||
|
||||
if (!node.data?.label && nodeTemplate) {
|
||||
return nodeTemplate.title;
|
||||
}
|
||||
|
||||
return t('nodes.unknownNode');
|
||||
}, [node.data.label, nodeTemplate, t]);
|
||||
|
||||
const versionComponent = useMemo(() => {
|
||||
if (!isInvocationNode(node) || !nodeTemplate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!node.data.version) {
|
||||
return (
|
||||
<Text as="span" color="error.500">
|
||||
{t('nodes.versionUnknown')}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (!nodeTemplate.version) {
|
||||
return (
|
||||
<Text as="span" color="error.500">
|
||||
{t('nodes.version')} {node.data.version} ({t('nodes.unknownTemplate')})
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (compare(node.data.version, nodeTemplate.version, '<')) {
|
||||
return (
|
||||
<Text as="span" color="error.500">
|
||||
{t('nodes.version')} {node.data.version} ({t('nodes.updateNode')})
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (compare(node.data.version, nodeTemplate.version, '>')) {
|
||||
return (
|
||||
<Text as="span" color="error.500">
|
||||
{t('nodes.version')} {node.data.version} ({t('nodes.updateApp')})
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text as="span">
|
||||
{t('nodes.version')} {node.data.version}
|
||||
</Text>
|
||||
);
|
||||
}, [node, nodeTemplate, t]);
|
||||
|
||||
if (!isInvocationNode(node)) {
|
||||
return <Text fontWeight="semibold">{t('nodes.unknownNode')}</Text>;
|
||||
}
|
||||
return nodeTemplate.title;
|
||||
}, [label, nodeTemplate.title]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text as="span" fontWeight="semibold">
|
||||
{title}
|
||||
</Text>
|
||||
{nodeTemplate?.nodePack && (
|
||||
<Text opacity={0.7}>
|
||||
{t('nodes.nodePack')}: {nodeTemplate.nodePack}
|
||||
</Text>
|
||||
)}
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{nodeTemplate?.description}
|
||||
<Text opacity={0.7}>
|
||||
{t('nodes.nodePack')}: {nodeTemplate.nodePack}
|
||||
</Text>
|
||||
{versionComponent}
|
||||
{node.data?.notes && <Text>{node.data.notes}</Text>}
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{nodeTemplate.description}
|
||||
</Text>
|
||||
<Version nodeVersion={version} templateVersion={nodeTemplate.version} />
|
||||
{notes && <Text>{notes}</Text>}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipContent.displayName = 'TooltipContent';
|
||||
|
||||
const Version = ({ nodeVersion, templateVersion }: { nodeVersion: string; templateVersion: string }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (compare(nodeVersion, templateVersion, '<')) {
|
||||
return (
|
||||
<Text as="span" color="error.500">
|
||||
{t('nodes.version')} {nodeVersion} ({t('nodes.updateNode')})
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (compare(nodeVersion, templateVersion, '>')) {
|
||||
return (
|
||||
<Text as="span" color="error.500">
|
||||
{t('nodes.version')} {nodeVersion} ({t('nodes.updateApp')})
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text as="span">
|
||||
{t('nodes.version')} {nodeVersion}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { FormControl, FormLabel, Textarea } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useNode } from 'features/nodes/hooks/useNode';
|
||||
import { useInvocationNodeNotes } from 'features/nodes/hooks/useNodeNotes';
|
||||
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
};
|
||||
|
||||
export const InvocationNodeNotesTextarea = memo(({ nodeId }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const node = useNode(nodeId);
|
||||
const { t } = useTranslation();
|
||||
const notes = useInvocationNodeNotes(nodeId);
|
||||
const handleNotesChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
|
||||
},
|
||||
[dispatch, nodeId]
|
||||
);
|
||||
if (!isInvocationNode(node)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FormControl orientation="vertical" h="full">
|
||||
<FormLabel>{t('nodes.notes')}</FormLabel>
|
||||
<Textarea value={node.data?.notes} onChange={handleNotesChanged} rows={10} resize="none" />
|
||||
<Textarea value={notes} onChange={handleNotesChanged} rows={10} resize="none" />
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(NotesTextarea);
|
||||
InvocationNodeNotesTextarea.displayName = 'InvocationNodeNotesTextarea';
|
||||
@@ -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;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { Node, NodeProps } from '@xyflow/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import InvocationNode from 'features/nodes/components/flow/nodes/Invocation/InvocationNode';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodes } from 'features/nodes/store/selectors';
|
||||
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
||||
import { memo, useMemo } from 'react';
|
||||
import type { NodeProps } from 'reactflow';
|
||||
|
||||
import InvocationNodeUnknownFallback from './InvocationNodeUnknownFallback';
|
||||
|
||||
const InvocationNodeWrapper = (props: NodeProps<InvocationNodeData>) => {
|
||||
const InvocationNodeWrapper = (props: NodeProps<Node<InvocationNodeData>>) => {
|
||||
const { data, selected } = props;
|
||||
const { id: nodeId, type, isOpen, label } = data;
|
||||
const templates = useStore($templates);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
|
||||
import { useIsIntermediate } from 'features/nodes/hooks/useIsIntermediate';
|
||||
import { useNodeHasImageOutput } from 'features/nodes/hooks/useNodeHasImageOutput';
|
||||
import { useNodeIsIntermediate } from 'features/nodes/hooks/useNodeIsIntermediate';
|
||||
import { nodeIsIntermediateChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -10,8 +10,8 @@ import { useTranslation } from 'react-i18next';
|
||||
const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const hasImageOutput = useHasImageOutput(nodeId);
|
||||
const isIntermediate = useIsIntermediate(nodeId);
|
||||
const hasImageOutput = useNodeHasImageOutput(nodeId);
|
||||
const isIntermediate = useNodeIsIntermediate(nodeId);
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(
|
||||
@@ -30,7 +30,7 @@ const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
|
||||
|
||||
return (
|
||||
<FormControl className="nopan">
|
||||
<FormLabel>{t('nodes.saveToGallery')} </FormLabel>
|
||||
<FormLabel m={0}>{t('nodes.saveToGallery')} </FormLabel>
|
||||
<Checkbox onChange={handleChange} isChecked={!isIntermediate} />
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel>{t('invocationCache.useCache')}</FormLabel>
|
||||
<FormLabel m={0}>{t('invocationCache.useCache')}</FormLabel>
|
||||
<Checkbox className="nopan" onChange={handleChange} isChecked={useCache} />
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
@@ -1,72 +1,79 @@
|
||||
import { Tooltip } from '@invoke-ai/ui-library';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Tooltip } from '@invoke-ai/ui-library';
|
||||
import type { HandleType } from '@xyflow/react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
|
||||
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
|
||||
import type { ValidationResult } from 'features/nodes/store/util/validateConnection';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY, MODEL_TYPES } from 'features/nodes/types/constants';
|
||||
import { type FieldInputTemplate, type FieldOutputTemplate, isSingle } from 'features/nodes/types/field';
|
||||
import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { HandleType } from 'reactflow';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
type FieldHandleProps = {
|
||||
fieldTemplate: FieldInputTemplate | FieldOutputTemplate;
|
||||
type Props = {
|
||||
handleType: HandleType;
|
||||
fieldTemplate: FieldInputTemplate | FieldOutputTemplate;
|
||||
isConnectionInProgress: boolean;
|
||||
isConnectionStartField: boolean;
|
||||
validationResult: ValidationResult;
|
||||
};
|
||||
|
||||
const FieldHandle = (props: FieldHandleProps) => {
|
||||
const { fieldTemplate, handleType, isConnectionInProgress, isConnectionStartField, validationResult } = props;
|
||||
const sx = {
|
||||
position: 'relative',
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 4,
|
||||
pointerEvents: 'none',
|
||||
'&[data-cardinality="SINGLE"]': {
|
||||
borderWidth: 0,
|
||||
},
|
||||
borderRadius: '100%',
|
||||
'&[data-is-model-field="true"], &[data-is-batch-field="true"]': {
|
||||
borderRadius: 4,
|
||||
},
|
||||
'&[data-is-batch-field="true"]': {
|
||||
transform: 'rotate(45deg)',
|
||||
},
|
||||
'&[data-is-connection-in-progress="true"][data-is-connection-start-field="false"][data-is-connection-valid="false"]':
|
||||
{
|
||||
filter: 'opacity(0.4) grayscale(0.7)',
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
'&[data-is-connection-in-progress="true"][data-is-connection-start-field="true"][data-is-connection-valid="false"]': {
|
||||
cursor: 'grab',
|
||||
},
|
||||
'&[data-is-connection-in-progress="false"] &[data-is-connection-valid="true"]': {
|
||||
cursor: 'crosshair',
|
||||
},
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
const handleStyleBase = {
|
||||
position: 'absolute',
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
zIndex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
} satisfies CSSProperties;
|
||||
|
||||
const targetHandleStyle = {
|
||||
...handleStyleBase,
|
||||
insetInlineStart: '-0.5rem',
|
||||
} satisfies CSSProperties;
|
||||
|
||||
const sourceHandleStyle = {
|
||||
...handleStyleBase,
|
||||
insetInlineEnd: '-0.5rem',
|
||||
} satisfies CSSProperties;
|
||||
|
||||
export const FieldHandle = memo((props: Props) => {
|
||||
const { fieldTemplate, isConnectionInProgress, isConnectionStartField, validationResult, handleType } = props;
|
||||
const { t } = useTranslation();
|
||||
const { name } = fieldTemplate;
|
||||
const type = fieldTemplate.type;
|
||||
const fieldTypeName = useFieldTypeName(type);
|
||||
const styles: CSSProperties = useMemo(() => {
|
||||
const isModelType = MODEL_TYPES.some((t) => t === type.name);
|
||||
const color = getFieldColor(type);
|
||||
const s: CSSProperties = {
|
||||
backgroundColor: !isSingle(type) ? colorTokenToCssVar('base.900') : color,
|
||||
position: 'absolute',
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
borderWidth: !isSingle(type) ? 4 : 0,
|
||||
borderStyle: 'solid',
|
||||
borderColor: color,
|
||||
borderRadius: isModelType || type.batch ? 4 : '100%',
|
||||
zIndex: 1,
|
||||
transformOrigin: 'center',
|
||||
};
|
||||
|
||||
if (type.batch) {
|
||||
s.transform = 'rotate(45deg) translateX(-0.3rem) translateY(-0.3rem)';
|
||||
}
|
||||
|
||||
if (handleType === 'target') {
|
||||
s.insetInlineStart = '-1rem';
|
||||
} else {
|
||||
s.insetInlineEnd = '-1rem';
|
||||
}
|
||||
|
||||
if (isConnectionInProgress && !isConnectionStartField && !validationResult.isValid) {
|
||||
s.filter = 'opacity(0.4) grayscale(0.7)';
|
||||
}
|
||||
|
||||
if (isConnectionInProgress && !validationResult.isValid) {
|
||||
if (isConnectionStartField) {
|
||||
s.cursor = 'grab';
|
||||
} else {
|
||||
s.cursor = 'not-allowed';
|
||||
}
|
||||
} else {
|
||||
s.cursor = 'crosshair';
|
||||
}
|
||||
|
||||
return s;
|
||||
}, [handleType, isConnectionInProgress, isConnectionStartField, type, validationResult.isValid]);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
|
||||
const isModelField = useMemo(() => MODEL_TYPES.some((t) => t === fieldTemplate.type.name), [fieldTemplate.type]);
|
||||
|
||||
const tooltip = useMemo(() => {
|
||||
if (isConnectionInProgress && validationResult.messageTKey) {
|
||||
@@ -83,12 +90,24 @@ const FieldHandle = (props: FieldHandleProps) => {
|
||||
>
|
||||
<Handle
|
||||
type={handleType}
|
||||
id={name}
|
||||
id={fieldTemplate.name}
|
||||
position={handleType === 'target' ? Position.Left : Position.Right}
|
||||
style={styles}
|
||||
/>
|
||||
style={handleType === 'target' ? targetHandleStyle : sourceHandleStyle}
|
||||
>
|
||||
<Box
|
||||
sx={sx}
|
||||
data-cardinality={fieldTemplate.type.cardinality}
|
||||
data-is-batch-field={fieldTemplate.type.batch}
|
||||
data-is-model-field={isModelField}
|
||||
data-is-connection-in-progress={isConnectionInProgress}
|
||||
data-is-connection-start-field={isConnectionStartField}
|
||||
data-is-connection-valid={validationResult.isValid}
|
||||
backgroundColor={fieldTemplate.type.cardinality === 'SINGLE' ? fieldColor : 'base.900'}
|
||||
borderColor={fieldColor}
|
||||
/>
|
||||
</Handle>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(FieldHandle);
|
||||
FieldHandle.displayName = 'FieldHandle';
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
|
||||
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
const FieldResetToDefaultValueButton = ({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const value = useFieldValue(nodeId, fieldName);
|
||||
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||
const isDisabled = useMemo(() => {
|
||||
return isEqual(value, fieldTemplate.default);
|
||||
}, [value, fieldTemplate.default]);
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(fieldValueReset({ nodeId, fieldName, value: fieldTemplate.default }));
|
||||
}, [dispatch, fieldName, fieldTemplate.default, nodeId]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
onClick={onClick}
|
||||
isDisabled={isDisabled}
|
||||
pointerEvents="auto"
|
||||
size="xs"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FieldResetToDefaultValueButton);
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
|
||||
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
||||
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
|
||||
import { isFieldInputInstance, isFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
kind: 'inputs' | 'outputs';
|
||||
}
|
||||
|
||||
const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
|
||||
const field = useFieldInputInstance(nodeId, fieldName);
|
||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
|
||||
const isInputTemplate = isFieldInputTemplate(fieldTemplate);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate?.type);
|
||||
const { t } = useTranslation();
|
||||
const fieldTitle = useMemo(() => {
|
||||
if (isFieldInputInstance(field)) {
|
||||
if (field.label && fieldTemplate?.title) {
|
||||
return `${field.label} (${fieldTemplate.title})`;
|
||||
}
|
||||
|
||||
if (field.label && !fieldTemplate) {
|
||||
return field.label;
|
||||
}
|
||||
|
||||
if (!field.label && fieldTemplate) {
|
||||
return fieldTemplate.title;
|
||||
}
|
||||
|
||||
return t('nodes.unknownField');
|
||||
} else {
|
||||
return fieldTemplate?.title || t('nodes.unknownField');
|
||||
}
|
||||
}, [field, fieldTemplate, t]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{fieldTitle}</Text>
|
||||
{fieldTemplate && (
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{fieldTemplate.description}
|
||||
</Text>
|
||||
)}
|
||||
{fieldTypeName && (
|
||||
<Text>
|
||||
{t('parameters.type')}: {fieldTypeName}
|
||||
</Text>
|
||||
)}
|
||||
{isInputTemplate && (
|
||||
<Text>
|
||||
{t('common.input')}: {startCase(fieldTemplate.input)}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FieldTooltipContent);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { CompositeNumberInput } from '@invoke-ai/ui-library';
|
||||
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const FloatFieldInput = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
|
||||
|
||||
return (
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className="nodrag"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
FloatFieldInput.displayName = 'FloatFieldInput ';
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const FloatFieldSlider = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
|
||||
|
||||
return (
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className="nodrag"
|
||||
marks
|
||||
withThumbTooltip
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
FloatFieldSlider.displayName = 'FloatFieldSlider ';
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NUMPY_RAND_MAX } from 'app/constants';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useFloatField = (props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: number) => {
|
||||
dispatch(fieldFloatValueChanged({ nodeId, fieldName: field.name, value }));
|
||||
},
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
|
||||
const min = useMemo(() => {
|
||||
let min = -NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.minimum)) {
|
||||
min = fieldTemplate.minimum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMinimum)) {
|
||||
min = fieldTemplate.exclusiveMinimum + 0.01;
|
||||
}
|
||||
return min;
|
||||
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
let max = NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.maximum)) {
|
||||
max = fieldTemplate.maximum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMaximum)) {
|
||||
max = fieldTemplate.exclusiveMaximum - 0.01;
|
||||
}
|
||||
return max;
|
||||
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
|
||||
|
||||
const step = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 0.1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
|
||||
const fineStep = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 0.01;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
|
||||
return {
|
||||
defaultValue: fieldTemplate.default,
|
||||
onChange,
|
||||
value: field.value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
fineStep,
|
||||
};
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import { Flex, FormControl } from '@invoke-ai/ui-library';
|
||||
import FieldResetToDefaultValueButton from 'features/nodes/components/flow/nodes/Invocation/fields/FieldResetToDefaultValueButton';
|
||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
|
||||
import EditableFieldTitle from './EditableFieldTitle';
|
||||
import FieldHandle from './FieldHandle';
|
||||
import FieldLinearViewToggle from './FieldLinearViewToggle';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
import { InputFieldWrapper } from './InputFieldWrapper';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isInvalid = useFieldIsInvalid(nodeId, fieldName);
|
||||
|
||||
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
|
||||
useConnectionState({ nodeId, fieldName, kind: 'inputs' });
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
if (fieldTemplate.input === 'connection' || isConnected) {
|
||||
return (
|
||||
<InputFieldWrapper shouldDim={shouldDim}>
|
||||
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
|
||||
<EditableFieldTitle
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="inputs"
|
||||
isInvalid={isInvalid}
|
||||
withTooltip
|
||||
shouldDim
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FieldHandle
|
||||
fieldTemplate={fieldTemplate}
|
||||
handleType="target"
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
/>
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InputFieldWrapper shouldDim={shouldDim}>
|
||||
<FormControl
|
||||
isInvalid={isInvalid}
|
||||
isDisabled={isConnected}
|
||||
// Without pointerEvents prop, disabled inputs don't trigger reactflow events. For example, when making a
|
||||
// connection, the mouse up to end the connection won't fire, leaving the connection in-progress.
|
||||
pointerEvents={isConnected ? 'none' : 'auto'}
|
||||
orientation="vertical"
|
||||
px={2}
|
||||
>
|
||||
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Flex gap={1}>
|
||||
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" isInvalid={isInvalid} withTooltip />
|
||||
{isHovered && <FieldResetToDefaultValueButton nodeId={nodeId} fieldName={fieldName} />}
|
||||
{isHovered && <FieldLinearViewToggle nodeId={nodeId} fieldName={fieldName} />}
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</Flex>
|
||||
</FormControl>
|
||||
|
||||
{fieldTemplate.input !== 'direct' && (
|
||||
<FieldHandle
|
||||
fieldTemplate={fieldTemplate}
|
||||
handleType="target"
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
/>
|
||||
)}
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InputField);
|
||||
@@ -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';
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Circle, Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
|
||||
import { InputFieldNotesIconButtonEditable } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonEditable';
|
||||
import { InputFieldResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton';
|
||||
import { useLinearViewFieldDnd } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
|
||||
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiMinusBold } from 'react-icons/pi';
|
||||
|
||||
import { InputFieldRenderer } from './InputFieldRenderer';
|
||||
import { InputFieldTitle } from './InputFieldTitle';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
const sx = {
|
||||
layerStyle: 'second',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
borderRadius: 'base',
|
||||
w: 'full',
|
||||
p: 2,
|
||||
'&[data-is-dragging=true]': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
transitionProperty: 'common',
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
export const InputFieldEditModeLinear = memo(({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRemoveField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [dndListState, isDragging] = useLinearViewFieldDnd(ref, { nodeId, fieldName });
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full">
|
||||
<Flex
|
||||
ref={ref}
|
||||
// This is used to trigger the post-move flash animation
|
||||
data-field-name={`${nodeId}-${fieldName}`}
|
||||
data-is-dragging={isDragging}
|
||||
onMouseEnter={handleMouseOver}
|
||||
onMouseLeave={handleMouseOut}
|
||||
sx={sx}
|
||||
>
|
||||
<Flex flexDir="column" w="full" gap={1}>
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} />
|
||||
<Spacer />
|
||||
{isMouseOverNode && <Circle me={2} size={2} borderRadius="full" bg="invokeBlue.500" />}
|
||||
<InputFieldNotesIconButtonEditable nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldResetToInitialValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<IconButton
|
||||
aria-label={t('nodes.removeLinearView')}
|
||||
tooltip={t('nodes.removeLinearView')}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={handleRemoveField}
|
||||
icon={<PiMinusBold />}
|
||||
/>
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<DndListDropIndicator dndState={dndListState} />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldEditModeLinear.displayName = 'InputFieldEditModeLinear';
|
||||
@@ -0,0 +1,145 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { Flex, FormControl } from '@invoke-ai/ui-library';
|
||||
import { firefoxDndFix } from 'features/dnd/util';
|
||||
import { FieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldHandle';
|
||||
import { InputFieldNotesIconButtonEditable } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButtonEditable';
|
||||
import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton';
|
||||
import { buildNodeFieldDndData } from 'features/nodes/components/sidePanel/builder/use-builder-dnd';
|
||||
import { useInputFieldConnectionState } from 'features/nodes/hooks/useInputFieldConnectionState';
|
||||
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
|
||||
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import type { RefObject } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { InputFieldAddRemoveLinearViewIconButton } from './InputFieldAddRemoveLinearViewIconButton';
|
||||
import { InputFieldRenderer } from './InputFieldRenderer';
|
||||
import { InputFieldTitle } from './InputFieldTitle';
|
||||
import { InputFieldWrapper } from './InputFieldWrapper';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
export const InputFieldEditModeNodes = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const dragHandleRef = useRef<HTMLDivElement>(null);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, fieldName);
|
||||
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
|
||||
const { isConnectionInProgress, isConnectionStartField, validationResult } = useInputFieldConnectionState(
|
||||
nodeId,
|
||||
fieldName
|
||||
);
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
const isDragging = useNodeFieldDnd({ nodeId, fieldName }, draggableRef, dragHandleRef);
|
||||
|
||||
if (fieldTemplate.input === 'connection' || isConnected) {
|
||||
return (
|
||||
<InputFieldWrapper>
|
||||
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
|
||||
<InputFieldTitle
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
isInvalid={isInvalid}
|
||||
isDisabled={(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField) || isConnected}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FieldHandle
|
||||
handleType="target"
|
||||
fieldTemplate={fieldTemplate}
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
/>
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InputFieldWrapper>
|
||||
<FormControl
|
||||
ref={draggableRef}
|
||||
isInvalid={isInvalid}
|
||||
isDisabled={isConnected}
|
||||
// Without pointerEvents prop, disabled inputs don't trigger reactflow events. For example, when making a
|
||||
// connection, the mouse up to end the connection won't fire, leaving the connection in-progress.
|
||||
pointerEvents={isConnected ? 'none' : 'auto'}
|
||||
orientation="vertical"
|
||||
px={2}
|
||||
opacity={isDragging ? 0.3 : 1}
|
||||
>
|
||||
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Flex className="nodrag" ref={dragHandleRef} gap={1}>
|
||||
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} isInvalid={isInvalid} />
|
||||
{isHovered && (
|
||||
<>
|
||||
<InputFieldNotesIconButtonEditable nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldResetToDefaultValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldAddRemoveLinearViewIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</Flex>
|
||||
</FormControl>
|
||||
|
||||
{fieldTemplate.input !== 'direct' && (
|
||||
<FieldHandle
|
||||
handleType="target"
|
||||
fieldTemplate={fieldTemplate}
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
/>
|
||||
)}
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldEditModeNodes.displayName = 'InputFieldEditModeNodes';
|
||||
|
||||
const useNodeFieldDnd = (
|
||||
fieldIdentifier: FieldIdentifier,
|
||||
draggableRef: RefObject<HTMLElement>,
|
||||
dragHandleRef: RefObject<HTMLElement>
|
||||
) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const draggableElement = draggableRef.current;
|
||||
const dragHandleElement = dragHandleRef.current;
|
||||
if (!draggableElement || !dragHandleElement) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
firefoxDndFix(draggableElement),
|
||||
draggable({
|
||||
element: draggableElement,
|
||||
dragHandle: dragHandleElement,
|
||||
getInitialData: () => buildNodeFieldDndData(fieldIdentifier),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [dragHandleRef, draggableRef, fieldIdentifier]);
|
||||
|
||||
return isDragging;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { InputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder';
|
||||
import { useInputFieldInstanceExists } from 'features/nodes/hooks/useInputFieldInstanceExists';
|
||||
import { useInputFieldTemplateExists } from 'features/nodes/hooks/useInputFieldTemplateExists';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}>;
|
||||
|
||||
export const InputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
|
||||
const hasInstance = useInputFieldInstanceExists(nodeId, fieldName);
|
||||
const hasTemplate = useInputFieldTemplateExists(nodeId, fieldName);
|
||||
|
||||
if (!hasTemplate || !hasInstance) {
|
||||
return <InputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
InputFieldGate.displayName = 'InputFieldGate';
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { FormHelperTextProps } from '@invoke-ai/ui-library';
|
||||
import { FormHelperText } from '@invoke-ai/ui-library';
|
||||
import { useInputFieldNotes } from 'features/nodes/hooks/useInputFieldNotes';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = FormHelperTextProps & {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldNotesHelperText = memo(({ nodeId, fieldName, ...rest }: Props) => {
|
||||
const notes = useInputFieldNotes(nodeId, fieldName);
|
||||
|
||||
if (!notes?.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormHelperText px={1} {...rest}>
|
||||
{notes}
|
||||
</FormHelperText>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldNotesHelperText.displayName = 'InputFieldNotesHelperText';
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Textarea,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useInputFieldNotes } from 'features/nodes/hooks/useInputFieldNotes';
|
||||
import { fieldNotesChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiNoteBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldNotesIconButtonEditable = memo(({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const notes = useInputFieldNotes(nodeId, fieldName);
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
dispatch(fieldNotesChanged({ nodeId, fieldName, val: e.target.value }));
|
||||
},
|
||||
[dispatch, fieldName, nodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
tooltip={t('nodes.notes')}
|
||||
aria-label={t('nodes.notes')}
|
||||
icon={<PiNoteBold />}
|
||||
pointerEvents="auto"
|
||||
size="xs"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p={2} w={256}>
|
||||
<FormControl orientation="vertical">
|
||||
<FormLabel>{t('nodes.notes')}</FormLabel>
|
||||
<Textarea
|
||||
className="nodrag nopan nowheel"
|
||||
fontSize="sm"
|
||||
value={notes ?? ''}
|
||||
onChange={onChange}
|
||||
p={2}
|
||||
resize="none"
|
||||
rows={5}
|
||||
/>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldNotesIconButtonEditable.displayName = 'InputFieldNotesIconButtonEditable';
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Box, Flex, IconButton, Popover, PopoverContent, PopoverTrigger, Text } from '@invoke-ai/ui-library';
|
||||
import { useInputFieldNotes } from 'features/nodes/hooks/useInputFieldNotes';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiNoteBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldNotesIconButtonReadonly = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const notes = useInputFieldNotes(nodeId, fieldName);
|
||||
|
||||
if (!notes?.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
tooltip={t('nodes.notes')}
|
||||
aria-label={t('nodes.notes')}
|
||||
icon={<PiNoteBold />}
|
||||
size="xs"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p={2} w={256}>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Text color="base.300" fontWeight="semibold" fontSize="sm">
|
||||
{t('nodes.notes')}
|
||||
</Text>
|
||||
<Box borderWidth={1} borderRadius="base" p={2} w="full" h="full">
|
||||
<Text fontSize="sm">{notes}</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldNotesIconButtonReadonly.displayName = 'InputFieldNotesIconButtonReadonly';
|
||||
@@ -1,12 +1,17 @@
|
||||
import { FloatFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput';
|
||||
import { FloatFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatFieldCollectionInputComponent';
|
||||
import { FloatGeneratorFieldInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatGeneratorFieldComponent';
|
||||
import { ImageFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent';
|
||||
import { IntegerFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerFieldCollectionInputComponent';
|
||||
import { IntegerGeneratorFieldInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerGeneratorFieldComponent';
|
||||
import ModelIdentifierFieldInputComponent from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent';
|
||||
import { NumberFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberFieldCollectionInputComponent';
|
||||
import { StringFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent';
|
||||
import { StringGeneratorFieldInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorFieldComponent';
|
||||
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
|
||||
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||
import { IntegerFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput';
|
||||
import { StringFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput';
|
||||
import { StringFieldTextarea } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import {
|
||||
isBoardFieldInputInstance,
|
||||
isBoardFieldInputTemplate,
|
||||
@@ -94,174 +99,265 @@ import ImageFieldInputComponent from './inputs/ImageFieldInputComponent';
|
||||
import IPAdapterModelFieldInputComponent from './inputs/IPAdapterModelFieldInputComponent';
|
||||
import LoRAModelFieldInputComponent from './inputs/LoRAModelFieldInputComponent';
|
||||
import MainModelFieldInputComponent from './inputs/MainModelFieldInputComponent';
|
||||
import NumberFieldInputComponent from './inputs/NumberFieldInputComponent';
|
||||
import RefinerModelFieldInputComponent from './inputs/RefinerModelFieldInputComponent';
|
||||
import SchedulerFieldInputComponent from './inputs/SchedulerFieldInputComponent';
|
||||
import SD3MainModelFieldInputComponent from './inputs/SD3MainModelFieldInputComponent';
|
||||
import SDXLMainModelFieldInputComponent from './inputs/SDXLMainModelFieldInputComponent';
|
||||
import SpandrelImageToImageModelFieldInputComponent from './inputs/SpandrelImageToImageModelFieldInputComponent';
|
||||
import StringFieldInputComponent from './inputs/StringFieldInputComponent';
|
||||
import T2IAdapterModelFieldInputComponent from './inputs/T2IAdapterModelFieldInputComponent';
|
||||
import T5EncoderModelFieldInputComponent from './inputs/T5EncoderModelFieldInputComponent';
|
||||
import VAEModelFieldInputComponent from './inputs/VAEModelFieldInputComponent';
|
||||
|
||||
type InputFieldProps = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
|
||||
const fieldInstance = useFieldInputInstance(nodeId, fieldName);
|
||||
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||
export const InputFieldRenderer = memo(({ nodeId, fieldName }: InputFieldProps) => {
|
||||
const field = useInputFieldInstance(nodeId, fieldName);
|
||||
const template = useInputFieldTemplate(nodeId, fieldName);
|
||||
|
||||
if (isStringFieldCollectionInputInstance(fieldInstance) && isStringFieldCollectionInputTemplate(fieldTemplate)) {
|
||||
return <StringFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isStringFieldCollectionInputTemplate(template)) {
|
||||
if (!isStringFieldCollectionInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <StringFieldCollectionInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isStringFieldInputInstance(fieldInstance) && isStringFieldInputTemplate(fieldTemplate)) {
|
||||
return <StringFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isStringFieldInputTemplate(template)) {
|
||||
if (!isStringFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
if (template.ui_component === 'textarea') {
|
||||
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else {
|
||||
return <StringFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
}
|
||||
|
||||
if (isBooleanFieldInputInstance(fieldInstance) && isBooleanFieldInputTemplate(fieldTemplate)) {
|
||||
return <BooleanFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isBooleanFieldInputTemplate(template)) {
|
||||
if (!isBooleanFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <BooleanFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isIntegerFieldInputInstance(fieldInstance) && isIntegerFieldInputTemplate(fieldTemplate)) {
|
||||
return <NumberFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isIntegerFieldInputTemplate(template)) {
|
||||
if (!isIntegerFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isFloatFieldInputInstance(fieldInstance) && isFloatFieldInputTemplate(fieldTemplate)) {
|
||||
return <NumberFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isFloatFieldInputTemplate(template)) {
|
||||
if (!isFloatFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isIntegerFieldCollectionInputInstance(fieldInstance) && isIntegerFieldCollectionInputTemplate(fieldTemplate)) {
|
||||
return <NumberFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isIntegerFieldCollectionInputTemplate(template)) {
|
||||
if (!isIntegerFieldCollectionInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <IntegerFieldCollectionInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isFloatFieldCollectionInputInstance(fieldInstance) && isFloatFieldCollectionInputTemplate(fieldTemplate)) {
|
||||
return <NumberFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isFloatFieldCollectionInputTemplate(template)) {
|
||||
if (!isFloatFieldCollectionInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <FloatFieldCollectionInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isEnumFieldInputInstance(fieldInstance) && isEnumFieldInputTemplate(fieldTemplate)) {
|
||||
return <EnumFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isEnumFieldInputTemplate(template)) {
|
||||
if (!isEnumFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <EnumFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isImageFieldCollectionInputInstance(fieldInstance) && isImageFieldCollectionInputTemplate(fieldTemplate)) {
|
||||
return <ImageFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isImageFieldCollectionInputTemplate(template)) {
|
||||
if (!isImageFieldCollectionInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <ImageFieldCollectionInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isImageFieldInputInstance(fieldInstance) && isImageFieldInputTemplate(fieldTemplate)) {
|
||||
return <ImageFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isImageFieldInputTemplate(template)) {
|
||||
if (!isImageFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <ImageFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isBoardFieldInputInstance(fieldInstance) && isBoardFieldInputTemplate(fieldTemplate)) {
|
||||
return <BoardFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isBoardFieldInputTemplate(template)) {
|
||||
if (!isBoardFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <BoardFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isMainModelFieldInputInstance(fieldInstance) && isMainModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <MainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isMainModelFieldInputTemplate(template)) {
|
||||
if (!isMainModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <MainModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isModelIdentifierFieldInputInstance(fieldInstance) && isModelIdentifierFieldInputTemplate(fieldTemplate)) {
|
||||
return <ModelIdentifierFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isModelIdentifierFieldInputTemplate(template)) {
|
||||
if (!isModelIdentifierFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <ModelIdentifierFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isSDXLRefinerModelFieldInputInstance(fieldInstance) && isSDXLRefinerModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <RefinerModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isSDXLRefinerModelFieldInputTemplate(template)) {
|
||||
if (!isSDXLRefinerModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <RefinerModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isVAEModelFieldInputInstance(fieldInstance) && isVAEModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <VAEModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isVAEModelFieldInputTemplate(template)) {
|
||||
if (!isVAEModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <VAEModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isT5EncoderModelFieldInputInstance(fieldInstance) && isT5EncoderModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <T5EncoderModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isT5EncoderModelFieldInputTemplate(template)) {
|
||||
if (!isT5EncoderModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <T5EncoderModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
if (isCLIPEmbedModelFieldInputInstance(fieldInstance) && isCLIPEmbedModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <CLIPEmbedModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isCLIPEmbedModelFieldInputTemplate(template)) {
|
||||
if (!isCLIPEmbedModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <CLIPEmbedModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isCLIPLEmbedModelFieldInputInstance(fieldInstance) && isCLIPLEmbedModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <CLIPLEmbedModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isCLIPLEmbedModelFieldInputTemplate(template)) {
|
||||
if (!isCLIPLEmbedModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <CLIPLEmbedModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isCLIPGEmbedModelFieldInputInstance(fieldInstance) && isCLIPGEmbedModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <CLIPGEmbedModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isCLIPGEmbedModelFieldInputTemplate(template)) {
|
||||
if (!isCLIPGEmbedModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <CLIPGEmbedModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isControlLoRAModelFieldInputInstance(fieldInstance) && isControlLoRAModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <ControlLoRAModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isControlLoRAModelFieldInputTemplate(template)) {
|
||||
if (!isControlLoRAModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <ControlLoRAModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isFluxVAEModelFieldInputInstance(fieldInstance) && isFluxVAEModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <FluxVAEModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isFluxVAEModelFieldInputTemplate(template)) {
|
||||
if (!isFluxVAEModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <FluxVAEModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isLoRAModelFieldInputInstance(fieldInstance) && isLoRAModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <LoRAModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isLoRAModelFieldInputTemplate(template)) {
|
||||
if (!isLoRAModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <LoRAModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isControlNetModelFieldInputInstance(fieldInstance) && isControlNetModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <ControlNetModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isControlNetModelFieldInputTemplate(template)) {
|
||||
if (!isControlNetModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <ControlNetModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isIPAdapterModelFieldInputInstance(fieldInstance) && isIPAdapterModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <IPAdapterModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isIPAdapterModelFieldInputTemplate(template)) {
|
||||
if (!isIPAdapterModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <IPAdapterModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isT2IAdapterModelFieldInputInstance(fieldInstance) && isT2IAdapterModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <T2IAdapterModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isT2IAdapterModelFieldInputTemplate(template)) {
|
||||
if (!isT2IAdapterModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <T2IAdapterModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (
|
||||
isSpandrelImageToImageModelFieldInputInstance(fieldInstance) &&
|
||||
isSpandrelImageToImageModelFieldInputTemplate(fieldTemplate)
|
||||
) {
|
||||
return (
|
||||
<SpandrelImageToImageModelFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
if (isSpandrelImageToImageModelFieldInputTemplate(template)) {
|
||||
if (!isSpandrelImageToImageModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <SpandrelImageToImageModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isColorFieldInputInstance(fieldInstance) && isColorFieldInputTemplate(fieldTemplate)) {
|
||||
return <ColorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isColorFieldInputTemplate(template)) {
|
||||
if (!isColorFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <ColorFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isFluxMainModelFieldInputInstance(fieldInstance) && isFluxMainModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <FluxMainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isFluxMainModelFieldInputTemplate(template)) {
|
||||
if (!isFluxMainModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <FluxMainModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isSD3MainModelFieldInputInstance(fieldInstance) && isSD3MainModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <SD3MainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isSD3MainModelFieldInputTemplate(template)) {
|
||||
if (!isSD3MainModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <SD3MainModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isSDXLMainModelFieldInputInstance(fieldInstance) && isSDXLMainModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <SDXLMainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isSDXLMainModelFieldInputTemplate(template)) {
|
||||
if (!isSDXLMainModelFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <SDXLMainModelFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isSchedulerFieldInputInstance(fieldInstance) && isSchedulerFieldInputTemplate(fieldTemplate)) {
|
||||
return <SchedulerFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isSchedulerFieldInputTemplate(template)) {
|
||||
if (!isSchedulerFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <SchedulerFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isFloatGeneratorFieldInputInstance(fieldInstance) && isFloatGeneratorFieldInputTemplate(fieldTemplate)) {
|
||||
return <FloatGeneratorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isFloatGeneratorFieldInputTemplate(template)) {
|
||||
if (!isFloatGeneratorFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <FloatGeneratorFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isIntegerGeneratorFieldInputInstance(fieldInstance) && isIntegerGeneratorFieldInputTemplate(fieldTemplate)) {
|
||||
return <IntegerGeneratorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isIntegerGeneratorFieldInputTemplate(template)) {
|
||||
if (!isIntegerGeneratorFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <IntegerGeneratorFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (isStringGeneratorFieldInputInstance(fieldInstance) && isStringGeneratorFieldInputTemplate(fieldTemplate)) {
|
||||
return <StringGeneratorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
if (isStringGeneratorFieldInputTemplate(template)) {
|
||||
if (!isStringGeneratorFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
return <StringGeneratorFieldInputComponent nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (fieldTemplate) {
|
||||
// Fallback for when there is no component for the type
|
||||
return null;
|
||||
}
|
||||
};
|
||||
return null;
|
||||
});
|
||||
|
||||
export default memo(InputFieldRenderer);
|
||||
InputFieldRenderer.displayName = 'InputFieldRenderer';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useInputFieldDefaultValue } from 'features/nodes/hooks/useInputFieldDefaultValue';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldResetToDefaultValueIconButton = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isValueChanged, resetToDefaultValue } = useInputFieldDefaultValue(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
pointerEvents="auto"
|
||||
size="xs"
|
||||
onClick={resetToDefaultValue}
|
||||
isDisabled={!isValueChanged}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldResetToDefaultValueIconButton.displayName = 'InputFieldResetToDefaultValueIconButton';
|
||||
@@ -0,0 +1,30 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useInputFieldInitialLinearViewValue } from 'features/nodes/hooks/useInputFieldInitialLinearViewValue';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldResetToInitialValueIconButton = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isValueChanged, resetToInitialLinearViewValue } = useInputFieldInitialLinearViewValue(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
pointerEvents="auto"
|
||||
size="xs"
|
||||
onClick={resetToInitialLinearViewValue}
|
||||
isDisabled={!isValueChanged}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldResetToInitialValueIconButton.displayName = 'InputFieldResetToInitialValueIconButton';
|
||||
@@ -1,37 +1,54 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Editable,
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
Flex,
|
||||
forwardRef,
|
||||
Tooltip,
|
||||
useEditableControls,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { Editable, EditableInput, EditablePreview, Flex, Tooltip, useEditableControls } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
|
||||
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
|
||||
import { InputFieldTooltip } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltip';
|
||||
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
|
||||
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
|
||||
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
const editablePreviewStyles: SystemStyleObject = {
|
||||
p: 0,
|
||||
fontWeight: 'semibold',
|
||||
textAlign: 'left',
|
||||
color: 'base.300',
|
||||
_hover: {
|
||||
fontWeight: 'semibold !important',
|
||||
},
|
||||
'&[data-is-invalid="true"]': {
|
||||
color: 'error.300',
|
||||
},
|
||||
'&[data-is-disabled="true"]': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
};
|
||||
|
||||
const editableInputStyles: SystemStyleObject = {
|
||||
p: 0,
|
||||
w: 'full',
|
||||
fontWeight: 'semibold',
|
||||
color: 'base.100',
|
||||
_focusVisible: {
|
||||
p: 0,
|
||||
textAlign: 'left',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
kind: 'inputs' | 'outputs';
|
||||
isInvalid?: boolean;
|
||||
withTooltip?: boolean;
|
||||
shouldDim?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
||||
const { nodeId, fieldName, kind, isInvalid = false, withTooltip = false, shouldDim = false } = props;
|
||||
const label = useFieldLabel(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
|
||||
export const InputFieldTitle = memo((props: Props) => {
|
||||
const { nodeId, fieldName, isInvalid, isDisabled } = props;
|
||||
const label = useInputFieldLabel(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -62,7 +79,6 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
as={Flex}
|
||||
ref={ref}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
alignItems="center"
|
||||
@@ -71,15 +87,14 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
||||
w="full"
|
||||
>
|
||||
<Tooltip
|
||||
label={withTooltip ? <FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="inputs" /> : undefined}
|
||||
label={<InputFieldTooltip nodeId={nodeId} fieldName={fieldName} />}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
>
|
||||
<EditablePreview
|
||||
fontWeight="semibold"
|
||||
sx={editablePreviewStyles}
|
||||
noOfLines={1}
|
||||
color={isInvalid ? 'error.300' : 'base.300'}
|
||||
opacity={shouldDim ? 0.5 : 1}
|
||||
data-is-invalid={isInvalid}
|
||||
data-is-disabled={isDisabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
<EditableInput className="nodrag" sx={editableInputStyles} />
|
||||
@@ -88,26 +103,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
const editableInputStyles: SystemStyleObject = {
|
||||
p: 0,
|
||||
w: 'full',
|
||||
fontWeight: 'semibold',
|
||||
color: 'base.100',
|
||||
_focusVisible: {
|
||||
p: 0,
|
||||
textAlign: 'left',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
};
|
||||
const editablePreviewStyles: SystemStyleObject = {
|
||||
p: 0,
|
||||
textAlign: 'left',
|
||||
_hover: {
|
||||
fontWeight: 'semibold !important',
|
||||
},
|
||||
};
|
||||
|
||||
export default memo(EditableFieldTitle);
|
||||
InputFieldTitle.displayName = 'InputFieldTitle';
|
||||
|
||||
const EditableControls = memo(() => {
|
||||
const { isEditing, getEditButtonProps } = useEditableControls();
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
export const InputFieldTooltip = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fieldInstance = useInputFieldInstance(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
|
||||
const fieldTitle = useMemo(() => {
|
||||
if (fieldInstance.label && fieldTemplate.title) {
|
||||
return `${fieldInstance.label} (${fieldTemplate.title})`;
|
||||
}
|
||||
|
||||
if (fieldInstance.label && !fieldTemplate.title) {
|
||||
return fieldInstance.label;
|
||||
}
|
||||
|
||||
return fieldTemplate.title;
|
||||
}, [fieldInstance, fieldTemplate]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{fieldTitle}</Text>
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{fieldTemplate.description}
|
||||
</Text>
|
||||
<Text>
|
||||
{t('parameters.type')}: {fieldTypeName}
|
||||
</Text>
|
||||
<Text>
|
||||
{t('common.input')}: {startCase(fieldTemplate.input)}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldTooltip.displayName = 'FieldTooltipContent';
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
|
||||
import { useInputFieldName } from 'features/nodes/hooks/useInputFieldName';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useInputFieldName(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<InputFieldWrapper>
|
||||
<FormControl isInvalid={true} alignItems="stretch" justifyContent="center" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
|
||||
{t('nodes.unknownInput', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldUnknownPlaceholder.displayName = 'InputFieldUnknownPlaceholder';
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Box, FormControl, FormLabel, Spacer } from '@invoke-ai/ui-library';
|
||||
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
|
||||
import { InputFieldResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton';
|
||||
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
|
||||
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldViewMode = memo(({ nodeId, fieldName }: Props) => {
|
||||
const label = useInputFieldLabel(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<FormControl w="full" gap={2} flexDir="column">
|
||||
<FormLabel fontSize="sm" display="flex" w="full" m={0} gap={2} ps={1}>
|
||||
{label || fieldTemplateTitle}
|
||||
<Spacer />
|
||||
<InputFieldResetToInitialValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
</FormLabel>
|
||||
<Box w="full" h="full">
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</Box>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldViewMode.displayName = 'InputFieldViewMode';
|
||||
@@ -1,27 +1,21 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
type InputFieldWrapperProps = PropsWithChildren<{
|
||||
shouldDim: boolean;
|
||||
}>;
|
||||
const sx = {
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.1s',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
export const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
|
||||
return (
|
||||
<Flex
|
||||
position="relative"
|
||||
minH={8}
|
||||
py={0.5}
|
||||
alignItems="center"
|
||||
opacity={shouldDim ? 0.5 : 1}
|
||||
transitionProperty="opacity"
|
||||
transitionDuration="0.1s"
|
||||
w="full"
|
||||
h="full"
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
export const InputFieldWrapper = memo(({ children }: PropsWithChildren) => {
|
||||
return <Flex sx={sx}>{children}</Flex>;
|
||||
});
|
||||
|
||||
InputFieldWrapper.displayName = 'InputFieldWrapper';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CompositeNumberInput } from '@invoke-ai/ui-library';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
|
||||
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const IntegerFieldInput = memo(
|
||||
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
|
||||
|
||||
return (
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className="nodrag"
|
||||
w="full"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IntegerFieldInput.displayName = 'IntegerFieldInput';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
|
||||
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const IntegerFieldSlider = memo(
|
||||
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
|
||||
|
||||
return (
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className="nodrag"
|
||||
w="full"
|
||||
marks
|
||||
withThumbTooltip
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IntegerFieldSlider.displayName = 'IntegerFieldSlider';
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NUMPY_RAND_MAX } from 'app/constants';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useIntegerField = (props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: number) => {
|
||||
dispatch(fieldIntegerValueChanged({ nodeId, fieldName: field.name, value: Math.floor(Number(value)) }));
|
||||
},
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
|
||||
const min = useMemo(() => {
|
||||
let min = -NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.minimum)) {
|
||||
min = fieldTemplate.minimum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMinimum)) {
|
||||
min = fieldTemplate.exclusiveMinimum + 1;
|
||||
}
|
||||
return min;
|
||||
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
let max = NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.maximum)) {
|
||||
max = fieldTemplate.maximum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMaximum)) {
|
||||
max = fieldTemplate.exclusiveMaximum - 1;
|
||||
}
|
||||
return max;
|
||||
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
|
||||
|
||||
const step = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
|
||||
const fineStep = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
|
||||
return {
|
||||
defaultValue: fieldTemplate.default,
|
||||
onChange,
|
||||
value: field.value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
fineStep,
|
||||
};
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}>;
|
||||
|
||||
export const InvocationInputFieldCheck = memo(({ nodeId, fieldName, children }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||
const instance = node.data.inputs[fieldName];
|
||||
const template = templates[node.data.type];
|
||||
const fieldTemplate = template?.inputs[fieldName];
|
||||
return {
|
||||
name: instance?.label || fieldTemplate?.title || fieldName,
|
||||
hasInstance: Boolean(instance),
|
||||
hasTemplate: Boolean(fieldTemplate),
|
||||
};
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
);
|
||||
const { hasInstance, hasTemplate, name } = useAppSelector(selector);
|
||||
|
||||
if (!hasTemplate || !hasInstance) {
|
||||
return (
|
||||
<Flex position="relative" minH={8} py={0.5} alignItems="center" w="full" h="full">
|
||||
<FormControl
|
||||
isInvalid={true}
|
||||
alignItems="stretch"
|
||||
justifyContent="center"
|
||||
flexDir="column"
|
||||
gap={2}
|
||||
h="full"
|
||||
w="full"
|
||||
>
|
||||
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
|
||||
{t('nodes.unknownInput', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck';
|
||||
@@ -1,116 +0,0 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Circle, Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
|
||||
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||
import { useLinearViewFieldDnd } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
|
||||
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
||||
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
import EditableFieldTitle from './EditableFieldTitle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
|
||||
type Props = {
|
||||
fieldIdentifier: FieldIdentifier;
|
||||
};
|
||||
|
||||
const sx = {
|
||||
layerStyle: 'second',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
borderRadius: 'base',
|
||||
w: 'full',
|
||||
p: 2,
|
||||
'&[data-is-dragging=true]': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
transitionProperty: 'common',
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
const LinearViewFieldInternal = ({ fieldIdentifier }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isValueChanged, onReset } = useFieldOriginalValue(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(fieldIdentifier.nodeId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRemoveField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldRemoved(fieldIdentifier));
|
||||
}, [dispatch, fieldIdentifier]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [dndListState, isDragging] = useLinearViewFieldDnd(ref, fieldIdentifier);
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full">
|
||||
<Flex
|
||||
ref={ref}
|
||||
// This is used to trigger the post-move flash animation
|
||||
data-field-name={`${fieldIdentifier.nodeId}-${fieldIdentifier.fieldName}`}
|
||||
data-is-dragging={isDragging}
|
||||
onMouseEnter={handleMouseOver}
|
||||
onMouseLeave={handleMouseOut}
|
||||
sx={sx}
|
||||
>
|
||||
<Flex flexDir="column" w="full">
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<EditableFieldTitle nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} kind="inputs" />
|
||||
<Spacer />
|
||||
{isMouseOverNode && <Circle me={2} size={2} borderRadius="full" bg="invokeBlue.500" />}
|
||||
{isValueChanged && (
|
||||
<IconButton
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
kind="inputs"
|
||||
/>
|
||||
}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
>
|
||||
<Flex h="full" alignItems="center">
|
||||
<Icon fontSize="sm" color="base.300" as={PiInfoBold} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
aria-label={t('nodes.removeLinearView')}
|
||||
tooltip={t('nodes.removeLinearView')}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRemoveField}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
/>
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<DndListDropIndicator dndState={dndListState} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const LinearViewField = ({ fieldIdentifier }: Props) => {
|
||||
return (
|
||||
<InvocationInputFieldCheck nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
|
||||
<LinearViewFieldInternal fieldIdentifier={fieldIdentifier} />
|
||||
</InvocationInputFieldCheck>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LinearViewField);
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Flex, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||
import { useFieldOutputTemplate } from 'features/nodes/hooks/useFieldOutputTemplate';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FieldHandle from './FieldHandle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
const OutputField = ({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const fieldTemplate = useFieldOutputTemplate(nodeId, fieldName);
|
||||
|
||||
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
|
||||
useConnectionState({ nodeId, fieldName, kind: 'outputs' });
|
||||
|
||||
if (!fieldTemplate) {
|
||||
return (
|
||||
<OutputFieldWrapper shouldDim={shouldDim}>
|
||||
<FormControl alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
|
||||
{t('nodes.unknownOutput', {
|
||||
name: fieldName,
|
||||
})}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OutputFieldWrapper shouldDim={shouldDim}>
|
||||
<Tooltip
|
||||
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="outputs" />}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
shouldWrapChildren
|
||||
>
|
||||
<FormControl isDisabled={isConnected} pe={2}>
|
||||
<FormLabel mb={0}>{fieldTemplate?.title}</FormLabel>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
<FieldHandle
|
||||
fieldTemplate={fieldTemplate}
|
||||
handleType="source"
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
/>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(OutputField);
|
||||
|
||||
type OutputFieldWrapperProps = PropsWithChildren<{
|
||||
shouldDim: boolean;
|
||||
}>;
|
||||
|
||||
const OutputFieldWrapper = memo(({ shouldDim, children }: OutputFieldWrapperProps) => (
|
||||
<Flex
|
||||
position="relative"
|
||||
minH={8}
|
||||
py={0.5}
|
||||
alignItems="center"
|
||||
opacity={shouldDim ? 0.5 : 1}
|
||||
transitionProperty="opacity"
|
||||
transitionDuration="0.1s"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
));
|
||||
|
||||
OutputFieldWrapper.displayName = 'OutputFieldWrapper';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { OutputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder';
|
||||
import { useOutputFieldTemplateExists } from 'features/nodes/hooks/useOutputFieldTemplateExists';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}>;
|
||||
|
||||
export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
|
||||
const hasTemplate = useOutputFieldTemplateExists(nodeId, fieldName);
|
||||
|
||||
if (!hasTemplate) {
|
||||
return <OutputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
OutputFieldGate.displayName = 'OutputFieldGate';
|
||||
@@ -0,0 +1,41 @@
|
||||
import { FieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldHandle';
|
||||
import { OutputFieldTitle } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle';
|
||||
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
|
||||
import { useOutputFieldConnectionState } from 'features/nodes/hooks/useOutputFieldConnectionState';
|
||||
import { useOutputFieldIsConnected } from 'features/nodes/hooks/useOutputFieldIsConnected';
|
||||
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}>;
|
||||
|
||||
export const OutputFieldNodesEditorView = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
|
||||
const isConnected = useOutputFieldIsConnected(nodeId, fieldName);
|
||||
const { isConnectionInProgress, isConnectionStartField, validationResult } = useOutputFieldConnectionState(
|
||||
nodeId,
|
||||
fieldName
|
||||
);
|
||||
|
||||
return (
|
||||
<OutputFieldWrapper>
|
||||
<OutputFieldTitle
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
isDisabled={(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField) || isConnected}
|
||||
/>
|
||||
<FieldHandle
|
||||
handleType="source"
|
||||
fieldTemplate={fieldTemplate}
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
/>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
OutputFieldNodesEditorView.displayName = 'OutputFieldNodesEditorView';
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { OutputFieldTooltip } from './OutputFieldTooltip';
|
||||
|
||||
const sx = {
|
||||
fontSize: 'sm',
|
||||
color: 'base.300',
|
||||
fontWeight: 'semibold',
|
||||
pe: 2,
|
||||
'&[data-is-disabled="true"]': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
isDisabled?: boolean;
|
||||
}>;
|
||||
|
||||
export const OutputFieldTitle = memo(({ nodeId, fieldName, isDisabled }: Props) => {
|
||||
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={<OutputFieldTooltip nodeId={nodeId} fieldName={fieldName} />}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
shouldWrapChildren
|
||||
>
|
||||
<Text data-is-disabled={isDisabled} sx={sx}>
|
||||
{fieldTemplate.title}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
OutputFieldTitle.displayName = 'OutputFieldTitle';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
|
||||
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
export const OutputFieldTooltip = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{fieldTemplate.title}</Text>
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{fieldTemplate.description}
|
||||
</Text>
|
||||
<Text>
|
||||
{t('parameters.type')}: {fieldTypeName}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
OutputFieldTooltip.displayName = 'OutputFieldTooltip';
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
|
||||
import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const OutputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useOutputFieldName(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<OutputFieldWrapper>
|
||||
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
|
||||
{t('nodes.unknownOutput', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
OutputFieldUnknownPlaceholder.displayName = 'OutputFieldUnknownPlaceholder';
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const sx = {
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.1s',
|
||||
justifyContent: 'flex-end',
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
export const OutputFieldWrapper = memo(({ children }: PropsWithChildren) => <Flex sx={sx}>{children}</Flex>);
|
||||
|
||||
OutputFieldWrapper.displayName = 'OutputFieldWrapper';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Input } from '@invoke-ai/ui-library';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/useStringField';
|
||||
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const StringFieldInput = memo(
|
||||
(props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
|
||||
const { value, onChange } = useStringField(props);
|
||||
|
||||
return <Input className="nodrag nowheel nopan" value={value} onChange={onChange} />;
|
||||
}
|
||||
);
|
||||
|
||||
StringFieldInput.displayName = 'StringFieldInput';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Textarea } from '@invoke-ai/ui-library';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/useStringField';
|
||||
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const StringFieldTextarea = memo(
|
||||
(props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
|
||||
const { value, onChange } = useStringField(props);
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
className="nodrag nowheel nopan"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
h="full"
|
||||
resize="none"
|
||||
fontSize="sm"
|
||||
p={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
StringFieldTextarea.displayName = 'StringFieldTextarea';
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Input, Textarea } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { fieldStringValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { FieldComponentProps } from './types';
|
||||
|
||||
const StringFieldInputComponent = (props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
|
||||
export const useStringField = (props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleValueChanged = useCallback(
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
dispatch(
|
||||
fieldStringValueChanged({
|
||||
@@ -24,11 +22,9 @@ const StringFieldInputComponent = (props: FieldComponentProps<StringFieldInputIn
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
|
||||
if (fieldTemplate.ui_component === 'textarea') {
|
||||
return <Textarea className="nodrag" onChange={handleValueChanged} value={field.value} rows={5} resize="none" />;
|
||||
}
|
||||
|
||||
return <Input className="nodrag" onChange={handleValueChanged} value={field.value} />;
|
||||
return {
|
||||
value: field.value,
|
||||
onChange,
|
||||
defaultValue: fieldTemplate.default,
|
||||
};
|
||||
};
|
||||
|
||||
export default memo(StringFieldInputComponent);
|
||||
@@ -27,7 +27,7 @@ const BooleanFieldInputComponent = (
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
|
||||
return <Switch className="nodrag" onChange={handleValueChanged} isChecked={field.value}></Switch>;
|
||||
return <Switch className="nodrag" onChange={handleValueChanged} isChecked={field.value} />;
|
||||
};
|
||||
|
||||
export default memo(BooleanFieldInputComponent);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { hexToRGBA, rgbaToHex } from 'common/util/colorCodeTransformers';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
@@ -49,7 +49,7 @@ const ColorFieldInputComponent = (props: FieldComponentProps<ColorFieldInputInst
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Flex flexDir="column" gap={2} w="full">
|
||||
<HexColorInput
|
||||
style={{
|
||||
background: colorTokenToCssVar('base.700'),
|
||||
@@ -67,7 +67,7 @@ const ColorFieldInputComponent = (props: FieldComponentProps<ColorFieldInputInst
|
||||
alpha
|
||||
/>
|
||||
<RgbaColorPicker className="nodrag" color={color} onChange={handleValueChanged} style={{ width: '100%' }} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Button,
|
||||
CompositeNumberInput,
|
||||
Divider,
|
||||
Flex,
|
||||
FormLabel,
|
||||
Grid,
|
||||
GridItem,
|
||||
IconButton,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { NUMPY_RAND_MAX } from 'app/constants';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
|
||||
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
|
||||
import { fieldFloatCollectionValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { FloatFieldCollectionInputInstance, FloatFieldCollectionInputTemplate } from 'features/nodes/types/field';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
|
||||
import type { FieldComponentProps } from './types';
|
||||
|
||||
const overlayscrollbarsOptions = getOverlayScrollbarsParams().options;
|
||||
|
||||
const sx = {
|
||||
borderWidth: 1,
|
||||
'&[data-error=true]': {
|
||||
borderColor: 'error.500',
|
||||
borderStyle: 'solid',
|
||||
},
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
export const FloatFieldCollectionInputComponent = memo(
|
||||
(props: FieldComponentProps<FloatFieldCollectionInputInstance, FloatFieldCollectionInputTemplate>) => {
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const store = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
|
||||
|
||||
const onChangeValue = useCallback(
|
||||
(value: FloatFieldCollectionInputInstance['value']) => {
|
||||
store.dispatch(fieldFloatCollectionValueChanged({ nodeId, fieldName: field.name, value }));
|
||||
},
|
||||
[field.name, nodeId, store]
|
||||
);
|
||||
const onRemoveNumber = useCallback(
|
||||
(index: number) => {
|
||||
const newValue = field.value ? [...field.value] : [];
|
||||
newValue.splice(index, 1);
|
||||
onChangeValue(newValue);
|
||||
},
|
||||
[field.value, onChangeValue]
|
||||
);
|
||||
|
||||
const onChangeNumber = useCallback(
|
||||
(index: number, value: number) => {
|
||||
const newValue = field.value ? [...field.value] : [];
|
||||
newValue[index] = value;
|
||||
onChangeValue(newValue);
|
||||
},
|
||||
[field.value, onChangeValue]
|
||||
);
|
||||
|
||||
const onAddNumber = useCallback(() => {
|
||||
const newValue = field.value ? [...field.value, 0] : [0];
|
||||
onChangeValue(newValue);
|
||||
}, [field.value, onChangeValue]);
|
||||
|
||||
const min = useMemo(() => {
|
||||
let min = -NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.minimum)) {
|
||||
min = fieldTemplate.minimum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMinimum)) {
|
||||
min = fieldTemplate.exclusiveMinimum + 0.01;
|
||||
}
|
||||
return min;
|
||||
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
let max = NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.maximum)) {
|
||||
max = fieldTemplate.maximum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMaximum)) {
|
||||
max = fieldTemplate.exclusiveMaximum - 0.01;
|
||||
}
|
||||
return max;
|
||||
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
|
||||
|
||||
const step = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 0.1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
|
||||
const fineStep = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 0.01;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
className="nodrag"
|
||||
position="relative"
|
||||
w="full"
|
||||
h="auto"
|
||||
maxH={64}
|
||||
alignItems="stretch"
|
||||
justifyContent="center"
|
||||
p={1}
|
||||
sx={sx}
|
||||
data-error={isInvalid}
|
||||
borderRadius="base"
|
||||
flexDir="column"
|
||||
gap={1}
|
||||
>
|
||||
<Button onClick={onAddNumber} variant="ghost">
|
||||
{t('nodes.addItem')}
|
||||
</Button>
|
||||
{field.value && field.value.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<OverlayScrollbarsComponent
|
||||
className="nowheel"
|
||||
defer
|
||||
style={overlayScrollbarsStyles}
|
||||
options={overlayscrollbarsOptions}
|
||||
>
|
||||
<Grid gap={1} gridTemplateColumns="auto 1fr auto" alignItems="center">
|
||||
{field.value.map((value, index) => (
|
||||
<FloatListItemContent
|
||||
key={index}
|
||||
value={value}
|
||||
index={index}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
onRemoveNumber={onRemoveNumber}
|
||||
onChangeNumber={onChangeNumber}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</OverlayScrollbarsComponent>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FloatFieldCollectionInputComponent.displayName = 'FloatFieldCollectionInputComponent';
|
||||
|
||||
type FloatListItemContentProps = {
|
||||
value: number;
|
||||
index: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
fineStep: number;
|
||||
onRemoveNumber: (index: number) => void;
|
||||
onChangeNumber: (index: number, value: number) => void;
|
||||
};
|
||||
|
||||
const FloatListItemContent = memo(
|
||||
({ value, index, min, max, step, fineStep, onRemoveNumber, onChangeNumber }: FloatListItemContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClickRemove = useCallback(() => {
|
||||
onRemoveNumber(index);
|
||||
}, [index, onRemoveNumber]);
|
||||
const onChange = useCallback(
|
||||
(value: number) => {
|
||||
onChangeNumber(index, value);
|
||||
},
|
||||
[index, onChangeNumber]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GridItem>
|
||||
<FormLabel ps={1} m={0}>
|
||||
{index + 1}.
|
||||
</FormLabel>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<CompositeNumberInput
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className="nodrag"
|
||||
flexGrow={1}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={onClickRemove}
|
||||
icon={<PiXBold />}
|
||||
aria-label={t('common.delete')}
|
||||
/>
|
||||
</GridItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
FloatListItemContent.displayName = 'FloatListItemContent';
|
||||
@@ -10,7 +10,7 @@ import { addImagesToNodeImageFieldCollectionDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
|
||||
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
|
||||
import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { ImageField } from 'features/nodes/types/common';
|
||||
import type { ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
|
||||
@@ -39,7 +39,7 @@ export const ImageFieldCollectionInputComponent = memo(
|
||||
const { nodeId, field } = props;
|
||||
const store = useAppStore();
|
||||
|
||||
const isInvalid = useFieldIsInvalid(nodeId, field.name);
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
|
||||
|
||||
const dndTargetData = useMemo<AddImagesToNodeImageFieldCollection>(
|
||||
() =>
|
||||
@@ -132,7 +132,7 @@ const ImageGridItemContent = memo(
|
||||
}
|
||||
|
||||
if (!query.data) {
|
||||
return <IAINoContentFallback icon={<PiExclamationMarkBold />} />;
|
||||
return <IAINoContentFallback icon={PiExclamationMarkBold} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,11 +12,9 @@ import {
|
||||
import { NUMPY_RAND_MAX } from 'app/constants';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
|
||||
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
|
||||
import { fieldNumberCollectionValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
|
||||
import { fieldIntegerCollectionValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type {
|
||||
FloatFieldCollectionInputInstance,
|
||||
FloatFieldCollectionInputTemplate,
|
||||
IntegerFieldCollectionInputInstance,
|
||||
IntegerFieldCollectionInputTemplate,
|
||||
} from 'features/nodes/types/field';
|
||||
@@ -38,41 +36,43 @@ const sx = {
|
||||
},
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
export const NumberFieldCollectionInputComponent = memo(
|
||||
(
|
||||
props:
|
||||
| FieldComponentProps<IntegerFieldCollectionInputInstance, IntegerFieldCollectionInputTemplate>
|
||||
| FieldComponentProps<FloatFieldCollectionInputInstance, FloatFieldCollectionInputTemplate>
|
||||
) => {
|
||||
export const IntegerFieldCollectionInputComponent = memo(
|
||||
(props: FieldComponentProps<IntegerFieldCollectionInputInstance, IntegerFieldCollectionInputTemplate>) => {
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const store = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isInvalid = useFieldIsInvalid(nodeId, field.name);
|
||||
const isIntegerField = useMemo(() => fieldTemplate.type.name === 'IntegerField', [fieldTemplate.type]);
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
|
||||
|
||||
const onChangeValue = useCallback(
|
||||
(value: IntegerFieldCollectionInputInstance['value']) => {
|
||||
store.dispatch(fieldIntegerCollectionValueChanged({ nodeId, fieldName: field.name, value }));
|
||||
},
|
||||
[field.name, nodeId, store]
|
||||
);
|
||||
|
||||
const onRemoveNumber = useCallback(
|
||||
(index: number) => {
|
||||
const newValue = field.value ? [...field.value] : [];
|
||||
newValue.splice(index, 1);
|
||||
store.dispatch(fieldNumberCollectionValueChanged({ nodeId, fieldName: field.name, value: newValue }));
|
||||
onChangeValue(newValue);
|
||||
},
|
||||
[field.name, field.value, nodeId, store]
|
||||
[field.value, onChangeValue]
|
||||
);
|
||||
|
||||
const onChangeNumber = useCallback(
|
||||
(index: number, value: number) => {
|
||||
const newValue = field.value ? [...field.value] : [];
|
||||
newValue[index] = value;
|
||||
store.dispatch(fieldNumberCollectionValueChanged({ nodeId, fieldName: field.name, value: newValue }));
|
||||
onChangeValue(newValue);
|
||||
},
|
||||
[field.name, field.value, nodeId, store]
|
||||
[field.value, onChangeValue]
|
||||
);
|
||||
|
||||
const onAddNumber = useCallback(() => {
|
||||
const newValue = field.value ? [...field.value, 0] : [0];
|
||||
store.dispatch(fieldNumberCollectionValueChanged({ nodeId, fieldName: field.name, value: newValue }));
|
||||
}, [field.name, field.value, nodeId, store]);
|
||||
onChangeValue(newValue);
|
||||
}, [field.value, onChangeValue]);
|
||||
|
||||
const min = useMemo(() => {
|
||||
let min = -NUMPY_RAND_MAX;
|
||||
@@ -98,17 +98,17 @@ export const NumberFieldCollectionInputComponent = memo(
|
||||
|
||||
const step = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return isIntegerField ? 1 : 0.1;
|
||||
return 1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf, isIntegerField]);
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
|
||||
const fineStep = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return isIntegerField ? 1 : 0.01;
|
||||
return 1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf, isIntegerField]);
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -140,7 +140,7 @@ export const NumberFieldCollectionInputComponent = memo(
|
||||
>
|
||||
<Grid gap={1} gridTemplateColumns="auto 1fr auto" alignItems="center">
|
||||
{field.value.map((value, index) => (
|
||||
<NumberListItemContent
|
||||
<IntegerListItemContent
|
||||
key={index}
|
||||
value={value}
|
||||
index={index}
|
||||
@@ -148,7 +148,6 @@ export const NumberFieldCollectionInputComponent = memo(
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
isIntegerField={isIntegerField}
|
||||
onRemoveNumber={onRemoveNumber}
|
||||
onChangeNumber={onChangeNumber}
|
||||
/>
|
||||
@@ -162,12 +161,11 @@ export const NumberFieldCollectionInputComponent = memo(
|
||||
}
|
||||
);
|
||||
|
||||
NumberFieldCollectionInputComponent.displayName = 'NumberFieldCollectionInputComponent';
|
||||
IntegerFieldCollectionInputComponent.displayName = 'IntegerFieldCollectionInputComponent';
|
||||
|
||||
type NumberListItemContentProps = {
|
||||
type IntegerListItemContentProps = {
|
||||
value: number;
|
||||
index: number;
|
||||
isIntegerField: boolean;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
@@ -176,28 +174,18 @@ type NumberListItemContentProps = {
|
||||
onChangeNumber: (index: number, value: number) => void;
|
||||
};
|
||||
|
||||
const NumberListItemContent = memo(
|
||||
({
|
||||
value,
|
||||
index,
|
||||
isIntegerField,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
fineStep,
|
||||
onRemoveNumber,
|
||||
onChangeNumber,
|
||||
}: NumberListItemContentProps) => {
|
||||
const IntegerListItemContent = memo(
|
||||
({ value, index, min, max, step, fineStep, onRemoveNumber, onChangeNumber }: IntegerListItemContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClickRemove = useCallback(() => {
|
||||
onRemoveNumber(index);
|
||||
}, [index, onRemoveNumber]);
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
onChangeNumber(index, isIntegerField ? Math.floor(Number(v)) : Number(v));
|
||||
(value: number) => {
|
||||
onChangeNumber(index, Math.floor(value));
|
||||
},
|
||||
[index, isIntegerField, onChangeNumber]
|
||||
[index, onChangeNumber]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -234,4 +222,4 @@ const NumberListItemContent = memo(
|
||||
);
|
||||
}
|
||||
);
|
||||
NumberListItemContent.displayName = 'NumberListItemContent';
|
||||
IntegerListItemContent.displayName = 'IntegerListItemContent';
|
||||
@@ -1,89 +0,0 @@
|
||||
import { CompositeNumberInput } from '@invoke-ai/ui-library';
|
||||
import { NUMPY_RAND_MAX } from 'app/constants';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { fieldNumberValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type {
|
||||
FloatFieldInputInstance,
|
||||
FloatFieldInputTemplate,
|
||||
IntegerFieldInputInstance,
|
||||
IntegerFieldInputTemplate,
|
||||
} from 'features/nodes/types/field';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import type { FieldComponentProps } from './types';
|
||||
|
||||
const NumberFieldInputComponent = (
|
||||
props: FieldComponentProps<
|
||||
IntegerFieldInputInstance | FloatFieldInputInstance,
|
||||
IntegerFieldInputTemplate | FloatFieldInputTemplate
|
||||
>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const isIntegerField = useMemo(() => fieldTemplate.type.name === 'IntegerField', [fieldTemplate.type]);
|
||||
|
||||
const handleValueChanged = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(
|
||||
fieldNumberValueChanged({
|
||||
nodeId,
|
||||
fieldName: field.name,
|
||||
value: isIntegerField ? Math.floor(Number(v)) : Number(v),
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, field.name, isIntegerField, nodeId]
|
||||
);
|
||||
|
||||
const min = useMemo(() => {
|
||||
let min = -NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.minimum)) {
|
||||
min = fieldTemplate.minimum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMinimum)) {
|
||||
min = fieldTemplate.exclusiveMinimum + 0.01;
|
||||
}
|
||||
return min;
|
||||
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
let max = NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.maximum)) {
|
||||
max = fieldTemplate.maximum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMaximum)) {
|
||||
max = fieldTemplate.exclusiveMaximum - 0.01;
|
||||
}
|
||||
return max;
|
||||
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
|
||||
|
||||
const step = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return isIntegerField ? 1 : 0.1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf, isIntegerField]);
|
||||
|
||||
const fineStep = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return isIntegerField ? 1 : 0.01;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf, isIntegerField]);
|
||||
|
||||
return (
|
||||
<CompositeNumberInput
|
||||
defaultValue={fieldTemplate.default}
|
||||
onChange={handleValueChanged}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className="nodrag"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NumberFieldInputComponent);
|
||||
@@ -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) => {
|
||||
|
||||
@@ -29,8 +29,7 @@ export const StringGeneratorDynamicPromptsCombinatorialSettings = memo(
|
||||
);
|
||||
|
||||
const arg = useMemo(() => {
|
||||
const { input, maxPrompts } = state;
|
||||
return { prompt: input, max_prompts: maxPrompts, combinatorial: true };
|
||||
return { prompt: state.input, max_prompts: state.maxPrompts, combinatorial: true };
|
||||
}, [state]);
|
||||
const [debouncedArg] = useDebounce(arg, 300);
|
||||
|
||||
@@ -38,13 +37,16 @@ export const StringGeneratorDynamicPromptsCombinatorialSettings = memo(
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
onChange({ ...state, values: loadingValues });
|
||||
} else if (data) {
|
||||
onChange({ ...state, values: data.prompts });
|
||||
} else {
|
||||
onChange({ ...state, values: [] });
|
||||
return;
|
||||
}
|
||||
}, [data, isLoading, loadingValues, onChange, state]);
|
||||
|
||||
if (!data) {
|
||||
onChange({ ...state, values: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({ ...state, values: data.prompts });
|
||||
}, [data, isLoading, onChange, state]);
|
||||
|
||||
return (
|
||||
<Flex gap={2} flexDir="column">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Checkbox, CompositeNumberInput, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { GeneratorTextareaWithFileUpload } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/GeneratorTextareaWithFileUpload';
|
||||
import { useViewContext } from 'features/nodes/contexts/ViewContext';
|
||||
import type { StringGeneratorDynamicPromptsRandom } from 'features/nodes/types/field';
|
||||
import { isNil, random } from 'lodash-es';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
@@ -14,6 +15,7 @@ type StringGeneratorDynamicPromptsRandomSettingsProps = {
|
||||
export const StringGeneratorDynamicPromptsRandomSettings = memo(
|
||||
({ state, onChange }: StringGeneratorDynamicPromptsRandomSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const view = useViewContext();
|
||||
const loadingValues = useMemo(() => [`<${t('nodes.generatorLoading')}>`], [t]);
|
||||
|
||||
const onChangeInput = useCallback(
|
||||
@@ -39,22 +41,25 @@ export const StringGeneratorDynamicPromptsRandomSettings = memo(
|
||||
);
|
||||
|
||||
const arg = useMemo(() => {
|
||||
const { input, count, seed } = state;
|
||||
return { prompt: input, max_prompts: count, combinatorial: false, seed: seed ?? random() };
|
||||
}, [state]);
|
||||
return { prompt: state.input, max_prompts: state.count, combinatorial: false, seed: state.seed ?? random() };
|
||||
}, [state.count, state.input, state.seed]);
|
||||
|
||||
const [debouncedArg] = useDebounce(arg, 300);
|
||||
|
||||
const { data, isLoading } = useDynamicPromptsQuery(debouncedArg);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
onChange({ ...state, values: loadingValues });
|
||||
} else if (data) {
|
||||
onChange({ ...state, values: data.prompts });
|
||||
} else {
|
||||
onChange({ ...state, values: [] });
|
||||
return;
|
||||
}
|
||||
}, [data, isLoading, loadingValues, onChange, state]);
|
||||
|
||||
if (!data) {
|
||||
onChange({ ...state, values: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({ ...state, values: data.prompts });
|
||||
}, [data, isLoading, onChange, state, view]);
|
||||
|
||||
return (
|
||||
<Flex gap={2} flexDir="column">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box, Flex, Textarea } from '@invoke-ai/ui-library';
|
||||
import type { Node, NodeProps } from '@xyflow/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
|
||||
import NodeTitle from 'features/nodes/components/flow/nodes/common/NodeTitle';
|
||||
@@ -7,9 +8,8 @@ import { notesNodeValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { NotesNodeData } from 'features/nodes/types/invocation';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import type { NodeProps } from 'reactflow';
|
||||
|
||||
const NotesNode = (props: NodeProps<NotesNodeData>) => {
|
||||
const NotesNode = (props: NodeProps<Node<NotesNodeData>>) => {
|
||||
const { id: nodeId, data, selected } = props;
|
||||
const { notes, isOpen } = data;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||
import { Box, useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
|
||||
import type { ChakraProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library';
|
||||
import type { NodeChange } from '@xyflow/react';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
|
||||
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import { nodesChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodes } from 'features/nodes/store/selectors';
|
||||
import { selectNodeOpacity } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from 'features/nodes/types/constants';
|
||||
import type { AnyNode } from 'features/nodes/types/invocation';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import type { MouseEvent, PropsWithChildren } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import type { NodeChange } from 'reactflow';
|
||||
|
||||
type NodeWrapperProps = PropsWithChildren & {
|
||||
nodeId: string;
|
||||
@@ -19,20 +19,75 @@ type NodeWrapperProps = PropsWithChildren & {
|
||||
width?: ChakraProps['w'];
|
||||
};
|
||||
|
||||
const containerSx: SystemStyleObject = {
|
||||
h: 'full',
|
||||
position: 'relative',
|
||||
borderRadius: 'base',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
cursor: 'grab',
|
||||
};
|
||||
|
||||
const shadowsSx: SystemStyleObject = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineEnd: 0,
|
||||
bottom: 0,
|
||||
insetInlineStart: 0,
|
||||
borderRadius: 'base',
|
||||
pointerEvents: 'none',
|
||||
zIndex: -1,
|
||||
shadow: 'var(--invoke-shadows-xl), var(--invoke-shadows-base), var(--invoke-shadows-base)',
|
||||
};
|
||||
|
||||
const inProgressSx: SystemStyleObject = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineEnd: 0,
|
||||
bottom: 0,
|
||||
insetInlineStart: 0,
|
||||
borderRadius: 'md',
|
||||
pointerEvents: 'none',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
opacity: 0.7,
|
||||
zIndex: -1,
|
||||
visibility: 'hidden',
|
||||
shadow: '0 0 0 2px var(--invoke-colors-yellow-400), 0 0 20px 2px var(--invoke-colors-orange-700)',
|
||||
'&[data-is-in-progress="true"]': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
};
|
||||
|
||||
const selectionOverlaySx: SystemStyleObject = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineEnd: 0,
|
||||
bottom: 0,
|
||||
insetInlineStart: 0,
|
||||
borderRadius: 'base',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
pointerEvents: 'none',
|
||||
visibility: 'hidden',
|
||||
opacity: 0.5,
|
||||
'&[data-is-selected="true"], &[data-is-hovered="true"]': { visibility: 'visible' },
|
||||
'&[data-is-selected="true"]': { shadow: '0 0 0 3px var(--invoke-colors-blue-300)' },
|
||||
'&[data-is-hovered="true"]': { shadow: '0 0 0 2px var(--invoke-colors-blue-300)' },
|
||||
'&[data-is-selected="true"][data-is-hovered="true"]': {
|
||||
opacity: 1,
|
||||
shadow: '0 0 0 3px var(--invoke-colors-blue-300)',
|
||||
},
|
||||
};
|
||||
|
||||
const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
const { nodeId, width, children, selected } = props;
|
||||
const store = useAppStore();
|
||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
|
||||
|
||||
const executionState = useExecutionState(nodeId);
|
||||
const executionState = useNodeExecutionState(nodeId);
|
||||
const isInProgress = executionState?.status === zNodeStatus.enum.IN_PROGRESS;
|
||||
|
||||
const [nodeInProgress, shadowsXl, shadowsBase] = useToken('shadows', [
|
||||
'nodeInProgress',
|
||||
'shadows.xl',
|
||||
'shadows.base',
|
||||
]);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const opacity = useAppSelector(selectNodeOpacity);
|
||||
@@ -42,7 +97,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) {
|
||||
const nodes = selectNodes(store.getState());
|
||||
const nodeChanges: NodeChange[] = [];
|
||||
const nodeChanges: NodeChange<AnyNode>[] = [];
|
||||
nodes.forEach(({ id, selected }) => {
|
||||
if (selected !== (id === nodeId)) {
|
||||
nodeChanges.push({ type: 'select', id, selected: id === nodeId });
|
||||
@@ -63,42 +118,14 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
onMouseEnter={handleMouseOver}
|
||||
onMouseLeave={handleMouseOut}
|
||||
className={DRAG_HANDLE_CLASSNAME}
|
||||
h="full"
|
||||
position="relative"
|
||||
borderRadius="base"
|
||||
w={width ? width : NODE_WIDTH}
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
cursor="grab"
|
||||
sx={containerSx}
|
||||
width={width || NODE_WIDTH}
|
||||
opacity={opacity}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
insetInlineEnd={0}
|
||||
bottom={0}
|
||||
insetInlineStart={0}
|
||||
borderRadius="base"
|
||||
pointerEvents="none"
|
||||
shadow={`${shadowsXl}, ${shadowsBase}, ${shadowsBase}`}
|
||||
zIndex={-1}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
insetInlineEnd={0}
|
||||
bottom={0}
|
||||
insetInlineStart={0}
|
||||
borderRadius="md"
|
||||
pointerEvents="none"
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
opacity={0.7}
|
||||
shadow={isInProgress ? nodeInProgress : undefined}
|
||||
zIndex={-1}
|
||||
/>
|
||||
<Box sx={shadowsSx} />
|
||||
<Box sx={inProgressSx} data-is-in-progress={isInProgress} />
|
||||
{children}
|
||||
<NodeSelectionOverlay isSelected={selected} isHovered={isMouseOverNode} />
|
||||
<Box sx={selectionOverlaySx} data-is-selected={selected} data-is-hovered={isMouseOverNode} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { ViewContextProvider } from 'features/nodes/contexts/ViewContext';
|
||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
|
||||
import WorkflowNodeInspectorPanel from './inspector/WorkflowNodeInspectorPanel';
|
||||
import WorkflowFieldsLinearViewPanel from './workflow/WorkflowPanel';
|
||||
|
||||
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
|
||||
|
||||
export const EditModeLeftPanelContent = memo(() => {
|
||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
|
||||
const handleDoubleClickHandle = useCallback(() => {
|
||||
if (!panelGroupRef.current) {
|
||||
return;
|
||||
}
|
||||
panelGroupRef.current.setLayout([50, 50]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ViewContextProvider view="edit-mode-linear">
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
<PanelGroup
|
||||
ref={panelGroupRef}
|
||||
id="workflow-panel-group"
|
||||
autoSaveId="workflow-panel-group"
|
||||
direction="vertical"
|
||||
style={panelGroupStyles}
|
||||
>
|
||||
<Panel id="workflow" collapsible minSize={25}>
|
||||
<WorkflowFieldsLinearViewPanel />
|
||||
</Panel>
|
||||
<ResizeHandle onDoubleClick={handleDoubleClickHandle} />
|
||||
<Panel id="inspector" collapsible minSize={25}>
|
||||
<WorkflowNodeInspectorPanel />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
</ViewContextProvider>
|
||||
);
|
||||
});
|
||||
|
||||
EditModeLeftPanelContent.displayName = 'EditModeLeftPanelContent';
|
||||
@@ -1,79 +0,0 @@
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||
import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
|
||||
import InspectorPanel from './inspector/InspectorPanel';
|
||||
import { WorkflowViewMode } from './viewMode/WorkflowViewMode';
|
||||
import WorkflowPanel from './workflow/WorkflowPanel';
|
||||
import { WorkflowListMenu } from './WorkflowListMenu/WorkflowListMenu';
|
||||
import { WorkflowListMenuTrigger } from './WorkflowListMenu/WorkflowListMenuTrigger';
|
||||
|
||||
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
|
||||
|
||||
const overlayScrollbarsStyles: CSSProperties = {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const NodeEditorPanelGroup = () => {
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const workflowListMenu = useWorkflowListMenu();
|
||||
|
||||
const handleDoubleClickHandle = useCallback(() => {
|
||||
if (!panelGroupRef.current) {
|
||||
return;
|
||||
}
|
||||
panelGroupRef.current.setLayout([50, 50]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
<WorkflowListMenuTrigger />
|
||||
<Flex w="full" h="full" position="relative">
|
||||
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
||||
{workflowListMenu.isOpen && (
|
||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||
<WorkflowListMenu />
|
||||
</Flex>
|
||||
</OverlayScrollbarsComponent>
|
||||
)}
|
||||
|
||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||
{mode === 'view' && <WorkflowViewMode />}
|
||||
{mode === 'edit' && (
|
||||
<PanelGroup
|
||||
ref={panelGroupRef}
|
||||
id="workflow-panel-group"
|
||||
autoSaveId="workflow-panel-group"
|
||||
direction="vertical"
|
||||
style={panelGroupStyles}
|
||||
>
|
||||
<Panel id="workflow" collapsible minSize={25}>
|
||||
<WorkflowPanel />
|
||||
</Panel>
|
||||
<ResizeHandle onDoubleClick={handleDoubleClickHandle} />
|
||||
<Panel id="inspector" collapsible minSize={25}>
|
||||
<InspectorPanel />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
)}
|
||||
</OverlayScrollbarsComponent>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NodeEditorPanelGroup);
|
||||
@@ -1,28 +1,34 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import UploadWorkflowButton from 'features/workflowLibrary/components/UploadWorkflowButton';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { WorkflowList } from './WorkflowList';
|
||||
import WorkflowSearch from './WorkflowSearch';
|
||||
import { WorkflowSortControl } from './WorkflowSortControl';
|
||||
|
||||
export const WorkflowListMenu = () => {
|
||||
export const WorkflowListMenu = memo(() => {
|
||||
const workflowCategories = useStore($workflowCategories);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
|
||||
<Flex w="full" h="full" flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
|
||||
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
||||
<WorkflowSearch />
|
||||
|
||||
<WorkflowSortControl />
|
||||
|
||||
<UploadWorkflowButton />
|
||||
</Flex>
|
||||
|
||||
{workflowCategories.map((category) => (
|
||||
<WorkflowList key={category} category={category} />
|
||||
))}
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
{workflowCategories.map((category) => (
|
||||
<WorkflowList key={category} category={category} />
|
||||
))}
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
WorkflowListMenu.displayName = 'WorkflowListMenu';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { EditModeLeftPanelContent } from 'features/nodes/components/sidePanel/EditModeLeftPanelContent';
|
||||
import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { ViewModeLeftPanelContent } from './viewMode/ViewModeLeftPanelContent';
|
||||
import { WorkflowListMenu } from './WorkflowListMenu/WorkflowListMenu';
|
||||
import { WorkflowListMenuTrigger } from './WorkflowListMenu/WorkflowListMenuTrigger';
|
||||
|
||||
const WorkflowsTabLeftPanel = () => {
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
const workflowListMenu = useWorkflowListMenu();
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
<WorkflowListMenuTrigger />
|
||||
<Flex w="full" h="full" position="relative">
|
||||
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
||||
{workflowListMenu.isOpen && <WorkflowListMenu />}
|
||||
{mode === 'view' && <ViewModeLeftPanelContent />}
|
||||
{mode === 'edit' && <EditModeLeftPanelContent />}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowsTabLeftPanel);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { TextareaProps } from '@invoke-ai/ui-library';
|
||||
import { chakra, forwardRef, typedMemo, useStyleConfig } from '@invoke-ai/ui-library';
|
||||
import type { ComponentProps } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
|
||||
const ChakraTextareaAutosize = chakra(TextareaAutosize);
|
||||
|
||||
export const AutosizeTextarea = typedMemo(
|
||||
forwardRef<ComponentProps<typeof ChakraTextareaAutosize> & TextareaProps, typeof ChakraTextareaAutosize>(
|
||||
({ variant, ...rest }, ref) => {
|
||||
const styles = useStyleConfig('Textarea', { variant });
|
||||
return <ChakraTextareaAutosize __css={styles} ref={ref} {...rest} />;
|
||||
}
|
||||
)
|
||||
);
|
||||
AutosizeTextarea.displayName = 'AutosizeTextarea';
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Flex, IconButton, type SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
ContainerContextProvider,
|
||||
DepthContextProvider,
|
||||
useDepthContext,
|
||||
} from 'features/nodes/components/sidePanel/builder/contexts';
|
||||
import { DividerElementComponent } from 'features/nodes/components/sidePanel/builder/DividerElementComponent';
|
||||
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
|
||||
import { HeadingElementComponent } from 'features/nodes/components/sidePanel/builder/HeadingElementComponent';
|
||||
import { NodeFieldElementComponent } from 'features/nodes/components/sidePanel/builder/NodeFieldElementComponent';
|
||||
import { TextElementComponent } from 'features/nodes/components/sidePanel/builder/TextElementComponent';
|
||||
import { formElementAdded, selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import type { ContainerElement } from 'features/nodes/types/workflow';
|
||||
import {
|
||||
buildContainer,
|
||||
CONTAINER_CLASS_NAME,
|
||||
isContainerElement,
|
||||
isDividerElement,
|
||||
isHeadingElement,
|
||||
isNodeFieldElement,
|
||||
isTextElement,
|
||||
} from 'features/nodes/types/workflow';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const sx: SystemStyleObject = {
|
||||
gap: 4,
|
||||
flex: '1 1 0',
|
||||
'&[data-container-direction="column"]': {
|
||||
flexDir: 'column',
|
||||
},
|
||||
'&[data-container-direction="row"]': {
|
||||
flexDir: 'row',
|
||||
},
|
||||
};
|
||||
|
||||
export const ContainerElementComponent = memo(({ id }: { id: string }) => {
|
||||
const el = useElement(id);
|
||||
const mode = useAppSelector(selectWorkflowFormMode);
|
||||
|
||||
if (!el || !isContainerElement(el)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mode === 'view') {
|
||||
return <ContainerElementComponentViewMode el={el} />;
|
||||
}
|
||||
|
||||
// mode === 'edit'
|
||||
return <ContainerElementComponentEditMode el={el} />;
|
||||
});
|
||||
ContainerElementComponent.displayName = 'ContainerElementComponent';
|
||||
|
||||
export const ContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }) => {
|
||||
const depth = useDepthContext();
|
||||
const { id, data } = el;
|
||||
const { children, direction } = data;
|
||||
|
||||
return (
|
||||
<DepthContextProvider depth={depth + 1}>
|
||||
<ContainerContextProvider id={id} direction={direction}>
|
||||
<Flex id={id} className={CONTAINER_CLASS_NAME} sx={sx} data-container-direction={direction}>
|
||||
{children.map((childId) => (
|
||||
<FormElementComponent key={childId} id={childId} />
|
||||
))}
|
||||
</Flex>
|
||||
</ContainerContextProvider>
|
||||
</DepthContextProvider>
|
||||
);
|
||||
});
|
||||
ContainerElementComponentViewMode.displayName = 'ContainerElementComponentViewMode';
|
||||
|
||||
export const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }) => {
|
||||
const depth = useDepthContext();
|
||||
const { id, data } = el;
|
||||
const { children, direction } = data;
|
||||
|
||||
return (
|
||||
<FormElementEditModeWrapper element={el}>
|
||||
<DepthContextProvider depth={depth + 1}>
|
||||
<ContainerContextProvider id={id} direction={direction}>
|
||||
<Flex id={id} className={CONTAINER_CLASS_NAME} sx={sx} data-container-direction={direction}>
|
||||
{children.map((childId) => (
|
||||
<FormElementComponent key={childId} id={childId} />
|
||||
))}
|
||||
{direction === 'row' && children.length < 3 && depth < 2 && <AddColumnButton el={el} />}
|
||||
{direction === 'column' && depth < 1 && <AddRowButton el={el} />}
|
||||
</Flex>
|
||||
</ContainerContextProvider>
|
||||
</DepthContextProvider>
|
||||
</FormElementEditModeWrapper>
|
||||
);
|
||||
});
|
||||
ContainerElementComponentEditMode.displayName = 'ContainerElementComponentEditMode';
|
||||
|
||||
const AddColumnButton = ({ el }: { el: ContainerElement }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
const element = buildContainer('column', [], el.id);
|
||||
dispatch(formElementAdded({ element, containerId: el.id }));
|
||||
}, [dispatch, el.id]);
|
||||
return (
|
||||
<IconButton onClick={onClick} aria-label="add column" icon={<PiPlusBold />} h="unset" variant="ghost" size="sm" />
|
||||
);
|
||||
};
|
||||
|
||||
const AddRowButton = ({ el }: { el: ContainerElement }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
const element = buildContainer('row', [], el.id);
|
||||
dispatch(formElementAdded({ element, containerId: el.id }));
|
||||
}, [dispatch, el.id]);
|
||||
return (
|
||||
<IconButton onClick={onClick} aria-label="add row" icon={<PiPlusBold />} w="unset" variant="ghost" size="sm" />
|
||||
);
|
||||
};
|
||||
|
||||
// TODO(psyche): Can we move this into a separate file and avoid circular dependencies between it and ContainerElementComponent?
|
||||
export const FormElementComponent = memo(({ id }: { id: string }) => {
|
||||
const el = useElement(id);
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isContainerElement(el)) {
|
||||
return <ContainerElementComponent key={id} id={id} />;
|
||||
}
|
||||
|
||||
if (isNodeFieldElement(el)) {
|
||||
return <NodeFieldElementComponent key={id} id={id} />;
|
||||
}
|
||||
|
||||
if (isDividerElement(el)) {
|
||||
return <DividerElementComponent key={id} id={id} />;
|
||||
}
|
||||
|
||||
if (isHeadingElement(el)) {
|
||||
return <HeadingElementComponent key={id} id={id} />;
|
||||
}
|
||||
|
||||
if (isTextElement(el)) {
|
||||
return <TextElementComponent key={id} id={id} />;
|
||||
}
|
||||
|
||||
assert<Equals<typeof el, never>>(false, `Unhandled type for element with id ${id}`);
|
||||
});
|
||||
FormElementComponent.displayName = 'FormElementComponent';
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
|
||||
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
|
||||
import { selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import type { DividerElement } from 'features/nodes/types/workflow';
|
||||
import { DIVIDER_CLASS_NAME, isDividerElement } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
|
||||
const sx: SystemStyleObject = {
|
||||
bg: 'base.700',
|
||||
flexShrink: 0,
|
||||
'&[data-orientation="horizontal"]': {
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
},
|
||||
'&[data-orientation="vertical"]': {
|
||||
height: '100%',
|
||||
width: '1px',
|
||||
},
|
||||
};
|
||||
|
||||
export const DividerElementComponent = memo(({ id }: { id: string }) => {
|
||||
const el = useElement(id);
|
||||
const mode = useAppSelector(selectWorkflowFormMode);
|
||||
|
||||
if (!el || !isDividerElement(el)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'view') {
|
||||
return <DividerElementComponentViewMode el={el} />;
|
||||
}
|
||||
|
||||
// mode === 'edit'
|
||||
return <DividerElementComponentEditMode el={el} />;
|
||||
});
|
||||
|
||||
DividerElementComponent.displayName = 'DividerElementComponent';
|
||||
|
||||
export const DividerElementComponentViewMode = memo(({ el }: { el: DividerElement }) => {
|
||||
const container = useContainerContext();
|
||||
const { id } = el;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
id={id}
|
||||
className={DIVIDER_CLASS_NAME}
|
||||
sx={sx}
|
||||
data-orientation={container?.direction === 'column' ? 'horizontal' : 'vertical'}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
DividerElementComponentViewMode.displayName = 'DividerElementComponentViewMode';
|
||||
|
||||
export const DividerElementComponentEditMode = memo(({ el }: { el: DividerElement }) => {
|
||||
const container = useContainerContext();
|
||||
const { id } = el;
|
||||
|
||||
return (
|
||||
<FormElementEditModeWrapper element={el}>
|
||||
<Flex
|
||||
id={id}
|
||||
className={DIVIDER_CLASS_NAME}
|
||||
sx={sx}
|
||||
data-orientation={container?.direction === 'column' ? 'horizontal' : 'vertical'}
|
||||
/>
|
||||
</FormElementEditModeWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
DividerElementComponentEditMode.displayName = 'DividerElementComponentEditMode';
|
||||
@@ -0,0 +1,127 @@
|
||||
// Adapted from https://github.com/alexreardon/pdnd-react-tailwind/blob/main/src/drop-indicator.tsx
|
||||
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/types';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import type { CenterOrEdge } from 'features/nodes/components/sidePanel/builder/center-or-closest-edge';
|
||||
|
||||
/**
|
||||
* Design decisions for the drop indicator's main line
|
||||
*/
|
||||
const line = {
|
||||
thickness: 2,
|
||||
backgroundColor: 'base.500',
|
||||
};
|
||||
|
||||
type DropIndicatorProps = {
|
||||
/**
|
||||
* The `edge` to draw a drop indicator on.
|
||||
*
|
||||
* `edge` is required as for the best possible performance
|
||||
* outcome you should only render this component when it needs to do something
|
||||
*
|
||||
* @example {closestEdge && <DropIndicator edge={closestEdge} />}
|
||||
*/
|
||||
edge: Edge;
|
||||
/**
|
||||
* `gap` allows you to position the drop indicator further away from the drop target.
|
||||
* `gap` should be the distance between your drop targets
|
||||
* a drop indicator will be rendered halfway between the drop targets
|
||||
* (the drop indicator will be offset by half of the `gap`)
|
||||
*
|
||||
* `gap` should be a valid CSS length.
|
||||
* @example "8px"
|
||||
* @example "var(--gap)"
|
||||
*/
|
||||
gap?: string;
|
||||
};
|
||||
|
||||
const lineStyles: SystemStyleObject = {
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
borderRadius: 'full',
|
||||
// Blocking pointer events to prevent the line from triggering drag events
|
||||
// Dragging over the line should count as dragging over the element behind it
|
||||
pointerEvents: 'none',
|
||||
background: line.backgroundColor,
|
||||
};
|
||||
|
||||
type Orientation = 'horizontal' | 'vertical';
|
||||
|
||||
const orientationStyles: Record<Orientation, SystemStyleObject> = {
|
||||
horizontal: {
|
||||
height: `${line.thickness}px`,
|
||||
left: 2,
|
||||
right: 2,
|
||||
},
|
||||
vertical: {
|
||||
width: `${line.thickness}px`,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const edgeToOrientationMap: Record<Edge, Orientation> = {
|
||||
top: 'horizontal',
|
||||
bottom: 'horizontal',
|
||||
left: 'vertical',
|
||||
right: 'vertical',
|
||||
};
|
||||
|
||||
const edgeStyles: Record<Edge, SystemStyleObject> = {
|
||||
top: {
|
||||
top: 'var(--local-line-offset)',
|
||||
},
|
||||
right: {
|
||||
right: 'var(--local-line-offset)',
|
||||
},
|
||||
bottom: {
|
||||
bottom: 'var(--local-line-offset)',
|
||||
},
|
||||
left: {
|
||||
left: 'var(--local-line-offset)',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* __Drop indicator__
|
||||
*
|
||||
* A drop indicator is used to communicate the intended resting place of the draggable item. The orientation of the drop indicator should always match the direction of the content flow.
|
||||
*/
|
||||
function DndDropIndicatorInternal({ edge, gap = '0px' }: DropIndicatorProps) {
|
||||
/**
|
||||
* To clearly communicate the resting place of a draggable item during a drag operation,
|
||||
* the drop indicator should be positioned half way between draggable items.
|
||||
*/
|
||||
const lineOffset = `calc(-0.5 * (${gap} + ${line.thickness}px))`;
|
||||
const orientation = edgeToOrientationMap[edge];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ ...lineStyles, ...orientationStyles[orientation], ...edgeStyles[edge], '--local-line-offset': lineOffset }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const DndListDropIndicator = ({
|
||||
activeDropRegion,
|
||||
gap,
|
||||
}: {
|
||||
activeDropRegion: CenterOrEdge | null;
|
||||
gap?: string;
|
||||
}) => {
|
||||
if (!activeDropRegion) {
|
||||
return null;
|
||||
}
|
||||
if (activeDropRegion === 'center') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DndDropIndicatorInternal
|
||||
edge={activeDropRegion}
|
||||
// This is the gap between items in the list, used to calculate the position of the drop indicator
|
||||
gap={gap || 'var(--invoke-space-2)'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
|
||||
import { DndListDropIndicator } from 'features/nodes/components/sidePanel/builder/DndListDropIndicator';
|
||||
import { EDIT_MODE_WRAPPER_CLASS_NAME, getEditModeWrapperId } from 'features/nodes/components/sidePanel/builder/shared';
|
||||
import { useDraggableFormElement } from 'features/nodes/components/sidePanel/builder/use-builder-dnd';
|
||||
import { formElementRemoved } from 'features/nodes/store/workflowSlice';
|
||||
import { type FormElement, isContainerElement } from 'features/nodes/types/workflow';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
|
||||
const getHeaderLabel = (el: FormElement) => {
|
||||
if (isContainerElement(el)) {
|
||||
if (el.data.direction === 'column') {
|
||||
return 'Column';
|
||||
}
|
||||
return 'Row';
|
||||
}
|
||||
return startCase(el.type);
|
||||
};
|
||||
|
||||
const wrapperSx: SystemStyleObject = {
|
||||
position: 'relative',
|
||||
flexDir: 'column',
|
||||
boxShadow: '0 0 0 1px var(--invoke-colors-base-750)',
|
||||
borderRadius: 'base',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
'&[data-is-dragging="true"]': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
'&[data-active-drop-region="center"]': {
|
||||
opacity: 1,
|
||||
bg: 'base.700',
|
||||
},
|
||||
};
|
||||
|
||||
const headerSx: SystemStyleObject = {
|
||||
w: 'full',
|
||||
ps: 2,
|
||||
h: 8,
|
||||
borderTopRadius: 'inherit',
|
||||
borderColor: 'inherit',
|
||||
alignItems: 'center',
|
||||
cursor: 'grab',
|
||||
bg: 'base.700',
|
||||
'&[data-depth="0"]': { bg: 'base.800' },
|
||||
'&[data-depth="1"]': { bg: 'base.800' },
|
||||
'&[data-depth="2"]': { bg: 'base.750' },
|
||||
};
|
||||
|
||||
export const FormElementEditModeWrapper = memo(
|
||||
({ element, children, ...rest }: { element: FormElement } & FlexProps) => {
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const dragHandleRef = useRef<HTMLDivElement>(null);
|
||||
const [activeDropRegion, isDragging] = useDraggableFormElement(element.id, draggableRef, dragHandleRef);
|
||||
const depth = useDepthContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const removeElement = useCallback(() => {
|
||||
dispatch(formElementRemoved({ id: element.id }));
|
||||
}, [dispatch, element.id]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
id={getEditModeWrapperId(element.id)}
|
||||
ref={draggableRef}
|
||||
sx={wrapperSx}
|
||||
className={EDIT_MODE_WRAPPER_CLASS_NAME}
|
||||
data-is-dragging={isDragging}
|
||||
data-active-drop-region={activeDropRegion}
|
||||
{...rest}
|
||||
>
|
||||
<Flex ref={dragHandleRef} sx={headerSx} data-depth={depth}>
|
||||
<Text fontWeight="semibold" noOfLines={1} wordBreak="break-all">
|
||||
{getHeaderLabel(element)} ({element.id})
|
||||
</Text>
|
||||
<Spacer />
|
||||
{element.parentId && (
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
onClick={removeElement}
|
||||
icon={<PiXBold />}
|
||||
variant="link"
|
||||
size="sm"
|
||||
alignSelf="stretch"
|
||||
colorScheme="error"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex w="full" p={4} alignItems="center" gap={4}>
|
||||
{children}
|
||||
</Flex>
|
||||
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FormElementEditModeWrapper.displayName = 'FormElementEditModeWrapper';
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
|
||||
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
|
||||
import { formElementHeadingDataChanged, selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import type { HeadingElement } from 'features/nodes/types/workflow';
|
||||
import { HEADING_CLASS_NAME, isHeadingElement } from 'features/nodes/types/workflow';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
|
||||
const LEVEL_TO_FONT_SIZE = {
|
||||
1: '4xl',
|
||||
2: '3xl',
|
||||
3: '2xl',
|
||||
4: 'xl',
|
||||
5: 'lg',
|
||||
} as const;
|
||||
|
||||
export const HeadingElementComponent = memo(({ id }: { id: string }) => {
|
||||
const el = useElement(id);
|
||||
const mode = useAppSelector(selectWorkflowFormMode);
|
||||
|
||||
if (!el || !isHeadingElement(el)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mode === 'view') {
|
||||
return <HeadingElementComponentViewMode el={el} />;
|
||||
}
|
||||
|
||||
// mode === 'edit'
|
||||
return <HeadingElementComponentEditMode el={el} />;
|
||||
});
|
||||
|
||||
HeadingElementComponent.displayName = 'HeadingElementComponent';
|
||||
|
||||
export const HeadingElementComponentViewMode = memo(({ el }: { el: HeadingElement }) => {
|
||||
const { id, data } = el;
|
||||
const { content, level } = data;
|
||||
|
||||
return (
|
||||
<Flex id={id} className={HEADING_CLASS_NAME}>
|
||||
<Text fontWeight="bold" fontSize={LEVEL_TO_FONT_SIZE[level]}>
|
||||
{content || 'Edit to add heading'}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
HeadingElementComponentViewMode.displayName = 'HeadingElementComponentViewMode';
|
||||
|
||||
export const HeadingElementComponentEditMode = memo(({ el }: { el: HeadingElement }) => {
|
||||
const { id } = el;
|
||||
|
||||
return (
|
||||
<FormElementEditModeWrapper element={el}>
|
||||
<Flex id={id} className={HEADING_CLASS_NAME} w="full">
|
||||
<EditableHeading el={el} />
|
||||
</Flex>
|
||||
</FormElementEditModeWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
HeadingElementComponentEditMode.displayName = 'HeadingElementComponentEditMode';
|
||||
|
||||
export const EditableHeading = memo(({ el }: { el: HeadingElement }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id, data } = el;
|
||||
const { content, level } = data;
|
||||
|
||||
const [localContent, setLocalContent] = useState(content);
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const onChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setLocalContent(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
const trimmedContent = localContent.trim();
|
||||
if (trimmedContent === content) {
|
||||
return;
|
||||
}
|
||||
setLocalContent(trimmedContent);
|
||||
dispatch(formElementHeadingDataChanged({ id, changes: { content: trimmedContent } }));
|
||||
}, [localContent, content, id, dispatch]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onBlur();
|
||||
} else if (e.key === 'Escape') {
|
||||
setLocalContent(content);
|
||||
}
|
||||
},
|
||||
[content, onBlur]
|
||||
);
|
||||
|
||||
return (
|
||||
<AutosizeTextarea
|
||||
ref={ref}
|
||||
placeholder="Heading"
|
||||
value={localContent}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
variant="outline"
|
||||
overflowWrap="anywhere"
|
||||
w="full"
|
||||
minRows={1}
|
||||
maxRows={10}
|
||||
resize="none"
|
||||
p={2}
|
||||
fontWeight="bold"
|
||||
fontSize={LEVEL_TO_FONT_SIZE[level]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EditableHeading.displayName = 'EditableHeading';
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
|
||||
import { InputFieldViewMode } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewMode';
|
||||
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
|
||||
import { selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import type { NodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { isNodeFieldElement, NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const NodeFieldElementComponent = memo(({ id }: { id: string }) => {
|
||||
const el = useElement(id);
|
||||
const mode = useAppSelector(selectWorkflowFormMode);
|
||||
|
||||
if (!el || !isNodeFieldElement(el)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mode === 'view') {
|
||||
return <NodeFieldElementComponentViewMode el={el} />;
|
||||
}
|
||||
|
||||
// mode === 'edit'
|
||||
return <NodeFieldElementComponentEditMode el={el} />;
|
||||
});
|
||||
|
||||
NodeFieldElementComponent.displayName = 'NodeFieldElementComponent';
|
||||
|
||||
export const NodeFieldElementComponentViewMode = memo(({ el }: { el: NodeFieldElement }) => {
|
||||
const { id, data } = el;
|
||||
const { fieldIdentifier } = data;
|
||||
|
||||
return (
|
||||
<Flex id={id} className={NODE_FIELD_CLASS_NAME}>
|
||||
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
|
||||
<InputFieldViewMode nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
|
||||
</InputFieldGate>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
NodeFieldElementComponentViewMode.displayName = 'NodeFieldElementComponentViewMode';
|
||||
|
||||
export const NodeFieldElementComponentEditMode = memo(({ el }: { el: NodeFieldElement }) => {
|
||||
const { id, data } = el;
|
||||
const { fieldIdentifier } = data;
|
||||
|
||||
return (
|
||||
<FormElementEditModeWrapper element={el}>
|
||||
<Flex id={id} className={NODE_FIELD_CLASS_NAME} w="full">
|
||||
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
|
||||
<InputFieldViewMode nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
|
||||
</InputFieldGate>
|
||||
</Flex>
|
||||
</FormElementEditModeWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
NodeFieldElementComponentEditMode.displayName = 'NodeFieldElementComponentEditMode';
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
|
||||
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
|
||||
import { formElementTextDataChanged, selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import type { TextElement } from 'features/nodes/types/workflow';
|
||||
import { isTextElement, TEXT_CLASS_NAME } from 'features/nodes/types/workflow';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
|
||||
export const TextElementComponent = memo(({ id }: { id: string }) => {
|
||||
const el = useElement(id);
|
||||
const mode = useAppSelector(selectWorkflowFormMode);
|
||||
|
||||
if (!el || !isTextElement(el)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mode === 'view') {
|
||||
return <TextElementComponentViewMode el={el} />;
|
||||
}
|
||||
|
||||
// mode === 'edit'
|
||||
return <TextElementComponentEditMode el={el} />;
|
||||
});
|
||||
TextElementComponent.displayName = 'TextElementComponent';
|
||||
|
||||
export const TextElementComponentViewMode = memo(({ el }: { el: TextElement }) => {
|
||||
const { id, data } = el;
|
||||
const { content, fontSize } = data;
|
||||
|
||||
return (
|
||||
<Flex id={id} className={TEXT_CLASS_NAME}>
|
||||
<Text fontSize={fontSize} overflowWrap="anywhere">
|
||||
{content || 'Edit to add text'}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
TextElementComponentViewMode.displayName = 'TextElementComponentViewMode';
|
||||
|
||||
export const TextElementComponentEditMode = memo(({ el }: { el: TextElement }) => {
|
||||
const { id } = el;
|
||||
|
||||
return (
|
||||
<FormElementEditModeWrapper element={el}>
|
||||
<Flex id={id} className={TEXT_CLASS_NAME} w="full">
|
||||
<EditableText el={el} />
|
||||
</Flex>
|
||||
</FormElementEditModeWrapper>
|
||||
);
|
||||
});
|
||||
TextElementComponentEditMode.displayName = 'TextElementComponentEditMode';
|
||||
|
||||
export const EditableText = memo(({ el }: { el: TextElement }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id, data } = el;
|
||||
const { content, fontSize } = data;
|
||||
const [localContent, setLocalContent] = useState(content);
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const onChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setLocalContent(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
const trimmedContent = localContent.trim();
|
||||
if (trimmedContent === content) {
|
||||
return;
|
||||
}
|
||||
setLocalContent(trimmedContent);
|
||||
dispatch(formElementTextDataChanged({ id, changes: { content: trimmedContent } }));
|
||||
}, [localContent, content, id, dispatch]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onBlur();
|
||||
} else if (e.key === 'Escape') {
|
||||
setLocalContent(content);
|
||||
}
|
||||
},
|
||||
[content, onBlur]
|
||||
);
|
||||
|
||||
return (
|
||||
<AutosizeTextarea
|
||||
ref={ref}
|
||||
placeholder="Text"
|
||||
value={localContent}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
fontSize={fontSize}
|
||||
variant="outline"
|
||||
overflowWrap="anywhere"
|
||||
w="full"
|
||||
minRows={1}
|
||||
maxRows={10}
|
||||
resize="none"
|
||||
p={2}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EditableText.displayName = 'EditableText';
|
||||
@@ -0,0 +1,109 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { firefoxDndFix } from 'features/dnd/util';
|
||||
import { FormElementComponent } from 'features/nodes/components/sidePanel/builder/ContainerElementComponent';
|
||||
import {
|
||||
buildAddFormElementDndData,
|
||||
useMonitorForFormElementDnd,
|
||||
} from 'features/nodes/components/sidePanel/builder/use-builder-dnd';
|
||||
import { formModeToggled, selectRootElementId, selectWorkflowFormMode } from 'features/nodes/store/workflowSlice';
|
||||
import type { FormElement } from 'features/nodes/types/workflow';
|
||||
import { buildContainer, buildDivider, buildHeading, buildText } from 'features/nodes/types/workflow';
|
||||
import type { RefObject } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const WorkflowBuilder = memo(() => {
|
||||
const mode = useAppSelector(selectWorkflowFormMode);
|
||||
const rootElementId = useAppSelector(selectRootElementId);
|
||||
useMonitorForFormElementDnd();
|
||||
|
||||
return (
|
||||
<ScrollableContent>
|
||||
<Flex w="full" justifyContent="center">
|
||||
<Flex flexDir="column" w={mode === 'view' ? '512px' : 'min-content'} minW="512px" gap={2}>
|
||||
<ButtonGroup isAttached={false} justifyContent="center">
|
||||
<ToggleModeButton />
|
||||
<AddFormElementDndButton type="container" />
|
||||
<AddFormElementDndButton type="divider" />
|
||||
<AddFormElementDndButton type="heading" />
|
||||
<AddFormElementDndButton type="text" />
|
||||
</ButtonGroup>
|
||||
{rootElementId && <FormElementComponent id={rootElementId} />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
);
|
||||
});
|
||||
|
||||
WorkflowBuilder.displayName = 'WorkflowBuilder';
|
||||
|
||||
const ToggleModeButton = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const mode = useAppSelector(selectWorkflowFormMode);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(formModeToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return <Button onClick={onClick}>{mode === 'view' ? 'Edit' : 'View'}</Button>;
|
||||
});
|
||||
ToggleModeButton.displayName = 'ToggleModeButton';
|
||||
|
||||
const useAddFormElementDnd = (type: Omit<FormElement['type'], 'node-field'>, draggableRef: RefObject<HTMLElement>) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const draggableElement = draggableRef.current;
|
||||
if (!draggableElement) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
firefoxDndFix(draggableElement),
|
||||
draggable({
|
||||
element: draggableElement,
|
||||
getInitialData: () => {
|
||||
if (type === 'container') {
|
||||
const element = buildContainer('row', []);
|
||||
return buildAddFormElementDndData(element);
|
||||
}
|
||||
if (type === 'divider') {
|
||||
const element = buildDivider();
|
||||
return buildAddFormElementDndData(element);
|
||||
}
|
||||
if (type === 'heading') {
|
||||
const element = buildHeading('default heading', 1);
|
||||
return buildAddFormElementDndData(element);
|
||||
}
|
||||
if (type === 'text') {
|
||||
const element = buildText('default text', 'sm');
|
||||
return buildAddFormElementDndData(element);
|
||||
}
|
||||
assert(false);
|
||||
},
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [draggableRef, type]);
|
||||
|
||||
return isDragging;
|
||||
};
|
||||
|
||||
const AddFormElementDndButton = ({ type }: { type: Omit<FormElement['type'], 'node-field'> }) => {
|
||||
const draggableRef = useRef<HTMLButtonElement>(null);
|
||||
const isDragging = useAddFormElementDnd(type, draggableRef);
|
||||
|
||||
return (
|
||||
<Button ref={draggableRef} variant="ghost" pointerEvents="auto" opacity={isDragging ? 0.3 : 1}>
|
||||
{type}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
// Adapted from https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/src/closest-edge.ts
|
||||
// This adaptation adds 'center' as a possible target
|
||||
import type { Input, Position } from '@atlaskit/pragmatic-drag-and-drop/types';
|
||||
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/types';
|
||||
|
||||
export type CenterOrEdge = 'center' | Edge;
|
||||
|
||||
// re-exporting type to make it easy to use
|
||||
|
||||
// When the DOM element is small, the closest-edge algorithm can result in a very small hitbox for the center
|
||||
// region, making it difficult for the user to hit the center. To mitigate this, when the center is allowed,
|
||||
// we use an absolute edge hitbox size of 10px or 1/4 of the element's size, whichever is smaller.
|
||||
|
||||
const getDistanceToCenterOrEdge: {
|
||||
[TKey in CenterOrEdge]: (
|
||||
rect: Pick<DOMRect, 'top' | 'right' | 'bottom' | 'left' | 'width' | 'height'>,
|
||||
client: Position,
|
||||
isCenterAllowed: boolean
|
||||
) => number;
|
||||
} = {
|
||||
top: (rect, client, isCenterAllowed) => {
|
||||
const distanceFromTop = Math.abs(client.y - rect.top);
|
||||
if (!isCenterAllowed) {
|
||||
return distanceFromTop;
|
||||
}
|
||||
const hitboxHeight = Math.min(rect.height / 4, 10);
|
||||
if (distanceFromTop <= hitboxHeight) {
|
||||
return 0;
|
||||
}
|
||||
return Infinity;
|
||||
},
|
||||
right: (rect, client, isCenterAllowed) => {
|
||||
const distanceFromRight = Math.abs(rect.right - client.x);
|
||||
if (!isCenterAllowed) {
|
||||
return distanceFromRight;
|
||||
}
|
||||
const hitboxWidth = Math.min(rect.width / 4, 10);
|
||||
if (distanceFromRight <= hitboxWidth) {
|
||||
return 0;
|
||||
}
|
||||
return Infinity;
|
||||
},
|
||||
bottom: (rect, client, isCenterAllowed) => {
|
||||
const distanceFromBottom = Math.abs(rect.bottom - client.y);
|
||||
if (!isCenterAllowed) {
|
||||
return distanceFromBottom;
|
||||
}
|
||||
const hitboxHeight = Math.min(rect.height / 4, 10);
|
||||
if (distanceFromBottom <= hitboxHeight) {
|
||||
return 0;
|
||||
}
|
||||
return Infinity;
|
||||
},
|
||||
left: (rect, client, isCenterAllowed) => {
|
||||
const distanceFromLeft = Math.abs(client.x - rect.left);
|
||||
if (!isCenterAllowed) {
|
||||
return distanceFromLeft;
|
||||
}
|
||||
const hitboxWidth = Math.min(rect.width / 4, 10);
|
||||
if (distanceFromLeft <= hitboxWidth) {
|
||||
return 0;
|
||||
}
|
||||
return Infinity;
|
||||
},
|
||||
center: (rect, client, _) => {
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
return Math.sqrt((client.x - centerX) ** 2 + (client.y - centerY) ** 2);
|
||||
},
|
||||
};
|
||||
|
||||
// using a symbol so we can guarantee a key with a unique value
|
||||
const uniqueKey = Symbol('centerWithClosestEdge');
|
||||
|
||||
/**
|
||||
* Adds a unique `Symbol` to the `userData` object. Use with `extractClosestEdge()` for type safe lookups.
|
||||
*/
|
||||
export function attachClosestCenterOrEdge(
|
||||
userData: Record<string | symbol, unknown>,
|
||||
{
|
||||
element,
|
||||
input,
|
||||
allowedCenterOrEdge,
|
||||
}: {
|
||||
element: Element;
|
||||
input: Input;
|
||||
allowedCenterOrEdge: CenterOrEdge[];
|
||||
}
|
||||
): Record<string | symbol, unknown> {
|
||||
const client: Position = {
|
||||
x: input.clientX,
|
||||
y: input.clientY,
|
||||
};
|
||||
// I tried caching the result of `getBoundingClientRect()` for a single
|
||||
// frame in order to improve performance.
|
||||
// However, on measurement I saw no improvement. So no longer caching
|
||||
const rect: DOMRect = element.getBoundingClientRect();
|
||||
|
||||
const isCenterAllowed = allowedCenterOrEdge.includes('center');
|
||||
|
||||
const entries = allowedCenterOrEdge.map((edge) => {
|
||||
return { edge, value: getDistanceToCenterOrEdge[edge](rect, client, isCenterAllowed) };
|
||||
});
|
||||
|
||||
// edge can be `null` when `allowedCenterOrEdge` is []
|
||||
const addClosestCenterOrEdge: CenterOrEdge | null = entries.sort((a, b) => a.value - b.value)[0]?.edge ?? null;
|
||||
|
||||
return {
|
||||
...userData,
|
||||
[uniqueKey]: addClosestCenterOrEdge,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value added by `attachClosestEdge()` to the `userData` object. It will return `null` if there is no value.
|
||||
*/
|
||||
export function extractClosestCenterOrEdge(userData: Record<string | symbol, unknown>): CenterOrEdge | null {
|
||||
return (userData[uniqueKey] as CenterOrEdge) ?? null;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ContainerElement, ElementId } from 'features/nodes/types/workflow';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, memo, useContext, useMemo } from 'react';
|
||||
|
||||
type ContainerContextValue = {
|
||||
id: ElementId;
|
||||
direction: ContainerElement['data']['direction'];
|
||||
};
|
||||
|
||||
const ContainerContext = createContext<ContainerContextValue | null>(null);
|
||||
|
||||
export const ContainerContextProvider = memo(
|
||||
({ id, direction, children }: PropsWithChildren<ContainerContextValue>) => {
|
||||
const ctxValue = useMemo(() => ({ id, direction }), [id, direction]);
|
||||
return <ContainerContext.Provider value={ctxValue}>{children}</ContainerContext.Provider>;
|
||||
}
|
||||
);
|
||||
ContainerContextProvider.displayName = 'ContainerContextProvider';
|
||||
|
||||
export const useContainerContext = () => {
|
||||
const container = useContext(ContainerContext);
|
||||
return container;
|
||||
};
|
||||
|
||||
const DepthContext = createContext<number>(0);
|
||||
|
||||
export const DepthContextProvider = memo(({ depth, children }: PropsWithChildren<{ depth: number }>) => {
|
||||
return <DepthContext.Provider value={depth}>{children}</DepthContext.Provider>;
|
||||
});
|
||||
DepthContextProvider.displayName = 'DepthContextProvider';
|
||||
|
||||
export const useDepthContext = () => {
|
||||
const depth = useContext(DepthContext);
|
||||
return depth;
|
||||
};
|
||||
@@ -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`;
|
||||
@@ -0,0 +1,373 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import {
|
||||
draggable,
|
||||
dropTargetForElements,
|
||||
monitorForElements,
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
import { firefoxDndFix, triggerPostMoveFlash } from 'features/dnd/util';
|
||||
import type { CenterOrEdge } from 'features/nodes/components/sidePanel/builder/center-or-closest-edge';
|
||||
import {
|
||||
attachClosestCenterOrEdge,
|
||||
extractClosestCenterOrEdge,
|
||||
} from 'features/nodes/components/sidePanel/builder/center-or-closest-edge';
|
||||
import { getEditModeWrapperId } from 'features/nodes/components/sidePanel/builder/shared';
|
||||
import { formElementAdded, formElementMoved } from 'features/nodes/store/workflowSlice';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import type { ElementId, FormElement } from 'features/nodes/types/workflow';
|
||||
import { buildNodeField, isContainerElement } from 'features/nodes/types/workflow';
|
||||
import type { RefObject } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const uniqueMoveFormElementKey = Symbol('move-form-element');
|
||||
type MoveFormElementDndData = {
|
||||
[uniqueMoveFormElementKey]: true;
|
||||
element: FormElement;
|
||||
};
|
||||
const buildMoveFormElementDndData = (element: FormElement): MoveFormElementDndData => ({
|
||||
[uniqueMoveFormElementKey]: true,
|
||||
element,
|
||||
});
|
||||
const isMoveFormElementDndData = (data: Record<string | symbol, unknown>): data is MoveFormElementDndData => {
|
||||
return uniqueMoveFormElementKey in data;
|
||||
};
|
||||
|
||||
const uniqueAddFormElementKey = Symbol('add-form-element');
|
||||
type AddFormElementDndData = {
|
||||
[uniqueAddFormElementKey]: true;
|
||||
element: FormElement;
|
||||
};
|
||||
export const buildAddFormElementDndData = (element: FormElement): AddFormElementDndData => ({
|
||||
[uniqueAddFormElementKey]: true,
|
||||
element,
|
||||
});
|
||||
const isAddFormElementDndData = (data: Record<string | symbol, unknown>): data is AddFormElementDndData => {
|
||||
return uniqueAddFormElementKey in data;
|
||||
};
|
||||
|
||||
const uniqueNodeFieldKey = Symbol('node-field');
|
||||
type NodeFieldDndData = {
|
||||
[uniqueNodeFieldKey]: true;
|
||||
fieldIdentifier: FieldIdentifier;
|
||||
};
|
||||
export const buildNodeFieldDndData = (fieldIdentifier: FieldIdentifier): NodeFieldDndData => ({
|
||||
[uniqueNodeFieldKey]: true,
|
||||
fieldIdentifier,
|
||||
});
|
||||
|
||||
const isNodeFieldDndData = (data: Record<string | symbol, unknown>): data is NodeFieldDndData => {
|
||||
return uniqueNodeFieldKey in data;
|
||||
};
|
||||
|
||||
const getElement = <T extends FormElement>(id: ElementId, guard?: (el: FormElement) => el is T): T => {
|
||||
const el = getStore().getState().workflow.form?.elements[id];
|
||||
assert(el);
|
||||
if (guard) {
|
||||
assert(guard(el));
|
||||
return el;
|
||||
} else {
|
||||
return el as T;
|
||||
}
|
||||
};
|
||||
|
||||
const adjustIndexForFormElementMoveDrop = (index: number, edge: Exclude<CenterOrEdge, 'center'>) => {
|
||||
if (edge === 'left' || edge === 'top') {
|
||||
return index - 1;
|
||||
}
|
||||
return index + 1;
|
||||
};
|
||||
|
||||
const adjustIndexForNodeFieldDrop = (index: number, edge: Exclude<CenterOrEdge, 'center'>) => {
|
||||
if (edge === 'left' || edge === 'top') {
|
||||
return index;
|
||||
}
|
||||
return index + 1;
|
||||
};
|
||||
|
||||
const flashElement = (elementId: ElementId) => {
|
||||
const element = document.querySelector(`#${getEditModeWrapperId(elementId)}`);
|
||||
if (element instanceof HTMLElement) {
|
||||
triggerPostMoveFlash(element, colorTokenToCssVar('base.700'));
|
||||
}
|
||||
};
|
||||
|
||||
export const useMonitorForFormElementDnd = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleMoveFormElementDrop = useCallback(
|
||||
(sourceData: MoveFormElementDndData, targetData: MoveFormElementDndData) => {
|
||||
if (sourceData.element.id === targetData.element.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closestCenterOrEdge = extractClosestCenterOrEdge(targetData);
|
||||
|
||||
if (closestCenterOrEdge === 'center') {
|
||||
// Move the element to the target container - should we double-check that the target is a container?
|
||||
flushSync(() => {
|
||||
dispatch(formElementMoved({ id: sourceData.element.id, containerId: targetData.element.id }));
|
||||
});
|
||||
// Flash the element that was moved
|
||||
flashElement(sourceData.element.id);
|
||||
} else if (closestCenterOrEdge) {
|
||||
// Move the element to the target's parent container at the correct index
|
||||
const { parentId } = targetData.element;
|
||||
assert(parentId !== undefined, 'Target element should have a parent');
|
||||
|
||||
const isReparenting = parentId !== sourceData.element.parentId;
|
||||
|
||||
const parentContainer = getElement(parentId, isContainerElement);
|
||||
const targetIndex = parentContainer.data.children.findIndex((elementId) => elementId === targetData.element.id);
|
||||
|
||||
let index: number | undefined = undefined;
|
||||
|
||||
if (!isReparenting) {
|
||||
const sourceIndex = parentContainer.data.children.findIndex(
|
||||
(elementId) => elementId === sourceData.element.id
|
||||
);
|
||||
if (
|
||||
sourceIndex === targetIndex ||
|
||||
sourceIndex === adjustIndexForFormElementMoveDrop(targetIndex, closestCenterOrEdge)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
index = targetIndex;
|
||||
} else {
|
||||
index = adjustIndexForFormElementMoveDrop(targetIndex, closestCenterOrEdge);
|
||||
}
|
||||
|
||||
flushSync(() => {
|
||||
dispatch(
|
||||
formElementMoved({
|
||||
id: sourceData.element.id,
|
||||
containerId: parentId,
|
||||
index,
|
||||
})
|
||||
);
|
||||
});
|
||||
// Flash the element that was moved
|
||||
flashElement(sourceData.element.id);
|
||||
} else {
|
||||
// No container, cannot do anything
|
||||
return;
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleAddFormElementDrop = useCallback(
|
||||
(sourceData: AddFormElementDndData, targetData: MoveFormElementDndData) => {
|
||||
const closestCenterOrEdge = extractClosestCenterOrEdge(targetData);
|
||||
|
||||
if (closestCenterOrEdge === 'center') {
|
||||
// Move the element to the target container - should we double-check that the target is a container?
|
||||
const { element } = sourceData;
|
||||
flushSync(() => {
|
||||
dispatch(formElementAdded({ element, containerId: targetData.element.id }));
|
||||
});
|
||||
flashElement(element.id);
|
||||
} else if (closestCenterOrEdge) {
|
||||
// Move the element to the target's parent container at the correct index
|
||||
const { parentId } = targetData.element;
|
||||
assert(parentId !== undefined, 'Target element should have a parent');
|
||||
const { element } = sourceData;
|
||||
|
||||
const parentContainer = getElement(parentId, isContainerElement);
|
||||
const targetIndex = parentContainer.data.children.findIndex((elementId) => elementId === targetData.element.id);
|
||||
|
||||
const index = adjustIndexForNodeFieldDrop(targetIndex, closestCenterOrEdge);
|
||||
|
||||
flushSync(() => {
|
||||
dispatch(
|
||||
formElementAdded({
|
||||
element,
|
||||
containerId: parentId,
|
||||
index,
|
||||
})
|
||||
);
|
||||
});
|
||||
flashElement(element.id);
|
||||
} else {
|
||||
// No container, cannot do anything
|
||||
return;
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleNodeFieldDrop = useCallback(
|
||||
(sourceData: NodeFieldDndData, targetData: MoveFormElementDndData) => {
|
||||
const closestCenterOrEdge = extractClosestCenterOrEdge(targetData);
|
||||
const { nodeId, fieldName } = sourceData.fieldIdentifier;
|
||||
|
||||
if (closestCenterOrEdge === 'center') {
|
||||
// Move the element to the target container - should we double-check that the target is a container?
|
||||
const element = buildNodeField(nodeId, fieldName, targetData.element.id);
|
||||
flushSync(() => {
|
||||
dispatch(formElementAdded({ element, containerId: targetData.element.id }));
|
||||
});
|
||||
flashElement(element.id);
|
||||
} else if (closestCenterOrEdge) {
|
||||
// Move the element to the target's parent container at the correct index
|
||||
const { parentId } = targetData.element;
|
||||
assert(parentId !== undefined, 'Target element should have a parent');
|
||||
const element = buildNodeField(nodeId, fieldName, parentId);
|
||||
|
||||
const parentContainer = getElement(parentId, isContainerElement);
|
||||
const targetIndex = parentContainer.data.children.findIndex((elementId) => elementId === targetData.element.id);
|
||||
|
||||
const index = adjustIndexForNodeFieldDrop(targetIndex, closestCenterOrEdge);
|
||||
|
||||
flushSync(() => {
|
||||
dispatch(
|
||||
formElementAdded({
|
||||
element,
|
||||
containerId: parentId,
|
||||
index,
|
||||
})
|
||||
);
|
||||
});
|
||||
flashElement(element.id);
|
||||
} else {
|
||||
// No container, cannot do anything
|
||||
return;
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return monitorForElements({
|
||||
canMonitor: ({ source }) =>
|
||||
isMoveFormElementDndData(source.data) ||
|
||||
isNodeFieldDndData(source.data) ||
|
||||
isAddFormElementDndData(source.data),
|
||||
onDrop: ({ location, source }) => {
|
||||
const target = location.current.dropTargets[0];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceData = source.data;
|
||||
const targetData = target.data;
|
||||
|
||||
if (isMoveFormElementDndData(targetData) && isMoveFormElementDndData(sourceData)) {
|
||||
handleMoveFormElementDrop(sourceData, targetData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMoveFormElementDndData(targetData) && isAddFormElementDndData(sourceData)) {
|
||||
handleAddFormElementDrop(sourceData, targetData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMoveFormElementDndData(targetData) && isNodeFieldDndData(sourceData)) {
|
||||
handleNodeFieldDrop(sourceData, targetData);
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [handleAddFormElementDrop, handleMoveFormElementDrop, handleNodeFieldDrop]);
|
||||
};
|
||||
|
||||
export const useDraggableFormElement = (
|
||||
elementId: ElementId,
|
||||
draggableRef: RefObject<HTMLElement>,
|
||||
dragHandleRef: RefObject<HTMLElement>
|
||||
) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [activeDropRegion, setActiveDropRegion] = useState<CenterOrEdge | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const draggableElement = draggableRef.current;
|
||||
const dragHandleElement = dragHandleRef.current;
|
||||
if (!draggableElement || !dragHandleElement) {
|
||||
return;
|
||||
}
|
||||
const _element = getElement(elementId);
|
||||
|
||||
return combine(
|
||||
firefoxDndFix(draggableElement),
|
||||
draggable({
|
||||
// The root element is not draggable
|
||||
canDrag: () => Boolean(_element.parentId),
|
||||
element: draggableElement,
|
||||
dragHandle: dragHandleElement,
|
||||
getInitialData: () => buildMoveFormElementDndData(getElement(elementId)),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element: draggableElement,
|
||||
canDrop: ({ source }) =>
|
||||
isMoveFormElementDndData(source.data) ||
|
||||
isNodeFieldDndData(source.data) ||
|
||||
isAddFormElementDndData(source.data),
|
||||
getData: ({ input }) => {
|
||||
const element = getElement(elementId);
|
||||
const container = element.parentId ? getElement(element.parentId, isContainerElement) : null;
|
||||
|
||||
const data = buildMoveFormElementDndData(element);
|
||||
|
||||
const allowedCenterOrEdge: CenterOrEdge[] = [];
|
||||
|
||||
if (isContainerElement(element)) {
|
||||
allowedCenterOrEdge.push('center');
|
||||
}
|
||||
|
||||
if (container?.data.direction === 'row') {
|
||||
allowedCenterOrEdge.push('left', 'right');
|
||||
}
|
||||
|
||||
if (container?.data.direction === 'column') {
|
||||
allowedCenterOrEdge.push('top', 'bottom');
|
||||
}
|
||||
|
||||
return attachClosestCenterOrEdge(data, {
|
||||
element: draggableElement,
|
||||
input,
|
||||
allowedCenterOrEdge,
|
||||
});
|
||||
},
|
||||
getIsSticky: () => true,
|
||||
onDrag: ({ self, location, source }) => {
|
||||
const innermostDropTargetElement = location.current.dropTargets.at(0)?.element;
|
||||
|
||||
// If the innermost target is not this draggable element, bail. We only want to react when dragging over _this_ element.
|
||||
if (!innermostDropTargetElement || innermostDropTargetElement !== draggableElement) {
|
||||
setActiveDropRegion(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const closestCenterOrEdge = extractClosestCenterOrEdge(self.data);
|
||||
|
||||
// Don't allow reparanting to the same container
|
||||
if (closestCenterOrEdge === 'center' && source.element === draggableElement) {
|
||||
setActiveDropRegion(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only need to update react state if nothing has changed.
|
||||
// Prevents re-rendering.
|
||||
setActiveDropRegion(closestCenterOrEdge);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setActiveDropRegion(null);
|
||||
},
|
||||
onDrop: () => {
|
||||
setActiveDropRegion(null);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [dragHandleRef, draggableRef, elementId]);
|
||||
|
||||
return [activeDropRegion, isDragging] as const;
|
||||
};
|
||||
@@ -4,7 +4,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
|
||||
import { InvocationNodeNotesTextarea } from 'features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea';
|
||||
import { useNodeIsInvocationNode } from 'features/nodes/hooks/useNodeIsInvocationNode';
|
||||
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
@@ -55,6 +56,7 @@ type ContentProps = {
|
||||
const Content = memo((props: ContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
const needsUpdate = useNodeNeedsUpdate(props.nodeId);
|
||||
const isInvocationNode = useNodeIsInvocationNode(props.nodeId);
|
||||
return (
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
@@ -74,7 +76,7 @@ const Content = memo((props: ContentProps) => {
|
||||
</Text>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
<NotesTextarea nodeId={props.nodeId} />
|
||||
{isInvocationNode && <InvocationNodeNotesTextarea nodeId={props.nodeId} />}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
@@ -35,7 +35,7 @@ const InspectorOutputsTab = () => {
|
||||
[templates]
|
||||
);
|
||||
const data = useAppSelector(selector);
|
||||
const nes = useExecutionState(data?.nodeId);
|
||||
const nes = useNodeExecutionState(data?.nodeId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!data || !nes) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user