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 + + + + + + + + + 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; };