diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index 3a57572f294..638755917e8 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -65,7 +65,7 @@
"@xyflow/react": "^12.5.3",
"async-mutex": "^0.5.0",
"chakra-react-select": "^4.9.2",
- "cmdk": "^1.0.0",
+ "cmdk": "^1.1.1",
"compare-versions": "^6.1.1",
"filesize": "^10.1.6",
"fracturedjsonjs": "^4.0.2",
@@ -98,7 +98,7 @@
"react-i18next": "^15.0.2",
"react-icons": "^5.3.0",
"react-redux": "9.1.2",
- "react-resizable-panels": "^2.1.4",
+ "react-resizable-panels": "^2.1.7",
"react-textarea-autosize": "^8.5.7",
"react-use": "^17.5.1",
"react-virtuoso": "^4.12.5",
diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml
index 3fff6b8e178..15ade298e46 100644
--- a/invokeai/frontend/web/pnpm-lock.yaml
+++ b/invokeai/frontend/web/pnpm-lock.yaml
@@ -45,8 +45,8 @@ dependencies:
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.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)
+ specifier: ^1.1.1
+ version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
compare-versions:
specifier: ^6.1.1
version: 6.1.1
@@ -144,8 +144,8 @@ dependencies:
specifier: 9.1.2
version: 9.1.2(@types/react@18.3.11)(react@18.3.1)(redux@5.0.1)
react-resizable-panels:
- specifier: ^2.1.4
- version: 2.1.4(react-dom@18.3.1)(react@18.3.1)
+ specifier: ^2.1.7
+ version: 2.1.7(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)
@@ -1999,284 +1999,268 @@ packages:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
- /@radix-ui/primitive@1.0.1:
- resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
- dependencies:
- '@babel/runtime': 7.25.7
+ /@radix-ui/primitive@1.1.2:
+ resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
dev: false
- /@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
+ /@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
'@types/react': 18.3.11
react: 18.3.1
dev: false
- /@radix-ui/react-context@1.0.1(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
+ /@radix-ui/react-context@1.1.2(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
'@types/react': 18.3.11
react: 18.3.1
dev: false
- /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
- resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
+ /@radix-ui/react-dialog@1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-EIdma8C0C/I6kL6sO02avaCRqi3fmWJpxH6mqbVScorW6nNktzKJT/le7VPho3o/7wCsyRg3z0+Q+Obr0Gy/VQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0
- react-dom: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/primitive': 1.0.1
- '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.11)(react@18.3.1)
- '@radix-ui/react-context': 1.0.1(@types/react@18.3.11)(react@18.3.1)
- '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.11)(react@18.3.1)
- '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-id': 1.0.1(@types/react@18.3.11)(react@18.3.1)
- '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-slot': 1.0.2(@types/react@18.3.11)(react@18.3.1)
- '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.6(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.0(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
'@types/react-dom': 18.3.0
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
- react-remove-scroll: 2.5.5(@types/react@18.3.11)(react@18.3.1)
+ react-remove-scroll: 2.6.3(@types/react@18.3.11)(react@18.3.1)
dev: false
- /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
- resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
+ /@radix-ui/react-dismissable-layer@1.1.6(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-7gpgMT2gyKym9Jz2ZhlRXSg2y6cNQIK8d/cqBZ0RBCaps8pFryCWXiUKI+uHGFrhMrbGUP7U6PWgiXzIxoyF3Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0
- react-dom: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/primitive': 1.0.1
- '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.11)(react@18.3.1)
- '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.11)(react@18.3.1)
- '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
- /@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
+ /@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
peerDependencies:
'@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
'@types/react': 18.3.11
react: 18.3.1
dev: false
- /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
- resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==}
+ /@radix-ui/react-focus-scope@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-4XaDlq0bPt7oJwR+0k0clCiCO/7lO7NKZTAaJBYxDNQT/vj4ig0/UvctrRscZaFREpRvUTkpKR96ov1e6jptQg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0
- react-dom: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.11)(react@18.3.1)
- '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
- /@radix-ui/react-id@1.0.1(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
+ /@radix-ui/react-id@1.1.1(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
'@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
react: 18.3.1
dev: false
- /@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
- resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
+ /@radix-ui/react-portal@1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0
- react-dom: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
- /@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
- resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
+ /@radix-ui/react-presence@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0
- react-dom: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.11)(react@18.3.1)
- '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
- /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
- resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
+ /@radix-ui/react-primitive@2.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0
- react-dom: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/react-slot': 1.0.2(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.0(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
- /@radix-ui/react-slot@1.0.2(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
+ /@radix-ui/react-slot@1.2.0(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==}
peerDependencies:
'@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
react: 18.3.1
dev: false
- /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
+ /@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
'@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
'@types/react': 18.3.11
react: 18.3.1
dev: false
- /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
+ /@radix-ui/react-use-controllable-state@1.1.1(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==}
peerDependencies:
'@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
react: 18.3.1
dev: false
- /@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
+ /@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
peerDependencies:
'@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
- '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@types/react': 18.3.11
react: 18.3.1
dev: false
- /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
+ /@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.25.7
'@types/react': 18.3.11
react: 18.3.1
dev: false
@@ -4582,14 +4566,16 @@ packages:
requiresBuild: true
dev: true
- /cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
- resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==}
+ /cmdk@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
peerDependencies:
- react: ^18.0.0
- react-dom: ^18.0.0
+ react: ^18 || ^19 || ^19.0.0-rc
+ react-dom: ^18 || ^19 || ^19.0.0-rc
dependencies:
- '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-dialog': 1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.11)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.0)(@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:
@@ -7735,23 +7721,20 @@ packages:
tslib: 2.7.0
dev: false
- /react-remove-scroll@2.5.5(@types/react@18.3.11)(react@18.3.1):
- resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
+ /react-remove-scroll-bar@2.3.8(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
peerDependencies:
- '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ '@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
- react-remove-scroll-bar: 2.3.6(@types/react@18.3.11)(react@18.3.1)
- react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1)
- tslib: 2.7.0
- use-callback-ref: 1.3.2(@types/react@18.3.11)(react@18.3.1)
- use-sidecar: 1.1.2(@types/react@18.3.11)(react@18.3.1)
+ react-style-singleton: 2.2.3(@types/react@18.3.11)(react@18.3.1)
+ tslib: 2.8.1
dev: false
/react-remove-scroll@2.6.0(@types/react@18.3.11)(react@18.3.1):
@@ -7773,11 +7756,30 @@ packages:
use-sidecar: 1.1.2(@types/react@18.3.11)(react@18.3.1)
dev: false
- /react-resizable-panels@2.1.4(react-dom@18.3.1)(react@18.3.1):
- resolution: {integrity: sha512-kzue8lsoSBdyyd2IfXLQMMhNujOxRoGVus+63K95fQqleGxTfvgYLTzbwYMOODeAHqnkjb3WV/Ks7f5+gDYZuQ==}
+ /react-remove-scroll@2.6.3(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==}
+ engines: {node: '>=10'}
peerDependencies:
- react: ^16.14.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.11
+ react: 18.3.1
+ react-remove-scroll-bar: 2.3.8(@types/react@18.3.11)(react@18.3.1)
+ react-style-singleton: 2.2.3(@types/react@18.3.11)(react@18.3.1)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@18.3.11)(react@18.3.1)
+ use-sidecar: 1.1.3(@types/react@18.3.11)(react@18.3.1)
+ dev: false
+
+ /react-resizable-panels@2.1.7(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==}
+ peerDependencies:
+ react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -7844,6 +7846,22 @@ packages:
tslib: 2.7.0
dev: false
+ /react-style-singleton@2.2.3(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.11
+ get-nonce: 1.0.1
+ react: 18.3.1
+ tslib: 2.8.1
+ 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'}
@@ -8845,6 +8863,10 @@ packages:
/tslib@2.7.0:
resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
+ /tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ dev: false
+
/tsutils@3.21.0(typescript@5.6.2):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
@@ -9037,6 +9059,21 @@ packages:
tslib: 2.7.0
dev: false
+ /use-callback-ref@1.3.3(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.11
+ react: 18.3.1
+ tslib: 2.8.1
+ 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:
@@ -9123,6 +9160,22 @@ packages:
tslib: 2.7.0
dev: false
+ /use-sidecar@1.1.3(@types/react@18.3.11)(react@18.3.1):
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.11
+ detect-node-es: 1.1.0
+ react: 18.3.1
+ tslib: 2.8.1
+ dev: false
+
/use-sync-external-store@1.2.2(react@18.3.1):
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
peerDependencies:
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 90b23fa1371..b885df54528 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -118,6 +118,8 @@
"error": "Error",
"error_withCount_one": "{{count}} error",
"error_withCount_other": "{{count}} errors",
+ "model_withCount_one": "{{count}} model",
+ "model_withCount_other": "{{count}} models",
"file": "File",
"folder": "Folder",
"format": "format",
@@ -138,6 +140,8 @@
"localSystem": "Local System",
"learnMore": "Learn More",
"modelManager": "Model Manager",
+ "noMatches": "No matches",
+ "noOptions": "No options",
"nodes": "Workflows",
"notInstalled": "Not $t(common.installed)",
"openInNewTab": "Open in New Tab",
@@ -197,7 +201,9 @@
"column": "Column",
"value": "Value",
"label": "Label",
- "systemInformation": "System Information"
+ "systemInformation": "System Information",
+ "compact": "Compact",
+ "full": "Full"
},
"hrf": {
"hrf": "High Resolution Fix",
@@ -768,6 +774,7 @@
"description": "Description",
"edit": "Edit",
"fileSize": "File Size",
+ "filterModels": "Filter models",
"fluxRedux": "FLUX Redux",
"height": "Height",
"huggingFace": "HuggingFace",
@@ -821,10 +828,12 @@
"modelUpdated": "Model Updated",
"modelUpdateFailed": "Model Update Failed",
"name": "Name",
- "noModelsInstalled": "No Models Installed",
+ "modelPickerFallbackNoModelsInstalled": "No models installed.",
+ "modelPickerFallbackNoModelsInstalled2": "Visit the Model Manager to install models.",
"noModelsInstalledDesc1": "Install models with the",
"noModelSelected": "No Model Selected",
- "noMatchingModels": "No matching Models",
+ "noMatchingModels": "No matching models",
+ "noModelsInstalled": "No models installed",
"none": "none",
"path": "Path",
"pathToConfig": "Path To Config",
@@ -871,7 +880,8 @@
"installingXModels_one": "Installing {{count}} model",
"installingXModels_other": "Installing {{count}} models",
"skippingXDuplicates_one": ", skipping {{count}} duplicate",
- "skippingXDuplicates_other": ", skipping {{count}} duplicates"
+ "skippingXDuplicates_other": ", skipping {{count}} duplicates",
+ "manageModels": "Manage Models"
},
"models": {
"addLora": "Add LoRA",
diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
index d7eaf49fe14..5225d908ac7 100644
--- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
+++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
@@ -12,6 +12,7 @@ import type { CustomStarUi } from 'app/store/nanostores/customStarUI';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { $isDebugging } from 'app/store/nanostores/isDebugging';
import { $logo } from 'app/store/nanostores/logo';
+import { $onClickGoToModelManager } from 'app/store/nanostores/onClickGoToModelManager';
import { $openAPISchemaUrl } from 'app/store/nanostores/openAPISchemaUrl';
import { $projectId, $projectName, $projectUrl } from 'app/store/nanostores/projectId';
import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId';
@@ -59,6 +60,10 @@ interface Props extends PropsWithChildren {
workflowTagCategories?: WorkflowTagCategory[];
workflowSortOptions?: WorkflowSortOption[];
loggingOverrides?: LoggingOverrides;
+ /**
+ * If provided, overrides in-app navigation to the model manager
+ */
+ onClickGoToModelManager?: () => void;
}
const InvokeAIUI = ({
@@ -81,6 +86,7 @@ const InvokeAIUI = ({
workflowTagCategories,
workflowSortOptions,
loggingOverrides,
+ onClickGoToModelManager,
}: Props) => {
useLayoutEffect(() => {
/*
@@ -205,6 +211,16 @@ const InvokeAIUI = ({
};
}, [logo]);
+ useEffect(() => {
+ if (onClickGoToModelManager) {
+ $onClickGoToModelManager.set(onClickGoToModelManager);
+ }
+
+ return () => {
+ $onClickGoToModelManager.set(undefined);
+ };
+ }, [onClickGoToModelManager]);
+
useEffect(() => {
if (workflowCategories) {
$workflowLibraryCategoriesOptions.set(workflowCategories);
diff --git a/invokeai/frontend/web/src/app/store/nanostores/onClickGoToModelManager.ts b/invokeai/frontend/web/src/app/store/nanostores/onClickGoToModelManager.ts
new file mode 100644
index 00000000000..fdc0d8a788b
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/nanostores/onClickGoToModelManager.ts
@@ -0,0 +1,3 @@
+import { atom } from 'nanostores';
+
+export const $onClickGoToModelManager = atom<(() => void) | undefined>(undefined);
diff --git a/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx b/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx
index d61a5e498c8..3a74e75f98c 100644
--- a/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx
+++ b/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx
@@ -7,7 +7,7 @@ import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/
import type { OverlayScrollbarsComponentRef } from 'overlayscrollbars-react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties, PropsWithChildren } from 'react';
-import { memo, useEffect, useMemo, useState } from 'react';
+import { memo, useEffect, useMemo, useRef } from 'react';
type Props = PropsWithChildren & {
maxHeight?: ChakraProps['maxHeight'];
@@ -22,9 +22,9 @@ const ScrollableContent = ({ children, maxHeight, overflowX = 'hidden', overflow
() => getOverlayScrollbarsParams({ overflowX, overflowY }).options,
[overflowX, overflowY]
);
- const [os, osRef] = useState(null);
+ const os = useRef(null);
useEffect(() => {
- const osInstance = os?.osInstance();
+ const osInstance = os.current?.osInstance();
if (!osInstance) {
return;
@@ -46,7 +46,7 @@ const ScrollableContent = ({ children, maxHeight, overflowX = 'hidden', overflow
return (
-
+
{children}
diff --git a/invokeai/frontend/web/src/common/components/Picker/Picker.tsx b/invokeai/frontend/web/src/common/components/Picker/Picker.tsx
new file mode 100644
index 00000000000..90addfbd484
--- /dev/null
+++ b/invokeai/frontend/web/src/common/components/Picker/Picker.tsx
@@ -0,0 +1,617 @@
+import type { BoxProps, InputProps } from '@invoke-ai/ui-library';
+import { Flex, Input, Text } from '@invoke-ai/ui-library';
+import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
+import { useStateImperative } from 'common/hooks/useStateImperative';
+import { fixedForwardRef } from 'common/util/fixedForwardRef';
+import { typedMemo } from 'common/util/typedMemo';
+import type { ChangeEvent, PropsWithChildren } from 'react';
+import type React from 'react';
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { useTranslation } from 'react-i18next';
+import { assert } from 'tsafe';
+
+/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+export type Group = {
+ id: string;
+ data: U;
+ options: T[];
+};
+
+const isGroup = (option: T | Group): option is Group => {
+ return option ? 'options' in option && Array.isArray(option.options) : false;
+};
+
+export type ImperativeModelPickerHandle = {
+ inputRef: React.RefObject;
+ rootRef: React.RefObject;
+ searchTerm: string;
+ setSearchTerm: (searchTerm: string) => void;
+};
+
+const DefaultOptionComponent = typedMemo(({ option }: { option: T }) => {
+ const { getOptionId } = usePickerContext();
+ return {getOptionId(option)};
+});
+DefaultOptionComponent.displayName = 'DefaultOptionComponent';
+
+const DefaultGroupComponent = typedMemo(
+ ({ group, children }: PropsWithChildren<{ group: Group }>) => {
+ return (
+
+ {group.id}
+
+ {children}
+
+
+ );
+ }
+);
+DefaultGroupComponent.displayName = 'DefaultGroupComponent';
+
+const NoOptionsFallbackWrapper = typedMemo(({ children }: PropsWithChildren) => {
+ const { t } = useTranslation();
+ return (
+
+ {typeof children === 'string' ? (
+ {children}
+ ) : (
+ (children ?? {t('common.noOptions')})
+ )}
+
+ );
+});
+NoOptionsFallbackWrapper.displayName = 'NoOptionsFallbackWrapper';
+
+const NoMatchesFallbackWrapper = typedMemo(({ children }: PropsWithChildren) => {
+ const { t } = useTranslation();
+ return (
+
+ {typeof children === 'string' ? (
+ {children}
+ ) : (
+ (children ?? {t('common.noMatches')})
+ )}
+
+ );
+});
+NoMatchesFallbackWrapper.displayName = 'NoMatchesFallbackWrapper';
+
+type PickerProps = {
+ /**
+ * The options to display in the picker. This can be a flat array of options or an array of groups.
+ */
+ options: (T | Group)[];
+ /**
+ * A function that returns the id of an option.
+ */
+ getOptionId: (option: T) => string;
+ /**
+ * A function that returns true if the option matches the search term.
+ */
+ isMatch: (option: T, searchTerm: string) => boolean;
+ /**
+ * A function that returns true if the option is disabled.
+ */
+ getIsDisabled?: (option: T) => boolean;
+ /**
+ * The currently selected item.
+ */
+ selectedItem?: T;
+ /**
+ * A function that is called when an option is selected.
+ */
+ onSelect?: (option: T) => void;
+ /**
+ * A function that is called when the picker is closed.
+ */
+ onClose?: () => void;
+ /**
+ * A ref to an imperative handle that can be used to control the picker.
+ */
+ handleRef?: React.Ref;
+ /**
+ * A custom search bar component. If not provided, a default search bar will be used.
+ */
+ SearchBarComponent?: ReturnType>;
+ /**
+ * A custom option component. If not provided, a default option component will be used.
+ */
+ OptionComponent?: React.ComponentType<
+ {
+ option: T;
+ } & BoxProps
+ >;
+ /**
+ * A custom group component. If not provided, a default group component will be used.
+ */
+ GroupComponent?: React.ComponentType } & BoxProps>>;
+ /**
+ * A fallback component to display when there are no options. If a string is provided, it will be formatted
+ * as a text element with appropriate styling. If a React node is provided, it will be rendered as is.
+ */
+ noOptionsFallback?: React.ReactNode;
+ /**
+ * A fallback component to display when there are no matches. If a string is provided, it will be formatted
+ * as a text element with appropriate styling. If a React node is provided, it will be rendered as is.
+ */
+ noMatchesFallback?: React.ReactNode;
+ /**
+ * An optional object that can be used to pass additional data to custom picker components.
+ */
+ extra: C;
+};
+
+type PickerContextState = {
+ options: (T | Group)[];
+ getOptionId: (option: T) => string;
+ isMatch: (option: T, searchTerm: string) => boolean;
+ getIsDisabled?: (option: T) => boolean;
+ setActiveOptionId: (id: string) => void;
+ onSelectById: (id: string) => void;
+ setSearchTerm: (searchTerm: string) => void;
+ SearchBarComponent: ReturnType>;
+ noOptionsFallback?: React.ReactNode;
+ noMatchesFallback?: React.ReactNode;
+ OptionComponent: React.ComponentType<{ option: T } & BoxProps>;
+ GroupComponent: React.ComponentType } & BoxProps>>;
+ extra: C;
+};
+
+/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+const PickerContext = createContext | null>(null);
+export const usePickerContext = (): PickerContextState => {
+ const context = useContext(PickerContext);
+ assert(context !== null, 'usePickerContext must be used within a PickerProvider');
+ return context;
+};
+
+export const getRegex = (searchTerm: string) => {
+ const terms = searchTerm
+ .trim()
+ .replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
+ .split(' ')
+ .filter((term) => term.length > 0);
+
+ if (terms.length === 0) {
+ return new RegExp('', 'gi');
+ }
+
+ // Create positive lookaheads for each term - matches in any order
+ const pattern = terms.map((term) => `(?=.*${term})`).join('');
+ return new RegExp(`${pattern}.+`, 'i');
+};
+
+const getFirstOption = (options: (T | Group)[]): T | undefined => {
+ const firstOptionOrGroup = options[0];
+ if (!firstOptionOrGroup) {
+ return;
+ }
+ if (isGroup(firstOptionOrGroup)) {
+ return firstOptionOrGroup.options[0];
+ } else {
+ return firstOptionOrGroup;
+ }
+};
+
+const getFirstOptionId = (
+ options: (T | Group)[],
+ getOptionId: (item: T) => string
+): string | undefined => {
+ const firstOptionOrGroup = getFirstOption(options);
+ if (firstOptionOrGroup) {
+ return getOptionId(firstOptionOrGroup);
+ } else {
+ return undefined;
+ }
+};
+
+const findOption = (
+ options: (T | Group)[],
+ id: string,
+ getOptionId: (item: T) => string
+): T | undefined => {
+ for (const optionOrGroup of options) {
+ if (isGroup(optionOrGroup)) {
+ const option = optionOrGroup.options.find((opt) => getOptionId(opt) === id);
+ if (option) {
+ return option;
+ }
+ } else {
+ if (getOptionId(optionOrGroup) === id) {
+ return optionOrGroup;
+ }
+ }
+ }
+};
+
+const flattenOptions = (options: (T | Group)[]): T[] => {
+ const flattened: T[] = [];
+ for (const optionOrGroup of options) {
+ if (isGroup(optionOrGroup)) {
+ flattened.push(...optionOrGroup.options);
+ } else {
+ flattened.push(optionOrGroup);
+ }
+ }
+ return flattened;
+};
+
+export const Picker = typedMemo((props: PickerProps) => {
+ const {
+ getOptionId,
+ options,
+ handleRef,
+ isMatch,
+ getIsDisabled,
+ onClose,
+ onSelect,
+ selectedItem,
+ SearchBarComponent = DefaultPickerSearchBarComponent,
+ noMatchesFallback,
+ noOptionsFallback,
+ OptionComponent = DefaultOptionComponent,
+ GroupComponent = DefaultGroupComponent,
+ extra,
+ } = props;
+ const [activeOptionId, setActiveOptionId, getActiveOptionId] = useStateImperative(() =>
+ getFirstOptionId(options, getOptionId)
+ );
+ const rootRef = useRef(null);
+ const inputRef = useRef(null);
+ const [filteredOptions, setFilteredOptions] = useState<(T | Group)[]>(options);
+ const flattenedOptions = useMemo(() => flattenOptions(options), [options]);
+ const flattenedFilteredOptions = useMemo(() => flattenOptions(filteredOptions), [filteredOptions]);
+ const [searchTerm, setSearchTerm] = useState('');
+ useImperativeHandle(handleRef, () => ({ inputRef, rootRef, searchTerm, setSearchTerm }), [searchTerm]);
+
+ const onChangeSearchTerm = useCallback((e: ChangeEvent) => {
+ setSearchTerm(e.target.value);
+ }, []);
+
+ useEffect(() => {
+ if (!searchTerm) {
+ setFilteredOptions(options);
+ setActiveOptionId(getFirstOptionId(options, getOptionId));
+ } else {
+ const lowercasedSearchTerm = searchTerm.toLowerCase();
+ const filtered: (T | Group)[] = [];
+ for (const item of props.options) {
+ if (isGroup(item)) {
+ const filteredItems = item.options.filter((item) => isMatch(item, lowercasedSearchTerm));
+ if (filteredItems.length > 0) {
+ filtered.push({ ...item, options: filteredItems });
+ }
+ } else {
+ if (isMatch(item, searchTerm)) {
+ filtered.push(item);
+ }
+ }
+ }
+ setFilteredOptions(filtered);
+ setActiveOptionId(getFirstOptionId(filtered, getOptionId));
+ }
+ }, [searchTerm, setActiveOptionId, props.options, options, getOptionId, isMatch]);
+
+ const onSelectById = useCallback(
+ (id: string) => {
+ const item = findOption(options, id, getOptionId);
+ if (!item) {
+ // Model not found? We should never get here.
+ return;
+ }
+ onSelect?.(item);
+ },
+ [getOptionId, options, onSelect]
+ );
+
+ const setValueAndScrollIntoView = useCallback(
+ (id: string) => {
+ setActiveOptionId(id);
+ const rootEl = rootRef.current;
+ if (!rootEl) {
+ return;
+ }
+ const itemEl = rootEl.querySelector(`#${CSS.escape(id)}`);
+ if (!itemEl) {
+ return;
+ }
+ itemEl.scrollIntoView({ block: 'nearest' });
+ },
+ [setActiveOptionId]
+ );
+
+ const prev = useCallback(
+ (e: React.KeyboardEvent) => {
+ e.preventDefault();
+ const activeOptionId = getActiveOptionId();
+ if (flattenedFilteredOptions.length === 0) {
+ return;
+ }
+ if (e.metaKey) {
+ const item = flattenedFilteredOptions.at(0);
+ if (item) {
+ setValueAndScrollIntoView(getOptionId(item));
+ }
+ return;
+ }
+ const currentIndex = flattenedFilteredOptions.findIndex((item) => getOptionId(item) === activeOptionId);
+ if (currentIndex < 0) {
+ return;
+ }
+ let newIndex = currentIndex - 1;
+ if (newIndex < 0) {
+ newIndex = flattenedFilteredOptions.length - 1;
+ }
+ const item = flattenedFilteredOptions.at(newIndex);
+ if (item) {
+ setValueAndScrollIntoView(getOptionId(item));
+ }
+ },
+ [getActiveOptionId, flattenedFilteredOptions, setValueAndScrollIntoView, getOptionId]
+ );
+
+ const next = useCallback(
+ (e: React.KeyboardEvent) => {
+ e.preventDefault();
+ const activeOptionId = getActiveOptionId();
+ if (flattenedFilteredOptions.length === 0) {
+ return;
+ }
+ if (e.metaKey) {
+ const item = flattenedFilteredOptions.at(-1);
+ if (item) {
+ setValueAndScrollIntoView(getOptionId(item));
+ }
+ return;
+ }
+
+ const currentIndex = flattenedFilteredOptions.findIndex((item) => getOptionId(item) === activeOptionId);
+ if (currentIndex < 0) {
+ return;
+ }
+ let newIndex = currentIndex + 1;
+ if (newIndex >= flattenedFilteredOptions.length) {
+ newIndex = 0;
+ }
+ const item = flattenedFilteredOptions.at(newIndex);
+ if (item) {
+ setValueAndScrollIntoView(getOptionId(item));
+ }
+ },
+ [getActiveOptionId, flattenedFilteredOptions, setValueAndScrollIntoView, getOptionId]
+ );
+
+ const onKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'ArrowUp') {
+ prev(e);
+ } else if (e.key === 'ArrowDown') {
+ next(e);
+ } else if (e.key === 'Enter') {
+ const activeOptionId = getActiveOptionId();
+ if (!activeOptionId) {
+ return;
+ }
+ onSelectById(activeOptionId);
+ } else if (e.key === 'Escape') {
+ onClose?.();
+ } else if (e.key === '/') {
+ e.preventDefault();
+ inputRef.current?.focus();
+ inputRef.current?.select();
+ }
+ },
+ [prev, next, getActiveOptionId, onSelectById, onClose]
+ );
+
+ const ctx = useMemo(
+ () =>
+ ({
+ options,
+ getOptionId,
+ isMatch,
+ getIsDisabled,
+ onSelectById,
+ setActiveOptionId,
+ SearchBarComponent,
+ noOptionsFallback,
+ noMatchesFallback,
+ OptionComponent,
+ GroupComponent,
+ extra,
+ setSearchTerm,
+ }) satisfies PickerContextState,
+ [
+ options,
+ getOptionId,
+ isMatch,
+ getIsDisabled,
+ onSelectById,
+ setActiveOptionId,
+ SearchBarComponent,
+ noOptionsFallback,
+ noMatchesFallback,
+ OptionComponent,
+ GroupComponent,
+ extra,
+ ]
+ );
+
+ return (
+
+
+
+
+ {flattenedOptions.length === 0 && {noOptionsFallback}}
+ {flattenedOptions.length > 0 && flattenedFilteredOptions.length === 0 && (
+ {noMatchesFallback}
+ )}
+ {flattenedOptions.length > 0 && flattenedFilteredOptions.length > 0 && (
+
+
+
+ )}
+
+
+
+ );
+});
+Picker.displayName = 'Picker';
+
+const DefaultPickerSearchBarComponent = typedMemo(
+ fixedForwardRef((props, ref) => {
+ return ;
+ })
+);
+DefaultPickerSearchBarComponent.displayName = 'DefaultPickerSearchBarComponent';
+
+const PickerList = typedMemo(
+ ({
+ items,
+ activeOptionId,
+ selectedItemId,
+ }: {
+ items: (T | Group)[];
+ activeOptionId: string | undefined;
+ selectedItemId: string | undefined;
+ }) => {
+ const { getOptionId, getIsDisabled } = usePickerContext();
+
+ if (items.length === 0) {
+ return (
+
+ );
+ }
+ return (
+
+ {items.map((itemOrGroup) => {
+ if (isGroup(itemOrGroup)) {
+ return (
+
+ );
+ } else {
+ const id = getOptionId(itemOrGroup);
+ return (
+
+ );
+ }
+ })}
+
+ );
+ }
+);
+PickerList.displayName = 'PickerList';
+
+const PickerOptionGroup = typedMemo(
+ ({
+ group,
+ activeOptionId,
+ selectedItemId,
+ }: {
+ group: Group;
+ activeOptionId: string | undefined;
+ selectedItemId: string | undefined;
+ }) => {
+ const { getOptionId, GroupComponent, getIsDisabled } = usePickerContext();
+
+ return (
+
+ {group.options.map((item) => {
+ const id = getOptionId(item);
+ return (
+
+ );
+ })}
+
+ );
+ }
+);
+PickerOptionGroup.displayName = 'PickerOptionGroup';
+
+const PickerOption = typedMemo(
+ (props: {
+ id: string;
+ option: T;
+ isActive: boolean;
+ isSelected: boolean;
+ isDisabled: boolean;
+ }) => {
+ const { OptionComponent, setActiveOptionId, onSelectById } = usePickerContext();
+ const { id, option, isActive, isDisabled, isSelected } = props;
+ const onPointerMove = useCallback(() => {
+ setActiveOptionId(id);
+ }, [id, setActiveOptionId]);
+ const onClick = useCallback(() => {
+ onSelectById(id);
+ }, [id, onSelectById]);
+ return (
+
+ );
+ }
+);
+PickerOption.displayName = 'PickerOption';
diff --git a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts
index 29ff7dfd5e5..b79ca940e0b 100644
--- a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts
+++ b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts
@@ -38,7 +38,7 @@ export const useModelCombobox = (arg: UseModelCombobox
}, [optionsFilter, getIsDisabled, modelConfigs, shouldShowModelDescriptions]);
const value = useMemo(
- () => options.find((m) => (selectedModel ? m.value === selectedModel.key : false)),
+ () => options.find((m) => (selectedModel ? m.value === selectedModel.key : false)) ?? null,
[options, selectedModel]
);
diff --git a/invokeai/frontend/web/src/common/hooks/useStateImperative.ts b/invokeai/frontend/web/src/common/hooks/useStateImperative.ts
new file mode 100644
index 00000000000..e716979437a
--- /dev/null
+++ b/invokeai/frontend/web/src/common/hooks/useStateImperative.ts
@@ -0,0 +1,32 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+const isInitialValueFunction = (value: T | (() => T)): value is () => T => {
+ return typeof value === 'function';
+};
+
+/**
+ * Extension of useState that provides imperative access to state value.
+ *
+ * @param initialValue - Initial state value or function that returns it
+ * @returns [state, setState, getState] - Standard state tuple plus getter
+ *
+ * @remarks
+ * - Only use getState() in event handlers and effects, not during rendering
+ * - In Concurrent Mode, getState() may return stale values before commit
+ */
+export const useStateImperative = (
+ initialValue: T | (() => T)
+): readonly [T, (newValue: T | ((prevState: T) => T)) => void, () => T] => {
+ const [state, setState] = useState(isInitialValueFunction(initialValue) ? initialValue() : initialValue);
+ const stateRef = useRef(state);
+
+ useEffect(() => {
+ stateRef.current = state;
+ }, [state]);
+
+ const getState = useCallback(() => {
+ return stateRef.current;
+ }, []);
+
+ return [state, setState, getState] as const;
+};
diff --git a/invokeai/frontend/web/src/common/util/fixedForwardRef.ts b/invokeai/frontend/web/src/common/util/fixedForwardRef.ts
new file mode 100644
index 00000000000..927ae974190
--- /dev/null
+++ b/invokeai/frontend/web/src/common/util/fixedForwardRef.ts
@@ -0,0 +1,13 @@
+import { forwardRef } from 'react';
+
+/**
+ * A forwardRef that works with generics and doesn't require the use of `as` to cast the type.
+ * See: https://www.totaltypescript.com/forwardref-with-generic-components
+ */
+export function fixedForwardRef(
+ render: (props: P, ref: React.Ref) => React.ReactNode
+): (props: P & React.RefAttributes) => React.ReactNode {
+ // @ts-expect-error: This is a workaround for forwardRef's crappy typing
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return forwardRef(render) as any;
+}
diff --git a/invokeai/frontend/web/src/common/util/typedMemo.ts b/invokeai/frontend/web/src/common/util/typedMemo.ts
index 321833cdba2..a084d95c81a 100644
--- a/invokeai/frontend/web/src/common/util/typedMemo.ts
+++ b/invokeai/frontend/web/src/common/util/typedMemo.ts
@@ -1,6 +1,10 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
import { memo } from 'react';
/**
* A typed version of React.memo, useful for components that take generics.
*/
-export const typedMemo: (c: T) => T = memo;
+export const typedMemo: >(
+ component: T,
+ propsAreEqual?: (prevProps: React.ComponentProps, nextProps: React.ComponentProps) => boolean
+) => T & { displayName?: string } = memo;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx
index 34f1607b954..a66424a5654 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx
@@ -2,6 +2,7 @@ import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { selectBase } from 'features/controlLayers/store/paramsSlice';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useControlLayerModels } from 'services/api/hooks/modelsByType';
@@ -61,6 +62,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
+
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSpandrel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSpandrel.tsx
index 5f649707999..76f033ee5ab 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSpandrel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSpandrel.tsx
@@ -13,6 +13,7 @@ import {
import { useModelCombobox } from 'common/hooks/useModelCombobox';
import type { SpandrelFilterConfig } from 'features/controlLayers/store/filters';
import { IMAGE_FILTERS } from 'features/controlLayers/store/filters';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import type { ChangeEvent } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -118,6 +119,7 @@ export const FilterSpandrel = ({ onChange, config }: Props) => {
/>
+
>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/CLIPVisionModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/CLIPVisionModel.tsx
index 6023fd579f7..2ef2d3644b4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/CLIPVisionModel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/CLIPVisionModel.tsx
@@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import type { CLIPVisionModelV2 } from 'features/controlLayers/store/types';
import { isCLIPVisionModelV2 } from 'features/controlLayers/store/types';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
@@ -54,6 +55,7 @@ export const CLIPVisionModel = memo(({ model, onChange }: Props) => {
value={clipVisionModelValue}
onChange={_onChangeCLIPVisionModel}
/>
+
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx
index 4e4d82f84d0..1e0a3b5c0a3 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx
@@ -2,6 +2,7 @@ import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { selectBase } from 'features/controlLayers/store/paramsSlice';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useIPAdapterOrFLUXReduxModels } from 'services/api/hooks/modelsByType';
@@ -66,6 +67,7 @@ export const IPAdapterModel = memo(({ isRegionalGuidance, modelKey, onChangeMode
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
+
);
diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx
index 11a77fa464c..90dacac0fa4 100644
--- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx
+++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx
@@ -6,6 +6,7 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { loraAdded, selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
import { selectBase } from 'features/controlLayers/store/paramsSlice';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLoRAModels } from 'services/api/hooks/modelsByType';
@@ -58,7 +59,7 @@ const LoRASelect = () => {
const noOptionsMessage = useCallback(() => t('models.noMatchingLoRAs'), [t]);
return (
-
+
{t('models.concepts')}
@@ -71,6 +72,7 @@ const LoRASelect = () => {
data-testid="add-lora"
sx={selectStyles}
/>
+
);
};
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge.tsx
index dd6d1eb2290..fffef0601bc 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge.tsx
@@ -7,7 +7,7 @@ type Props = {
base: BaseModelType;
};
-const BASE_COLOR_MAP: Record = {
+export const BASE_COLOR_MAP: Record = {
any: 'base',
'sd-1': 'green',
'sd-2': 'teal',
@@ -15,12 +15,12 @@ const BASE_COLOR_MAP: Record = {
sdxl: 'invokeBlue',
'sdxl-refiner': 'invokeBlue',
flux: 'gold',
- cogview4: 'orange',
+ cogview4: 'red',
};
const ModelBaseBadge = ({ base }: Props) => {
return (
-
+
{MODEL_TYPE_SHORT_MAP[base]}
);
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage.tsx
index d273d92bdb5..2b8085ff5ba 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage.tsx
@@ -15,7 +15,6 @@ const ModelImage = ({ image_url }: Props) => {
{
const { nodeId, field } = props;
- const { t } = useTranslation();
- const disabledTabs = useAppSelector((s) => s.config.disabledTabs);
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useCLIPEmbedModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: CLIPEmbedModelConfig | null) => {
if (!value) {
return;
@@ -34,32 +29,15 @@ const CLIPEmbedModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
- const required = props.fieldTemplate.required;
return (
-
-
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CLIPGEmbedModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CLIPGEmbedModelFieldInputComponent.tsx
index 7c2860a3677..6f2719a6a16 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CLIPGEmbedModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CLIPGEmbedModelFieldInputComponent.tsx
@@ -1,11 +1,8 @@
-import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldCLIPGEmbedValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { CLIPGEmbedModelFieldInputInstance, CLIPGEmbedModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
-import { useTranslation } from 'react-i18next';
import { useCLIPEmbedModels } from 'services/api/hooks/modelsByType';
import { type CLIPGEmbedModelConfig, isCLIPGEmbedModelConfig } from 'services/api/types';
@@ -15,12 +12,10 @@ type Props = FieldComponentProps {
const { nodeId, field } = props;
- const { t } = useTranslation();
- const disabledTabs = useAppSelector((s) => s.config.disabledTabs);
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useCLIPEmbedModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: CLIPGEmbedModelConfig | null) => {
if (!value) {
return;
@@ -35,32 +30,15 @@ const CLIPGEmbedModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs: modelConfigs.filter((config) => isCLIPGEmbedModelConfig(config)),
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
- const required = props.fieldTemplate.required;
return (
-
-
-
-
-
-
-
+ isCLIPGEmbedModelConfig(config))}
+ isLoadingConfigs={isLoading}
+ onChange={onChange}
+ required={props.fieldTemplate.required}
+ />
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CLIPLEmbedModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CLIPLEmbedModelFieldInputComponent.tsx
index c49c60ac751..1ae0ec2d200 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CLIPLEmbedModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CLIPLEmbedModelFieldInputComponent.tsx
@@ -1,11 +1,8 @@
-import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldCLIPLEmbedValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { CLIPLEmbedModelFieldInputInstance, CLIPLEmbedModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
-import { useTranslation } from 'react-i18next';
import { useCLIPEmbedModels } from 'services/api/hooks/modelsByType';
import { type CLIPLEmbedModelConfig, isCLIPLEmbedModelConfig } from 'services/api/types';
@@ -15,12 +12,10 @@ type Props = FieldComponentProps {
const { nodeId, field } = props;
- const { t } = useTranslation();
- const disabledTabs = useAppSelector((s) => s.config.disabledTabs);
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useCLIPEmbedModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: CLIPLEmbedModelConfig | null) => {
if (!value) {
return;
@@ -35,32 +30,15 @@ const CLIPLEmbedModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs: modelConfigs.filter((config) => isCLIPLEmbedModelConfig(config)),
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
- const required = props.fieldTemplate.required;
return (
-
-
-
-
-
-
-
+ isCLIPLEmbedModelConfig(config))}
+ isLoadingConfigs={isLoading}
+ onChange={onChange}
+ required={props.fieldTemplate.required}
+ />
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlLoraModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlLoraModelFieldInputComponent.tsx
index 9056fbf1024..0280504b0d4 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlLoraModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlLoraModelFieldInputComponent.tsx
@@ -1,16 +1,13 @@
-import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldControlLoRAModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type {
ControlLoRAModelFieldInputInstance,
ControlLoRAModelFieldInputTemplate,
} from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
-import { useTranslation } from 'react-i18next';
import { useControlLoRAModel } from 'services/api/hooks/modelsByType';
-import { type ControlLoRAModelConfig, isControlLoRAModelConfig } from 'services/api/types';
+import type { ControlLoRAModelConfig } from 'services/api/types';
import type { FieldComponentProps } from './types';
@@ -18,12 +15,10 @@ type Props = FieldComponentProps {
const { nodeId, field } = props;
- const { t } = useTranslation();
- const disabledTabs = useAppSelector((s) => s.config.disabledTabs);
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useControlLoRAModel();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: ControlLoRAModelConfig | null) => {
if (!value) {
return;
@@ -38,32 +33,15 @@ const ControlLoRAModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs: modelConfigs.filter((config) => isControlLoRAModelConfig(config)),
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
- const required = props.fieldTemplate.required;
return (
-
-
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx
index c0d1ed3971f..e05f4f98373 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldControlNetModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { ControlNetModelFieldInputInstance, ControlNetModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useControlNetModels } from 'services/api/hooks/modelsByType';
@@ -17,7 +15,7 @@ const ControlNetModelFieldInputComponent = (props: Props) => {
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useControlNetModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: ControlNetModelConfig | null) => {
if (!value) {
return;
@@ -33,25 +31,14 @@ const ControlNetModelFieldInputComponent = (props: Props) => {
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- selectedModel: field.value,
- isLoading,
- });
-
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxMainModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxMainModelFieldInputComponent.tsx
index da43f03386c..afe4c298e6a 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxMainModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxMainModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, Flex, FormControl } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { FluxMainModelFieldInputInstance, FluxMainModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useFluxModels } from 'services/api/hooks/modelsByType';
@@ -16,7 +14,7 @@ const FluxMainModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useFluxModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: MainModelConfig | null) => {
if (!value) {
return;
@@ -31,25 +29,15 @@ const FluxMainModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxReduxModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxReduxModelFieldInputComponent.tsx
index 9e6cdfad948..0f7a58cf011 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxReduxModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxReduxModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldFluxReduxModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { FluxReduxModelFieldInputInstance, FluxReduxModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useFluxReduxModels } from 'services/api/hooks/modelsByType';
@@ -18,7 +16,7 @@ const FluxReduxModelFieldInputComponent = (
const [modelConfigs, { isLoading }] = useFluxReduxModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: FLUXReduxModelConfig | null) => {
if (!value) {
return;
@@ -34,19 +32,14 @@ const FluxReduxModelFieldInputComponent = (
[dispatch, field.name, nodeId]
);
- const { options, value, onChange } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- selectedModel: field.value,
- isLoading,
- });
-
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxVAEModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxVAEModelFieldInputComponent.tsx
index d42e96a4db2..bb27319d0fa 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxVAEModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FluxVAEModelFieldInputComponent.tsx
@@ -1,11 +1,8 @@
-import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldFluxVAEModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { FluxVAEModelFieldInputInstance, FluxVAEModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
-import { useTranslation } from 'react-i18next';
import { useFluxVAEModels } from 'services/api/hooks/modelsByType';
import type { VAEModelConfig } from 'services/api/types';
@@ -15,11 +12,9 @@ type Props = FieldComponentProps {
const { nodeId, field } = props;
- const { t } = useTranslation();
- const disabledTabs = useAppSelector((s) => s.config.disabledTabs);
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useFluxVAEModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: VAEModelConfig | null) => {
if (!value) {
return;
@@ -34,27 +29,15 @@ const FluxVAEModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
return (
-
-
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx
index 93720093b53..2969d11c3b0 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldIPAdapterModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { IPAdapterModelFieldInputInstance, IPAdapterModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useIPAdapterModels } from 'services/api/hooks/modelsByType';
@@ -17,7 +15,7 @@ const IPAdapterModelFieldInputComponent = (
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useIPAdapterModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: IPAdapterModelConfig | null) => {
if (!value) {
return;
@@ -33,19 +31,14 @@ const IPAdapterModelFieldInputComponent = (
[dispatch, field.name, nodeId]
);
- const { options, value, onChange } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- selectedModel: field.value,
- isLoading,
- });
-
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LLaVAModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LLaVAModelFieldInputComponent.tsx
index 5d4d53e8522..0e7154ddedc 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LLaVAModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LLaVAModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, FormControl } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldLLaVAModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { LLaVAModelFieldInputInstance, LLaVAModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useLLaVAModels } from 'services/api/hooks/modelsByType';
@@ -16,7 +14,7 @@ const LLaVAModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useLLaVAModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: LlavaOnevisionConfig | null) => {
if (!value) {
return;
@@ -32,23 +30,14 @@ const LLaVAModelFieldInputComponent = (props: Props) => {
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- selectedModel: field.value,
- isLoading,
- });
-
return (
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx
index c260c2cdccd..01c40e43616 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, FormControl } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldLoRAModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { LoRAModelFieldInputInstance, LoRAModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useLoRAModels } from 'services/api/hooks/modelsByType';
@@ -16,7 +14,7 @@ const LoRAModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useLoRAModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: LoRAModelConfig | null) => {
if (!value) {
return;
@@ -32,23 +30,14 @@ const LoRAModelFieldInputComponent = (props: Props) => {
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- selectedModel: field.value,
- isLoading,
- });
-
return (
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx
index 224c07d86d8..acf6842024e 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, Flex, FormControl } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { MainModelFieldInputInstance, MainModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useNonSDXLMainModels } from 'services/api/hooks/modelsByType';
@@ -16,7 +14,7 @@ const MainModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useNonSDXLMainModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: MainModelConfig | null) => {
if (!value) {
return;
@@ -31,25 +29,15 @@ const MainModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox.tsx
new file mode 100644
index 00000000000..d0d1da0b5d3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox.tsx
@@ -0,0 +1,51 @@
+import { Combobox, FormControl } from '@invoke-ai/ui-library';
+import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { typedMemo } from 'common/util/typedMemo';
+import type { ModelIdentifierField } from 'features/nodes/types/common';
+import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
+import type { AnyModelConfig } from 'services/api/types';
+
+type Props = {
+ value: ModelIdentifierField | undefined;
+ modelConfigs: T[];
+ isLoadingConfigs: boolean;
+ onChange: (value: T | null) => void;
+ required: boolean;
+ groupByType?: boolean;
+};
+
+const _ModelFieldCombobox = ({
+ value: _value,
+ modelConfigs,
+ isLoadingConfigs,
+ onChange: _onChange,
+ required,
+ groupByType,
+}: Props) => {
+ const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
+ modelConfigs,
+ onChange: _onChange,
+ isLoading: isLoadingConfigs,
+ selectedModel: _value,
+ groupByType,
+ });
+
+ return (
+
+
+
+ );
+};
+
+export const ModelFieldCombobox = typedMemo(_ModelFieldCombobox);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent.tsx
index 15198f7be67..37185fe6efc 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent.tsx
@@ -1,9 +1,7 @@
-import { Combobox, Flex, FormControl } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldModelIdentifierValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { ModelIdentifierFieldInputInstance, ModelIdentifierFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback, useMemo } from 'react';
import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models';
@@ -17,7 +15,7 @@ const ModelIdentifierFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { data, isLoading } = useGetModelConfigsQuery();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: AnyModelConfig | null) => {
if (!value) {
return;
@@ -41,26 +39,15 @@ const ModelIdentifierFieldInputComponent = (props: Props) => {
return modelConfigsAdapterSelectors.selectAll(data);
}, [data]);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- groupByType: true,
- });
-
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx
index 7869e5af460..3970a1f4cb8 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, Flex, FormControl } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldRefinerModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type {
SDXLRefinerModelFieldInputInstance,
SDXLRefinerModelFieldInputTemplate,
@@ -19,7 +17,7 @@ const RefinerModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useRefinerModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: MainModelConfig | null) => {
if (!value) {
return;
@@ -34,25 +32,15 @@ const RefinerModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SD3MainModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SD3MainModelFieldInputComponent.tsx
index 6b61bbfd19a..c1aa942781c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SD3MainModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SD3MainModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, Flex, FormControl } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { SD3MainModelFieldInputInstance, SD3MainModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useSD3Models } from 'services/api/hooks/modelsByType';
@@ -16,7 +14,7 @@ const SD3MainModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useSD3Models();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: MainModelConfig | null) => {
if (!value) {
return;
@@ -31,29 +29,15 @@ const SD3MainModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx
index af78704e211..dbc21ce81f0 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, Flex, FormControl } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { SDXLMainModelFieldInputInstance, SDXLMainModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useSDXLModels } from 'services/api/hooks/modelsByType';
@@ -16,7 +14,7 @@ const SDXLMainModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useSDXLModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: MainModelConfig | null) => {
if (!value) {
return;
@@ -31,25 +29,15 @@ const SDXLMainModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SigLipModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SigLipModelFieldInputComponent.tsx
index dfa850c6c3d..1ea330cb64e 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SigLipModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SigLipModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldSigLipModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { SigLipModelFieldInputInstance, SigLipModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useSigLipModels } from 'services/api/hooks/modelsByType';
@@ -18,7 +16,7 @@ const SigLipModelFieldInputComponent = (
const [modelConfigs, { isLoading }] = useSigLipModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: SigLipModelConfig | null) => {
if (!value) {
return;
@@ -34,19 +32,14 @@ const SigLipModelFieldInputComponent = (
[dispatch, field.name, nodeId]
);
- const { options, value, onChange } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- selectedModel: field.value,
- isLoading,
- });
-
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SpandrelImageToImageModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SpandrelImageToImageModelFieldInputComponent.tsx
index cb27e0afa60..1f855982ba0 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SpandrelImageToImageModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SpandrelImageToImageModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldSpandrelImageToImageModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type {
SpandrelImageToImageModelFieldInputInstance,
SpandrelImageToImageModelFieldInputTemplate,
@@ -21,7 +19,7 @@ const SpandrelImageToImageModelFieldInputComponent = (
const [modelConfigs, { isLoading }] = useSpandrelImageToImageModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: SpandrelImageToImageModelConfig | null) => {
if (!value) {
return;
@@ -37,19 +35,14 @@ const SpandrelImageToImageModelFieldInputComponent = (
[dispatch, field.name, nodeId]
);
- const { options, value, onChange } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- selectedModel: field.value,
- isLoading,
- });
-
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx
index 270d4e7c658..2c9cf308794 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldT2IAdapterModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { T2IAdapterModelFieldInputInstance, T2IAdapterModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useT2IAdapterModels } from 'services/api/hooks/modelsByType';
@@ -18,7 +16,7 @@ const T2IAdapterModelFieldInputComponent = (
const [modelConfigs, { isLoading }] = useT2IAdapterModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: T2IAdapterModelConfig | null) => {
if (!value) {
return;
@@ -34,19 +32,14 @@ const T2IAdapterModelFieldInputComponent = (
[dispatch, field.name, nodeId]
);
- const { options, value, onChange } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- selectedModel: field.value,
- isLoading,
- });
-
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T5EncoderModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T5EncoderModelFieldInputComponent.tsx
index eeb659f6456..d7e43a4de7c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T5EncoderModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T5EncoderModelFieldInputComponent.tsx
@@ -1,12 +1,8 @@
-import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldT5EncoderValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { T5EncoderModelFieldInputInstance, T5EncoderModelFieldInputTemplate } from 'features/nodes/types/field';
-import { selectIsModelsTabDisabled } from 'features/system/store/configSlice';
import { memo, useCallback } from 'react';
-import { useTranslation } from 'react-i18next';
import { useT5EncoderModels } from 'services/api/hooks/modelsByType';
import type { T5EncoderBnbQuantizedLlmInt8bModelConfig, T5EncoderModelConfig } from 'services/api/types';
@@ -16,11 +12,9 @@ type Props = FieldComponentProps {
const { nodeId, field } = props;
- const { t } = useTranslation();
- const isModelsTabDisabled = useAppSelector(selectIsModelsTabDisabled);
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useT5EncoderModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: T5EncoderBnbQuantizedLlmInt8bModelConfig | T5EncoderModelConfig | null) => {
if (!value) {
return;
@@ -35,31 +29,14 @@ const T5EncoderModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- isLoading,
- selectedModel: field.value,
- });
- const required = props.fieldTemplate.required;
return (
-
-
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx
index ae1d3fec51a..f8fcc93daf0 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx
@@ -1,8 +1,6 @@
-import { Combobox, Flex, FormControl } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
+import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox';
import { fieldVaeModelValueChanged } from 'features/nodes/store/nodesSlice';
-import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { VAEModelFieldInputInstance, VAEModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useVAEModels } from 'services/api/hooks/modelsByType';
@@ -16,7 +14,7 @@ const VAEModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useVAEModels();
- const _onChange = useCallback(
+ const onChange = useCallback(
(value: VAEModelConfig | null) => {
if (!value) {
return;
@@ -31,30 +29,15 @@ const VAEModelFieldInputComponent = (props: Props) => {
},
[dispatch, field.name, nodeId]
);
- const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
- modelConfigs,
- onChange: _onChange,
- selectedModel: field.value,
- isLoading,
- });
- const required = props.fieldTemplate.required;
return (
-
-
-
-
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx
index 82e26b025ab..8ca13df69e0 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx
@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
import { clipEmbedModelSelected, selectCLIPEmbedModel } from 'features/controlLayers/store/paramsSlice';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCLIPEmbedModels } from 'services/api/hooks/modelsByType';
@@ -31,9 +32,10 @@ const ParamCLIPEmbedModelSelect = () => {
});
return (
-
+
{t('modelManager.clipEmbed')}
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx
index d7ba8b40618..715c89e4cce 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx
@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
import { clipGEmbedModelSelected, selectCLIPGEmbedModel } from 'features/controlLayers/store/paramsSlice';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCLIPEmbedModels } from 'services/api/hooks/modelsByType';
@@ -32,9 +33,10 @@ const ParamCLIPEmbedModelSelect = () => {
});
return (
-
+
{t('modelManager.clipGEmbed')}
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx
index e10d2bc2a2f..c2e8a57e0cc 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx
@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
import { clipLEmbedModelSelected, selectCLIPLEmbedModel } from 'features/controlLayers/store/paramsSlice';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCLIPEmbedModels } from 'services/api/hooks/modelsByType';
@@ -32,9 +33,10 @@ const ParamCLIPEmbedModelSelect = () => {
});
return (
-
+
{t('modelManager.clipLEmbed')}
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx
index 5e451b835f6..5de9f04b685 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx
@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
import { selectT5EncoderModel, t5EncoderModelSelected } from 'features/controlLayers/store/paramsSlice';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useT5EncoderModels } from 'services/api/hooks/modelsByType';
@@ -31,9 +32,10 @@ const ParamT5EncoderModelSelect = () => {
});
return (
-
+
{t('modelManager.t5Encoder')}
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx
index cd29a8035bd..1a21a5045dd 100644
--- a/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx
@@ -1,31 +1,35 @@
import type { IconButtonProps } from '@invoke-ai/ui-library';
import { IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { $onClickGoToModelManager } from 'app/store/nanostores/onClickGoToModelManager';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectIsModelsTabDisabled } from 'features/system/store/configSlice';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiGearSixFill } from 'react-icons/pi';
+import { PiCubeBold } from 'react-icons/pi';
export const NavigateToModelManagerButton = memo((props: Omit) => {
+ const isModelsTabDisabled = useAppSelector(selectIsModelsTabDisabled);
+ const onClickGoToModelManager = useStore($onClickGoToModelManager);
+
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const isModelsTabDisabled = useAppSelector(selectIsModelsTabDisabled);
- const handleClick = useCallback(() => {
+ const onClick = useCallback(() => {
dispatch(setActiveTab('models'));
}, [dispatch]);
- if (isModelsTabDisabled) {
+ if (isModelsTabDisabled && !onClickGoToModelManager) {
return null;
}
return (
}
- tooltip={`${t('common.goTo')} ${t('ui.tabs.modelsTab')}`}
- aria-label={`${t('common.goTo')} ${t('ui.tabs.modelsTab')}`}
- onClick={handleClick}
+ icon={}
+ tooltip={`${t('modelManager.manageModels')}`}
+ aria-label={`${t('modelManager.manageModels')}`}
+ onClick={onClickGoToModelManager ?? onClick}
size="sm"
variant="ghost"
{...props}
diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx
index 4d8d3b7fd27..ecebcb63a2a 100644
--- a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx
@@ -1,9 +1,11 @@
-import { Box, Combobox, Flex, FormControl, FormLabel, Icon, Spacer, Tooltip } from '@invoke-ai/ui-library';
+import { Box, Combobox, Flex, FormControl, FormLabel, Icon, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { selectModelKey } from 'features/controlLayers/store/paramsSlice';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
+import { UseDefaultSettingsButton } from 'features/parameters/components/MainModel/UseDefaultSettingsButton';
import { modelSelected } from 'features/parameters/store/actions';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useMemo } from 'react';
@@ -77,21 +79,17 @@ const ParamMainModelSelect = () => {
}, [selectedModel]);
return (
-
-
-
- {t('modelManager.model')}
+
+
+ {t('modelManager.model')}
+
+ {isFluxDevSelected && (
+
+
+
+
- {isFluxDevSelected ? (
-
-
-
-
-
- ) : (
-
- )}
-
+ )}
{
/>
+
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/ParamPostProcessingModel.tsx b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/ParamPostProcessingModel.tsx
index 58eb99faf88..996f5a151bc 100644
--- a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/ParamPostProcessingModel.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/ParamPostProcessingModel.tsx
@@ -1,6 +1,7 @@
-import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
+import { Box, Combobox, Flex, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { postProcessingModelChanged, selectPostProcessingModel } from 'features/parameters/store/upscaleSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -38,18 +39,21 @@ const ParamPostProcessingModel = () => {
return (
{t('upscaling.postProcessingModel')}
-
-
-
-
-
+
+
+
+
+
+
+
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx
index 7bb9d03d278..c8a0d8a9818 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx
@@ -1,7 +1,8 @@
-import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
+import { Box, Combobox, Flex, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { selectUpscaleModel, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -41,18 +42,21 @@ const ParamSpandrelModel = () => {
{t('upscaling.upscaleModel')}
-
-
-
-
-
+
+
+
+
+
+
+
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx
index eda71eeea1c..e63d81ca9d1 100644
--- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx
@@ -4,6 +4,7 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { fluxVAESelected, selectFLUXVAE } from 'features/controlLayers/store/paramsSlice';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useFluxVAEModels } from 'services/api/hooks/modelsByType';
@@ -32,11 +33,12 @@ const ParamFLUXVAEModelSelect = () => {
});
return (
-
+
{t('modelManager.vae')}
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx
index 40dcfdadca5..3ae7e943af1 100644
--- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx
@@ -4,6 +4,7 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { selectBase, selectVAE, vaeSelected } from 'features/controlLayers/store/paramsSlice';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useVAEModels } from 'services/api/hooks/modelsByType';
@@ -38,7 +39,7 @@ const ParamVAEModelSelect = () => {
});
return (
-
+
{t('modelManager.vae')}
@@ -50,6 +51,7 @@ const ParamVAEModelSelect = () => {
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
+
);
};
diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx
index 75b00624a69..39f94064461 100644
--- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx
@@ -1,9 +1,10 @@
-import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
+import { Combobox, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
import { refinerModelChanged, selectRefinerModel } from 'features/controlLayers/store/paramsSlice';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
@@ -39,27 +40,26 @@ const ParamSDXLRefinerModelSelect = () => {
}, [_onChange]);
return (
-
+
{t('sdxl.refinermodel')}
-
-
- }
- aria-label={t('common.reset')}
- onClick={onReset}
- isDisabled={!value}
- />
-
+
+ }
+ aria-label={t('common.reset')}
+ onClick={onReset}
+ isDisabled={!value}
+ />
+
);
};
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
index 79997dfbd76..d612361298e 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
@@ -11,11 +11,9 @@ import ParamCFGScale from 'features/parameters/components/Core/ParamCFGScale';
import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
import ParamScheduler from 'features/parameters/components/Core/ParamScheduler';
import ParamSteps from 'features/parameters/components/Core/ParamSteps';
-import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
-import ParamMainModelSelect from 'features/parameters/components/MainModel/ParamMainModelSelect';
-import { UseDefaultSettingsButton } from 'features/parameters/components/MainModel/UseDefaultSettingsButton';
import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale';
import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler';
+import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker';
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -68,17 +66,9 @@ export const GenerationSettingsAccordion = memo(() => {
>
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
new file mode 100644
index 00000000000..f681fc26208
--- /dev/null
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
@@ -0,0 +1,444 @@
+import type { BoxProps, ButtonProps, InputProps, SystemStyleObject } from '@invoke-ai/ui-library';
+import {
+ Badge,
+ Button,
+ Flex,
+ FormLabel,
+ IconButton,
+ Input,
+ InputGroup,
+ InputRightElement,
+ Popover,
+ PopoverArrow,
+ PopoverBody,
+ PopoverContent,
+ PopoverTrigger,
+ Portal,
+ Spacer,
+ Text,
+} from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
+import type { Group, ImperativeModelPickerHandle } from 'common/components/Picker/Picker';
+import { getRegex, Picker, usePickerContext } from 'common/components/Picker/Picker';
+import { useDisclosure } from 'common/hooks/useBoolean';
+import { fixedForwardRef } from 'common/util/fixedForwardRef';
+import { typedMemo } from 'common/util/typedMemo';
+import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels';
+import { BASE_COLOR_MAP } from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
+import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage';
+import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
+import { UseDefaultSettingsButton } from 'features/parameters/components/MainModel/UseDefaultSettingsButton';
+import { modelSelected } from 'features/parameters/store/actions';
+import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants';
+import { selectCompactModelPicker } from 'features/ui/store/uiSelectors';
+import { compactModelPickerToggled, setActiveTab } from 'features/ui/store/uiSlice';
+import { filesize } from 'filesize';
+import { isEqual } from 'lodash-es';
+import type { PropsWithChildren } from 'react';
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
+import { PiArrowsInLineVerticalBold, PiArrowsOutLineVerticalBold, PiCaretDownBold, PiXBold } from 'react-icons/pi';
+import { useMainModels } from 'services/api/hooks/modelsByType';
+import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
+import type { AnyModelConfig, BaseModelType } from 'services/api/types';
+
+const getOptionId = (modelConfig: AnyModelConfig) => modelConfig.key;
+
+type GroupData = {
+ base: BaseModelType;
+};
+
+type BaseModelTypeFilters = { [key in BaseModelType]?: boolean };
+
+type PickerExtraContext = {
+ toggleBaseModelTypeFilter: (baseModelType: BaseModelType) => void;
+ basesWithModels: BaseModelType[];
+ baseModelTypeFilters: BaseModelTypeFilters;
+};
+
+const ModelManagerLink = memo((props: ButtonProps) => {
+ const dispatch = useAppDispatch();
+ const onClick = useCallback(() => {
+ dispatch(setActiveTab('models'));
+ $installModelsTab.set(3);
+ }, [dispatch]);
+ return ;
+});
+ModelManagerLink.displayName = 'ModelManagerLink';
+
+const components = {
+ LinkComponent: ,
+};
+
+const NoOptionsFallback = memo(() => {
+ const { t } = useTranslation();
+ return (
+
+ {t('modelManager.modelPickerFallbackNoModelsInstalled')}
+
+
+
+
+ );
+});
+NoOptionsFallback.displayName = 'NoOptionsFallback';
+
+export const MainModelPicker = memo(() => {
+ const { t } = useTranslation();
+ const [modelConfigs] = useMainModels();
+ const basesWithModels = useMemo(() => {
+ const bases: BaseModelType[] = [];
+ for (const modelConfig of modelConfigs) {
+ if (!bases.includes(modelConfig.base)) {
+ bases.push(modelConfig.base);
+ }
+ }
+ return bases;
+ }, [modelConfigs]);
+ const [baseModelTypeFilters, setBaseModelTypeFilters] = useState({});
+ useEffect(() => {
+ const newFilters: BaseModelTypeFilters = {};
+ if (isEqual(Object.keys(baseModelTypeFilters), basesWithModels)) {
+ return;
+ }
+ for (const base of basesWithModels) {
+ if (newFilters[base] === undefined) {
+ newFilters[base] = false;
+ } else {
+ newFilters[base] = baseModelTypeFilters[base];
+ }
+ }
+ setBaseModelTypeFilters(newFilters);
+ }, [baseModelTypeFilters, basesWithModels]);
+ const toggleBaseModelTypeFilter = useCallback(
+ (baseModelType: BaseModelType) => {
+ setBaseModelTypeFilters((prev) => {
+ const newFilters: BaseModelTypeFilters = {};
+ for (const base of basesWithModels) {
+ newFilters[base] = baseModelType === base ? !prev[base] : prev[base];
+ }
+ return newFilters;
+ });
+ },
+ [basesWithModels]
+ );
+ const extra = useMemo(
+ () => ({ toggleBaseModelTypeFilter, basesWithModels, baseModelTypeFilters }),
+ [toggleBaseModelTypeFilter, basesWithModels, baseModelTypeFilters]
+ );
+ const grouped = useMemo[]>(() => {
+ // When all groups are disabled, we show all models
+ const areAllGroupsDisabled = Object.values(baseModelTypeFilters).every((v) => !v);
+ const groups: {
+ [base in BaseModelType]?: Group;
+ } = {};
+
+ for (const modelConfig of modelConfigs) {
+ let group = groups[modelConfig.base];
+ if (!group && (baseModelTypeFilters[modelConfig.base] || areAllGroupsDisabled)) {
+ group = {
+ id: modelConfig.base,
+ data: { base: modelConfig.base },
+ options: [],
+ };
+ groups[modelConfig.base] = group;
+ }
+ if (group) {
+ group.options.push(modelConfig);
+ }
+ }
+
+ const sortedGroups: Group[] = [];
+
+ if (groups['flux']) {
+ sortedGroups.push(groups['flux']);
+ delete groups['flux'];
+ }
+ if (groups['cogview4']) {
+ sortedGroups.push(groups['cogview4']);
+ delete groups['cogview4'];
+ }
+ if (groups['sd-1']) {
+ sortedGroups.push(groups['sd-1']);
+ delete groups['sd-1'];
+ }
+ if (groups['sd-2']) {
+ sortedGroups.push(groups['sd-2']);
+ delete groups['sd-2'];
+ }
+ if (groups['sd-3']) {
+ sortedGroups.push(groups['sd-3']);
+ delete groups['sd-3'];
+ }
+ sortedGroups.push(...Object.values(groups));
+
+ return sortedGroups;
+ }, [baseModelTypeFilters, modelConfigs]);
+ const modelConfig = useSelectedModelConfig();
+ const popover = useDisclosure(false);
+ const pickerRef = useRef(null);
+ const dispatch = useAppDispatch();
+
+ const onClose = useCallback(() => {
+ popover.close();
+ pickerRef.current?.setSearchTerm('');
+ }, [popover]);
+
+ const onSelect = useCallback(
+ (model: AnyModelConfig) => {
+ dispatch(modelSelected(model));
+ onClose();
+ },
+ [dispatch, onClose]
+ );
+
+ return (
+
+
+
+ {t('modelManager.model')}
+
+
+
+
+
+
+
+
+
+
+
+ handleRef={pickerRef}
+ options={grouped}
+ getOptionId={getOptionId}
+ onSelect={onSelect}
+ selectedItem={modelConfig}
+ isMatch={isMatch}
+ OptionComponent={PickerOptionComponent}
+ GroupComponent={PickerGroupComponent}
+ SearchBarComponent={SearchBarComponent}
+ noOptionsFallback={}
+ noMatchesFallback={t('modelManager.noMatchingModels')}
+ extra={extra}
+ />
+
+
+
+
+ );
+});
+MainModelPicker.displayName = 'MainModelPicker';
+
+const SearchBarComponent = typedMemo(
+ fixedForwardRef((props, ref) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const compactModelPicker = useAppSelector(selectCompactModelPicker);
+ const { extra, setSearchTerm, options } = usePickerContext();
+ const onToggleCompact = useCallback(() => {
+ dispatch(compactModelPickerToggled());
+ }, [dispatch]);
+ const onClearSearchTerm = useCallback(() => {
+ setSearchTerm('');
+ }, [setSearchTerm]);
+ return (
+
+
+
+
+ {props.value && (
+
+ }
+ />
+
+ )}
+
+
+ : }
+ onClick={onToggleCompact}
+ isDisabled={options.length === 0}
+ />
+
+
+ {extra.basesWithModels.map((base) => (
+
+ ))}
+
+
+ );
+ })
+);
+SearchBarComponent.displayName = 'SearchBarComponent';
+
+const ModelBaseFilterButton = memo(({ base }: { base: BaseModelType }) => {
+ const { extra } = usePickerContext();
+
+ const onClick = useCallback(() => {
+ extra.toggleBaseModelTypeFilter(base);
+ }, [base, extra]);
+
+ return (
+
+ {MODEL_TYPE_SHORT_MAP[base]}
+
+ );
+});
+ModelBaseFilterButton.displayName = 'ModelBaseFilterButton';
+
+const PickerGroupComponent = memo(
+ ({ group, children }: PropsWithChildren<{ group: Group }>) => {
+ return (
+
+
+
+ {children}
+
+
+ );
+ }
+);
+PickerGroupComponent.displayName = 'PickerGroupComponent';
+
+const groupSx = {
+ flexDir: 'column',
+ flex: 1,
+ ps: 2,
+ pe: 4,
+ py: 1,
+ userSelect: 'none',
+ position: 'sticky',
+ top: 0,
+ bg: 'base.800',
+ minH: 8,
+} satisfies SystemStyleObject;
+
+const GroupHeader = memo(({ group, ...rest }: { group: Group } & BoxProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {MODEL_TYPE_SHORT_MAP[group.data.base]}
+
+
+ {t('common.model_withCount', { count: group.options.length })}
+
+
+
+ );
+});
+GroupHeader.displayName = 'GroupHeader';
+
+const optionSx: SystemStyleObject = {
+ p: 2,
+ gap: 2,
+ cursor: 'pointer',
+ borderRadius: 'base',
+ '&[data-selected="true"]': {
+ bg: 'base.700',
+ '&[data-active="true"]': {
+ bg: 'base.650',
+ },
+ },
+ '&[data-active="true"]': {
+ bg: 'base.750',
+ },
+ '&[data-disabled="true"]': {
+ cursor: 'not-allowed',
+ opacity: 0.5,
+ },
+ '&[data-is-compact="true"]': {
+ py: 1,
+ },
+ scrollMarginTop: '42px', // magic number, this is the height of the header
+};
+
+const optionNameSx: SystemStyleObject = {
+ fontSize: 'sm',
+ noOfLines: 1,
+ fontWeight: 'semibold',
+ '&[data-is-compact="true"]': {
+ fontWeight: 'normal',
+ },
+};
+
+const PickerOptionComponent = typedMemo(({ option, ...rest }: { option: AnyModelConfig } & BoxProps) => {
+ const compactModelPicker = useAppSelector(selectCompactModelPicker);
+
+ return (
+
+ {!compactModelPicker && }
+
+
+
+ {option.name}
+
+
+ {option.file_size > 0 && (
+
+ {filesize(option.file_size)}
+
+ )}
+
+ {option.description && !compactModelPicker && {option.description}}
+
+
+ );
+});
+PickerOptionComponent.displayName = 'PickerItemComponent';
+
+const BASE_KEYWORDS: { [key in BaseModelType]?: string[] } = {
+ 'sd-1': ['sd1', 'sd1.4', 'sd1.5', 'sd-1'],
+ 'sd-2': ['sd2', 'sd2.0', 'sd2.1', 'sd-2'],
+ 'sd-3': ['sd3', 'sd3.0', 'sd3.5', 'sd-3'],
+};
+
+const isMatch = (model: AnyModelConfig, searchTerm: string) => {
+ const regex = getRegex(searchTerm);
+ const bases = BASE_KEYWORDS[model.base] ?? [model.base];
+ const testString =
+ `${model.name} ${bases.join(' ')} ${model.type} ${model.description ?? ''} ${model.format}`.toLowerCase();
+
+ if (testString.includes(searchTerm) || regex.test(testString)) {
+ return true;
+ }
+
+ return false;
+};
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
index 3990c28aa2b..cd552ba80b1 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
@@ -5,3 +5,4 @@ export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTa
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
export const selectActiveTabCanvasRightPanel = createSelector(selectUiSlice, (ui) => ui.activeTabCanvasRightPanel);
+export const selectCompactModelPicker = createSelector(selectUiSlice, (ui) => ui.compactModelPicker);
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
index 8e3fe6b3508..dd84431fb55 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
@@ -16,6 +16,7 @@ const initialUIState: UIState = {
accordions: {},
expanders: {},
shouldShowNotificationV2: true,
+ compactModelPicker: false,
};
export const uiSlice = createSlice({
@@ -45,6 +46,9 @@ export const uiSlice = createSlice({
shouldShowNotificationChanged: (state, action: PayloadAction) => {
state.shouldShowNotificationV2 = action.payload;
},
+ compactModelPickerToggled: (state) => {
+ state.compactModelPicker = !state.compactModelPicker;
+ },
},
extraReducers(builder) {
builder.addCase(workflowLoaded, (state) => {
@@ -64,6 +68,7 @@ export const {
accordionStateChanged,
expanderStateChanged,
shouldShowNotificationChanged,
+ compactModelPickerToggled,
} = uiSlice.actions;
export const selectUiSlice = (state: RootState) => state.ui;
diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
index 0c06b179703..cf71f0e8a67 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
@@ -34,4 +34,8 @@ export interface UIState {
* Whether or not to show the user the open notification. Bump version to reset users who may have closed previous version.
*/
shouldShowNotificationV2: boolean;
+ /**
+ * Whether or not to use compact view for the model picker.
+ */
+ compactModelPicker: boolean;
}
diff --git a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
index ddd06eff280..24c6da56b33 100644
--- a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
+++ b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
@@ -56,7 +56,6 @@ const buildModelsHook =
return [modelConfigs, result] as const;
};
-
export const useMainModels = buildModelsHook(isNonRefinerMainModelConfig);
export const useNonSDXLMainModels = buildModelsHook(isNonSDXLMainModelConfig);
export const useRefinerModels = buildModelsHook(isRefinerMainModelModelConfig);
diff --git a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts
index 6f35a5fe911..adf197b74a9 100644
--- a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts
+++ b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts
@@ -5,7 +5,7 @@ import { useGetModelConfigQuery } from 'services/api/endpoints/models';
export const useSelectedModelConfig = () => {
const key = useAppSelector(selectModelKey);
- const { currentData: modelConfig } = useGetModelConfigQuery(key ?? skipToken);
+ const { data: modelConfig } = useGetModelConfigQuery(key ?? skipToken);
return modelConfig;
};