From 6249982d821306b2bece320ff12aacc862b38871 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 05:57:21 +1000 Subject: [PATCH 001/442] fix(ui): stuck viewer when spamming toggle There are a number of bugs with `framer-motion` that can result in sync issues with AnimatePresence and the conditionally rendered component. You can see this if you rapidly click an accordion, occasionally it gets out of sync and is closed when it should be open. This is a bigger problem with the viewer where the user may hold down the `z` key. It's trivial to get it to lock up. For now, just remove the animation entirely. Upstream issues for reference: https://github.com/framer/motion/issues/2023 https://github.com/framer/motion/issues/2618 https://github.com/framer/motion/issues/2554 --- .../components/ImageViewer/ImageViewer.tsx | 90 ++++++++----------- 1 file changed, 35 insertions(+), 55 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx index 949e72fad1..dcd4d4c304 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -5,8 +5,6 @@ import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/To import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; import type { InvokeTabName } from 'features/ui/store/tabMap'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { AnimationProps } from 'framer-motion'; -import { AnimatePresence, motion } from 'framer-motion'; import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -14,18 +12,6 @@ import CurrentImageButtons from './CurrentImageButtons'; import CurrentImagePreview from './CurrentImagePreview'; import { EditorButton } from './EditorButton'; -const initial: AnimationProps['initial'] = { - opacity: 0, -}; -const animate: AnimationProps['animate'] = { - opacity: 1, - transition: { duration: 0.07 }, -}; -const exit: AnimationProps['exit'] = { - opacity: 0, - transition: { duration: 0.07 }, -}; - const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows']; export const ImageViewer = memo(() => { @@ -42,50 +28,44 @@ export const ImageViewer = memo(() => { useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]); useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]); - // The AnimatePresence mode must be wait - else framer can get confused if you spam the toggle button + if (!shouldShowViewer) { + return null; + } + return ( - - {shouldShowViewer && ( - - - - - - - - - - - - - - - - + + + + + + - - )} - + + + + + + + + + + + ); }); From 3bd5d9a8e4e5357b20cf8e9d9ce95fdc4207a0d7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 06:00:04 +1000 Subject: [PATCH 002/442] fix(ui): memoize FloatingImageViewer Maybe this will fix @JPPhoto's issue? --- .../ImageViewer/FloatingImageViewer.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx index 1107e86ff3..1d91dafd1c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx @@ -2,7 +2,7 @@ import { Flex, IconButton, Spacer, Text, useShiftModifier } from '@invoke-ai/ui- import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview'; import { isFloatingImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import { useCallback, useLayoutEffect, useRef } from 'react'; +import { memo, useCallback, useLayoutEffect, useRef } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { PiHourglassBold, PiXBold } from 'react-icons/pi'; @@ -29,7 +29,7 @@ const enableResizing = { topLeft: false, }; -const FloatingImageViewerComponent = () => { +const FloatingImageViewerComponent = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const shift = useShiftModifier(); @@ -148,9 +148,11 @@ const FloatingImageViewerComponent = () => { ); -}; +}); -export const FloatingImageViewer = () => { +FloatingImageViewerComponent.displayName = 'FloatingImageViewerComponent'; + +export const FloatingImageViewer = memo(() => { const isOpen = useAppSelector((s) => s.gallery.isFloatingImageViewerOpen); if (!isOpen) { @@ -158,9 +160,11 @@ export const FloatingImageViewer = () => { } return ; -}; +}); -export const ToggleFloatingImageViewerButton = () => { +FloatingImageViewer.displayName = 'FloatingImageViewer'; + +export const ToggleFloatingImageViewerButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isOpen = useAppSelector((s) => s.gallery.isFloatingImageViewerOpen); @@ -181,4 +185,6 @@ export const ToggleFloatingImageViewerButton = () => { boxSize={8} /> ); -}; +}); + +ToggleFloatingImageViewerButton.displayName = 'ToggleFloatingImageViewerButton'; From a9bf651c6920ac3ab7806a2924c0f1a60291cde3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 07:06:54 +1000 Subject: [PATCH 003/442] chore(ui): bump all deps --- invokeai/frontend/web/package.json | 78 +- invokeai/frontend/web/pnpm-lock.yaml | 5010 ++++++++++++-------------- 2 files changed, 2316 insertions(+), 2772 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 25a77cf918..a598d0a2c7 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -52,48 +52,48 @@ }, "dependencies": { "@chakra-ui/react-use-size": "^2.1.0", - "@dagrejs/dagre": "^1.1.1", - "@dagrejs/graphlib": "^2.2.1", + "@dagrejs/dagre": "^1.1.2", + "@dagrejs/graphlib": "^2.2.2", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", - "@fontsource-variable/inter": "^5.0.17", + "@fontsource-variable/inter": "^5.0.18", "@invoke-ai/ui-library": "^0.0.25", "@nanostores/react": "^0.7.2", - "@reduxjs/toolkit": "2.2.2", + "@reduxjs/toolkit": "2.2.3", "@roarr/browser-log-writer": "^1.3.0", "chakra-react-select": "^4.7.6", "compare-versions": "^6.1.0", "dateformat": "^5.0.3", - "framer-motion": "^11.0.22", - "i18next": "^23.10.1", - "i18next-http-backend": "^2.5.0", + "framer-motion": "^11.1.8", + "i18next": "^23.11.3", + "i18next-http-backend": "^2.5.1", "idb-keyval": "^6.2.1", "jsondiffpatch": "^0.6.0", "konva": "^9.3.6", "lodash-es": "^4.17.21", - "nanostores": "^0.10.0", + "nanostores": "^0.10.3", "new-github-issue-url": "^1.0.0", - "overlayscrollbars": "^2.6.1", - "overlayscrollbars-react": "^0.5.5", + "overlayscrollbars": "^2.7.3", + "overlayscrollbars-react": "^0.5.6", "query-string": "^9.0.0", - "react": "^18.2.0", + "react": "^18.3.1", "react-colorful": "^5.6.1", - "react-dom": "^18.2.0", + "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-error-boundary": "^4.0.13", - "react-hook-form": "^7.51.2", + "react-hook-form": "^7.51.4", "react-hotkeys-hook": "4.5.0", - "react-i18next": "^14.1.0", - "react-icons": "^5.0.1", + "react-i18next": "^14.1.1", + "react-icons": "^5.2.0", "react-konva": "^18.2.10", - "react-redux": "9.1.0", - "react-resizable-panels": "^2.0.16", + "react-redux": "9.1.2", + "react-resizable-panels": "^2.0.19", "react-rnd": "^10.4.10", "react-select": "5.8.0", "react-use": "^17.5.0", - "react-virtuoso": "^4.7.5", - "reactflow": "^11.10.4", + "react-virtuoso": "^4.7.10", + "reactflow": "^11.11.3", "redux-dynamic-middlewares": "^2.2.0", "redux-remember": "^5.1.0", "redux-undo": "^1.1.0", @@ -105,8 +105,8 @@ "use-device-pixel-ratio": "^1.1.2", "use-image": "^1.1.1", "uuid": "^9.0.1", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.3" + "zod": "^3.23.6", + "zod-validation-error": "^3.2.0" }, "peerDependencies": { "@chakra-ui/react": "^2.8.2", @@ -117,19 +117,19 @@ "devDependencies": { "@invoke-ai/eslint-config-react": "^0.0.14", "@invoke-ai/prettier-config-react": "^0.0.7", - "@storybook/addon-essentials": "^8.0.4", - "@storybook/addon-interactions": "^8.0.4", - "@storybook/addon-links": "^8.0.4", - "@storybook/addon-storysource": "^8.0.4", - "@storybook/manager-api": "^8.0.4", - "@storybook/react": "^8.0.4", - "@storybook/react-vite": "^8.0.4", - "@storybook/theming": "^8.0.4", + "@storybook/addon-essentials": "^8.0.10", + "@storybook/addon-interactions": "^8.0.10", + "@storybook/addon-links": "^8.0.10", + "@storybook/addon-storysource": "^8.0.10", + "@storybook/manager-api": "^8.0.10", + "@storybook/react": "^8.0.10", + "@storybook/react-vite": "^8.0.10", + "@storybook/theming": "^8.0.10", "@types/dateformat": "^5.0.2", "@types/lodash-es": "^4.17.12", - "@types/node": "^20.11.30", - "@types/react": "^18.2.73", - "@types/react-dom": "^18.2.22", + "@types/node": "^20.12.10", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", "@types/uuid": "^9.0.8", "@vitejs/plugin-react-swc": "^3.6.0", "concurrently": "^8.2.2", @@ -137,20 +137,20 @@ "eslint": "^8.57.0", "eslint-plugin-i18next": "^6.0.3", "eslint-plugin-path": "^1.3.0", - "knip": "^5.6.1", + "knip": "^5.12.3", "openapi-types": "^12.1.3", "openapi-typescript": "^6.7.5", "prettier": "^3.2.5", "rollup-plugin-visualizer": "^5.12.0", - "storybook": "^8.0.4", + "storybook": "^8.0.10", "ts-toolbelt": "^9.6.0", "tsafe": "^1.6.6", - "typescript": "^5.4.3", - "vite": "^5.2.6", - "vite-plugin-css-injected-by-js": "^3.5.0", - "vite-plugin-dts": "^3.8.0", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vite-plugin-css-injected-by-js": "^3.5.1", + "vite-plugin-dts": "^3.9.1", "vite-plugin-eslint": "^1.8.1", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.4.0" + "vitest": "^1.6.0" } } diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 3d688dddce..2a3710ae9c 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -7,43 +7,43 @@ settings: dependencies: '@chakra-ui/react': specifier: ^2.8.2 - version: 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) + version: 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/react-use-size': specifier: ^2.1.0 - version: 2.1.0(react@18.2.0) + version: 2.1.0(react@18.3.1) '@dagrejs/dagre': - specifier: ^1.1.1 - version: 1.1.1 + specifier: ^1.1.2 + version: 1.1.2 '@dagrejs/graphlib': - specifier: ^2.2.1 - version: 2.2.1 + specifier: ^2.2.2 + version: 2.2.2 '@dnd-kit/core': specifier: ^6.1.0 - version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + version: 6.1.0(react-dom@18.3.1)(react@18.3.1) '@dnd-kit/sortable': specifier: ^8.0.0 - version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.3.1) '@dnd-kit/utilities': specifier: ^3.2.2 - version: 3.2.2(react@18.2.0) + version: 3.2.2(react@18.3.1) '@fontsource-variable/inter': - specifier: ^5.0.17 - version: 5.0.17 + specifier: ^5.0.18 + version: 5.0.18 '@invoke-ai/ui-library': specifier: ^0.0.25 - version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) + version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.18)(@internationalized/date@3.5.3)(@types/react@18.3.1)(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1) '@nanostores/react': specifier: ^0.7.2 - version: 0.7.2(nanostores@0.10.0)(react@18.2.0) + version: 0.7.2(nanostores@0.10.3)(react@18.3.1) '@reduxjs/toolkit': - specifier: 2.2.2 - version: 2.2.2(react-redux@9.1.0)(react@18.2.0) + specifier: 2.2.3 + version: 2.2.3(react-redux@9.1.2)(react@18.3.1) '@roarr/browser-log-writer': specifier: ^1.3.0 version: 1.3.0 chakra-react-select: specifier: ^4.7.6 - version: 4.7.6(@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.11.4)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + version: 4.7.6(@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.11.4)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) compare-versions: specifier: ^6.1.0 version: 6.1.0 @@ -51,14 +51,14 @@ dependencies: specifier: ^5.0.3 version: 5.0.3 framer-motion: - specifier: ^11.0.22 - version: 11.0.22(react-dom@18.2.0)(react@18.2.0) + specifier: ^11.1.8 + version: 11.1.8(react-dom@18.3.1)(react@18.3.1) i18next: - specifier: ^23.10.1 - version: 23.10.1 + specifier: ^23.11.3 + version: 23.11.3 i18next-http-backend: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^2.5.1 + version: 2.5.1 idb-keyval: specifier: ^6.2.1 version: 6.2.1 @@ -72,71 +72,71 @@ dependencies: specifier: ^4.17.21 version: 4.17.21 nanostores: - specifier: ^0.10.0 - version: 0.10.0 + specifier: ^0.10.3 + version: 0.10.3 new-github-issue-url: specifier: ^1.0.0 version: 1.0.0 overlayscrollbars: - specifier: ^2.6.1 - version: 2.6.1 + specifier: ^2.7.3 + version: 2.7.3 overlayscrollbars-react: - specifier: ^0.5.5 - version: 0.5.5(overlayscrollbars@2.6.1)(react@18.2.0) + specifier: ^0.5.6 + version: 0.5.6(overlayscrollbars@2.7.3)(react@18.3.1) query-string: specifier: ^9.0.0 version: 9.0.0 react: - specifier: ^18.2.0 - version: 18.2.0 + specifier: ^18.3.1 + version: 18.3.1 react-colorful: specifier: ^5.6.1 - version: 5.6.1(react-dom@18.2.0)(react@18.2.0) + version: 5.6.1(react-dom@18.3.1)(react@18.3.1) react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) react-dropzone: specifier: ^14.2.3 - version: 14.2.3(react@18.2.0) + version: 14.2.3(react@18.3.1) react-error-boundary: specifier: ^4.0.13 - version: 4.0.13(react@18.2.0) + version: 4.0.13(react@18.3.1) react-hook-form: - specifier: ^7.51.2 - version: 7.51.2(react@18.2.0) + specifier: ^7.51.4 + version: 7.51.4(react@18.3.1) react-hotkeys-hook: specifier: 4.5.0 - version: 4.5.0(react-dom@18.2.0)(react@18.2.0) + version: 4.5.0(react-dom@18.3.1)(react@18.3.1) react-i18next: - specifier: ^14.1.0 - version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) + specifier: ^14.1.1 + version: 14.1.1(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1) react-icons: - specifier: ^5.0.1 - version: 5.0.1(react@18.2.0) + specifier: ^5.2.0 + version: 5.2.0(react@18.3.1) react-konva: specifier: ^18.2.10 - version: 18.2.10(konva@9.3.6)(react-dom@18.2.0)(react@18.2.0) + version: 18.2.10(konva@9.3.6)(react-dom@18.3.1)(react@18.3.1) react-redux: - specifier: 9.1.0 - version: 9.1.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1) + specifier: 9.1.2 + version: 9.1.2(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1) react-resizable-panels: - specifier: ^2.0.16 - version: 2.0.16(react-dom@18.2.0)(react@18.2.0) + specifier: ^2.0.19 + version: 2.0.19(react-dom@18.3.1)(react@18.3.1) react-rnd: specifier: ^10.4.10 - version: 10.4.10(react-dom@18.2.0)(react@18.2.0) + version: 10.4.10(react-dom@18.3.1)(react@18.3.1) react-select: specifier: 5.8.0 - version: 5.8.0(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + version: 5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) react-use: specifier: ^17.5.0 - version: 17.5.0(react-dom@18.2.0)(react@18.2.0) + version: 17.5.0(react-dom@18.3.1)(react@18.3.1) react-virtuoso: - specifier: ^4.7.5 - version: 4.7.5(react-dom@18.2.0)(react@18.2.0) + specifier: ^4.7.10 + version: 4.7.10(react-dom@18.3.1)(react@18.3.1) reactflow: - specifier: ^11.10.4 - version: 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + specifier: ^11.11.3 + version: 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) redux-dynamic-middlewares: specifier: ^2.2.0 version: 2.2.0 @@ -160,54 +160,54 @@ dependencies: version: 4.7.5 use-debounce: specifier: ^10.0.0 - version: 10.0.0(react@18.2.0) + version: 10.0.0(react@18.3.1) use-device-pixel-ratio: specifier: ^1.1.2 - version: 1.1.2(react@18.2.0) + version: 1.1.2(react@18.3.1) use-image: specifier: ^1.1.1 - version: 1.1.1(react-dom@18.2.0)(react@18.2.0) + version: 1.1.1(react-dom@18.3.1)(react@18.3.1) uuid: specifier: ^9.0.1 version: 9.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.6 + version: 3.23.6 zod-validation-error: - specifier: ^3.0.3 - version: 3.0.3(zod@3.22.4) + specifier: ^3.2.0 + version: 3.2.0(zod@3.23.6) devDependencies: '@invoke-ai/eslint-config-react': specifier: ^0.0.14 - version: 0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.3) + version: 0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) '@invoke-ai/prettier-config-react': specifier: ^0.0.7 version: 0.0.7(prettier@3.2.5) '@storybook/addon-essentials': - specifier: ^8.0.4 - version: 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) '@storybook/addon-interactions': - specifier: ^8.0.4 - version: 8.0.4(vitest@1.4.0) + specifier: ^8.0.10 + version: 8.0.10(vitest@1.6.0) '@storybook/addon-links': - specifier: ^8.0.4 - version: 8.0.4(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(react@18.3.1) '@storybook/addon-storysource': - specifier: ^8.0.4 - version: 8.0.4 + specifier: ^8.0.10 + version: 8.0.10 '@storybook/manager-api': - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1) '@storybook/react': - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5) '@storybook/react-vite': - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3)(vite@5.2.6) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5)(vite@5.2.11) '@storybook/theming': - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1) '@types/dateformat': specifier: ^5.0.2 version: 5.0.2 @@ -215,20 +215,20 @@ devDependencies: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^20.11.30 - version: 20.11.30 + specifier: ^20.12.10 + version: 20.12.10 '@types/react': - specifier: ^18.2.73 - version: 18.2.73 + specifier: ^18.3.1 + version: 18.3.1 '@types/react-dom': - specifier: ^18.2.22 - version: 18.2.22 + specifier: ^18.3.0 + version: 18.3.0 '@types/uuid': specifier: ^9.0.8 version: 9.0.8 '@vitejs/plugin-react-swc': specifier: ^3.6.0 - version: 3.6.0(vite@5.2.6) + version: 3.6.0(vite@5.2.11) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -245,8 +245,8 @@ devDependencies: specifier: ^1.3.0 version: 1.3.0(eslint@8.57.0) knip: - specifier: ^5.6.1 - version: 5.6.1(@types/node@20.11.30)(typescript@5.4.3) + specifier: ^5.12.3 + version: 5.12.3(@types/node@20.12.10)(typescript@5.4.5) openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -260,8 +260,8 @@ devDependencies: specifier: ^5.12.0 version: 5.12.0 storybook: - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1) ts-toolbelt: specifier: ^9.6.0 version: 9.6.0 @@ -269,34 +269,29 @@ devDependencies: specifier: ^1.6.6 version: 1.6.6 typescript: - specifier: ^5.4.3 - version: 5.4.3 + specifier: ^5.4.5 + version: 5.4.5 vite: - specifier: ^5.2.6 - version: 5.2.6(@types/node@20.11.30) + specifier: ^5.2.11 + version: 5.2.11(@types/node@20.12.10) vite-plugin-css-injected-by-js: - specifier: ^3.5.0 - version: 3.5.0(vite@5.2.6) + specifier: ^3.5.1 + version: 3.5.1(vite@5.2.11) vite-plugin-dts: - specifier: ^3.8.0 - version: 3.8.0(@types/node@20.11.30)(typescript@5.4.3)(vite@5.2.6) + specifier: ^3.9.1 + version: 3.9.1(@types/node@20.12.10)(typescript@5.4.5)(vite@5.2.11) vite-plugin-eslint: specifier: ^1.8.1 - version: 1.8.1(eslint@8.57.0)(vite@5.2.6) + version: 1.8.1(eslint@8.57.0)(vite@5.2.11) vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.4.3)(vite@5.2.6) + version: 4.3.2(typescript@5.4.5)(vite@5.2.11) vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.30) + specifier: ^1.6.0 + version: 1.6.0(@types/node@20.12.10) packages: - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: true - /@adobe/css-tools@4.3.3: resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} dev: true @@ -348,7 +343,7 @@ packages: - '@internationalized/date' dev: false - /@ark-ui/react@1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0): + /@ark-ui/react@1.3.0(@internationalized/date@3.5.3)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-JHjNoIX50+mUCTaEGMjfGQWGGi31pKsV646jZJlR/1xohpYJigzg8BvO97cTsVk8fwtur+cm11gz3Nf7f5QUnA==} peerDependencies: react: '>=18.0.0' @@ -378,7 +373,7 @@ packages: '@zag-js/progress': 0.32.1 '@zag-js/radio-group': 0.32.1 '@zag-js/rating-group': 0.32.1 - '@zag-js/react': 0.32.1(react-dom@18.2.0)(react@18.2.0) + '@zag-js/react': 0.32.1(react-dom@18.3.1)(react@18.3.1) '@zag-js/select': 0.32.1 '@zag-js/slider': 0.32.1 '@zag-js/splitter': 0.32.1 @@ -389,8 +384,8 @@ packages: '@zag-js/toggle-group': 0.32.1 '@zag-js/tooltip': 0.32.1 '@zag-js/types': 0.32.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@internationalized/date' dev: false @@ -406,28 +401,28 @@ packages: resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.24.2 + '@babel/highlight': 7.24.5 picocolors: 1.0.0 - /@babel/compat-data@7.24.1: - resolution: {integrity: sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==} + /@babel/compat-data@7.24.4: + resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} engines: {node: '>=6.9.0'} dev: true - /@babel/core@7.24.3: - resolution: {integrity: sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==} + /@babel/core@7.24.5: + resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.1 + '@babel/generator': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helpers': 7.24.1 - '@babel/parser': 7.24.1 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helpers': 7.24.5 + '@babel/parser': 7.24.5 '@babel/template': 7.24.0 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 + '@babel/traverse': 7.24.5 + '@babel/types': 7.24.5 convert-source-map: 2.0.0 debug: 4.3.4 gensync: 1.0.0-beta.2 @@ -437,11 +432,11 @@ packages: - supports-color dev: true - /@babel/generator@7.24.1: - resolution: {integrity: sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==} + /@babel/generator@7.24.5: + resolution: {integrity: sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 @@ -451,65 +446,65 @@ packages: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-compilation-targets@7.23.6: resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/compat-data': 7.24.1 + '@babel/compat-data': 7.24.4 '@babel/helper-validator-option': 7.23.5 browserslist: 4.23.0 lru-cache: 5.1.1 semver: 6.3.1 dev: true - /@babel/helper-create-class-features-plugin@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==} + /@babel/helper-create-class-features-plugin@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.24.5 '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.3) + '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-split-export-declaration': 7.24.5 semver: 6.3.1 dev: true - /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.3): + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.5): resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 regexpu-core: 5.3.2 semver: 6.3.1 dev: true - /@babel/helper-define-polyfill-provider@0.6.1(@babel/core@7.24.3): - resolution: {integrity: sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==} + /@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.5): + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 debug: 4.3.4 lodash.debounce: 4.0.8 resolve: 1.22.8 @@ -527,106 +522,106 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.24.0 - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/helper-member-expression-to-functions@7.23.0: - resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} + /@babel/helper-member-expression-to-functions@7.24.5: + resolution: {integrity: sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-module-imports@7.24.3: resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 - /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.3): - resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + /@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-module-imports': 7.24.3 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-simple-access': 7.24.5 + '@babel/helper-split-export-declaration': 7.24.5 + '@babel/helper-validator-identifier': 7.24.5 dev: true /@babel/helper-optimise-call-expression@7.22.5: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/helper-plugin-utils@7.24.0: - resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==} + /@babel/helper-plugin-utils@7.24.5: + resolution: {integrity: sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.3): + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.5): resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-wrap-function': 7.22.20 + '@babel/helper-wrap-function': 7.24.5 dev: true - /@babel/helper-replace-supers@7.24.1(@babel/core@7.24.3): + /@babel/helper-replace-supers@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.24.5 '@babel/helper-optimise-call-expression': 7.22.5 dev: true - /@babel/helper-simple-access@7.22.5: - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + /@babel/helper-simple-access@7.24.5: + resolution: {integrity: sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-skip-transparent-expression-wrappers@7.22.5: resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/helper-split-export-declaration@7.22.6: - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + /@babel/helper-split-export-declaration@7.24.5: + resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-string-parser@7.24.1: resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + /@babel/helper-validator-identifier@7.24.5: + resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} engines: {node: '>=6.9.0'} /@babel/helper-validator-option@7.23.5: @@ -634,974 +629,986 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/helper-wrap-function@7.22.20: - resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} + /@babel/helper-wrap-function@7.24.5: + resolution: {integrity: sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-function-name': 7.23.0 '@babel/template': 7.24.0 - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/helpers@7.24.1: - resolution: {integrity: sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==} + /@babel/helpers@7.24.5: + resolution: {integrity: sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==} engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.24.0 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 + '@babel/traverse': 7.24.5 + '@babel/types': 7.24.5 transitivePeerDependencies: - supports-color dev: true - /@babel/highlight@7.24.2: - resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} + /@babel/highlight@7.24.5: + resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.5 chalk: 2.4.2 js-tokens: 4.0.0 picocolors: 1.0.0 - /@babel/parser@7.24.1: - resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==} + /@babel/parser@7.24.5: + resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.1(@babel/core@7.24.3): + /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.24.5 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.1(@babel/core@7.24.3): + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) dev: true - /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.1(@babel/core@7.24.3): + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.3): + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.5): resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 dev: true - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.3): + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.5): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.3): + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.5): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.3): + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.5): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-flow@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-flow@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-import-attributes@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-import-attributes@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.3): + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.5): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.3): + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.5): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.3): + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.5): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.3): + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.5): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.3): + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.5): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.3): + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.5): resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-arrow-functions@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-arrow-functions@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-async-generator-functions@7.24.3(@babel/core@7.24.3): + /@babel/plugin-transform-async-generator-functions@7.24.3(@babel/core@7.24.5): resolution: {integrity: sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.3) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.5) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-async-to-generator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-async-to-generator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-module-imports': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-block-scoped-functions@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-block-scoped-functions@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-block-scoping@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==} + /@babel/plugin-transform-block-scoping@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-class-properties@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-class-properties@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-class-static-block@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-FUHlKCn6J3ERiu8Dv+4eoz7w8+kFLSyeVG4vDAikwADGjUCoHw/JHokyGtr8OR4UjpwPVivyF+h8Q5iv/JmrtA==} + /@babel/plugin-transform-class-static-block@7.24.4(@babel/core@7.24.5): + resolution: {integrity: sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-classes@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==} + /@babel/plugin-transform-classes@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.3) - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) + '@babel/helper-split-export-declaration': 7.24.5 globals: 11.12.0 dev: true - /@babel/plugin-transform-computed-properties@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-computed-properties@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/template': 7.24.0 dev: true - /@babel/plugin-transform-destructuring@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==} + /@babel/plugin-transform-destructuring@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-dotall-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-dotall-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-duplicate-keys@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-duplicate-keys@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-dynamic-import@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-dynamic-import@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-exponentiation-operator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-exponentiation-operator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-export-namespace-from@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-export-namespace-from@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-flow-strip-types@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-flow-strip-types@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-flow': 7.24.1(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-flow': 7.24.1(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-for-of@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-for-of@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true - /@babel/plugin-transform-function-name@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-function-name@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-json-strings@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-json-strings@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-literals@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-literals@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-logical-assignment-operators@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-logical-assignment-operators@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-member-expression-literals@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-member-expression-literals@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-modules-amd@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-modules-amd@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-modules-commonjs@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-modules-commonjs@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-simple-access': 7.22.5 + '@babel/core': 7.24.5 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-simple-access': 7.24.5 dev: true - /@babel/plugin-transform-modules-systemjs@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-modules-systemjs@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-validator-identifier': 7.24.5 dev: true - /@babel/plugin-transform-modules-umd@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-modules-umd@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.3): + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.5): resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-new-target@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-new-target@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-nullish-coalescing-operator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-nullish-coalescing-operator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-numeric-separator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-numeric-separator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-object-rest-spread@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA==} + /@babel/plugin-transform-object-rest-spread@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-transform-parameters': 7.24.1(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-object-super@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-object-super@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-optional-catch-binding@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-optional-catch-binding@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-optional-chaining@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg==} + /@babel/plugin-transform-optional-chaining@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-parameters@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==} + /@babel/plugin-transform-parameters@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-private-methods@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-private-methods@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-private-property-in-object@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==} + /@babel/plugin-transform-private-property-in-object@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.3) + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-property-literals@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-property-literals@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-regenerator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-regenerator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 regenerator-transform: 0.15.2 dev: true - /@babel/plugin-transform-reserved-words@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-reserved-words@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-shorthand-properties@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-shorthand-properties@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-spread@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-spread@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true - /@babel/plugin-transform-sticky-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-sticky-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-template-literals@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-template-literals@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-typeof-symbol@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA==} + /@babel/plugin-transform-typeof-symbol@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-typescript@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-liYSESjX2fZ7JyBFkYG78nfvHlMKE6IpNdTVnxmlYUR+j5ZLsitFbaAE+eJSK2zPPkNWNw4mXL51rQ8WrvdK0w==} + /@babel/plugin-transform-typescript@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.3) + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-unicode-escapes@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-unicode-escapes@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-unicode-property-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-unicode-property-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-unicode-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-unicode-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-unicode-sets-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-unicode-sets-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/preset-env@7.24.3(@babel/core@7.24.3): - resolution: {integrity: sha512-fSk430k5c2ff8536JcPvPWK4tZDwehWLGlBp0wrsBUjZVdeQV6lePbwKWZaZfK2vnh/1kQX1PzAJWsnBmVgGJA==} + /@babel/preset-env@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.24.1 - '@babel/core': 7.24.3 + '@babel/compat-data': 7.24.4 + '@babel/core': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.3) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.3) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.3) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-syntax-import-attributes': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.3) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.3) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.3) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.3) - '@babel/plugin-transform-arrow-functions': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-async-generator-functions': 7.24.3(@babel/core@7.24.3) - '@babel/plugin-transform-async-to-generator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-block-scoped-functions': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-block-scoping': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-class-static-block': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-classes': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-computed-properties': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-destructuring': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-dotall-regex': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-duplicate-keys': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-dynamic-import': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-exponentiation-operator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-export-namespace-from': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-for-of': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-function-name': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-json-strings': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-literals': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-logical-assignment-operators': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-member-expression-literals': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-amd': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-systemjs': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-umd': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.3) - '@babel/plugin-transform-new-target': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-numeric-separator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-object-rest-spread': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-object-super': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-optional-catch-binding': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-optional-chaining': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-parameters': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-private-property-in-object': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-property-literals': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-regenerator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-reserved-words': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-shorthand-properties': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-spread': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-sticky-regex': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-template-literals': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-typeof-symbol': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-unicode-escapes': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-unicode-property-regex': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-unicode-regex': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-unicode-sets-regex': 7.24.1(@babel/core@7.24.3) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.3) - babel-plugin-polyfill-corejs2: 0.4.10(@babel/core@7.24.3) - babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.3) - babel-plugin-polyfill-regenerator: 0.6.1(@babel/core@7.24.3) - core-js-compat: 3.36.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.5) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.5) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.5) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-syntax-import-attributes': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.5) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.5) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.5) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.5) + '@babel/plugin-transform-arrow-functions': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-async-generator-functions': 7.24.3(@babel/core@7.24.5) + '@babel/plugin-transform-async-to-generator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-block-scoped-functions': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-block-scoping': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-class-static-block': 7.24.4(@babel/core@7.24.5) + '@babel/plugin-transform-classes': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-computed-properties': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-destructuring': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-dotall-regex': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-duplicate-keys': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-dynamic-import': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-exponentiation-operator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-export-namespace-from': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-for-of': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-function-name': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-json-strings': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-literals': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-logical-assignment-operators': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-member-expression-literals': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-amd': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-systemjs': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-umd': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.5) + '@babel/plugin-transform-new-target': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-numeric-separator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-object-rest-spread': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-object-super': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-optional-catch-binding': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-private-property-in-object': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-property-literals': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-regenerator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-reserved-words': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-shorthand-properties': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-spread': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-sticky-regex': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-template-literals': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-typeof-symbol': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-unicode-escapes': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-unicode-property-regex': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-unicode-regex': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-unicode-sets-regex': 7.24.1(@babel/core@7.24.5) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.5) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.5) + babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.5) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.5) + core-js-compat: 3.37.0 semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true - /@babel/preset-flow@7.24.1(@babel/core@7.24.3): + /@babel/preset-flow@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-sWCV2G9pcqZf+JHyv/RyqEIpFypxdCSxWIxQjpdaQxenNog7cN1pr76hg8u0Fz8Qgg0H4ETkGcJnXL8d4j0PPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-transform-flow-strip-types': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-transform-flow-strip-types': 7.24.1(@babel/core@7.24.5) dev: true - /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.3): + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.5): resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/types': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/types': 7.24.5 esutils: 2.0.3 dev: true - /@babel/preset-typescript@7.24.1(@babel/core@7.24.3): + /@babel/preset-typescript@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-typescript': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-typescript': 7.24.5(@babel/core@7.24.5) dev: true - /@babel/register@7.23.7(@babel/core@7.24.3): + /@babel/register@7.23.7(@babel/core@7.24.5): resolution: {integrity: sha512-EjJeB6+kvpk+Y5DAkEAmbOBEFkh9OASx0huoEkqYTFxAZHzOAX2Oh5uwAUuL2rUddqfM0SA+KPXV2TbzoZ2kvQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -1625,127 +1632,134 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 + dev: false + + /@babel/runtime@7.24.5: + resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 /@babel/template@7.24.0: resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.24.2 - '@babel/parser': 7.24.1 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 dev: true - /@babel/traverse@7.24.1: - resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} + /@babel/traverse@7.24.5: + resolution: {integrity: sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.1 + '@babel/generator': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.24.1 - '@babel/types': 7.24.0 + '@babel/helper-split-export-declaration': 7.24.5 + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/types@7.24.0: - resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + /@babel/types@7.24.5: + resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.24.1 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.5 to-fast-properties: 2.0.0 /@base2/pretty-print-object@1.0.1: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} dev: true - /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0): + /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/descendant': 3.1.0(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/descendant': 3.1.0(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/alert@2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/alert@2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-jHg4LYMRNOJH830ViLuicjb3F+v6iriE/2G5T+Sd0Hna04nukNJ1MxUmBPE+vI22me2dIflfelu2v9wdB6Pojw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/anatomy@2.2.2: resolution: {integrity: sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==} dev: false - /@chakra-ui/avatar@2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/avatar@2.3.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-8gKSyLfygnaotbJbDMHDiJoF38OHXUYVme4gGxZ1fLnQEdPVEaIWfH+NndIjOM0z8S+YEFnT9KyGMUtvPrBk3g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/breadcrumb@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/breadcrumb@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-4cWCG24flYBxjruRi4RJREWTGF74L/KzI2CognAW/d/zWR0CjiScuJhf37Am3LFbCySP6WSoyBOtTIoTA4yLEA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/breakpoint-utils@2.0.8: @@ -1754,334 +1768,334 @@ packages: '@chakra-ui/shared-utils': 2.0.5 dev: false - /@chakra-ui/button@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/button@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-95CplwlRKmmUXkdEp/21VkEWgnwcx2TOBG6NfYlsuLBDHSLlo5FKIiE2oSi4zXc4TLcopGcWPNcm/NDaSC5pvA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/card@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/card@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-xUB/k5MURj4CtPAhdSoXZidUbm8j3hci9vnc+eZJVDqhDOShNlD6QeniQNRPRys4lWAQLCbFcrwL29C8naDi6g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/checkbox@2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/checkbox@2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-85g38JIXMEv6M+AcyIGLh7igNtfpAN6KGQFYxY9tBj0eWvWk4NKQxvqqyVta0bSAyIl1rixNIIezNpNWk2iO4g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@zag-js/focus-visible': 0.16.0 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/clickable@2.1.0(react@18.2.0): + /@chakra-ui/clickable@2.1.0(react@18.3.1): resolution: {integrity: sha512-flRA/ClPUGPYabu+/GLREZVZr9j2uyyazCAUHAdrTUEdDYCr31SVGhgh7dgKdtq23bOvAQJpIJjw/0Bs0WvbXw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/close-button@2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/close-button@2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-gnpENKOanKexswSVpVz7ojZEALl2x5qjLYNqSQGbxz+aP9sOXPfUS56ebyBrre7T7exuWGiFeRwnM0oVeGPaiw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/color-mode@2.2.0(react@18.2.0): + /@chakra-ui/color-mode@2.2.0(react@18.3.1): resolution: {integrity: sha512-niTEA8PALtMWRI9wJ4LL0CSBDo8NBfLNp4GD6/0hstcm3IlbBHTVKxN6HwSaoNYfphDQLxCjT4yG+0BJA5tFpg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/control-box@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/control-box@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-gVrRDyXFdMd8E7rulL0SKeoljkLQiPITFnsyMO8EFHNZ+AHt5wK4LIguYVEq88APqAGZGfHFWXr79RYrNiE3Mg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/counter@2.1.0(react@18.2.0): + /@chakra-ui/counter@2.1.0(react@18.3.1): resolution: {integrity: sha512-s6hZAEcWT5zzjNz2JIWUBzRubo9la/oof1W7EKZVVfPYHERnl5e16FmBC79Yfq8p09LQ+aqFKm/etYoJMMgghw==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/number-utils': 2.0.7 - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.4)(react@18.2.0): + /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.4)(react@18.3.1): resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==} peerDependencies: '@emotion/react': '>=10.0.35' react: '>=18' dependencies: - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - react: 18.2.0 + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/descendant@3.1.0(react@18.2.0): + /@chakra-ui/descendant@3.1.0(react@18.3.1): resolution: {integrity: sha512-VxCIAir08g5w27klLyi7PVo8BxhW4tgU/lxQyujkmi4zx7hT9ZdrcQLAted/dAa+aSIZ14S1oV0Q9lGjsAdxUQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/dom-utils@2.1.0: resolution: {integrity: sha512-ZmF2qRa1QZ0CMLU8M1zCfmw29DmPNtfjR9iTo74U5FPr3i1aoAh7fbJ4qAlZ197Xw9eAW28tvzQuoVWeL5C7fQ==} dev: false - /@chakra-ui/editable@3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/editable@3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-j2JLrUL9wgg4YA6jLlbU88370eCRyor7DZQD9lzpY95tSOXpTljeg3uF9eOmDnCs6fxp3zDWIfkgMm/ExhcGTg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/event-utils@2.0.8: resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==} dev: false - /@chakra-ui/focus-lock@2.1.0(@types/react@18.2.73)(react@18.2.0): + /@chakra-ui/focus-lock@2.1.0(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - react: 18.2.0 - react-focus-lock: 2.11.1(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-focus-lock: 2.11.1(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/form-control@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/form-control@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-wehLC1t4fafCVJ2RvJQT2jyqsAwX7KymmiGqBu7nQoQz8ApTkGABWpo/QwDh3F/dBLrouHDoOvGmYTqft3Mirw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/hooks@2.2.1(react@18.2.0): + /@chakra-ui/hooks@2.2.1(react@18.3.1): resolution: {integrity: sha512-RQbTnzl6b1tBjbDPf9zGRo9rf/pQMholsOudTxjy4i9GfTfz6kgp5ValGjQm2z7ng6Z31N1cnjZ1AlSzQ//ZfQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-utils': 2.0.12(react@18.2.0) + '@chakra-ui/react-utils': 2.0.12(react@18.3.1) '@chakra-ui/utils': 2.0.15 compute-scroll-into-view: 3.0.3 copy-to-clipboard: 3.3.3 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/icon@3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/icon@3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/icons@2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/icons@2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/image@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/image@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-bskumBYKLiLMySIWDGcz0+D9Th0jPvmX6xnRMs4o92tT3Od/bW26lahmV2a2Op2ItXeCmRMY+XxJH5Gy1i46VA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/input@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/input@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-GiBbb3EqAA8Ph43yGa6Mc+kUPjh4Spmxp1Pkelr8qtudpc3p2PJOOebLpd90mcqw8UePPa+l6YhhPtp6o0irhw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/object-utils': 2.1.0 - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/layout@2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/layout@2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-nXuZ6WRbq0WdgnRgLw+QuxWAHuhDtVX8ElWqcTK+cSMFg/52eVP47czYBE5F35YhnoW2XBwfNoNgZ7+e8Z01Rg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/breakpoint-utils': 2.0.8 - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/object-utils': 2.1.0 - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/lazy-utils@2.0.5: resolution: {integrity: sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg==} dev: false - /@chakra-ui/live-region@2.1.0(react@18.2.0): + /@chakra-ui/live-region@2.1.0(react@18.3.1): resolution: {integrity: sha512-ZOxFXwtaLIsXjqnszYYrVuswBhnIHHP+XIgK1vC6DePKtyK590Wg+0J0slDwThUAd4MSSIUa/nNX84x1GMphWw==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/media-query@3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/media-query@3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-IsTGgFLoICVoPRp9ykOgqmdMotJG0CnPsKvGQeSFOB/dZfIujdVb14TYxDU4+MURXry1MhJ7LzZhv+Ml7cr8/g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/breakpoint-utils': 2.0.8 - '@chakra-ui/react-env': 3.1.0(react@18.2.0) + '@chakra-ui/react-env': 3.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0): + /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.2.0) - '@chakra-ui/descendant': 3.1.0(react@18.2.0) + '@chakra-ui/clickable': 2.1.0(react@18.3.1) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-outside-click': 2.2.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.2.0) - '@chakra-ui/descendant': 3.1.0(react@18.2.0) + '@chakra-ui/clickable': 2.1.0(react@18.3.1) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-outside-click': 2.2.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2089,25 +2103,25 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) aria-hidden: 1.2.3 - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2115,44 +2129,44 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) aria-hidden: 1.2.3 - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/number-input@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/number-input@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-pfOdX02sqUN0qC2ysuvgVDiws7xZ20XDIlcNhva55Jgm095xjm8eVdIBfNm3SFbSUNxyXvLTW/YQanX74tKmuA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/counter': 2.1.0(react@18.2.0) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-interval': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/counter': 2.1.0(react@18.3.1) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-interval': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/number-utils@2.0.7: @@ -2163,103 +2177,103 @@ packages: resolution: {integrity: sha512-tgIZOgLHaoti5PYGPTwK3t/cqtcycW0owaiOXoZOcpwwX/vlVb+H1jFsQyWiiwQVPt9RkoSLtxzXamx+aHH+bQ==} dev: false - /@chakra-ui/pin-input@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/pin-input@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-x4vBqLStDxJFMt+jdAHHS8jbh294O53CPQJoL4g228P513rHylV/uPscYUHrVJXRxsHfRztQO9k45jjTYaPRMw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/descendant': 3.1.0(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0): + /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/popper@3.1.0(react@18.2.0): + /@chakra-ui/popper@3.1.0(react@18.3.1): resolution: {integrity: sha512-ciDdpdYbeFG7og6/6J8lkTFxsSvwTdMLFkpVylAF6VNC22jssiWfquj2eyD4rJnzkRFPvIWJq8hvbfhsm+AjSg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@popperjs/core': 2.11.8 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/portal@2.1.0(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/portal@2.1.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-9q9KWf6SArEcIq1gGofNcFPSWEyl+MfJjEUg/un1SMlQjaROOh3zYr+6JAwvcORiX7tyHosnmWC3d3wI2aPSQg==} peerDependencies: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/progress@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/progress@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-qUXuKbuhN60EzDD9mHR7B67D7p/ZqNS2Aze4Pbl1qGGZfulPW0PY8Rof32qDtttDQBkzQIzFGE8d9QpAemToIQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/provider@2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/provider@2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2267,229 +2281,229 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-env': 3.1.0(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/radio@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/radio@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-n10M46wJrMGbonaghvSRnZ9ToTv/q76Szz284gv4QUWvyljQACcGrXIONUnQ3BIwbOfkRqSk7Xl/JgZtVfll+w==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) '@zag-js/focus-visible': 0.16.0 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-children-utils@2.0.6(react@18.2.0): + /@chakra-ui/react-children-utils@2.0.6(react@18.3.1): resolution: {integrity: sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-context@2.1.0(react@18.2.0): + /@chakra-ui/react-context@2.1.0(react@18.3.1): resolution: {integrity: sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-env@3.1.0(react@18.2.0): + /@chakra-ui/react-env@3.1.0(react@18.3.1): resolution: {integrity: sha512-Vr96GV2LNBth3+IKzr/rq1IcnkXv+MLmwjQH6C8BRtn3sNskgDFD5vLkVXcEhagzZMCh8FR3V/bzZPojBOyNhw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-types@2.0.7(react@18.2.0): + /@chakra-ui/react-types@2.0.7(react@18.3.1): resolution: {integrity: sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-animation-state@2.1.0(react@18.2.0): + /@chakra-ui/react-use-animation-state@2.1.0(react@18.3.1): resolution: {integrity: sha512-CFZkQU3gmDBwhqy0vC1ryf90BVHxVN8cTLpSyCpdmExUEtSEInSCGMydj2fvn7QXsz/za8JNdO2xxgJwxpLMtg==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-callback-ref@2.1.0(react@18.2.0): + /@chakra-ui/react-use-callback-ref@2.1.0(react@18.3.1): resolution: {integrity: sha512-efnJrBtGDa4YaxDzDE90EnKD3Vkh5a1t3w7PhnRQmsphLy3g2UieasoKTlT2Hn118TwDjIv5ZjHJW6HbzXA9wQ==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-controllable-state@2.1.0(react@18.2.0): + /@chakra-ui/react-use-controllable-state@2.1.0(react@18.3.1): resolution: {integrity: sha512-QR/8fKNokxZUs4PfxjXuwl0fj/d71WPrmLJvEpCTkHjnzu7LnYvzoe2wB867IdooQJL0G1zBxl0Dq+6W1P3jpg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-disclosure@2.1.0(react@18.2.0): + /@chakra-ui/react-use-disclosure@2.1.0(react@18.3.1): resolution: {integrity: sha512-Ax4pmxA9LBGMyEZJhhUZobg9C0t3qFE4jVF1tGBsrLDcdBeLR9fwOogIPY9Hf0/wqSlAryAimICbr5hkpa5GSw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-event-listener@2.1.0(react@18.2.0): + /@chakra-ui/react-use-event-listener@2.1.0(react@18.3.1): resolution: {integrity: sha512-U5greryDLS8ISP69DKDsYcsXRtAdnTQT+jjIlRYZ49K/XhUR/AqVZCK5BkR1spTDmO9H8SPhgeNKI70ODuDU/Q==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-focus-effect@2.1.0(react@18.2.0): + /@chakra-ui/react-use-focus-effect@2.1.0(react@18.3.1): resolution: {integrity: sha512-xzVboNy7J64xveLcxTIJ3jv+lUJKDwRM7Szwn9tNzUIPD94O3qwjV7DDCUzN2490nSYDF4OBMt/wuDBtaR3kUQ==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-focus-on-pointer-down@2.1.0(react@18.2.0): + /@chakra-ui/react-use-focus-on-pointer-down@2.1.0(react@18.3.1): resolution: {integrity: sha512-2jzrUZ+aiCG/cfanrolsnSMDykCAbv9EK/4iUyZno6BYb3vziucmvgKuoXbMPAzWNtwUwtuMhkby8rc61Ue+Lg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-interval@2.1.0(react@18.2.0): + /@chakra-ui/react-use-interval@2.1.0(react@18.3.1): resolution: {integrity: sha512-8iWj+I/+A0J08pgEXP1J1flcvhLBHkk0ln7ZvGIyXiEyM6XagOTJpwNhiu+Bmk59t3HoV/VyvyJTa+44sEApuw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-latest-ref@2.1.0(react@18.2.0): + /@chakra-ui/react-use-latest-ref@2.1.0(react@18.3.1): resolution: {integrity: sha512-m0kxuIYqoYB0va9Z2aW4xP/5b7BzlDeWwyXCH6QpT2PpW3/281L3hLCm1G0eOUcdVlayqrQqOeD6Mglq+5/xoQ==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-merge-refs@2.1.0(react@18.2.0): + /@chakra-ui/react-use-merge-refs@2.1.0(react@18.3.1): resolution: {integrity: sha512-lERa6AWF1cjEtWSGjxWTaSMvneccnAVH4V4ozh8SYiN9fSPZLlSG3kNxfNzdFvMEhM7dnP60vynF7WjGdTgQbQ==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-outside-click@2.2.0(react@18.2.0): + /@chakra-ui/react-use-outside-click@2.2.0(react@18.3.1): resolution: {integrity: sha512-PNX+s/JEaMneijbgAM4iFL+f3m1ga9+6QK0E5Yh4s8KZJQ/bLwZzdhMz8J/+mL+XEXQ5J0N8ivZN28B82N1kNw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-pan-event@2.1.0(react@18.2.0): + /@chakra-ui/react-use-pan-event@2.1.0(react@18.3.1): resolution: {integrity: sha512-xmL2qOHiXqfcj0q7ZK5s9UjTh4Gz0/gL9jcWPA6GVf+A0Od5imEDa/Vz+533yQKWiNSm1QGrIj0eJAokc7O4fg==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/event-utils': 2.0.8 - '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.3.1) framesync: 6.1.2 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-previous@2.1.0(react@18.2.0): + /@chakra-ui/react-use-previous@2.1.0(react@18.3.1): resolution: {integrity: sha512-pjxGwue1hX8AFcmjZ2XfrQtIJgqbTF3Qs1Dy3d1krC77dEsiCUbQ9GzOBfDc8pfd60DrB5N2tg5JyHbypqh0Sg==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-safe-layout-effect@2.1.0(react@18.2.0): + /@chakra-ui/react-use-safe-layout-effect@2.1.0(react@18.3.1): resolution: {integrity: sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-size@2.1.0(react@18.2.0): + /@chakra-ui/react-use-size@2.1.0(react@18.3.1): resolution: {integrity: sha512-tbLqrQhbnqOjzTaMlYytp7wY8BW1JpL78iG7Ru1DlV4EWGiAmXFGvtnEt9HftU0NJ0aJyjgymkxfVGI55/1Z4A==} peerDependencies: react: '>=18' dependencies: '@zag-js/element-size': 0.10.5 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-timeout@2.1.0(react@18.2.0): + /@chakra-ui/react-use-timeout@2.1.0(react@18.3.1): resolution: {integrity: sha512-cFN0sobKMM9hXUhyCofx3/Mjlzah6ADaEl/AXl5Y+GawB5rgedgAcu2ErAgarEkwvsKdP6c68CKjQ9dmTQlJxQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-update-effect@2.1.0(react@18.2.0): + /@chakra-ui/react-use-update-effect@2.1.0(react@18.3.1): resolution: {integrity: sha512-ND4Q23tETaR2Qd3zwCKYOOS1dfssojPLJMLvUtUbW5M9uW1ejYWgGUobeAiOVfSplownG8QYMmHTP86p/v0lbA==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-utils@2.0.12(react@18.2.0): + /@chakra-ui/react-utils@2.0.12(react@18.3.1): resolution: {integrity: sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/utils': 2.0.15 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2498,69 +2512,69 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/counter': 2.1.0(react@18.2.0) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.2.0) - '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/hooks': 2.2.1(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/live-region': 2.1.0(react@18.2.0) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/counter': 2.1.0(react@18.3.1) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.3.1) + '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/hooks': 2.2.1(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/live-region': 2.1.0(react@18.3.1) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) + '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-env': 3.1.0(react@18.3.1) + '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) + '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) '@chakra-ui/utils': 2.0.15 - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2569,162 +2583,162 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/counter': 2.1.0(react@18.2.0) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.2.0) - '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/hooks': 2.2.1(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/live-region': 2.1.0(react@18.2.0) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/counter': 2.1.0(react@18.3.1) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.3.1) + '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/hooks': 2.2.1(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/live-region': 2.1.0(react@18.3.1) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-env': 3.1.0(react@18.3.1) + '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) '@chakra-ui/utils': 2.0.15 - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/shared-utils@2.0.5: resolution: {integrity: sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q==} dev: false - /@chakra-ui/skeleton@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/skeleton@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-JNRuMPpdZGd6zFVKjVQ0iusu3tXAdI29n4ZENYwAJEMf/fN0l12sVeirOxkJ7oEL0yOx2AgEYFSKdbcAgfUsAQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-use-previous': 2.1.0(react@18.2.0) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-use-previous': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/skip-nav@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/skip-nav@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-Hk+FG+vadBSH0/7hwp9LJnLjkO0RPGnx7gBJWI4/SpoJf3e4tZlWYtwGj0toYY4aGKl93jVghuwGbDBEMoHDug==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/slider@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/slider@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-lUOBcLMCnFZiA/s2NONXhELJh6sY5WtbRykPtclGfynqqOo47lwWJx+VP7xaeuhDOPcWSSecWc9Y1BfPOCz9cQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/number-utils': 2.0.7 - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-pan-event': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-size': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-pan-event': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-size': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/spinner@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/spinner@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-hczbnoXt+MMv/d3gE+hjQhmkzLiKuoTo42YhUG7Bs9OSv2lg1fZHW1fGNRFP3wTi6OIbD044U1P9HK+AOgFH3g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/stat@2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/stat@2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-LDn0d/LXQNbAn2KaR3F1zivsZCewY4Jsy1qShmfBMKwn6rI8yVlbvu6SiA3OpHS0FhxbsZxQI6HefEoIgtqY6Q==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/stepper@2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/stepper@2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-ky77lZbW60zYkSXhYz7kbItUpAQfEdycT0Q4bkHLxfqbuiGMf8OmgZOQkOB9uM4v0zPwy2HXhe0vq4Dd0xa55Q==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/styled-system@2.9.2: @@ -2735,106 +2749,106 @@ packages: lodash.mergewith: 4.6.2 dev: false - /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0): + /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/system@2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0): + /@chakra-ui/system@2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1): resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} peerDependencies: '@emotion/react': ^11.0.0 '@emotion/styled': ^11.0.0 react: '>=18' dependencies: - '@chakra-ui/color-mode': 2.2.0(react@18.2.0) + '@chakra-ui/color-mode': 2.2.0(react@18.3.1) '@chakra-ui/object-utils': 2.1.0 - '@chakra-ui/react-utils': 2.0.12(react@18.2.0) + '@chakra-ui/react-utils': 2.0.12(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 '@chakra-ui/theme-utils': 2.0.21 '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - react: 18.2.0 + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 react-fast-compare: 3.2.2 dev: false - /@chakra-ui/table@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/table@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-o5OrjoHCh5uCLdiUb0Oc0vq9rIAeHSIRScc2ExTC9Qg/uVZl2ygLrjToCaKfaaKl1oQexIeAcZDKvPG8tVkHyQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/tabs@3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/tabs@3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-6Mlclp8L9lqXmsGWF5q5gmemZXOiOYuh0SGT/7PgJVNPz3LXREXlXg2an4MBUD8W5oTkduCX+3KTMCwRrVrDYw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.2.0) - '@chakra-ui/descendant': 3.1.0(react@18.2.0) + '@chakra-ui/clickable': 2.1.0(react@18.3.1) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/tag@3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/tag@3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-Bdel79Dv86Hnge2PKOU+t8H28nm/7Y3cKd4Kfk9k3lOpUh4+nkSGe58dhRzht59lEqa4N9waCgQiBdkydjvBXQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/textarea@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/textarea@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-ip7tvklVCZUb2fOHDb23qPy/Fr2mzDOGdkrpbNi50hDCiV4hFX02jdQJdi3ydHZUyVgZVBKPOJ+lT9i7sKA2wA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/theme-tools@2.1.2(@chakra-ui/styled-system@2.9.2): @@ -2868,7 +2882,7 @@ packages: '@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2) dev: false - /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==} peerDependencies: '@chakra-ui/system': 2.6.2 @@ -2876,22 +2890,22 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-timeout': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-timeout': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==} peerDependencies: '@chakra-ui/system': 2.6.2 @@ -2899,22 +2913,22 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-timeout': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-timeout': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2923,20 +2937,20 @@ packages: react-dom: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2945,39 +2959,39 @@ packages: react-dom: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/transition@2.1.0(framer-motion@10.18.0)(react@18.2.0): + /@chakra-ui/transition@2.1.0(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} peerDependencies: framer-motion: '>=4.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/transition@2.1.0(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/transition@2.1.0(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} peerDependencies: framer-motion: '>=4.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/utils@2.0.15: @@ -2989,14 +3003,14 @@ packages: lodash.mergewith: 4.6.2 dev: false - /@chakra-ui/visually-hidden@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/visually-hidden@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-KmKDg01SrQ7VbTD3+cPWf/UfpF5MSwm3v7MWi0n5t8HnnadT13MF0MJCDSXbBWnzLv1ZKJ6zlyAOeARWX+DpjQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@colors/colors@1.5.0: @@ -3006,14 +3020,14 @@ packages: dev: true optional: true - /@dagrejs/dagre@1.1.1: - resolution: {integrity: sha512-AQfT6pffEuPE32weFzhS/u3UpX+bRXUARIXL7UqLaxz497cN8pjuBlX6axO4IIECE2gBV8eLFQkGCtKX5sDaUA==} + /@dagrejs/dagre@1.1.2: + resolution: {integrity: sha512-F09dphqvHsbe/6C2t2unbmpr5q41BNPEfJCdn8Z7aEBpVSy/zFQ/b4SWsweQjWNsYMDvE2ffNUN8X0CeFsEGNw==} dependencies: - '@dagrejs/graphlib': 2.2.1 + '@dagrejs/graphlib': 2.2.2 dev: false - /@dagrejs/graphlib@2.2.1: - resolution: {integrity: sha512-xJsN1v6OAxXk6jmNdM+OS/bBE8nDCwM0yDNprXR18ZNatL6to9ggod9+l2XtiLhXfLm0NkE7+Er/cpdlM+SkUA==} + /@dagrejs/graphlib@2.2.2: + resolution: {integrity: sha512-CbyGpCDKsiTg/wuk79S7Muoj8mghDGAESWGxcSyhHX5jD35vYMBZochYVFzlHxynpE9unpu6O+4ZuhrLxASsOg==} engines: {node: '>17.0.0'} dev: false @@ -3022,46 +3036,46 @@ packages: engines: {node: '>=10.0.0'} dev: true - /@dnd-kit/accessibility@3.1.0(react@18.2.0): + /@dnd-kit/accessibility@3.1.0(react@18.3.1): resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false - /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): + /@dnd-kit/core@6.1.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@dnd-kit/accessibility': 3.1.0(react@18.2.0) - '@dnd-kit/utilities': 3.2.2(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@dnd-kit/accessibility': 3.1.0(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) tslib: 2.6.2 dev: false - /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.3.1): resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} peerDependencies: '@dnd-kit/core': ^6.1.0 react: '>=16.8.0' dependencies: - '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) - '@dnd-kit/utilities': 3.2.2(react@18.2.0) - react: 18.2.0 + '@dnd-kit/core': 6.1.0(react-dom@18.3.1)(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 tslib: 2.6.2 dev: false - /@dnd-kit/utilities@3.2.2(react@18.2.0): + /@dnd-kit/utilities@3.2.2(react@18.3.1): resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false @@ -3069,10 +3083,10 @@ packages: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: '@babel/helper-module-imports': 7.24.3 - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 - '@emotion/serialize': 1.1.3 + '@emotion/serialize': 1.1.4 babel-plugin-macros: 3.1.0 convert-source-map: 1.9.0 escape-string-regexp: 4.0.0 @@ -3119,7 +3133,7 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false - /@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0): + /@emotion/react@11.11.4(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} peerDependencies: '@types/react': '*' @@ -3128,20 +3142,20 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/babel-plugin': 11.11.0 '@emotion/cache': 11.11.0 - '@emotion/serialize': 1.1.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/serialize': 1.1.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.73 + '@types/react': 18.3.1 hoist-non-react-statics: 3.3.2 - react: 18.2.0 + react: 18.3.1 dev: false - /@emotion/serialize@1.1.3: - resolution: {integrity: sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==} + /@emotion/serialize@1.1.4: + resolution: {integrity: sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==} dependencies: '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 @@ -3154,8 +3168,8 @@ packages: resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} dev: false - /@emotion/styled@11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} + /@emotion/styled@11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==} peerDependencies: '@emotion/react': ^11.0.0-rc.0 '@types/react': '*' @@ -3164,27 +3178,27 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/babel-plugin': 11.11.0 '@emotion/is-prop-valid': 1.2.2 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/serialize': 1.1.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/serialize': 1.1.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) '@emotion/utils': 1.2.1 - '@types/react': 18.2.73 - react: 18.2.0 + '@types/react': 18.3.1 + react: 18.3.1 dev: false /@emotion/unitless@0.8.1: resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} dev: false - /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): + /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.3.1): resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 /@emotion/utils@1.2.1: resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} @@ -3472,39 +3486,39 @@ packages: engines: {node: '>=14'} dev: true - /@floating-ui/core@1.6.0: - resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} + /@floating-ui/core@1.6.1: + resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==} dependencies: - '@floating-ui/utils': 0.2.1 + '@floating-ui/utils': 0.2.2 dev: false /@floating-ui/dom@1.5.4: resolution: {integrity: sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==} dependencies: - '@floating-ui/core': 1.6.0 - '@floating-ui/utils': 0.2.1 + '@floating-ui/core': 1.6.1 + '@floating-ui/utils': 0.2.2 dev: false - /@floating-ui/dom@1.6.3: - resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==} + /@floating-ui/dom@1.6.5: + resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} dependencies: - '@floating-ui/core': 1.6.0 - '@floating-ui/utils': 0.2.1 + '@floating-ui/core': 1.6.1 + '@floating-ui/utils': 0.2.2 dev: false - /@floating-ui/utils@0.2.1: - resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + /@floating-ui/utils@0.2.2: + resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} dev: false - /@fontsource-variable/inter@5.0.17: - resolution: {integrity: sha512-sa80nNnqF8kzhBvqusWiL9vlPMVpdmOwMmDBup46Jggsr1VBqo+YuzwB36Ls+X6uHJtb8Yv3ALBHL/zGmT862A==} + /@fontsource-variable/inter@5.0.18: + resolution: {integrity: sha512-rJzSrtJ3b7djiGFvRuTe6stDfbYJGhdQSfn2SI2WfXviee7Er0yKAHE5u7FU7OWVQQQ1x3+cxdmx9NdiAkcrcA==} dev: false /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} dependencies: - '@humanwhocodes/object-schema': 2.0.2 + '@humanwhocodes/object-schema': 2.0.3 debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: @@ -3516,8 +3530,8 @@ packages: engines: {node: '>=12.22'} dev: true - /@humanwhocodes/object-schema@2.0.2: - resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + /@humanwhocodes/object-schema@2.0.3: + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} dev: true /@internationalized/date@3.5.3: @@ -3526,32 +3540,32 @@ packages: '@swc/helpers': 0.5.11 dev: false - /@internationalized/number@3.5.1: - resolution: {integrity: sha512-N0fPU/nz15SwR9IbfJ5xaS9Ss/O5h1sVXMZf43vc9mxEG48ovglvvzBjF53aHlq20uoR6c+88CrIXipU/LSzwg==} + /@internationalized/number@3.5.2: + resolution: {integrity: sha512-4FGHTi0rOEX1giSkt5MH4/te0eHBq3cvAYsfLlpguV6pzJAReXymiYpE5wPCqKqjkUO3PIsyvk+tBiIV1pZtbA==} dependencies: '@swc/helpers': 0.5.11 dev: false - /@invoke-ai/eslint-config-react@0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.3): + /@invoke-ai/eslint-config-react@0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-6ZUY9zgdDhv2WUoLdDKOQdU9ImnH0CBOFtRlOaNOh34IOsNRfn+JA7wqA0PKnkiNrlfPkIQWhn4GRJp68NT5bw==} peerDependencies: eslint: ^8.56.0 prettier: ^3.2.5 typescript: ^5.3.3 dependencies: - '@typescript-eslint/eslint-plugin': 7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.3) - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.4.0)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.8.0)(eslint@8.57.0) eslint-plugin-react: 7.34.1(eslint@8.57.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) + eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) eslint-plugin-react-refresh: 0.4.6(eslint@8.57.0) - eslint-plugin-simple-import-sort: 12.0.0(eslint@8.57.0) - eslint-plugin-storybook: 0.8.0(eslint@8.57.0)(typescript@5.4.3) - eslint-plugin-unused-imports: 3.1.0(@typescript-eslint/eslint-plugin@7.4.0)(eslint@8.57.0) + eslint-plugin-simple-import-sort: 12.1.0(eslint@8.57.0) + eslint-plugin-storybook: 0.8.0(eslint@8.57.0)(typescript@5.4.5) + eslint-plugin-unused-imports: 3.2.0(@typescript-eslint/eslint-plugin@7.8.0)(eslint@8.57.0) prettier: 3.2.5 - typescript: 5.4.3 + typescript: 5.4.5 transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -3566,36 +3580,36 @@ packages: prettier: 3.2.5 dev: true - /@invoke-ai/ui-library@0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): + /@invoke-ai/ui-library@0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.18)(@internationalized/date@3.5.3)(@types/react@18.3.1)(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Fmjdlu62NXHgairYXGjcuCrxPEAl1G6Q6ban8g3excF6pDDdBeS7CmSNCyEDMxnSIOZrQlI04OhaMB17Imi9Uw==} peerDependencies: '@fontsource-variable/inter': ^5.0.16 react: ^18.2.0 react-dom: ^18.2.0 dependencies: - '@ark-ui/react': 1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0) + '@ark-ui/react': 1.3.0(@internationalized/date@3.5.3)(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/anatomy': 2.2.2 - '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react': 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react': 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 '@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2) - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - '@fontsource-variable/inter': 5.0.17 - '@nanostores/react': 0.7.2(nanostores@0.9.5)(react@18.2.0) - chakra-react-select: 4.7.6(@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.11.4)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + '@fontsource-variable/inter': 5.0.18 + '@nanostores/react': 0.7.2(nanostores@0.9.5)(react@18.3.1) + chakra-react-select: 4.7.6(@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.11.4)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) lodash-es: 4.17.21 nanostores: 0.9.5 - overlayscrollbars: 2.6.1 - overlayscrollbars-react: 0.5.5(overlayscrollbars@2.6.1)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-i18next: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) - react-icons: 5.0.1(react@18.2.0) - react-select: 5.8.0(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + overlayscrollbars: 2.7.3 + overlayscrollbars-react: 0.5.6(overlayscrollbars@2.7.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-i18next: 14.1.1(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1) + react-icons: 5.2.0(react@18.3.1) + react-select: 5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@chakra-ui/form-control' - '@chakra-ui/icon' @@ -3628,7 +3642,7 @@ packages: '@sinclair/typebox': 0.27.8 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.4.3)(vite@5.2.6): + /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.4.5)(vite@5.2.11): resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} peerDependencies: typescript: '>= 4.3.x' @@ -3640,9 +3654,9 @@ packages: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 - react-docgen-typescript: 2.2.2(typescript@5.4.3) - typescript: 5.4.3 - vite: 5.2.6(@types/node@20.11.30) + react-docgen-typescript: 2.2.2(typescript@5.4.5) + typescript: 5.4.5 + vite: 5.2.11(@types/node@20.12.10) dev: true /@jridgewell/gen-mapping@0.3.5: @@ -3674,38 +3688,38 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@mdx-js/react@3.0.1(@types/react@18.2.73)(react@18.2.0): + /@mdx-js/react@3.0.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==} peerDependencies: '@types/react': '>=16' react: '>=16' dependencies: - '@types/mdx': 2.0.12 - '@types/react': 18.2.73 - react: 18.2.0 + '@types/mdx': 2.0.13 + '@types/react': 18.3.1 + react: 18.3.1 dev: true - /@microsoft/api-extractor-model@7.28.13(@types/node@20.11.30): + /@microsoft/api-extractor-model@7.28.13(@types/node@20.12.10): resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==} dependencies: '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@20.11.30) + '@rushstack/node-core-library': 4.0.2(@types/node@20.12.10) transitivePeerDependencies: - '@types/node' dev: true - /@microsoft/api-extractor@7.43.0(@types/node@20.11.30): + /@microsoft/api-extractor@7.43.0(@types/node@20.12.10): resolution: {integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==} hasBin: true dependencies: - '@microsoft/api-extractor-model': 7.28.13(@types/node@20.11.30) + '@microsoft/api-extractor-model': 7.28.13(@types/node@20.12.10) '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@20.11.30) + '@rushstack/node-core-library': 4.0.2(@types/node@20.12.10) '@rushstack/rig-package': 0.5.2 - '@rushstack/terminal': 0.10.0(@types/node@20.11.30) - '@rushstack/ts-command-line': 4.19.1(@types/node@20.11.30) + '@rushstack/terminal': 0.10.0(@types/node@20.12.10) + '@rushstack/ts-command-line': 4.19.1(@types/node@20.12.10) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 @@ -3729,18 +3743,18 @@ packages: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: true - /@nanostores/react@0.7.2(nanostores@0.10.0)(react@18.2.0): + /@nanostores/react@0.7.2(nanostores@0.10.3)(react@18.3.1): resolution: {integrity: sha512-e3OhHJFv3NMSFYDgREdlAQqkyBTHJM91s31kOZ4OvZwJKdFk5BLk0MLbh51EOGUz9QGX2aCHfy1RvweSi7fgwA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: nanostores: ^0.9.0 || ^0.10.0 react: '>=18.0.0' dependencies: - nanostores: 0.10.0 - react: 18.2.0 + nanostores: 0.10.3 + react: 18.3.1 dev: false - /@nanostores/react@0.7.2(nanostores@0.9.5)(react@18.2.0): + /@nanostores/react@0.7.2(nanostores@0.9.5)(react@18.3.1): resolution: {integrity: sha512-e3OhHJFv3NMSFYDgREdlAQqkyBTHJM91s31kOZ4OvZwJKdFk5BLk0MLbh51EOGUz9QGX2aCHfy1RvweSi7fgwA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: @@ -3748,7 +3762,7 @@ packages: react: '>=18.0.0' dependencies: nanostores: 0.9.5 - react: 18.2.0 + react: 18.3.1 dev: false /@ndelangen/get-tarball@3.0.9: @@ -3801,59 +3815,6 @@ packages: fastq: 1.17.1 dev: true - /@npmcli/git@5.0.4: - resolution: {integrity: sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - '@npmcli/promise-spawn': 7.0.1 - lru-cache: 10.2.0 - npm-pick-manifest: 9.0.0 - proc-log: 3.0.0 - promise-inflight: 1.0.1 - promise-retry: 2.0.1 - semver: 7.6.0 - which: 4.0.0 - transitivePeerDependencies: - - bluebird - dev: true - - /@npmcli/map-workspaces@3.0.4: - resolution: {integrity: sha512-Z0TbvXkRbacjFFLpVpV0e2mheCh+WzQpcqL+4xp49uNJOxOnIAPZyXtUxZ5Qn3QBTGKA11Exjd9a5411rBrhDg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - '@npmcli/name-from-folder': 2.0.0 - glob: 10.3.10 - minimatch: 9.0.3 - read-package-json-fast: 3.0.2 - dev: true - - /@npmcli/name-from-folder@2.0.0: - resolution: {integrity: sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - - /@npmcli/package-json@5.0.0: - resolution: {integrity: sha512-OI2zdYBLhQ7kpNPaJxiflofYIpkNLi+lnGdzqUOfRmCF3r2l1nadcjtCYMJKv/Utm/ZtlffaUuTiAktPHbc17g==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - '@npmcli/git': 5.0.4 - glob: 10.3.10 - hosted-git-info: 7.0.1 - json-parse-even-better-errors: 3.0.1 - normalize-package-data: 6.0.0 - proc-log: 3.0.0 - semver: 7.6.0 - transitivePeerDependencies: - - bluebird - dev: true - - /@npmcli/promise-spawn@7.0.1: - resolution: {integrity: sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - which: 4.0.0 - dev: true - /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3861,135 +3822,11 @@ packages: dev: true optional: true - /@pnpm/constants@7.1.1: - resolution: {integrity: sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==} - engines: {node: '>=16.14'} - dev: true - - /@pnpm/core-loggers@9.0.6(@pnpm/logger@5.0.0): - resolution: {integrity: sha512-iK67SGbp+06bA/elpg51wygPFjNA7JKHtKkpLxqXXHw+AjFFBC3f2OznJsCIuDK6HdGi5UhHLYqo5QxJ2gMqJQ==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - dependencies: - '@pnpm/logger': 5.0.0 - '@pnpm/types': 9.4.2 - dev: true - - /@pnpm/error@5.0.3: - resolution: {integrity: sha512-ONJU5cUeoeJSy50qOYsMZQHTA/9QKmGgh1ATfEpCLgtbdwqUiwD9MxHNeXUYYI/pocBCz6r1ZCFqiQvO+8SUKA==} - engines: {node: '>=16.14'} - dependencies: - '@pnpm/constants': 7.1.1 - dev: true - - /@pnpm/fetching-types@5.0.0: - resolution: {integrity: sha512-o9gdO1v8Uc5P2fBBuW6GSpfTqIivQmQlqjQJdFiQX0m+tgxlrMRneIg392jZuc6fk7kFqjLheInlslgJfwY+4Q==} - engines: {node: '>=16.14'} - dependencies: - '@zkochan/retry': 0.2.0 - node-fetch: 3.0.0-beta.9 - transitivePeerDependencies: - - domexception - dev: true - - /@pnpm/graceful-fs@3.2.0: - resolution: {integrity: sha512-vRoXJxscDpHak7YE9SqCkzfrayn+Lw+YueOeHIPEqkgokrHeYgYeONoc2kGh0ObHaRtNSsonozVfJ456kxLNvA==} - engines: {node: '>=16.14'} - dependencies: - graceful-fs: 4.2.11 - dev: true - - /@pnpm/logger@5.0.0: - resolution: {integrity: sha512-YfcB2QrX+Wx1o6LD1G2Y2fhDhOix/bAY/oAnMpHoNLsKkWIRbt1oKLkIFvxBMzLwAEPqnYWguJrYC+J6i4ywbw==} - engines: {node: '>=12.17'} - dependencies: - bole: 5.0.11 - ndjson: 2.0.0 - dev: true - - /@pnpm/npm-package-arg@1.0.0: - resolution: {integrity: sha512-oQYP08exi6mOPdAZZWcNIGS+KKPsnNwUBzSuAEGWuCcqwMAt3k/WVCqVIXzBxhO5sP2b43og69VHmPj6IroKqw==} - engines: {node: '>=14.6'} - dependencies: - hosted-git-info: 4.1.0 - semver: 7.6.0 - validate-npm-package-name: 4.0.0 - dev: true - - /@pnpm/npm-resolver@18.1.1(@pnpm/logger@5.0.0): - resolution: {integrity: sha512-NptzncmMD5ZMimbjWkGpMzuBRhlCY+sh7mzypPdBOTNlh5hmEQe/VaRKjNK4V9/b0C/llElkvIePL6acybu86w==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - dependencies: - '@pnpm/core-loggers': 9.0.6(@pnpm/logger@5.0.0) - '@pnpm/error': 5.0.3 - '@pnpm/fetching-types': 5.0.0 - '@pnpm/graceful-fs': 3.2.0 - '@pnpm/logger': 5.0.0 - '@pnpm/resolve-workspace-range': 5.0.1 - '@pnpm/resolver-base': 11.1.0 - '@pnpm/types': 9.4.2 - '@zkochan/retry': 0.2.0 - encode-registry: 3.0.1 - load-json-file: 6.2.0 - lru-cache: 10.2.0 - normalize-path: 3.0.0 - p-limit: 3.1.0 - p-memoize: 4.0.1 - parse-npm-tarball-url: 3.0.0 - path-temp: 2.1.0 - ramda: /@pnpm/ramda@0.28.1 - rename-overwrite: 5.0.0 - semver: 7.6.0 - ssri: 10.0.5 - version-selector-type: 3.0.0 - transitivePeerDependencies: - - domexception - dev: true - - /@pnpm/ramda@0.28.1: - resolution: {integrity: sha512-zcAG+lvU0fMziNeGXpPyCyCJYp5ZVrPElEE4t14jAmViaihohocZ+dDkcRIyAomox8pQsuZnv1EyHR+pOhmUWw==} - dev: true - - /@pnpm/resolve-workspace-range@5.0.1: - resolution: {integrity: sha512-yQ0pMthlw8rTgS/C9hrjne+NEnnSNevCjtdodd7i15I59jMBYciHifZ/vjg0NY+Jl+USTc3dBE+0h/4tdYjMKg==} - engines: {node: '>=16.14'} - dependencies: - semver: 7.6.0 - dev: true - - /@pnpm/resolver-base@11.1.0: - resolution: {integrity: sha512-y2qKaj18pwe1VWc3YXEitdYFo+WqOOt60aqTUuOVkJAirUzz0DzuYh3Ifct4znYWPdgUXHaN5DMphNF5iL85rA==} - engines: {node: '>=16.14'} - dependencies: - '@pnpm/types': 9.4.2 - dev: true - - /@pnpm/types@9.4.2: - resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} - engines: {node: '>=16.14'} - dev: true - - /@pnpm/workspace.pkgs-graph@2.0.15(@pnpm/logger@5.0.0): - resolution: {integrity: sha512-Txxd5FzzVfBfGCTngISaxFlJzZhzdS8BUrCEtAWJfZOFbQzpWy27rzkaS7TaWW2dHiFcCVYzPI/2vgxfeRansA==} - engines: {node: '>=16.14'} - dependencies: - '@pnpm/npm-package-arg': 1.0.0 - '@pnpm/npm-resolver': 18.1.1(@pnpm/logger@5.0.0) - '@pnpm/resolve-workspace-range': 5.0.1 - ramda: /@pnpm/ramda@0.28.1 - transitivePeerDependencies: - - '@pnpm/logger' - - domexception - dev: true - /@popperjs/core@2.11.8: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false - /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.73)(react@18.2.0): + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: '@types/react': '*' @@ -3998,12 +3835,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 - '@types/react': 18.2.73 - react: 18.2.0 + '@babel/runtime': 7.24.5 + '@types/react': 18.3.1 + react: 18.3.1 dev: true - /@radix-ui/react-slot@1.0.2(@types/react@18.2.73)(react@18.2.0): + /@radix-ui/react-slot@1.0.2(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: '@types/react': '*' @@ -4012,46 +3849,46 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0) - '@types/react': 18.2.73 - react: 18.2.0 + '@babel/runtime': 7.24.5 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@types/react': 18.3.1 + react: 18.3.1 dev: true - /@reactflow/background@11.3.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==} + /@reactflow/background@11.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-hkvpVEhgvfTDyCvdlitw4ioKCYLaaiRXnuEG+1QM3Np+7N1DiWF1XOv5I8AFyNoJL07yXEkbECUTsHvkBvcG5A==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/controls@11.2.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==} + /@reactflow/controls@11.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-3xgEg6ALIVkAQCS4NiBjb7ad8Cb3D8CtA7Vvl4Hf5Ar2PIVs6FOaeft9s2iDZGtsWP35ECDYId1rIFVhQL8r+A==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/core@11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==} + /@reactflow/core@11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-+adHdUa7fJSEM93fWfjQwyWXeI92a1eLKwWbIstoCakHpL8UjzwhEh6sn+mN2h/59MlVI7Ehr1iGTt3MsfcIFA==} peerDependencies: react: '>=17' react-dom: '>=17' @@ -4060,74 +3897,74 @@ packages: '@types/d3-drag': 3.0.7 '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 - classcat: 5.0.4 + classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/minimap@11.7.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==} + /@reactflow/minimap@11.7.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-m2MvdiGSyOu44LEcERDEl1Aj6x//UQRWo3HEAejNU4HQTlJnYrSN8tgrYF8TxC1+c/9UdyzQY5VYgrTwW4QWdg==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 - classcat: 5.0.4 + classcat: 5.0.5 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-resizer@2.2.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==} + /@reactflow/node-resizer@2.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-X7ceQ2s3jFLgbkg03n2RYr4hm3jTVrzkW2W/8ANv/SZfuVmF8XJxlERuD8Eka5voKqLda0ywIZGAbw9GoHLfUQ==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-toolbar@1.3.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==} + /@reactflow/node-toolbar@1.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-aknvNICO10uWdthFSpgD6ctY/CTBeJUMV9co8T9Ilugr08Nb89IQ4uD0dPmr031ewMQxixtYIkw+sSDDzd2aaQ==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reduxjs/toolkit@2.2.2(react-redux@9.1.0)(react@18.2.0): - resolution: {integrity: sha512-454GZrEx3G6QSYwIx9ROaso1HR6sTH8qyZBe3KEsdWVGU3ayV8jYCwdaEJV3vl9V6+pi3GRl+7Xl7AeDna6qwQ==} + /@reduxjs/toolkit@2.2.3(react-redux@9.1.2)(react@18.3.1): + resolution: {integrity: sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -4137,9 +3974,9 @@ packages: react-redux: optional: true dependencies: - immer: 10.0.4 - react: 18.2.0 - react-redux: 9.1.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1) + immer: 10.1.1 + react: 18.3.1 + react-redux: 9.1.2(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1) redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.0 @@ -4150,7 +3987,7 @@ packages: engines: {node: '>=12.0'} dependencies: boolean: 3.2.0 - globalthis: 1.0.3 + globalthis: 1.0.4 liqe: 3.8.0 dev: false @@ -4176,119 +4013,135 @@ packages: picomatch: 2.3.1 dev: true - /@rollup/rollup-android-arm-eabi@4.13.1: - resolution: {integrity: sha512-4C4UERETjXpC4WpBXDbkgNVgHyWfG3B/NKY46e7w5H134UDOFqUJKpsLm0UYmuupW+aJmRgeScrDNfvZ5WV80A==} + /@rollup/rollup-android-arm-eabi@4.17.2: + resolution: {integrity: sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.13.1: - resolution: {integrity: sha512-TrTaFJ9pXgfXEiJKQ3yQRelpQFqgRzVR9it8DbeRzG0RX7mKUy0bqhCFsgevwXLJepQKTnLl95TnPGf9T9AMOA==} + /@rollup/rollup-android-arm64@4.17.2: + resolution: {integrity: sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.13.1: - resolution: {integrity: sha512-fz7jN6ahTI3cKzDO2otQuybts5cyu0feymg0bjvYCBrZQ8tSgE8pc0sSNEuGvifrQJWiwx9F05BowihmLxeQKw==} + /@rollup/rollup-darwin-arm64@4.17.2: + resolution: {integrity: sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.13.1: - resolution: {integrity: sha512-WTvdz7SLMlJpektdrnWRUN9C0N2qNHwNbWpNo0a3Tod3gb9leX+yrYdCeB7VV36OtoyiPAivl7/xZ3G1z5h20g==} + /@rollup/rollup-darwin-x64@4.17.2: + resolution: {integrity: sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.13.1: - resolution: {integrity: sha512-dBHQl+7wZzBYcIF6o4k2XkAfwP2ks1mYW2q/Gzv9n39uDcDiAGDqEyml08OdY0BIct0yLSPkDTqn4i6czpBLLw==} + /@rollup/rollup-linux-arm-gnueabihf@4.17.2: + resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.13.1: - resolution: {integrity: sha512-bur4JOxvYxfrAmocRJIW0SADs3QdEYK6TQ7dTNz6Z4/lySeu3Z1H/+tl0a4qDYv0bCdBpUYM0sYa/X+9ZqgfSQ==} + /@rollup/rollup-linux-arm-musleabihf@4.17.2: + resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.17.2: + resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.13.1: - resolution: {integrity: sha512-ssp77SjcDIUSoUyj7DU7/5iwM4ZEluY+N8umtCT9nBRs3u045t0KkW02LTyHouHDomnMXaXSZcCSr2bdMK63kA==} + /@rollup/rollup-linux-arm64-musl@4.17.2: + resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.13.1: - resolution: {integrity: sha512-Jv1DkIvwEPAb+v25/Unrnnq9BO3F5cbFPT821n3S5litkz+O5NuXuNhqtPx5KtcwOTtaqkTsO+IVzJOsxd11aQ==} + /@rollup/rollup-linux-powerpc64le-gnu@4.17.2: + resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.17.2: + resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-s390x-gnu@4.13.1: - resolution: {integrity: sha512-U564BrhEfaNChdATQaEODtquCC7Ez+8Hxz1h5MAdMYj0AqD0GA9rHCpElajb/sQcaFL6NXmHc5O+7FXpWMa73Q==} + /@rollup/rollup-linux-s390x-gnu@4.17.2: + resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==} cpu: [s390x] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.13.1: - resolution: {integrity: sha512-zGRDulLTeDemR8DFYyFIQ8kMP02xpUsX4IBikc7lwL9PrwR3gWmX2NopqiGlI2ZVWMl15qZeUjumTwpv18N7sQ==} + /@rollup/rollup-linux-x64-gnu@4.17.2: + resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.13.1: - resolution: {integrity: sha512-VTk/MveyPdMFkYJJPCkYBw07KcTkGU2hLEyqYMsU4NjiOfzoaDTW9PWGRsNwiOA3qI0k/JQPjkl/4FCK1smskQ==} + /@rollup/rollup-linux-x64-musl@4.17.2: + resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.13.1: - resolution: {integrity: sha512-L+hX8Dtibb02r/OYCsp4sQQIi3ldZkFI0EUkMTDwRfFykXBPptoz/tuuGqEd3bThBSLRWPR6wsixDSgOx/U3Zw==} + /@rollup/rollup-win32-arm64-msvc@4.17.2: + resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.13.1: - resolution: {integrity: sha512-+dI2jVPfM5A8zme8riEoNC7UKk0Lzc7jCj/U89cQIrOjrZTCWZl/+IXUeRT2rEZ5j25lnSA9G9H1Ob9azaF/KQ==} + /@rollup/rollup-win32-ia32-msvc@4.17.2: + resolution: {integrity: sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.13.1: - resolution: {integrity: sha512-YY1Exxo2viZ/O2dMHuwQvimJ0SqvL+OAWQLLY6rvXavgQKjhQUzn7nc1Dd29gjB5Fqi00nrBWctJBOyfVMIVxw==} + /@rollup/rollup-win32-x64-msvc@4.17.2: + resolution: {integrity: sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /@rushstack/node-core-library@4.0.2(@types/node@20.11.30): + /@rushstack/node-core-library@4.0.2(@types/node@20.12.10): resolution: {integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==} peerDependencies: '@types/node': '*' @@ -4296,7 +4149,7 @@ packages: '@types/node': optional: true dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.10 fs-extra: 7.0.1 import-lazy: 4.0.0 jju: 1.4.0 @@ -4312,7 +4165,7 @@ packages: strip-json-comments: 3.1.1 dev: true - /@rushstack/terminal@0.10.0(@types/node@20.11.30): + /@rushstack/terminal@0.10.0(@types/node@20.12.10): resolution: {integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==} peerDependencies: '@types/node': '*' @@ -4320,15 +4173,15 @@ packages: '@types/node': optional: true dependencies: - '@rushstack/node-core-library': 4.0.2(@types/node@20.11.30) - '@types/node': 20.11.30 + '@rushstack/node-core-library': 4.0.2(@types/node@20.12.10) + '@types/node': 20.12.10 supports-color: 8.1.1 dev: true - /@rushstack/ts-command-line@4.19.1(@types/node@20.11.30): + /@rushstack/ts-command-line@4.19.1(@types/node@20.12.10): resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==} dependencies: - '@rushstack/terminal': 0.10.0(@types/node@20.11.30) + '@rushstack/terminal': 0.10.0(@types/node@20.12.10) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -4350,14 +4203,14 @@ packages: p-map: 4.0.0 dev: true - /@socket.io/component-emitter@3.1.0: - resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + /@socket.io/component-emitter@3.1.2: + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} dev: false - /@storybook/addon-actions@8.0.4: - resolution: {integrity: sha512-EyCWo+8T11/TJGYNL/AXtW4yaB+q1v2E9mixbumryCLxpTl2NtaeGZ4e0dlwfIMuw/7RWgHk2uIypcIPR/UANQ==} + /@storybook/addon-actions@8.0.10: + resolution: {integrity: sha512-IEuc30UAFl7Ws0GwaY/whjBnGaViVEVjmPc+MXUym2wwwJbnCbI+BKJxPoYi/I7QJb5aUNToAE6pl2pDda2g3Q==} dependencies: - '@storybook/core-events': 8.0.4 + '@storybook/core-events': 8.0.10 '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 dequal: 2.0.3 @@ -4365,18 +4218,18 @@ packages: uuid: 9.0.1 dev: true - /@storybook/addon-backgrounds@8.0.4: - resolution: {integrity: sha512-fef0KD2GhJx2zpicOf8iL7k2LiIsNzEbGaQpIIjoy4DMqM1hIfNCt3DGTLH7LN5O8G+NVCLS1xmQg7RLvIVSCA==} + /@storybook/addon-backgrounds@8.0.10: + resolution: {integrity: sha512-445SUQqOH5xFJWlNeMu74FEgk26O9Zm/5aqnvmeteB0Q2JLaw7k2q9i/W6XFu97QkRxqA1EGbDxLR3+e1xCjaA==} dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 ts-dedent: 2.2.0 dev: true - /@storybook/addon-controls@8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-K5EYBTsUOTJlvIdA7p6Xj31wnV+RbZAkk56UKQvA7nJD7oDuLOq3E9u46F/uZD1vxddd9zFhf2iONfMe3KTTwQ==} + /@storybook/addon-controls@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-MAUtIJGayNSsfn3VZ6SjQwpRkb4ky+10oVfos+xX9GQ5+7RCs+oYMuE4+aiQvvfXNdV8v0pUGPUPeUzqfJmhOA==} dependencies: - '@storybook/blocks': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@storybook/blocks': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) lodash: 4.17.21 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -4387,26 +4240,26 @@ packages: - supports-color dev: true - /@storybook/addon-docs@8.0.4: - resolution: {integrity: sha512-m0Y7qGAMnNPLEOEgzW/SBm8GX0xabJBaRN+aYijO6UKTln7F6oXXVve+xPC0Y4s6Gc9HZFdJY8WXZr1YSGEUVA==} + /@storybook/addon-docs@8.0.10: + resolution: {integrity: sha512-y+Agoez/hXZHKUMIZHU96T5V1v0cs4ArSNfjqDg9DPYcyQ88ihJNb6ZabIgzmEaJF/NncCW+LofWeUtkTwalkw==} dependencies: - '@babel/core': 7.24.3 - '@mdx-js/react': 3.0.1(@types/react@18.2.73)(react@18.2.0) - '@storybook/blocks': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 8.0.4 - '@storybook/components': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@storybook/csf-plugin': 8.0.4 - '@storybook/csf-tools': 8.0.4 + '@babel/core': 7.24.5 + '@mdx-js/react': 3.0.1(@types/react@18.3.1)(react@18.3.1) + '@storybook/blocks': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@storybook/client-logger': 8.0.10 + '@storybook/components': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@storybook/csf-plugin': 8.0.10 + '@storybook/csf-tools': 8.0.10 '@storybook/global': 5.0.0 - '@storybook/node-logger': 8.0.4 - '@storybook/preview-api': 8.0.4 - '@storybook/react-dom-shim': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 - '@types/react': 18.2.73 + '@storybook/node-logger': 8.0.10 + '@storybook/preview-api': 8.0.10 + '@storybook/react-dom-shim': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 + '@types/react': 18.3.1 fs-extra: 11.2.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) rehype-external-links: 3.0.0 rehype-slug: 6.0.0 ts-dedent: 2.2.0 @@ -4415,22 +4268,22 @@ packages: - supports-color dev: true - /@storybook/addon-essentials@8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-mUIqhAkSz6Qv7nRqAAyCqMLiXBWVsY/8qN7HEIoaMQgdFq38KW3rYwNdzd2JLeXNWP1bBXwfvfcFe7/eqhYJFA==} + /@storybook/addon-essentials@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-Uy3+vm7QX+b/9rhW/iFa3EYAAbV1T2LljY9Bj4aTPZHas9Bpvl5ZPnOm/PhybcE8UFHEoVTJ0v3uWb0dsUEigw==} dependencies: - '@storybook/addon-actions': 8.0.4 - '@storybook/addon-backgrounds': 8.0.4 - '@storybook/addon-controls': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-docs': 8.0.4 - '@storybook/addon-highlight': 8.0.4 - '@storybook/addon-measure': 8.0.4 - '@storybook/addon-outline': 8.0.4 - '@storybook/addon-toolbars': 8.0.4 - '@storybook/addon-viewport': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/manager-api': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 8.0.4 - '@storybook/preview-api': 8.0.4 + '@storybook/addon-actions': 8.0.10 + '@storybook/addon-backgrounds': 8.0.10 + '@storybook/addon-controls': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@storybook/addon-docs': 8.0.10 + '@storybook/addon-highlight': 8.0.10 + '@storybook/addon-measure': 8.0.10 + '@storybook/addon-outline': 8.0.10 + '@storybook/addon-toolbars': 8.0.10 + '@storybook/addon-viewport': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/manager-api': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/node-logger': 8.0.10 + '@storybook/preview-api': 8.0.10 ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -4440,19 +4293,19 @@ packages: - supports-color dev: true - /@storybook/addon-highlight@8.0.4: - resolution: {integrity: sha512-tnEiVaJlXL07v8JBox+QtRPVruoy0YovOTAOWY7fKDiKzF1I9wLaJjQF3wOsvwspHTHu00OZw2gsazgXiH4wLQ==} + /@storybook/addon-highlight@8.0.10: + resolution: {integrity: sha512-40GB82t1e2LCCjqXcC6Z5lq1yIpA1+Yl5E2tKeggOVwg5HHAX02ESNDdBaIOlCqMkU3WKzjGPurDNOLUAbsV2g==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/addon-interactions@8.0.4(vitest@1.4.0): - resolution: {integrity: sha512-wTEOnVUbF1lNJxxocr5IKmpgnmwyO8YsQf6Baw3tTWCHAa/MaWWQYq1OA6CfFfmVGGRjv/w2GTuf1Vyq99O7mg==} + /@storybook/addon-interactions@8.0.10(vitest@1.6.0): + resolution: {integrity: sha512-6yFNmk6+7082/8TRVyjUsKlwumalEdO0XQ5amPbVGuECzc3HFn0ELwzPrQ4TBlN5MRtX4+buoh5dc/1RUDrh9w==} dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.0.4 - '@storybook/test': 8.0.4(vitest@1.4.0) - '@storybook/types': 8.0.4 + '@storybook/instrumenter': 8.0.10 + '@storybook/test': 8.0.10(vitest@1.6.0) + '@storybook/types': 8.0.10 polished: 4.3.1 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -4463,54 +4316,54 @@ packages: - vitest dev: true - /@storybook/addon-links@8.0.4(react@18.2.0): - resolution: {integrity: sha512-SzE+JPZ4mxjprZqbLHf8Hx7UA2fXfMajFjeY9c3JREKQrDoOF1e4r28nAoVsZYF+frWxQB51U4+hOqjlx06wEA==} + /@storybook/addon-links@8.0.10(react@18.3.1): + resolution: {integrity: sha512-+mIyH2UcrgQfAyRM4+ARkB/D0OOY8UMwkZsD8dD23APZ8oru7W/NHX3lXl0WjPfQcOIx/QwWNWI3+DgVZJY3jw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: react: optional: true dependencies: - '@storybook/csf': 0.1.3 + '@storybook/csf': 0.1.7 '@storybook/global': 5.0.0 - react: 18.2.0 + react: 18.3.1 ts-dedent: 2.2.0 dev: true - /@storybook/addon-measure@8.0.4: - resolution: {integrity: sha512-GZYKo2ss5Br+dfHinoK3bgTaS90z3oKKDkhv6lrFfjjU1mDYzzMJpxajQhd3apCYxHLr3MbUqMQibWu2T/q2DQ==} + /@storybook/addon-measure@8.0.10: + resolution: {integrity: sha512-quXQwmZJUhOxDIlbXTH6aKYQkwkDpL0UQRkUZn1xuZ2sVKJeaee73QSWqw8HDD4Rz9huS+OrAdVoq/Cz5FoC6A==} dependencies: '@storybook/global': 5.0.0 tiny-invariant: 1.3.3 dev: true - /@storybook/addon-outline@8.0.4: - resolution: {integrity: sha512-6J9ezNDUxdA3rMCh8sUEQbUwAgkrr+M9QdiFr1t+gKrk5FKP5gwubw1sr3sF1IRB9+s/AjljcOtJAVulSfq05w==} + /@storybook/addon-outline@8.0.10: + resolution: {integrity: sha512-1eDO2s/vHhhSJo7W5SetqjleUBTZLI08VNP89c4j7vdRKiMZ1DYhr0dqUGIC3w7cDsawI/nQ24wancHHayAnqw==} dependencies: '@storybook/global': 5.0.0 ts-dedent: 2.2.0 dev: true - /@storybook/addon-storysource@8.0.4: - resolution: {integrity: sha512-qFoB/s4vjjHYFJA6rnOVTeXZ99Y4RTXhCjUrrY2B/c9hssZbEyP/oj57ojQsaIENK8ItCoD7sOExqANwx41qqw==} + /@storybook/addon-storysource@8.0.10: + resolution: {integrity: sha512-LCNgp5pWyI9ZlJMFeN0nvt9gvgHMWneDjfUoAHTOP7Smi0xz4lUDYKB4P53kgE1peHn2+nxAauSBdA1IEFBIRA==} dependencies: - '@storybook/source-loader': 8.0.4 + '@storybook/source-loader': 8.0.10 estraverse: 5.3.0 tiny-invariant: 1.3.3 dev: true - /@storybook/addon-toolbars@8.0.4: - resolution: {integrity: sha512-yodRXDYog/90cNEy84kg6s7L+nxQ+egBjHBTsav1L4cJmQI/uAX8yISHHiX4I5ppNc120Jz3UdHdRxXRlo345g==} + /@storybook/addon-toolbars@8.0.10: + resolution: {integrity: sha512-67HP6mTJU/gjRju01Z5HjeqoRiJMDlrMvMvjGBg7w5+tPNtjYqdelfe2+kcfU+Hf6dfcuqaBDwaUUGSv+RYtRQ==} dev: true - /@storybook/addon-viewport@8.0.4: - resolution: {integrity: sha512-E5IKOsxKcOtlOYc0cWgzVJohQB+dVBWwaJcg5FlslToknfVB9M0kfQ/SQcp3KB0C9/cOmJK1Jm388InW+EjrBQ==} + /@storybook/addon-viewport@8.0.10: + resolution: {integrity: sha512-NJ88Nd/tXreHLyLeF3VP+b8Fu2KtUuJ0L4JYpEMmcdaejGARTrJJOU+pcZBiUqEHFeXQ8rDY8DKXhUJZQFQ1Wg==} dependencies: memoizerific: 1.11.3 dev: true - /@storybook/blocks@8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-9dRXk9zLJVPOmEWsSXm10XUmIfvS/tVgeBgFXNbusFQZXPpexIPNdRgB004pDGg9RvlY78ykpnd3yP143zaXMg==} + /@storybook/blocks@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-LOaxvcO2d4dT4YoWlQ0bq/c8qA3aHoqtyuvBjwbVn+359bjMtgj/91YuP9Y2+ggZZ4p+ttgvk39PcmJlNXlJsw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4520,30 +4373,30 @@ packages: react-dom: optional: true dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/components': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 8.0.4 - '@storybook/csf': 0.1.3 - '@storybook/docs-tools': 8.0.4 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/components': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@storybook/core-events': 8.0.10 + '@storybook/csf': 0.1.7 + '@storybook/docs-tools': 8.0.10 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.2.0)(react@18.2.0) - '@storybook/manager-api': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 8.0.4 - '@storybook/theming': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 - '@types/lodash': 4.17.0 + '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) + '@storybook/manager-api': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/preview-api': 8.0.10 + '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 + '@types/lodash': 4.17.1 color-convert: 2.0.1 dequal: 2.0.3 lodash: 4.17.21 - markdown-to-jsx: 7.3.2(react@18.2.0) + markdown-to-jsx: 7.3.2(react@18.3.1) memoizerific: 1.11.3 polished: 4.3.1 - react: 18.2.0 - react-colorful: 5.6.1(react-dom@18.2.0)(react@18.2.0) - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-colorful: 5.6.1(react-dom@18.3.1)(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) telejson: 7.2.0 - tocbot: 4.25.0 + tocbot: 4.27.19 ts-dedent: 2.2.0 util-deprecate: 1.0.2 transitivePeerDependencies: @@ -4552,17 +4405,17 @@ packages: - supports-color dev: true - /@storybook/builder-manager@8.0.4: - resolution: {integrity: sha512-BafYVxq77uuTmXdjYo5by42OyOrb6qcpWYKva3ntWK2ZhTaLJlwwqAOdahT1DVzi4VeUP6465YvsTCzIE8fuIw==} + /@storybook/builder-manager@8.0.10: + resolution: {integrity: sha512-lo57jeeYuYCKYrmGOdLg25rMyiGYSTwJ+zYsQ3RvClVICjP6X0I1RCKAJDzkI0BixH6s1+w5ynD6X3PtDnhUuw==} dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 8.0.4 - '@storybook/manager': 8.0.4 - '@storybook/node-logger': 8.0.4 + '@storybook/core-common': 8.0.10 + '@storybook/manager': 8.0.10 + '@storybook/node-logger': 8.0.10 '@types/ejs': 3.1.5 '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.20.2) browser-assert: 1.2.1 - ejs: 3.1.9 + ejs: 3.1.10 esbuild: 0.20.2 esbuild-plugin-alias: 0.2.1 express: 4.19.2 @@ -4574,8 +4427,8 @@ packages: - supports-color dev: true - /@storybook/builder-vite@8.0.4(typescript@5.4.3)(vite@5.2.6): - resolution: {integrity: sha512-Whb001bGkoGQ6/byp9QTQJ4NO61Qa5bh1p5WEEMJ5wYvHm83b+B/IwwilUfU5mL9bJB/RjbwyKcSQqGP6AxMzA==} + /@storybook/builder-vite@8.0.10(typescript@5.4.5)(vite@5.2.11): + resolution: {integrity: sha512-Rod/2jYvF4Ng1MjIMZEXe/3z0lPuxkRtetCTr3ECPgi83lHXpHJ+N0NVfJEMs+pXsVqkLP3iGt2hLn6D6yFMwA==} peerDependencies: '@preact/preset-vite': '*' typescript: '>= 4.3.x' @@ -4589,55 +4442,55 @@ packages: vite-plugin-glimmerx: optional: true dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/csf-plugin': 8.0.4 - '@storybook/node-logger': 8.0.4 - '@storybook/preview': 8.0.4 - '@storybook/preview-api': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/csf-plugin': 8.0.10 + '@storybook/node-logger': 8.0.10 + '@storybook/preview': 8.0.10 + '@storybook/preview-api': 8.0.10 + '@storybook/types': 8.0.10 '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 0.9.3 express: 4.19.2 find-cache-dir: 3.3.2 fs-extra: 11.2.0 - magic-string: 0.30.8 + magic-string: 0.30.10 ts-dedent: 2.2.0 - typescript: 5.4.3 - vite: 5.2.6(@types/node@20.11.30) + typescript: 5.4.5 + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - encoding - supports-color dev: true - /@storybook/channels@8.0.4: - resolution: {integrity: sha512-haKV+8RbiSzLjicowUfc7h2fTClZHX/nz9SRUecf4IEZUEu2T78OgM/TzqZvL7rA3+/fKqp5iI+3PN3OA75Sdg==} + /@storybook/channels@8.0.10: + resolution: {integrity: sha512-3JLxfD7czlx31dAGvAYJ4J4BNE/Y2+hhj/dsV3xlQTHKVpnWknaoeYEC1a6YScyfsH6W+XmP2rzZKzH4EkLSGQ==} dependencies: - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 '@storybook/global': 5.0.0 telejson: 7.2.0 tiny-invariant: 1.3.3 dev: true - /@storybook/cli@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-8jb8hrulRMfyFyNXFEapxHBS51xb42ZZGfVAacXIsHOJtjOd5CnOoSUYn0aOkVl19VF/snoa9JOW7BaW/50Eqw==} + /@storybook/cli@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-KUZEO2lyvOS2sRJEFXovt6+5b65iWsh7F8e8S1cM20fCM1rZAlWtwmoxmDVXDmyEp0wTrq4FrRxKnbo9UO518w==} hasBin: true dependencies: - '@babel/core': 7.24.3 - '@babel/types': 7.24.0 + '@babel/core': 7.24.5 + '@babel/types': 7.24.5 '@ndelangen/get-tarball': 3.0.9 - '@storybook/codemod': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/core-server': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/csf-tools': 8.0.4 - '@storybook/node-logger': 8.0.4 - '@storybook/telemetry': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/codemod': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/core-server': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/csf-tools': 8.0.10 + '@storybook/node-logger': 8.0.10 + '@storybook/telemetry': 8.0.10 + '@storybook/types': 8.0.10 '@types/semver': 7.5.8 '@yarnpkg/fslib': 2.10.3 '@yarnpkg/libzip': 2.3.0 @@ -4645,14 +4498,14 @@ packages: commander: 6.2.1 cross-spawn: 7.0.3 detect-indent: 6.1.0 - envinfo: 7.11.1 + envinfo: 7.13.0 execa: 5.1.1 find-up: 5.0.0 fs-extra: 11.2.0 get-npm-tarball-url: 2.1.0 giget: 1.2.3 globby: 11.1.0 - jscodeshift: 0.15.2(@babel/preset-env@7.24.3) + jscodeshift: 0.15.2(@babel/preset-env@7.24.5) leven: 3.1.0 ora: 5.4.1 prettier: 3.2.5 @@ -4673,26 +4526,26 @@ packages: - utf-8-validate dev: true - /@storybook/client-logger@8.0.4: - resolution: {integrity: sha512-2SeEg3PT/d0l/+EAVtyj9hmMLTyTPp+bRBSzxYouBjtJPM1jrdKpFagj1o3uBRovwWm9SIVX6/ZsoRC33PEV1g==} + /@storybook/client-logger@8.0.10: + resolution: {integrity: sha512-u38SbZNAunZzxZNHMJb9jkUwFkLyWxmvp4xtiRM3u9sMUShXoTnzbw1yKrxs+kYJjg+58UQPZ1JhEBRcHt5Oww==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/codemod@8.0.4: - resolution: {integrity: sha512-bysG46P4wjlR3RCpr/ntNAUaupWpzLcWYWti3iNtIyZ/iPrX6KtXoA9QCIwJZrlv41us6F+KEZbzLzkgWbymtQ==} + /@storybook/codemod@8.0.10: + resolution: {integrity: sha512-t45jKGs/eyR/nKVX6QgRtMZSAjJo5aXWWk3B24xVbW6ywr0jt1LC100FkHG4Af8cApIfh8uUmS9X05hMG5zGGA==} dependencies: - '@babel/core': 7.24.3 - '@babel/preset-env': 7.24.3(@babel/core@7.24.3) - '@babel/types': 7.24.0 - '@storybook/csf': 0.1.3 - '@storybook/csf-tools': 8.0.4 - '@storybook/node-logger': 8.0.4 - '@storybook/types': 8.0.4 + '@babel/core': 7.24.5 + '@babel/preset-env': 7.24.5(@babel/core@7.24.5) + '@babel/types': 7.24.5 + '@storybook/csf': 0.1.7 + '@storybook/csf-tools': 8.0.10 + '@storybook/node-logger': 8.0.10 + '@storybook/types': 8.0.10 '@types/cross-spawn': 6.0.6 cross-spawn: 7.0.3 globby: 11.1.0 - jscodeshift: 0.15.2(@babel/preset-env@7.24.3) + jscodeshift: 0.15.2(@babel/preset-env@7.24.5) lodash: 4.17.21 prettier: 3.2.5 recast: 0.23.6 @@ -4701,34 +4554,34 @@ packages: - supports-color dev: true - /@storybook/components@8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-i5ngl5GTOLB9nZ1cmpxTjtWct5IuH9UxzFC73a0jHMkCwN26w16IqufRVDaoQv0AvZN4pd4fNM2in/XVHA10dw==} + /@storybook/components@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-eo+oDDcm35YBB3dtDYDfcjJypNVPmRty85VWpAOBsJXpwp/fgU8csx0DM3KmhrQ4cWLf2WzcFowJwI1w+J88Sw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.73)(react@18.2.0) - '@storybook/client-logger': 8.0.4 - '@storybook/csf': 0.1.3 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) + '@storybook/client-logger': 8.0.10 + '@storybook/csf': 0.1.7 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 + '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) + '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) util-deprecate: 1.0.2 transitivePeerDependencies: - '@types/react' dev: true - /@storybook/core-common@8.0.4: - resolution: {integrity: sha512-dzFRLm5FxUa2EFE6Rx/KLDTJNLBIp1S2/+Q1K+rG8V+CLvewCc2Cd486rStZqSXEKI7vDnsRs/aMla+N0X/++Q==} + /@storybook/core-common@8.0.10: + resolution: {integrity: sha512-hsFlPieputaDQoxstnPa3pykTc4bUwEDgCHf8U43+/Z7qmLOQ9fpG+2CFW930rsCRghYpPreOvsmhY7lsGKWLQ==} dependencies: - '@storybook/core-events': 8.0.4 - '@storybook/csf-tools': 8.0.4 - '@storybook/node-logger': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/core-events': 8.0.10 + '@storybook/csf-tools': 8.0.10 + '@storybook/node-logger': 8.0.10 + '@storybook/types': 8.0.10 '@yarnpkg/fslib': 2.10.3 '@yarnpkg/libzip': 2.3.0 chalk: 4.1.2 @@ -4740,7 +4593,7 @@ packages: find-cache-dir: 3.3.2 find-up: 5.0.0 fs-extra: 11.2.0 - glob: 10.3.10 + glob: 10.3.12 handlebars: 4.7.8 lazy-universal-dotenv: 4.0.0 node-fetch: 2.7.0 @@ -4758,34 +4611,34 @@ packages: - supports-color dev: true - /@storybook/core-events@8.0.4: - resolution: {integrity: sha512-1FgLacIGi9i6/fyxw7ZJDC621RK47IMaA3keH4lc11ASRzCSwJ4YOrXjBFjfPc79EF2BuX72DDJNbhj6ynfF3g==} + /@storybook/core-events@8.0.10: + resolution: {integrity: sha512-TuHPS6p5ZNr4vp4butLb4R98aFx0NRYCI/7VPhJEUH5rPiqNzE3PZd8DC8rnVxavsJ+jO1/y+egNKXRYkEcoPQ==} dependencies: ts-dedent: 2.2.0 dev: true - /@storybook/core-server@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-/633Pp7LPcDWXkPLSW+W9VUYUbVkdVBG6peXjuzogV0vzdM0dM9af/T0uV2NQxUhzoy6/7QdSDljE+eEOBs2Lw==} + /@storybook/core-server@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-HYDw2QFBxg1X/d6g0rUhirOB5Jq6g90HBnyrZzxKoqKWJCNsCADSgM+h9HgtUw0jA97qBpIqmNO9n3mXFPWU/Q==} dependencies: '@aw-web-design/x-default-browser': 1.4.126 - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 8.0.4 - '@storybook/channels': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/csf': 0.1.3 - '@storybook/csf-tools': 8.0.4 + '@storybook/builder-manager': 8.0.10 + '@storybook/channels': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/csf': 0.1.7 + '@storybook/csf-tools': 8.0.10 '@storybook/docs-mdx': 3.0.0 '@storybook/global': 5.0.0 - '@storybook/manager': 8.0.4 - '@storybook/manager-api': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 8.0.4 - '@storybook/preview-api': 8.0.4 - '@storybook/telemetry': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/manager': 8.0.10 + '@storybook/manager-api': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/node-logger': 8.0.10 + '@storybook/preview-api': 8.0.10 + '@storybook/telemetry': 8.0.10 + '@storybook/types': 8.0.10 '@types/detect-port': 1.3.5 - '@types/node': 18.19.26 + '@types/node': 18.19.32 '@types/pretty-hrtime': 1.0.3 '@types/semver': 7.5.8 better-opn: 3.0.2 @@ -4809,7 +4662,7 @@ packages: util: 0.12.5 util-deprecate: 1.0.2 watchpack: 2.4.1 - ws: 8.16.0 + ws: 8.17.0 transitivePeerDependencies: - bufferutil - encoding @@ -4819,24 +4672,24 @@ packages: - utf-8-validate dev: true - /@storybook/csf-plugin@8.0.4: - resolution: {integrity: sha512-pEgctWuS/qeKMFZJJUM2JuKwjKBt27ye+216ft7xhNqpsrmCgumJYrkU/ii2CsFJU/qr5Fu9EYw+N+vof1OalQ==} + /@storybook/csf-plugin@8.0.10: + resolution: {integrity: sha512-0EsyEx/06sCjI8sn40r7cABtBU1vUKPMPD+S5mJiZymm73BgdARj0qZOlLoK2LP+t2pcaB/Cn7KX/uyhhv7M2g==} dependencies: - '@storybook/csf-tools': 8.0.4 - unplugin: 1.10.0 + '@storybook/csf-tools': 8.0.10 + unplugin: 1.10.1 transitivePeerDependencies: - supports-color dev: true - /@storybook/csf-tools@8.0.4: - resolution: {integrity: sha512-dMSZxWnXBhmXGOZZOAJ4DKZRCYdA0HaqqZ4/eF9MLLsI+qvW4EklcpjVY6bsIzACgubRWtRZkTpxTnjExi/N1A==} + /@storybook/csf-tools@8.0.10: + resolution: {integrity: sha512-xUc6fVIKoCujf/7JZhkYjrVXeNsTSoDrZFNmqLEmtfktJVqYdXY4LuSAtlBmAIyETi09ULTuuVexrcKFwjzuBA==} dependencies: - '@babel/generator': 7.24.1 - '@babel/parser': 7.24.1 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 - '@storybook/csf': 0.1.3 - '@storybook/types': 8.0.4 + '@babel/generator': 7.24.5 + '@babel/parser': 7.24.5 + '@babel/traverse': 7.24.5 + '@babel/types': 7.24.5 + '@storybook/csf': 0.1.7 + '@storybook/types': 8.0.10 fs-extra: 11.2.0 recast: 0.23.6 ts-dedent: 2.2.0 @@ -4850,8 +4703,8 @@ packages: lodash: 4.17.21 dev: true - /@storybook/csf@0.1.3: - resolution: {integrity: sha512-IPZvXXo4b3G+gpmgBSBqVM81jbp2ePOKsvhgJdhyZJtkYQCII7rg9KKLQhvBQM5sLaF1eU6r0iuwmyynC9d9SA==} + /@storybook/csf@0.1.7: + resolution: {integrity: sha512-53JeLZBibjQxi0Ep+/AJTfxlofJlxy1jXcSKENlnKxHjWEYyHQCumMP5yTFjf7vhNnMjEpV3zx6t23ssFiGRyw==} dependencies: type-fest: 2.19.0 dev: true @@ -4860,12 +4713,13 @@ packages: resolution: {integrity: sha512-NmiGXl2HU33zpwTv1XORe9XG9H+dRUC1Jl11u92L4xr062pZtrShLmD4VKIsOQujxhhOrbxpwhNOt+6TdhyIdQ==} dev: true - /@storybook/docs-tools@8.0.4: - resolution: {integrity: sha512-PONfG8j/AOHi79NbEkneFRZIscrShbA0sgA+62zeejH4r9+fuIkIKtLnKcAxvr8Bm6uo9aSQbISJZUcBG42WhQ==} + /@storybook/docs-tools@8.0.10: + resolution: {integrity: sha512-rg9KS81vEh13VMr4mAgs+7L4kYqoRtG7kVfV1WHxzJxjR3wYcVR0kP9gPTWV4Xha/TA3onHu9sxKxMTWha0urQ==} dependencies: - '@storybook/core-common': 8.0.4 - '@storybook/preview-api': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/core-common': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/preview-api': 8.0.10 + '@storybook/types': 8.0.10 '@types/doctrine': 0.0.3 assert: 2.1.0 doctrine: 3.0.0 @@ -4879,41 +4733,41 @@ packages: resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} dev: true - /@storybook/icons@1.2.9(react-dom@18.2.0)(react@18.2.0): + /@storybook/icons@1.2.9(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg==} engines: {node: '>=14.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: true - /@storybook/instrumenter@8.0.4: - resolution: {integrity: sha512-lkHv1na12oMTZvuDbzufgqrtFlV1XqdXrAAg7YXZOia/oMz6Z/XMldEqwLPUCLGVodbFJofrpE67Wtw8dNTDQg==} + /@storybook/instrumenter@8.0.10: + resolution: {integrity: sha512-6IYjWeQFA5x68xRoW5dU4yAc1Hwq1ZBkZbXVgJbr5LJw5x+y8eKdZzIaOmSsSKOI96R7J5YWWd2WA1Q0nRurtg==} dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.0.4 - '@vitest/utils': 1.4.0 + '@storybook/preview-api': 8.0.10 + '@vitest/utils': 1.6.0 util: 0.12.5 dev: true - /@storybook/manager-api@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-TudiRmWlsi8kdjwqW0DDLen76Zp4Sci/AnvTbZvZOWe8C2mruxcr6aaGwuIug6y+uxIyXDvURF6Cek5Twz4isg==} + /@storybook/manager-api@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-LLu6YKQLWf5QB3h3RO8IevjLrSOew7aidIQPr9DIr9xC8wA7N2fQabr+qrJdE306p3cHZ0nzhYNYZxSjm4Dvdw==} dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/csf': 0.1.3 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/csf': 0.1.7 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.2.0)(react@18.2.0) - '@storybook/router': 8.0.4 - '@storybook/theming': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 + '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) + '@storybook/router': 8.0.10 + '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 @@ -4925,68 +4779,68 @@ packages: - react-dom dev: true - /@storybook/manager@8.0.4: - resolution: {integrity: sha512-M5IofDSxbIQIdAglxUtZOGKjZ1EAq1Mdbh4UolVsF1PKF6dAvBQJLVW6TiLjEbmPBtqgeYKMgrmmYiFNqVcdBQ==} + /@storybook/manager@8.0.10: + resolution: {integrity: sha512-bojGglUQNry48L4siURc2zQKswavLzMh69rqsfL3ZXx+i+USfRfB7593azTlaZh0q6HO4bUAjB24RfQCyifLLQ==} dev: true - /@storybook/node-logger@8.0.4: - resolution: {integrity: sha512-cALLHuX53vLQsoJamGRlquh2pfhPq9copXou2JTmFT6mrCcipo77SzhBDfeeuhaGv6vUWPfmGjPBEHXWGPe4+g==} + /@storybook/node-logger@8.0.10: + resolution: {integrity: sha512-UMmaUaA3VOX/mKLsSvOnbZre2/1tZ6hazA6H0eAnClKb51jRD1AJrsBYK+uHr/CAp7t710bB5U8apPov7hayDw==} dev: true - /@storybook/preview-api@8.0.4: - resolution: {integrity: sha512-uZCgZ/7BZkFTNudCBWx3YPFVdReMQSZJj9EfQVhQaPmfGORHGMvZMRsQXl0ONhPy7zDD4rVQxu5dSKWmIiYoWQ==} + /@storybook/preview-api@8.0.10: + resolution: {integrity: sha512-uZ6btF7Iloz9TnDcKLQ5ydi2YK0cnulv/8FLQhBCwSrzLLLb+T2DGz0cAeuWZEvMUNWNmkWJ9PAFQFs09/8p/Q==} dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/csf': 0.1.3 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/csf': 0.1.7 '@storybook/global': 5.0.0 - '@storybook/types': 8.0.4 - '@types/qs': 6.9.14 + '@storybook/types': 8.0.10 + '@types/qs': 6.9.15 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 - qs: 6.12.0 + qs: 6.12.1 tiny-invariant: 1.3.3 ts-dedent: 2.2.0 util-deprecate: 1.0.2 dev: true - /@storybook/preview@8.0.4: - resolution: {integrity: sha512-dJa13bIxQBfa5ZsXAeL6X/oXI6b87Fy31pvpKPkW1o+7M6MC4OvwGQBqgAd7m8yn6NuIHxrdwjEupa7l7PGb6w==} + /@storybook/preview@8.0.10: + resolution: {integrity: sha512-op7gZqop8PSFyPA4tc1Zds8jG6VnskwpYUUsa44pZoEez9PKEFCf4jE+7AQwbBS3hnuCb0CKBfASN8GRyoznbw==} dev: true - /@storybook/react-dom-shim@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-H8bci23e+G40WsdYPuPrhAjCeeXypXuAV6mTVvLHGKH+Yb+3wiB1weaXrot/TgzPbkDNybuhTI3Qm48FPLt0bw==} + /@storybook/react-dom-shim@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-3x8EWEkZebpWpp1pwXEzdabGINwOQt8odM5+hsOlDRtFZBmUqmmzK0rtn7orlcGlOXO4rd6QuZj4Tc5WV28dVQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: true - /@storybook/react-vite@8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3)(vite@5.2.6): - resolution: {integrity: sha512-SlAsLSDc9I1nhMbf0YgXCHaZbnjzDdv458xirmUj4aJhn45e8yhmODpkPYQ8nGn45VWYMyd0sC66lJNWRvI/FA==} + /@storybook/react-vite@8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5)(vite@5.2.11): + resolution: {integrity: sha512-J0Tw1jWSQYzc37AWaJCbrFQLlWsCHby0ie0yPx8DVehlnTT6xZWkohiKBq5iwMyYfF9SGrOfZ/dVRiB5q2sOIA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^4.0.0 || ^5.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.4.3)(vite@5.2.6) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.4.5)(vite@5.2.11) '@rollup/pluginutils': 5.1.0 - '@storybook/builder-vite': 8.0.4(typescript@5.4.3)(vite@5.2.6) - '@storybook/node-logger': 8.0.4 - '@storybook/react': 8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) + '@storybook/builder-vite': 8.0.10(typescript@5.4.5)(vite@5.2.11) + '@storybook/node-logger': 8.0.10 + '@storybook/react': 8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5) find-up: 5.0.0 - magic-string: 0.30.8 - react: 18.2.0 + magic-string: 0.30.10 + react: 18.3.1 react-docgen: 7.0.3 - react-dom: 18.2.0(react@18.2.0) + react-dom: 18.3.1(react@18.3.1) resolve: 1.22.8 tsconfig-paths: 4.2.0 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -4996,8 +4850,8 @@ packages: - vite-plugin-glimmerx dev: true - /@storybook/react@8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3): - resolution: {integrity: sha512-p4wQSJIhG48UD2fZ6tFDT9zaqrVnvZxjV18+VjSi3dez/pDoEMJ3SWZWcmeDenKwvvk+SPdRH7k5mUHW1Rh0xg==} + /@storybook/react@8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5): + resolution: {integrity: sha512-/MIMc02TNmiNXDzk55dm9+ujfNE5LVNeqqK+vxXWLlCZ0aXRAd1/ZLYeRFuYLgEETB7mh7IP8AXjvM68NX5HYg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5007,15 +4861,15 @@ packages: typescript: optional: true dependencies: - '@storybook/client-logger': 8.0.4 - '@storybook/docs-tools': 8.0.4 + '@storybook/client-logger': 8.0.10 + '@storybook/docs-tools': 8.0.10 '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.0.4 - '@storybook/react-dom-shim': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 + '@storybook/preview-api': 8.0.10 + '@storybook/react-dom-shim': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 - '@types/node': 18.19.26 + '@types/node': 18.19.32 acorn: 7.4.1 acorn-jsx: 5.3.2(acorn@7.4.1) acorn-walk: 7.2.0 @@ -5023,43 +4877,43 @@ packages: html-tags: 3.3.1 lodash: 4.17.21 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-element-to-jsx-string: 15.0.0(react-dom@18.2.0)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-element-to-jsx-string: 15.0.0(react-dom@18.3.1)(react@18.3.1) semver: 7.6.0 ts-dedent: 2.2.0 type-fest: 2.19.0 - typescript: 5.4.3 + typescript: 5.4.5 util-deprecate: 1.0.2 transitivePeerDependencies: - encoding - supports-color dev: true - /@storybook/router@8.0.4: - resolution: {integrity: sha512-hlR80QvmLBflAqMeGcgtDuSe6TJlzdizwEAkBLE1lDvFI6tvvEyAliCAXBpIDdOZTe0u/zeeJkOUXKSx33caoQ==} + /@storybook/router@8.0.10: + resolution: {integrity: sha512-AZhgiet+EK0ZsPbaDgbbVTAHW2LAMCP1z/Un2uMBbdDeD0Ys29Af47AbEj/Ome5r1cqasLvzq2WXJlVXPNB0Zw==} dependencies: - '@storybook/client-logger': 8.0.4 + '@storybook/client-logger': 8.0.10 memoizerific: 1.11.3 - qs: 6.12.0 + qs: 6.12.1 dev: true - /@storybook/source-loader@8.0.4: - resolution: {integrity: sha512-pqaOMMV+dZvjbTdOzuc5RCFN9mGJ81GDtiaxYTKiIYrAyeK+V4JEZi7vHxCbZV4Tci5byeGNhx4FvQgVF2q0Wg==} + /@storybook/source-loader@8.0.10: + resolution: {integrity: sha512-bv9FRPzELjcoMJLWLDqkUNh1zY0DiCgcvM+9qsZva8pxAD4fzrX+mgCS2vZVJHRg8wMAhw/ymdXixDUodHAvsw==} dependencies: - '@storybook/csf': 0.1.3 - '@storybook/types': 8.0.4 + '@storybook/csf': 0.1.7 + '@storybook/types': 8.0.10 estraverse: 5.3.0 lodash: 4.17.21 prettier: 3.2.5 dev: true - /@storybook/telemetry@8.0.4: - resolution: {integrity: sha512-Q3ITY6J46R/TrrPRIU1fs3WNs69ExpTJZ9UlB8087qOUyV90Ex33SYk3i10xVWRczxCmyC1V58Xuht6nxz7mNQ==} + /@storybook/telemetry@8.0.10: + resolution: {integrity: sha512-s4Uc+KZQkdmD2d+64Qf8wYknhQZwmjf2CxjIjv9b4KLsU/nyfDheK7Fzd1jhBKb2UQUlLW5HhZkBgs1RsZcDHA==} dependencies: - '@storybook/client-logger': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/csf-tools': 8.0.4 + '@storybook/client-logger': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/csf-tools': 8.0.10 chalk: 4.1.2 detect-package-manager: 2.0.1 fetch-retry: 5.0.6 @@ -5070,19 +4924,18 @@ packages: - supports-color dev: true - /@storybook/test@8.0.4(vitest@1.4.0): - resolution: {integrity: sha512-/uvE8Rtu7tIcuyQBUzKq7uuDCsjmADI18BApLdwo/qthmN8ERDxRSz0Ngj2gvBMQFv99At8ESi/xh6oFGu3rWg==} + /@storybook/test@8.0.10(vitest@1.6.0): + resolution: {integrity: sha512-VqjzKJiOCjaZ0CjLeKygYk8uetiaiKbpIox+BrND9GtpEBHcRZA5AeFY2P1aSCOhsaDwuh4KRBxJWFug7DhWGQ==} dependencies: - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/instrumenter': 8.0.4 - '@storybook/preview-api': 8.0.4 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/instrumenter': 8.0.10 + '@storybook/preview-api': 8.0.10 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.2(vitest@1.4.0) + '@testing-library/jest-dom': 6.4.5(vitest@1.6.0) '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) '@vitest/expect': 1.3.1 - '@vitest/spy': 1.4.0 - chai: 4.4.1 + '@vitest/spy': 1.6.0 util: 0.12.5 transitivePeerDependencies: - '@jest/globals' @@ -5092,8 +4945,8 @@ packages: - vitest dev: true - /@storybook/theming@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-NxtTU2wMC0lj375ejoT3Npdcqwv6NeUpLaJl6EZCMXSR41ve9WG4suUNWQ63olhqKxirjzAz0IL7ggH7c3hPvA==} + /@storybook/theming@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-7NHt7bMC7lPkwz9KdDpa6DkLoQZz5OV6jsx/qY91kcdLo1rpnRPAiVlJvmWesFxi1oXOpVDpHHllWzf8KDBv8A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5103,24 +4956,24 @@ packages: react-dom: optional: true dependencies: - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) - '@storybook/client-logger': 8.0.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) + '@storybook/client-logger': 8.0.10 '@storybook/global': 5.0.0 memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: true - /@storybook/types@8.0.4: - resolution: {integrity: sha512-OO7QY+qZFCYkItDUBACtIV32p75O7sNziAiyS1V2Oxgo7Ln7fwZwr3mJcA1ruBed6ZcrW3c87k7Xs40T2zAWcg==} + /@storybook/types@8.0.10: + resolution: {integrity: sha512-S/hKS7+SqNnYIehwxdQ4M2nnlfGDdYWAXdtPCVJCmS+YF2amgAxeuisiHbUg7eypds6VL0Oxk/j2nPEHOHk9pg==} dependencies: - '@storybook/channels': 8.0.4 + '@storybook/channels': 8.0.10 '@types/express': 4.17.21 file-system-cache: 2.3.0 dev: true - /@swc/core-darwin-arm64@1.4.11: - resolution: {integrity: sha512-C1j1Qp/IHSelVWdEnT7f0iONWxQz6FAqzjCF2iaL+0vFg4V5f2nlgrueY8vj5pNNzSGhrAlxsMxEIp4dj1MXkg==} + /@swc/core-darwin-arm64@1.5.3: + resolution: {integrity: sha512-kRmmV2XqWegzGXvJfVVOj10OXhLgaVOOBjaX3p3Aqg7Do5ksg+bY5wi1gAN/Eul7B08Oqf7GG7WJevjDQGWPOg==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] @@ -5128,8 +4981,8 @@ packages: dev: true optional: true - /@swc/core-darwin-x64@1.4.11: - resolution: {integrity: sha512-0TTy3Ni8ncgaMCchSQ7FK8ZXQLlamy0FXmGWbR58c+pVZWYZltYPTmheJUvVcR0H2+gPAymRKyfC0iLszDALjg==} + /@swc/core-darwin-x64@1.5.3: + resolution: {integrity: sha512-EYs0+ovaRw6ZN9GBr2nIeC7gUXWA0q4RYR+Og3Vo0Qgv2Mt/XudF44A2lPK9X7M3JIfu6JjnxnTuvsK1Lqojfw==} engines: {node: '>=10'} cpu: [x64] os: [darwin] @@ -5137,8 +4990,8 @@ packages: dev: true optional: true - /@swc/core-linux-arm-gnueabihf@1.4.11: - resolution: {integrity: sha512-XJLB71uw0rog4DjYAPxFGAuGCBQpgJDlPZZK6MTmZOvI/1t0+DelJ24IjHIxk500YYM26Yv47xPabqFPD7I2zQ==} + /@swc/core-linux-arm-gnueabihf@1.5.3: + resolution: {integrity: sha512-RBVUTidSf4wgPdv98VrgJ4rMzMDN/3LBWdT7l+R7mNFH+mtID7ZAhTON0o/m1HkECgAgi1xcbTOVAw1xgd5KLA==} engines: {node: '>=10'} cpu: [arm] os: [linux] @@ -5146,8 +4999,8 @@ packages: dev: true optional: true - /@swc/core-linux-arm64-gnu@1.4.11: - resolution: {integrity: sha512-vYQwzJvm/iu052d5Iw27UFALIN5xSrGkPZXxLNMHPySVko2QMNNBv35HLatkEQHbQ3X+VKSW9J9SkdtAvAVRAQ==} + /@swc/core-linux-arm64-gnu@1.5.3: + resolution: {integrity: sha512-DCC6El3MiTYfv98CShxz/g2s4Pxn6tV0mldCQ0UdRqaN2ApUn7E+zTrqaj5bk7yII3A43WhE9Mr6wNPbXUeVyg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -5155,8 +5008,8 @@ packages: dev: true optional: true - /@swc/core-linux-arm64-musl@1.4.11: - resolution: {integrity: sha512-eV+KduiRYUFjPsvbZuJ9aknQH9Tj0U2/G9oIZSzLx/18WsYi+upzHbgxmIIHJ2VJgfd7nN40RI/hMtxNsUzR/g==} + /@swc/core-linux-arm64-musl@1.5.3: + resolution: {integrity: sha512-p04ysjYXEyaCGpJvwHm0T0nkPawXtdKBTThWnlh8M5jYULVNVA1YmC9azG2Avs1GDaLgBPVUgodmFYpdSupOYA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -5164,8 +5017,8 @@ packages: dev: true optional: true - /@swc/core-linux-x64-gnu@1.4.11: - resolution: {integrity: sha512-WA1iGXZ2HpqM1OR9VCQZJ8sQ1KP2or9O4bO8vWZo6HZJIeoQSo7aa9waaCLRpkZvkng1ct/TF/l6ymqSNFXIzQ==} + /@swc/core-linux-x64-gnu@1.5.3: + resolution: {integrity: sha512-/l4KJu0xwYm6tcVSOvF8RbXrIeIHJAhWnKvuX4ZnYKFkON968kB8Ghx+1yqBQcZf36tMzSuZUC5xBUA9u66lGA==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -5173,8 +5026,8 @@ packages: dev: true optional: true - /@swc/core-linux-x64-musl@1.4.11: - resolution: {integrity: sha512-UkVJToKf0owwQYRnGvjHAeYVDfeimCEcx0VQSbJoN7Iy0ckRZi7YPlmWJU31xtKvikE2bQWCOVe0qbSDqqcWXA==} + /@swc/core-linux-x64-musl@1.5.3: + resolution: {integrity: sha512-54DmSnrTXq4fYEKNR0nFAImG3+FxsHlQ6Tol/v3l+rxmg2K0FeeDOpH7wTXeWhMGhFlGrLIyLSnA+SzabfoDIA==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -5182,8 +5035,8 @@ packages: dev: true optional: true - /@swc/core-win32-arm64-msvc@1.4.11: - resolution: {integrity: sha512-35khwkyly7lF5NDSyvIrukBMzxPorgc5iTSDfVO/LvnmN5+fm4lTlrDr4tUfTdOhv3Emy7CsKlsNAeFRJ+Pm+w==} + /@swc/core-win32-arm64-msvc@1.5.3: + resolution: {integrity: sha512-piUMqoHNwDXChBfaaFIMzYgoxepfd8Ci1uXXNVEnuiRKz3FiIcNLmvXaBD7lKUwKcnGgVziH/CrndX6SldKQNQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] @@ -5191,8 +5044,8 @@ packages: dev: true optional: true - /@swc/core-win32-ia32-msvc@1.4.11: - resolution: {integrity: sha512-Wx8/6f0ufgQF2pbVPsJ2dAmFLwIOW+xBE5fxnb7VnEbGkTgP1qMDWiiAtD9rtvDSuODG3i1AEmAak/2HAc6i6A==} + /@swc/core-win32-ia32-msvc@1.5.3: + resolution: {integrity: sha512-zV5utPYBUzYhBOomCByAjKAvfVBcOCJtnszx7Zlfz7SAv/cGm8D1QzPDCvv6jDhIlUtLj6KyL8JXeFr+f95Fjw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] @@ -5200,8 +5053,8 @@ packages: dev: true optional: true - /@swc/core-win32-x64-msvc@1.4.11: - resolution: {integrity: sha512-0xRFW6K9UZQH2NVC/0pVB0GJXS45lY24f+6XaPBF1YnMHd8A8GoHl7ugyM5yNUTe2AKhSgk5fJV00EJt/XBtdQ==} + /@swc/core-win32-x64-msvc@1.5.3: + resolution: {integrity: sha512-QmUiXiPIV5gBADfDh8e2jKynEhyRC+dcKP/zF9y5KqDUErYzlhocLd68uYS4uIegP6AylYlmigHgcaktGEE9VQ==} engines: {node: '>=10'} cpu: [x64] os: [win32] @@ -5209,8 +5062,8 @@ packages: dev: true optional: true - /@swc/core@1.4.11: - resolution: {integrity: sha512-WKEakMZxkVwRdgMN4AMJ9K5nysY8g8npgQPczmjBeNK5In7QEAZAJwnyccrWwJZU0XjVeHn2uj+XbOKdDW17rg==} + /@swc/core@1.5.3: + resolution: {integrity: sha512-pSEglypnBGLHBoBcv3aYS7IM2t2LRinubYMyP88UoFIcD2pear2CeB15CbjJ2IzuvERD0ZL/bthM7cDSR9g+aQ==} engines: {node: '>=10'} requiresBuild: true peerDependencies: @@ -5222,16 +5075,16 @@ packages: '@swc/counter': 0.1.3 '@swc/types': 0.1.6 optionalDependencies: - '@swc/core-darwin-arm64': 1.4.11 - '@swc/core-darwin-x64': 1.4.11 - '@swc/core-linux-arm-gnueabihf': 1.4.11 - '@swc/core-linux-arm64-gnu': 1.4.11 - '@swc/core-linux-arm64-musl': 1.4.11 - '@swc/core-linux-x64-gnu': 1.4.11 - '@swc/core-linux-x64-musl': 1.4.11 - '@swc/core-win32-arm64-msvc': 1.4.11 - '@swc/core-win32-ia32-msvc': 1.4.11 - '@swc/core-win32-x64-msvc': 1.4.11 + '@swc/core-darwin-arm64': 1.5.3 + '@swc/core-darwin-x64': 1.5.3 + '@swc/core-linux-arm-gnueabihf': 1.5.3 + '@swc/core-linux-arm64-gnu': 1.5.3 + '@swc/core-linux-arm64-musl': 1.5.3 + '@swc/core-linux-x64-gnu': 1.5.3 + '@swc/core-linux-x64-musl': 1.5.3 + '@swc/core-win32-arm64-msvc': 1.5.3 + '@swc/core-win32-ia32-msvc': 1.5.3 + '@swc/core-win32-x64-msvc': 1.5.3 dev: true /@swc/counter@0.1.3: @@ -5255,7 +5108,7 @@ packages: engines: {node: '>=14'} dependencies: '@babel/code-frame': 7.24.2 - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@types/aria-query': 5.0.4 aria-query: 5.1.3 chalk: 4.1.2 @@ -5264,8 +5117,8 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.4.2(vitest@1.4.0): - resolution: {integrity: sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==} + /@testing-library/jest-dom@6.4.5(vitest@1.6.0): + resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: '@jest/globals': '>= 28' @@ -5286,14 +5139,14 @@ packages: optional: true dependencies: '@adobe/css-tools': 4.3.3 - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 aria-query: 5.3.0 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - vitest: 1.4.0(@types/node@20.11.30) + vitest: 1.6.0(@types/node@20.12.10) dev: true /@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4): @@ -5316,8 +5169,8 @@ packages: /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: - '@babel/parser': 7.24.1 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.5 @@ -5326,39 +5179,39 @@ packages: /@types/babel__generator@7.6.8: resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@types/babel__template@7.4.4: resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} dependencies: - '@babel/parser': 7.24.1 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 dev: true /@types/babel__traverse@7.20.5: resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true /@types/cross-spawn@6.0.6: resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true /@types/d3-array@3.2.1: @@ -5564,16 +5417,16 @@ packages: resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} dev: true - /@types/emscripten@1.39.10: - resolution: {integrity: sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==} + /@types/emscripten@1.39.11: + resolution: {integrity: sha512-dOeX2BeNA7j6BTEqJQL3ut0bRCfsyQMd5i4FT8JfHfYhAOuJPCGh0dQFbxVJxUyQ+75x6enhDdndGb624/QszA==} dev: true /@types/escodegen@0.0.6: resolution: {integrity: sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==} dev: true - /@types/eslint@8.56.6: - resolution: {integrity: sha512-ymwc+qb1XkjT/gfoQwxIeHZ6ixH23A+tCT2ADSA/DPVKzAjwYkTXBMCQ/f6fe4wEa85Lhp26VPeUxI7wMhAi7A==} + /@types/eslint@8.56.10: + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} dependencies: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 @@ -5587,11 +5440,11 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/express-serve-static-core@4.17.43: - resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} + /@types/express-serve-static-core@4.19.0: + resolution: {integrity: sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==} dependencies: - '@types/node': 20.11.30 - '@types/qs': 6.9.14 + '@types/node': 20.12.10 + '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 dev: true @@ -5600,9 +5453,9 @@ packages: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.17.43 - '@types/qs': 6.9.14 - '@types/serve-static': 1.15.5 + '@types/express-serve-static-core': 4.19.0 + '@types/qs': 6.9.15 + '@types/serve-static': 1.15.7 dev: true /@types/find-cache-dir@3.2.1: @@ -5617,7 +5470,7 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true /@types/hast@3.0.4: @@ -5645,42 +5498,38 @@ packages: /@types/lodash-es@4.17.12: resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} dependencies: - '@types/lodash': 4.17.0 + '@types/lodash': 4.17.1 dev: true /@types/lodash.mergewith@4.6.7: resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==} dependencies: - '@types/lodash': 4.17.0 + '@types/lodash': 4.17.1 dev: false - /@types/lodash@4.17.0: - resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} + /@types/lodash@4.17.1: + resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==} - /@types/mdx@2.0.12: - resolution: {integrity: sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==} + /@types/mdx@2.0.13: + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} dev: true /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} dev: true - /@types/mime@3.0.4: - resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} - dev: true - /@types/minimatch@5.1.2: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} dev: true - /@types/node@18.19.26: - resolution: {integrity: sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==} + /@types/node@18.19.32: + resolution: {integrity: sha512-2bkg93YBSDKk8DLmmHnmj/Rwr18TLx7/n+I23BigFwgexUJoMHZOd8X1OFxuF/W3NN0S2W2E5sVabI5CPinNvA==} dependencies: undici-types: 5.26.5 dev: true - /@types/node@20.11.30: - resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} + /@types/node@20.12.10: + resolution: {integrity: sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==} dependencies: undici-types: 5.26.5 dev: true @@ -5693,10 +5542,6 @@ packages: resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} dev: false - /@types/picomatch@2.3.3: - resolution: {integrity: sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==} - dev: true - /@types/pretty-hrtime@1.0.3: resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==} dev: true @@ -5704,34 +5549,34 @@ packages: /@types/prop-types@15.7.12: resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} - /@types/qs@6.9.14: - resolution: {integrity: sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==} + /@types/qs@6.9.15: + resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} dev: true /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true - /@types/react-dom@18.2.22: - resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==} + /@types/react-dom@18.3.0: + resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 dev: true /@types/react-reconciler@0.28.8: resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 dev: false /@types/react-transition-group@4.4.10: resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 dev: false - /@types/react@18.2.73: - resolution: {integrity: sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==} + /@types/react@18.3.1: + resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} dependencies: '@types/prop-types': 15.7.12 csstype: 3.1.3 @@ -5748,15 +5593,15 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true - /@types/serve-static@1.15.5: - resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} + /@types/serve-static@1.15.7: + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} dependencies: '@types/http-errors': 2.0.4 - '@types/mime': 3.0.4 - '@types/node': 20.11.30 + '@types/node': 20.12.10 + '@types/send': 0.17.4 dev: true /@types/unist@3.0.2: @@ -5771,8 +5616,8 @@ packages: resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} dev: true - /@typescript-eslint/eslint-plugin@7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.3): - resolution: {integrity: sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==} + /@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: '@typescript-eslint/parser': ^7.0.0 @@ -5783,25 +5628,25 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) - '@typescript-eslint/scope-manager': 7.4.0 - '@typescript-eslint/type-utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) - '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) - '@typescript-eslint/visitor-keys': 7.4.0 + '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/scope-manager': 7.8.0 + '@typescript-eslint/type-utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.8.0 debug: 4.3.4 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.3) - typescript: 5.4.3 + ts-api-utils: 1.3.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@7.4.0(eslint@8.57.0)(typescript@5.4.3): - resolution: {integrity: sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==} + /@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -5810,13 +5655,13 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 7.4.0 - '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) - '@typescript-eslint/visitor-keys': 7.4.0 + '@typescript-eslint/scope-manager': 7.8.0 + '@typescript-eslint/types': 7.8.0 + '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.8.0 debug: 4.3.4 eslint: 8.57.0 - typescript: 5.4.3 + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true @@ -5829,16 +5674,16 @@ packages: '@typescript-eslint/visitor-keys': 5.62.0 dev: true - /@typescript-eslint/scope-manager@7.4.0: - resolution: {integrity: sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==} + /@typescript-eslint/scope-manager@7.8.0: + resolution: {integrity: sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==} engines: {node: ^18.18.0 || >=20.0.0} dependencies: - '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/visitor-keys': 7.4.0 + '@typescript-eslint/types': 7.8.0 + '@typescript-eslint/visitor-keys': 7.8.0 dev: true - /@typescript-eslint/type-utils@7.4.0(eslint@8.57.0)(typescript@5.4.3): - resolution: {integrity: sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==} + /@typescript-eslint/type-utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -5847,12 +5692,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) - '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) + '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) debug: 4.3.4 eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.4.3) - typescript: 5.4.3 + ts-api-utils: 1.3.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true @@ -5862,12 +5707,12 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/types@7.4.0: - resolution: {integrity: sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==} + /@typescript-eslint/types@7.8.0: + resolution: {integrity: sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==} engines: {node: ^18.18.0 || >=20.0.0} dev: true - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.3): + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.5): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5882,14 +5727,14 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.0 - tsutils: 3.21.0(typescript@5.4.3) - typescript: 5.4.3 + tsutils: 3.21.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree@7.4.0(typescript@5.4.3): - resolution: {integrity: sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==} + /@typescript-eslint/typescript-estree@7.8.0(typescript@5.4.5): + resolution: {integrity: sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: typescript: '*' @@ -5897,20 +5742,20 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/visitor-keys': 7.4.0 + '@typescript-eslint/types': 7.8.0 + '@typescript-eslint/visitor-keys': 7.8.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - minimatch: 9.0.3 + minimatch: 9.0.4 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.3) - typescript: 5.4.3 + ts-api-utils: 1.3.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.3): + /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5921,7 +5766,7 @@ packages: '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.3) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) eslint: 8.57.0 eslint-scope: 5.1.1 semver: 7.6.0 @@ -5930,8 +5775,8 @@ packages: - typescript dev: true - /@typescript-eslint/utils@7.4.0(eslint@8.57.0)(typescript@5.4.3): - resolution: {integrity: sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==} + /@typescript-eslint/utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -5939,9 +5784,9 @@ packages: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 7.4.0 - '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) + '@typescript-eslint/scope-manager': 7.8.0 + '@typescript-eslint/types': 7.8.0 + '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: @@ -5957,11 +5802,11 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@typescript-eslint/visitor-keys@7.4.0: - resolution: {integrity: sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==} + /@typescript-eslint/visitor-keys@7.8.0: + resolution: {integrity: sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==} engines: {node: ^18.18.0 || >=20.0.0} dependencies: - '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/types': 7.8.0 eslint-visitor-keys: 3.4.3 dev: true @@ -5969,13 +5814,13 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react-swc@3.6.0(vite@5.2.6): + /@vitejs/plugin-react-swc@3.6.0(vite@5.2.11): resolution: {integrity: sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==} peerDependencies: vite: ^4 || ^5 dependencies: - '@swc/core': 1.4.11 - vite: 5.2.6(@types/node@20.11.30) + '@swc/core': 1.5.3 + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - '@swc/helpers' dev: true @@ -5988,26 +5833,26 @@ packages: chai: 4.4.1 dev: true - /@vitest/expect@1.4.0: - resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} + /@vitest/expect@1.6.0: + resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} dependencies: - '@vitest/spy': 1.4.0 - '@vitest/utils': 1.4.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 chai: 4.4.1 dev: true - /@vitest/runner@1.4.0: - resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} + /@vitest/runner@1.6.0: + resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} dependencies: - '@vitest/utils': 1.4.0 + '@vitest/utils': 1.6.0 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.4.0: - resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} + /@vitest/snapshot@1.6.0: + resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} dependencies: - magic-string: 0.30.8 + magic-string: 0.30.10 pathe: 1.1.2 pretty-format: 29.7.0 dev: true @@ -6018,8 +5863,8 @@ packages: tinyspy: 2.2.1 dev: true - /@vitest/spy@1.4.0: - resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} + /@vitest/spy@1.6.0: + resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} dependencies: tinyspy: 2.2.1 dev: true @@ -6033,8 +5878,8 @@ packages: pretty-format: 29.7.0 dev: true - /@vitest/utils@1.4.0: - resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} + /@vitest/utils@1.6.0: + resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -6061,24 +5906,24 @@ packages: path-browserify: 1.0.1 dev: true - /@vue/compiler-core@3.4.21: - resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==} + /@vue/compiler-core@3.4.26: + resolution: {integrity: sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==} dependencies: - '@babel/parser': 7.24.1 - '@vue/shared': 3.4.21 + '@babel/parser': 7.24.5 + '@vue/shared': 3.4.26 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.0 dev: true - /@vue/compiler-dom@3.4.21: - resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} + /@vue/compiler-dom@3.4.26: + resolution: {integrity: sha512-4CWbR5vR9fMg23YqFOhr6t6WB1Fjt62d6xdFPyj8pxrYub7d+OgZaObMsoxaF9yBUHPMiPFK303v61PwAuGvZA==} dependencies: - '@vue/compiler-core': 3.4.21 - '@vue/shared': 3.4.21 + '@vue/compiler-core': 3.4.26 + '@vue/shared': 3.4.26 dev: true - /@vue/language-core@1.8.27(typescript@5.4.3): + /@vue/language-core@1.8.27(typescript@5.4.5): resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} peerDependencies: typescript: '*' @@ -6088,18 +5933,18 @@ packages: dependencies: '@volar/language-core': 1.11.1 '@volar/source-map': 1.11.1 - '@vue/compiler-dom': 3.4.21 - '@vue/shared': 3.4.21 + '@vue/compiler-dom': 3.4.26 + '@vue/shared': 3.4.26 computeds: 0.0.1 - minimatch: 9.0.3 + minimatch: 9.0.4 muggle-string: 0.3.1 path-browserify: 1.0.1 - typescript: 5.4.3 + typescript: 5.4.5 vue-template-compiler: 2.7.16 dev: true - /@vue/shared@3.4.21: - resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==} + /@vue/shared@3.4.26: + resolution: {integrity: sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==} dev: true /@xobotyi/scrollbar-width@1.9.5: @@ -6128,7 +5973,7 @@ packages: resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==} engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'} dependencies: - '@types/emscripten': 1.39.10 + '@types/emscripten': 1.39.11 tslib: 1.14.1 dev: true @@ -6409,7 +6254,7 @@ packages: /@zag-js/number-input@0.32.1: resolution: {integrity: sha512-atyIOvoMITb4hZtQym7yD6I7grvPW83UeMFO8hCQg3HWwd2zR4+63mouWuyMoWb4QrzVFRVQBaU8OG5xGlknEw==} dependencies: - '@internationalized/number': 3.5.1 + '@internationalized/number': 3.5.2 '@zag-js/anatomy': 0.32.1 '@zag-js/core': 0.32.1 '@zag-js/dom-event': 0.32.1 @@ -6519,7 +6364,7 @@ packages: '@zag-js/utils': 0.32.1 dev: false - /@zag-js/react@0.32.1(react-dom@18.2.0)(react@18.2.0): + /@zag-js/react@0.32.1(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-b1SB7hXXv1K6CmXkcy5Y7mb0YRWkyvulyhK8VW5O5hIAPuGxOTx70psmVeZbmVzhjdORCiro9jKx8Ec0LfolFg==} peerDependencies: react: '>=18.0.0' @@ -6529,8 +6374,8 @@ packages: '@zag-js/store': 0.32.1 '@zag-js/types': 0.32.1 proxy-compare: 2.5.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false /@zag-js/rect-utils@0.32.1: @@ -6693,18 +6538,6 @@ packages: resolution: {integrity: sha512-Vzieo4vNulzY/0zqmVfeYW/LcFJp5xtEoyUgR1FBctH8uBPBRhTIEXxKtoMablW6/vccOVo7zcu0UrR5Vx+eYQ==} dev: false - /@zkochan/retry@0.2.0: - resolution: {integrity: sha512-WhB+2B/ZPlW2Xy/kMJBrMbqecWXcbDDgn0K0wKBAgO2OlBTz1iLJrRWduo+DGGn0Akvz1Lu4Xvls7dJojximWw==} - engines: {node: '>=10'} - dev: true - - /@zkochan/rimraf@2.1.3: - resolution: {integrity: sha512-mCfR3gylCzPC+iqdxEA6z5SxJeOgzgbwmyxanKriIne5qZLswDe/M43aD3p5MNzwzXRhbZg/OX+MpES6Zk1a6A==} - engines: {node: '>=12.10'} - dependencies: - rimraf: 3.0.2 - dev: true - /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -6874,7 +6707,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-object-atoms: 1.0.0 get-intrinsic: 1.2.4 is-string: 1.0.7 @@ -6898,7 +6731,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-object-atoms: 1.0.0 es-shim-unscopables: 1.0.2 @@ -6910,7 +6743,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-object-atoms: 1.0.0 es-shim-unscopables: 1.0.2 @@ -6922,7 +6755,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 dev: true @@ -6932,7 +6765,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 dev: true @@ -6941,7 +6774,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 dev: true @@ -6950,7 +6783,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-shim-unscopables: 1.0.2 dev: true @@ -6962,7 +6795,7 @@ packages: array-buffer-byte-length: 1.0.1 call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 get-intrinsic: 1.2.4 is-array-buffer: 3.0.4 @@ -7006,55 +6839,55 @@ packages: possible-typed-array-names: 1.0.0 dev: true - /babel-core@7.0.0-bridge.0(@babel/core@7.24.3): + /babel-core@7.0.0-bridge.0(@babel/core@7.24.5): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 dev: true /babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 cosmiconfig: 7.1.0 resolve: 1.22.8 dev: false - /babel-plugin-polyfill-corejs2@0.4.10(@babel/core@7.24.3): - resolution: {integrity: sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==} + /babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.5): + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/compat-data': 7.24.1 - '@babel/core': 7.24.3 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.3) + '@babel/compat-data': 7.24.4 + '@babel/core': 7.24.5 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true - /babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.3): + /babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.5): resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.3) - core-js-compat: 3.36.1 + '@babel/core': 7.24.5 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) + core-js-compat: 3.37.0 transitivePeerDependencies: - supports-color dev: true - /babel-plugin-polyfill-regenerator@0.6.1(@babel/core@7.24.3): - resolution: {integrity: sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==} + /babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.5): + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) transitivePeerDependencies: - supports-color dev: true @@ -7117,13 +6950,6 @@ packages: - supports-color dev: true - /bole@5.0.11: - resolution: {integrity: sha512-KB0Ye0iMAW5BnNbnLfMSQcnI186hKUzE2fpkZWqcxsoTR7eqzlTidSOMYPHJOn/yR7VGH7uSZp37qH9q2Et0zQ==} - dependencies: - fast-safe-stringify: 2.1.1 - individual: 3.0.0 - dev: true - /boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} dev: false @@ -7170,10 +6996,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001600 - electron-to-chromium: 1.4.719 + caniuse-lite: 1.0.30001616 + electron-to-chromium: 1.4.757 node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) + update-browserslist-db: 1.0.15(browserslist@4.23.0) dev: true /buffer-from@1.1.2: @@ -7187,12 +7013,6 @@ packages: ieee754: 1.2.1 dev: true - /builtins@5.0.1: - resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} - dependencies: - semver: 7.6.0 - dev: true - /bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -7223,8 +7043,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - /caniuse-lite@1.0.30001600: - resolution: {integrity: sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==} + /caniuse-lite@1.0.30001616: + resolution: {integrity: sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==} dev: true /chai@4.4.1: @@ -7240,7 +7060,7 @@ packages: type-detect: 4.0.8 dev: true - /chakra-react-select@4.7.6(@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.11.4)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /chakra-react-select@4.7.6(@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.11.4)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-ZL43hyXPnWf1g/HjsZDecbeJ4F2Q6tTPYJozlKWkrQ7lIX7ORP0aZYwmc5/Wly4UNzMimj2Vuosl6MmIXH+G2g==} peerDependencies: '@chakra-ui/form-control': ^2.0.0 @@ -7254,17 +7074,17 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-select: 5.7.7(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-select: 5.7.7(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false @@ -7334,8 +7154,8 @@ packages: consola: 3.2.3 dev: true - /classcat@5.0.4: - resolution: {integrity: sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==} + /classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} dev: false /clean-stack@2.2.0: @@ -7501,6 +7321,10 @@ packages: yargs: 17.7.2 dev: true + /confbox@0.1.7: + resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + dev: true + /consola@3.2.3: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -7541,8 +7365,8 @@ packages: toggle-selection: 1.0.6 dev: false - /core-js-compat@3.36.1: - resolution: {integrity: sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==} + /core-js-compat@3.37.0: + resolution: {integrity: sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==} dependencies: browserslist: 4.23.0 dev: true @@ -7676,11 +7500,6 @@ packages: d3-transition: 3.0.1(d3-selection@3.0.0) dev: false - /data-uri-to-buffer@3.0.1: - resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} - engines: {node: '>= 6'} - dev: true - /data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -7712,7 +7531,7 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: true /dateformat@5.0.3: @@ -7941,7 +7760,7 @@ packages: /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 csstype: 3.1.3 dev: false @@ -7961,10 +7780,10 @@ packages: dependencies: chalk: 4.1.2 fs-extra: 11.2.0 - glob: 10.3.10 + glob: 10.3.12 ora: 5.4.1 tslib: 2.6.2 - typescript: 5.4.3 + typescript: 5.4.5 yargs: 17.7.2 dev: true @@ -7993,16 +7812,16 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true - /ejs@3.1.9: - resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + /ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} hasBin: true dependencies: - jake: 10.8.7 + jake: 10.9.1 dev: true - /electron-to-chromium@1.4.719: - resolution: {integrity: sha512-FbWy2Q2YgdFzkFUW/W5jBjE9dj+804+98E4Pup78JBPnbdb3pv6IneY2JCPKdeKLh3AOKHQeYf+KwLr7mxGh6Q==} + /electron-to-chromium@1.4.757: + resolution: {integrity: sha512-jftDaCknYSSt/+KKeXzH3LX5E2CvRLm75P3Hj+J/dv3CL0qUYcOt13d5FN1NiL5IJbbhzHrb3BomeG2tkSlZmw==} dev: true /emoji-regex@8.0.0: @@ -8013,13 +7832,6 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true - /encode-registry@3.0.1: - resolution: {integrity: sha512-6qOwkl1g0fv0DN3Y3ggr2EaZXN71aoAqPp3p/pVaWSBSIo+YjLOWN61Fva43oVyQNPf7kgm8lkudzlzojwE2jw==} - engines: {node: '>=10'} - dependencies: - mem: 8.1.1 - dev: true - /encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -8034,7 +7846,7 @@ packages: /engine.io-client@6.5.3: resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} dependencies: - '@socket.io/component-emitter': 3.1.0 + '@socket.io/component-emitter': 3.1.2 debug: 4.3.4 engine.io-parser: 5.2.2 ws: 8.11.0 @@ -8055,16 +7867,12 @@ packages: engines: {node: '>=0.12'} dev: true - /envinfo@7.11.1: - resolution: {integrity: sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==} + /envinfo@7.13.0: + resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} engines: {node: '>=4'} hasBin: true dev: true - /err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - dev: true - /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -8076,8 +7884,8 @@ packages: stackframe: 1.3.4 dev: false - /es-abstract@1.23.2: - resolution: {integrity: sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==} + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} engines: {node: '>= 0.4'} dependencies: array-buffer-byte-length: 1.0.1 @@ -8095,7 +7903,7 @@ packages: function.prototype.name: 1.1.6 get-intrinsic: 1.2.4 get-symbol-description: 1.0.2 - globalthis: 1.0.3 + globalthis: 1.0.4 gopd: 1.0.1 has-property-descriptors: 1.0.2 has-proto: 1.0.3 @@ -8152,18 +7960,18 @@ packages: stop-iteration-iterator: 1.0.0 dev: true - /es-iterator-helpers@1.0.18: - resolution: {integrity: sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==} + /es-iterator-helpers@1.0.19: + resolution: {integrity: sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-set-tostringtag: 2.0.3 function-bind: 1.1.2 get-intrinsic: 1.2.4 - globalthis: 1.0.3 + globalthis: 1.0.4 has-property-descriptors: 1.0.2 has-proto: 1.0.3 has-symbols: 1.0.3 @@ -8301,7 +8109,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.1(@typescript-eslint/parser@7.4.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): + /eslint-module-utils@2.8.1(@typescript-eslint/parser@7.8.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} engines: {node: '>=4'} peerDependencies: @@ -8322,7 +8130,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) debug: 3.2.7 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -8338,7 +8146,7 @@ packages: requireindex: 1.1.0 dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.4.0)(eslint@8.57.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.8.0)(eslint@8.57.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -8348,7 +8156,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -8357,7 +8165,7 @@ packages: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.4.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.8.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8383,8 +8191,8 @@ packages: load-tsconfig: 0.2.5 dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.57.0): - resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + /eslint-plugin-react-hooks@4.6.2(eslint@8.57.0): + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 @@ -8412,7 +8220,7 @@ packages: array.prototype.toreversed: 1.1.2 array.prototype.tosorted: 1.1.3 doctrine: 2.1.0 - es-iterator-helpers: 1.0.18 + es-iterator-helpers: 1.0.19 eslint: 8.57.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.5 @@ -8427,22 +8235,22 @@ packages: string.prototype.matchall: 4.0.11 dev: true - /eslint-plugin-simple-import-sort@12.0.0(eslint@8.57.0): - resolution: {integrity: sha512-8o0dVEdAkYap0Cn5kNeklaKcT1nUsa3LITWEuFk3nJifOoD+5JQGoyDUW2W/iPWwBsNBJpyJS9y4je/BgxLcyQ==} + /eslint-plugin-simple-import-sort@12.1.0(eslint@8.57.0): + resolution: {integrity: sha512-Y2fqAfC11TcG/WP3TrI1Gi3p3nc8XJyEOJYHyEPEGI/UAgNx6akxxlX74p7SbAQdLcgASKhj8M0GKvH3vq/+ig==} peerDependencies: eslint: '>=5.0.0' dependencies: eslint: 8.57.0 dev: true - /eslint-plugin-storybook@0.8.0(eslint@8.57.0)(typescript@5.4.3): + /eslint-plugin-storybook@0.8.0(eslint@8.57.0)(typescript@5.4.5): resolution: {integrity: sha512-CZeVO5EzmPY7qghO2t64oaFM+8FTaD4uzOEjHKp516exyTKo+skKAL9GI3QALS2BXhyALJjNtwbmr1XinGE8bA==} engines: {node: '>= 18'} peerDependencies: eslint: '>=6' dependencies: '@storybook/csf': 0.0.1 - '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 requireindex: 1.2.0 ts-dedent: 2.2.0 @@ -8451,8 +8259,8 @@ packages: - typescript dev: true - /eslint-plugin-unused-imports@3.1.0(@typescript-eslint/eslint-plugin@7.4.0)(eslint@8.57.0): - resolution: {integrity: sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==} + /eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@7.8.0)(eslint@8.57.0): + resolution: {integrity: sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/eslint-plugin': 6 - 7 @@ -8461,7 +8269,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 eslint-rule-composer: 0.3.0 dev: true @@ -8532,7 +8340,7 @@ packages: lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 + optionator: 0.9.4 strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: @@ -8700,10 +8508,6 @@ packages: boolean: 3.2.0 dev: false - /fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - dev: true - /fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} dev: false @@ -8718,16 +8522,6 @@ packages: reusify: 1.0.4 dev: true - /fetch-blob@2.1.2: - resolution: {integrity: sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==} - engines: {node: ^10.17.0 || >=12.3.0} - peerDependencies: - domexception: '*' - peerDependenciesMeta: - domexception: - optional: true - dev: true - /fetch-retry@5.0.6: resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==} dev: true @@ -8739,6 +8533,13 @@ packages: flat-cache: 3.2.0 dev: true + /file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + dependencies: + flat-cache: 4.0.1 + dev: true + /file-selector@0.6.0: resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} engines: {node: '>= 12'} @@ -8849,12 +8650,20 @@ packages: rimraf: 3.0.2 dev: true + /flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + dev: true + /flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} dev: true - /flow-parser@0.232.0: - resolution: {integrity: sha512-U8vcKyYdM+Kb0tPzfPJ5JyPMU0uXKwHxp0L6BcEc+wBlbTW9qRhOqV5DeGXclgclVvtqQNGEG8Strj/b6c/IxA==} + /flow-parser@0.235.1: + resolution: {integrity: sha512-s04193L4JE+ntEcQXbD6jxRRlyj9QXcgEl2W6xSjH4l9x4b0eHoCHfbYHjqf9LdZFUiM5LhgpiqsvLj/AyOyYQ==} engines: {node: '>=0.4.0'} dev: true @@ -8890,7 +8699,7 @@ packages: engines: {node: '>= 0.6'} dev: true - /framer-motion@10.18.0(react-dom@18.2.0)(react@18.2.0): + /framer-motion@10.18.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==} peerDependencies: react: ^18.0.0 @@ -8901,15 +8710,15 @@ packages: react-dom: optional: true dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) tslib: 2.6.2 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 dev: false - /framer-motion@11.0.22(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-kWyldNJLyKDvLWjPYFmgngQYLiU8973BtAeVBc83r2cnil/NBUQJb1ff/6/EweNQYb5BW3PaXFjZa4D3pn/W2Q==} + /framer-motion@11.1.8(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-W2OGZmNfUarhh6A/rLXernq/JthjekbgeRWqzigPpbaShe/+HfQKUDSjiEdL302XOlINtO+SCFCiR1hlqN3uOA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 @@ -8922,8 +8731,8 @@ packages: react-dom: optional: true dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) tslib: 2.6.2 dev: false @@ -8942,15 +8751,6 @@ packages: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} dev: true - /fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - dev: true - /fs-extra@11.1.1: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} @@ -9006,7 +8806,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 functions-have-names: 1.2.3 dev: true @@ -9113,16 +8913,16 @@ packages: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} dev: true - /glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + /glob@10.3.12: + resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true dependencies: foreground-child: 3.1.1 jackspeak: 2.3.6 - minimatch: 9.0.3 - minipass: 7.0.4 - path-scurry: 1.10.1 + minimatch: 9.0.4 + minipass: 7.1.0 + path-scurry: 1.10.2 dev: true /glob@7.2.3: @@ -9148,11 +8948,12 @@ packages: type-fest: 0.20.2 dev: true - /globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + /globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} dependencies: define-properties: 1.2.1 + gopd: 1.0.1 /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -9284,20 +9085,6 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true - /hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} - dependencies: - lru-cache: 6.0.0 - dev: true - - /hosted-git-info@7.0.1: - resolution: {integrity: sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - lru-cache: 10.2.0 - dev: true - /html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} dependencies: @@ -9334,18 +9121,18 @@ packages: resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} dev: false - /i18next-http-backend@2.5.0: - resolution: {integrity: sha512-Z/aQsGZk1gSxt2/DztXk92DuDD20J+rNudT7ZCdTrNOiK8uQppfvdjq9+DFQfpAnFPn3VZS+KQIr1S/W1KxhpQ==} + /i18next-http-backend@2.5.1: + resolution: {integrity: sha512-+rNX1tghdVxdfjfPt0bI1sNg5ahGW9kA7OboG7b4t03Fp69NdDlRIze6yXhIbN8rbHxJ8IP4dzRm/okZ15lkQg==} dependencies: cross-fetch: 4.0.0 transitivePeerDependencies: - encoding dev: false - /i18next@23.10.1: - resolution: {integrity: sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==} + /i18next@23.11.3: + resolution: {integrity: sha512-Pq/aSKowir7JM0rj+Wa23Kb6KKDUGno/HjG+wRQu0PxoTbpQ4N89MAT0rFGvXmLkRLNMb1BbBOKGozl01dabzg==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: false /iconv-lite@0.4.24: @@ -9372,8 +9159,8 @@ packages: engines: {node: '>= 4'} dev: true - /immer@10.0.4: - resolution: {integrity: sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==} + /immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} dev: false /import-fresh@3.3.0: @@ -9398,10 +9185,6 @@ packages: engines: {node: '>=8'} dev: true - /individual@3.0.0: - resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} - dev: true - /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -9726,11 +9509,6 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - dev: true - /isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} @@ -9751,13 +9529,13 @@ packages: set-function-name: 2.0.2 dev: true - /its-fine@1.1.3(react@18.2.0): - resolution: {integrity: sha512-mncCA+yb6tuh5zK26cHqKlsSyxm4zdm4YgJpxycyx6p9fgxgK5PLu3iDVpKhzTn57Yrv3jk/r0aK0RFTT1OjFw==} + /its-fine@1.2.5(react@18.3.1): + resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==} peerDependencies: react: '>=18.0' dependencies: '@types/react-reconciler': 0.28.8 - react: 18.2.0 + react: 18.3.1 dev: false /jackspeak@2.3.6: @@ -9769,8 +9547,8 @@ packages: '@pkgjs/parseargs': 0.11.0 dev: true - /jake@10.8.7: - resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} + /jake@10.9.1: + resolution: {integrity: sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==} engines: {node: '>=10'} hasBin: true dependencies: @@ -9796,8 +9574,8 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - /js-tokens@8.0.3: - resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + /js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} dev: true /js-yaml@4.1.0: @@ -9807,7 +9585,7 @@ packages: argparse: 2.0.1 dev: true - /jscodeshift@0.15.2(@babel/preset-env@7.24.3): + /jscodeshift@0.15.2(@babel/preset-env@7.24.5): resolution: {integrity: sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==} hasBin: true peerDependencies: @@ -9816,20 +9594,20 @@ packages: '@babel/preset-env': optional: true dependencies: - '@babel/core': 7.24.3 - '@babel/parser': 7.24.1 - '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-optional-chaining': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.3) - '@babel/preset-env': 7.24.3(@babel/core@7.24.3) - '@babel/preset-flow': 7.24.1(@babel/core@7.24.3) - '@babel/preset-typescript': 7.24.1(@babel/core@7.24.3) - '@babel/register': 7.23.7(@babel/core@7.24.3) - babel-core: 7.0.0-bridge.0(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/parser': 7.24.5 + '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.5) + '@babel/preset-env': 7.24.5(@babel/core@7.24.5) + '@babel/preset-flow': 7.24.1(@babel/core@7.24.5) + '@babel/preset-typescript': 7.24.1(@babel/core@7.24.5) + '@babel/register': 7.23.7(@babel/core@7.24.5) + babel-core: 7.0.0-bridge.0(@babel/core@7.24.5) chalk: 4.1.2 - flow-parser: 0.232.0 + flow-parser: 0.235.1 graceful-fs: 4.2.11 micromatch: 4.0.5 neo-async: 2.6.2 @@ -9859,11 +9637,6 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - /json-parse-even-better-errors@3.0.1: - resolution: {integrity: sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -9872,10 +9645,6 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true - /json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: true - /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -9889,10 +9658,6 @@ packages: hasBin: true dev: true - /jsonc-parser@3.2.1: - resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} - dev: true - /jsondiffpatch@0.6.0: resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9948,8 +9713,8 @@ packages: engines: {node: '>= 8'} dev: false - /knip@5.6.1(@types/node@20.11.30)(typescript@5.4.3): - resolution: {integrity: sha512-occwYqHrV6KSyM1DbpWj8qQ8pCQzsdxVxYbjhYcryoXxWmHG2scyxxB4HyxVmp3Xdora4Px+3ZV5QQDi2ArerA==} + /knip@5.12.3(@types/node@20.12.10)(typescript@5.4.5): + resolution: {integrity: sha512-LL+NsE+3H0TkUnQW6icHQ+5qSrPENmjHJyMHgzjiZPmunstrIsaRG+QjahnzoH/FjMjVJwrdwVOSvksa8ixFbw==} engines: {node: '>=18.6.0'} hasBin: true peerDependencies: @@ -9958,31 +9723,24 @@ packages: dependencies: '@ericcornelissen/bash-parser': 0.5.2 '@nodelib/fs.walk': 2.0.0 - '@npmcli/map-workspaces': 3.0.4 - '@npmcli/package-json': 5.0.0 - '@pnpm/logger': 5.0.0 - '@pnpm/workspace.pkgs-graph': 2.0.15(@pnpm/logger@5.0.0) '@snyk/github-codeowners': 1.1.0 - '@types/node': 20.11.30 - '@types/picomatch': 2.3.3 + '@types/node': 20.12.10 easy-table: 1.2.0 fast-glob: 3.3.2 + file-entry-cache: 8.0.0 jiti: 1.21.0 js-yaml: 4.1.0 - micromatch: 4.0.5 minimist: 1.2.8 picocolors: 1.0.0 - picomatch: 4.0.1 + picomatch: 4.0.2 pretty-ms: 9.0.0 + resolve: 1.22.8 smol-toml: 1.1.4 strip-json-comments: 5.0.1 summary: 2.1.0 - typescript: 5.4.3 - zod: 3.22.4 - zod-validation-error: 3.0.3(zod@3.22.4) - transitivePeerDependencies: - - bluebird - - domexception + typescript: 5.4.5 + zod: 3.23.6 + zod-validation-error: 3.2.0(zod@3.23.6) dev: true /kolorist@1.8.0: @@ -10026,16 +9784,6 @@ packages: ts-error: 1.0.6 dev: false - /load-json-file@6.2.0: - resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} - engines: {node: '>=8'} - dependencies: - graceful-fs: 4.2.11 - parse-json: 5.2.0 - strip-bom: 4.0.0 - type-fest: 0.6.0 - dev: true - /load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -10045,8 +9793,8 @@ packages: resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} dependencies: - mlly: 1.6.1 - pkg-types: 1.0.3 + mlly: 1.7.0 + pkg-types: 1.1.0 dev: true /locate-path@3.0.0: @@ -10123,8 +9871,8 @@ packages: get-func-name: 2.0.2 dev: true - /lru-cache@10.2.0: - resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + /lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} dev: true @@ -10159,9 +9907,8 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /magic-string@0.30.8: - resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} - engines: {node: '>=12'} + /magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 dev: true @@ -10181,13 +9928,6 @@ packages: semver: 6.3.1 dev: true - /map-age-cleaner@0.1.3: - resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} - engines: {node: '>=6'} - dependencies: - p-defer: 1.0.0 - dev: true - /map-obj@2.0.0: resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==} engines: {node: '>=4'} @@ -10197,13 +9937,13 @@ packages: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} dev: true - /markdown-to-jsx@7.3.2(react@18.2.0): + /markdown-to-jsx@7.3.2(react@18.3.1): resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==} engines: {node: '>= 10'} peerDependencies: react: '>= 0.14.0' dependencies: - react: 18.2.0 + react: 18.3.1 dev: true /mdn-data@2.0.14: @@ -10215,22 +9955,6 @@ packages: engines: {node: '>= 0.6'} dev: true - /mem@6.1.1: - resolution: {integrity: sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==} - engines: {node: '>=8'} - dependencies: - map-age-cleaner: 0.1.3 - mimic-fn: 3.1.0 - dev: true - - /mem@8.1.1: - resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} - engines: {node: '>=10'} - dependencies: - map-age-cleaner: 0.1.3 - mimic-fn: 3.1.0 - dev: true - /memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} dev: false @@ -10290,11 +10014,6 @@ packages: engines: {node: '>=6'} dev: true - /mimic-fn@3.1.0: - resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} - engines: {node: '>=8'} - dev: true - /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -10324,8 +10043,8 @@ packages: brace-expansion: 2.0.1 dev: true - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 @@ -10347,8 +10066,8 @@ packages: engines: {node: '>=8'} dev: true - /minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + /minipass@7.1.0: + resolution: {integrity: sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==} engines: {node: '>=16 || 14 >=14.17'} dev: true @@ -10370,12 +10089,12 @@ packages: hasBin: true dev: true - /mlly@1.6.1: - resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} + /mlly@1.7.0: + resolution: {integrity: sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==} dependencies: acorn: 8.11.3 pathe: 1.1.2 - pkg-types: 1.0.3 + pkg-types: 1.1.0 ufo: 1.5.3 dev: true @@ -10398,7 +10117,7 @@ packages: resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} dev: true - /nano-css@5.6.1(react-dom@18.2.0)(react@18.2.0): + /nano-css@5.6.1(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==} peerDependencies: react: '*' @@ -10409,11 +10128,11 @@ packages: csstype: 3.1.3 fastest-stable-stringify: 2.0.2 inline-style-prefixer: 7.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) rtl-css-js: 1.16.1 stacktrace-js: 2.0.2 - stylis: 4.3.1 + stylis: 4.3.2 dev: false /nanoid@3.3.7: @@ -10422,8 +10141,8 @@ packages: hasBin: true dev: true - /nanostores@0.10.0: - resolution: {integrity: sha512-Poy5+9wFXOD0jAstn4kv9n686U2BFw48z/W8lms8cS8lcbRz7BU20JxZ3e/kkKQVfRrkm4yLWCUA6GQINdvJCQ==} + /nanostores@0.10.3: + resolution: {integrity: sha512-Nii8O1XqmawqSCf9o2aWqVxhKRN01+iue9/VEd1TiJCr9VT5XxgPFbF1Edl1XN6pwJcZRsl8Ki+z01yb/T/C2g==} engines: {node: ^18.0.0 || >=20.0.0} dev: false @@ -10436,18 +10155,6 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /ndjson@2.0.0: - resolution: {integrity: sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==} - engines: {node: '>=10'} - hasBin: true - dependencies: - json-stringify-safe: 5.0.1 - minimist: 1.2.8 - readable-stream: 3.6.2 - split2: 3.2.2 - through2: 4.0.2 - dev: true - /nearley@2.20.1: resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} hasBin: true @@ -10494,16 +10201,6 @@ packages: dependencies: whatwg-url: 5.0.0 - /node-fetch@3.0.0-beta.9: - resolution: {integrity: sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==} - engines: {node: ^10.17 || >=12.3} - dependencies: - data-uri-to-buffer: 3.0.1 - fetch-blob: 2.1.2 - transitivePeerDependencies: - - domexception - dev: true - /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true @@ -10517,53 +10214,11 @@ packages: validate-npm-package-license: 3.0.4 dev: true - /normalize-package-data@6.0.0: - resolution: {integrity: sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - hosted-git-info: 7.0.1 - is-core-module: 2.13.1 - semver: 7.6.0 - validate-npm-package-license: 3.0.4 - dev: true - /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} dev: true - /npm-install-checks@6.3.0: - resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - semver: 7.6.0 - dev: true - - /npm-normalize-package-bin@3.0.1: - resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - - /npm-package-arg@11.0.1: - resolution: {integrity: sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - hosted-git-info: 7.0.1 - proc-log: 3.0.0 - semver: 7.6.0 - validate-npm-package-name: 5.0.0 - dev: true - - /npm-pick-manifest@9.0.0: - resolution: {integrity: sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - npm-install-checks: 6.3.0 - npm-normalize-package-bin: 3.0.1 - npm-package-arg: 11.0.1 - semver: 7.6.0 - dev: true - /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -10644,7 +10299,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-object-atoms: 1.0.0 dev: true @@ -10654,7 +10309,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 dev: true /object.hasown@1.1.4: @@ -10662,7 +10317,7 @@ packages: engines: {node: '>= 0.4'} dependencies: define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-object-atoms: 1.0.0 dev: true @@ -10732,20 +10387,20 @@ packages: fast-glob: 3.3.2 js-yaml: 4.1.0 supports-color: 9.4.0 - undici: 5.28.3 + undici: 5.28.4 yargs-parser: 21.1.1 dev: true - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 + word-wrap: 1.2.5 dev: true /ora@5.4.1: @@ -10763,25 +10418,20 @@ packages: wcwidth: 1.0.1 dev: true - /overlayscrollbars-react@0.5.5(overlayscrollbars@2.6.1)(react@18.2.0): - resolution: {integrity: sha512-PakK1QEV/PAi4XniiTykcSeyoBmfDvgv2uBQ290IaY5ThrwvWg3Zk3Z39hosJYkyrS4mJ0zuIWtlHX4AKd2nZQ==} + /overlayscrollbars-react@0.5.6(overlayscrollbars@2.7.3)(react@18.3.1): + resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==} peerDependencies: overlayscrollbars: ^2.0.0 react: '>=16.8.0' dependencies: - overlayscrollbars: 2.6.1 - react: 18.2.0 + overlayscrollbars: 2.7.3 + react: 18.3.1 dev: false - /overlayscrollbars@2.6.1: - resolution: {integrity: sha512-V+ZAqWMYMyGBJNRDEcdRC7Ch+WT9RBx9hY8bfJSMyFObQeJoecs1Vqg7ZAzBVcpN6sCUXFAZldCbeySwmmD0RA==} + /overlayscrollbars@2.7.3: + resolution: {integrity: sha512-HmNo8RPtuGUjBhUbVpZBHH7SHci5iSAdg5zSekCZVsjzaM6z8MIr3F9RXrzf4y7m+fOY0nx0+y0emr1fqQmfoA==} dev: false - /p-defer@1.0.0: - resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} - engines: {node: '>=4'} - dev: true - /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -10831,14 +10481,6 @@ packages: aggregate-error: 3.1.0 dev: true - /p-memoize@4.0.1: - resolution: {integrity: sha512-km0sP12uE0dOZ5qP+s7kGVf07QngxyG0gS8sYFvFWhqlgzOsSy+m71aUejf/0akxj5W7gE//2G74qTv6b4iMog==} - engines: {node: '>=10'} - dependencies: - mem: 6.1.1 - mimic-fn: 3.1.0 - dev: true - /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -10868,13 +10510,6 @@ packages: engines: {node: '>=18'} dev: true - /parse-npm-tarball-url@3.0.0: - resolution: {integrity: sha512-InpdgIdNe5xWMEUcrVQUniQKwnggBtJ7+SCwh7zQAZwbbIYZV9XdgJyhtmDSSvykFyQXoe4BINnzKTfCwWLs5g==} - engines: {node: '>=8.15'} - dependencies: - semver: 6.3.1 - dev: true - /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -10912,19 +10547,12 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + /path-scurry@1.10.2: + resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} engines: {node: '>=16 || 14 >=14.17'} dependencies: - lru-cache: 10.2.0 - minipass: 7.0.4 - dev: true - - /path-temp@2.1.0: - resolution: {integrity: sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==} - engines: {node: '>=8.15'} - dependencies: - unique-string: 2.0.0 + lru-cache: 10.2.2 + minipass: 7.1.0 dev: true /path-to-regexp@0.1.7: @@ -10959,8 +10587,8 @@ packages: engines: {node: '>=8.6'} dev: true - /picomatch@4.0.1: - resolution: {integrity: sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==} + /picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} dev: true @@ -10995,11 +10623,11 @@ packages: find-up: 5.0.0 dev: true - /pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + /pkg-types@1.1.0: + resolution: {integrity: sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==} dependencies: - jsonc-parser: 3.2.1 - mlly: 1.6.1 + confbox: 0.1.7 + mlly: 1.7.0 pathe: 1.1.2 dev: true @@ -11007,7 +10635,7 @@ packages: resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} engines: {node: '>=10'} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: true /possible-typed-array-names@1.0.0: @@ -11050,7 +10678,7 @@ packages: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 - react-is: 18.2.0 + react-is: 18.3.1 dev: true /pretty-hrtime@1.0.3: @@ -11065,11 +10693,6 @@ packages: parse-ms: 4.0.0 dev: true - /proc-log@3.0.0: - resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true @@ -11079,23 +10702,6 @@ packages: engines: {node: '>= 0.6.0'} dev: true - /promise-inflight@1.0.1: - resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} - peerDependencies: - bluebird: '*' - peerDependenciesMeta: - bluebird: - optional: true - dev: true - - /promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - dependencies: - err-code: 2.0.3 - retry: 0.12.0 - dev: true - /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -11157,8 +10763,8 @@ packages: side-channel: 1.0.6 dev: true - /qs@6.12.0: - resolution: {integrity: sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==} + /qs@6.12.1: + resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} engines: {node: '>=0.6'} dependencies: side-channel: 1.0.6 @@ -11208,49 +10814,49 @@ packages: unpipe: 1.0.0 dev: true - /re-resizable@6.9.14(react-dom@18.2.0)(react@18.2.0): + /re-resizable@6.9.14(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-2UbPrpezMr6gkHKNCRA/N6QGGU237SKOZ78yMHId204A/oXWSAREAIuGZNQ9qlrJosewzcsv2CphZH3u7hC6ng==} peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-clientside-effect@1.2.6(react@18.2.0): + /react-clientside-effect@1.2.6(react@18.3.1): resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: '@babel/runtime': 7.24.1 - react: 18.2.0 + react: 18.3.1 dev: false - /react-colorful@5.6.1(react-dom@18.2.0)(react@18.2.0): + /react-colorful@5.6.1(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - /react-docgen-typescript@2.2.2(typescript@5.4.3): + /react-docgen-typescript@2.2.2(typescript@5.4.5): resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: typescript: '>= 4.3.x' dependencies: - typescript: 5.4.3 + typescript: 5.4.5 dev: true /react-docgen@7.0.3: resolution: {integrity: sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ==} engines: {node: '>=16.14.0'} dependencies: - '@babel/core': 7.24.3 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 + '@babel/core': 7.24.5 + '@babel/traverse': 7.24.5 + '@babel/types': 7.24.5 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.5 '@types/doctrine': 0.0.9 @@ -11262,16 +10868,16 @@ packages: - supports-color dev: true - /react-dom@18.2.0(react@18.2.0): - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + /react-dom@18.3.1(react@18.3.1): + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: - react: ^18.2.0 + react: ^18.3.1 dependencies: loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 + react: 18.3.1 + scheduler: 0.23.2 - /react-draggable@4.4.6(react-dom@18.2.0)(react@18.2.0): + /react-draggable@4.4.6(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} peerDependencies: react: '>= 16.3.0' @@ -11279,11 +10885,11 @@ packages: dependencies: clsx: 1.2.1 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-dropzone@14.2.3(react@18.2.0): + /react-dropzone@14.2.3(react@18.3.1): resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} engines: {node: '>= 10.13'} peerDependencies: @@ -11292,10 +10898,10 @@ packages: attr-accept: 2.2.2 file-selector: 0.6.0 prop-types: 15.8.1 - react: 18.2.0 + react: 18.3.1 dev: false - /react-element-to-jsx-string@15.0.0(react-dom@18.2.0)(react@18.2.0): + /react-element-to-jsx-string@15.0.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==} peerDependencies: react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 @@ -11303,25 +10909,25 @@ packages: dependencies: '@base2/pretty-print-object': 1.0.1 is-plain-object: 5.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) react-is: 18.1.0 dev: true - /react-error-boundary@4.0.13(react@18.2.0): + /react-error-boundary@4.0.13(react@18.3.1): resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} peerDependencies: react: '>=16.13.1' dependencies: - '@babel/runtime': 7.24.1 - react: 18.2.0 + '@babel/runtime': 7.24.5 + react: 18.3.1 dev: false /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false - /react-focus-lock@2.11.1(@types/react@18.2.73)(react@18.2.0): + /react-focus-lock@2.11.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -11331,36 +10937,36 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@types/react': 18.2.73 + '@types/react': 18.3.1 focus-lock: 1.3.3 prop-types: 15.8.1 - react: 18.2.0 - react-clientside-effect: 1.2.6(react@18.2.0) - use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-clientside-effect: 1.2.6(react@18.3.1) + use-callback-ref: 1.3.1(@types/react@18.3.1)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.1)(react@18.3.1) dev: false - /react-hook-form@7.51.2(react@18.2.0): - resolution: {integrity: sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==} + /react-hook-form@7.51.4(react@18.3.1): + resolution: {integrity: sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==} engines: {node: '>=12.22.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /react-hotkeys-hook@4.5.0(react-dom@18.2.0)(react@18.2.0): + /react-hotkeys-hook@4.5.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==} peerDependencies: react: '>=16.8.1' react-dom: '>=16.8.1' dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-i18next@14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==} + /react-i18next@14.1.1(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-QSiKw+ihzJ/CIeIYWrarCmXJUySHDwQr5y8uaNIkbxoGRm/5DukkxZs+RPla79IKyyDPzC/DRlgQCABHtrQuQQ==} peerDependencies: i18next: '>= 23.2.3' react: '>= 16.8.0' @@ -11372,19 +10978,19 @@ packages: react-native: optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 html-parse-stringify: 3.0.1 - i18next: 23.10.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + i18next: 23.11.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-icons@5.0.1(react@18.2.0): - resolution: {integrity: sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==} + /react-icons@5.2.0(react@18.3.1): + resolution: {integrity: sha512-n52Y7Eb4MgQZHsSZOhSXv1zs2668/hBYKfSRIvKh42yExjyhZu0d1IK2CLLZ3BZB1oo13lDfwx2vOh2z9FTV6Q==} peerDependencies: react: '*' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false /react-is@16.13.1: @@ -11398,11 +11004,11 @@ packages: resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==} dev: true - /react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} dev: true - /react-konva@18.2.10(konva@9.3.6)(react-dom@18.2.0)(react@18.2.0): + /react-konva@18.2.10(konva@9.3.6)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==} peerDependencies: konva: ^8.0.1 || ^7.2.5 || ^9.0.0 @@ -11410,48 +11016,45 @@ packages: react-dom: '>=18.0.0' dependencies: '@types/react-reconciler': 0.28.8 - its-fine: 1.1.3(react@18.2.0) + its-fine: 1.2.5(react@18.3.1) konva: 9.3.6 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-reconciler: 0.29.0(react@18.2.0) - scheduler: 0.23.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-reconciler: 0.29.2(react@18.3.1) + scheduler: 0.23.2 dev: false - /react-reconciler@0.29.0(react@18.2.0): - resolution: {integrity: sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==} + /react-reconciler@0.29.2(react@18.3.1): + resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==} engines: {node: '>=0.10.0'} peerDependencies: - react: ^18.2.0 + react: ^18.3.1 dependencies: loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 + react: 18.3.1 + scheduler: 0.23.2 dev: false - /react-redux@9.1.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1): - resolution: {integrity: sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==} + /react-redux@9.1.2(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1): + resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==} peerDependencies: '@types/react': ^18.2.25 react: ^18.0 - react-native: '>=0.69' redux: ^5.0.0 peerDependenciesMeta: '@types/react': optional: true - react-native: - optional: true redux: optional: true dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 '@types/use-sync-external-store': 0.0.3 - react: 18.2.0 + react: 18.3.1 redux: 5.0.1 - use-sync-external-store: 1.2.0(react@18.2.0) + use-sync-external-store: 1.2.2(react@18.3.1) dev: false - /react-remove-scroll-bar@2.3.5(@types/react@18.2.73)(react@18.2.0): + /react-remove-scroll-bar@2.3.5(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} engines: {node: '>=10'} peerDependencies: @@ -11461,13 +11064,13 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 - react-style-singleton: 2.2.1(@types/react@18.2.73)(react@18.2.0) + '@types/react': 18.3.1 + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.3.1) tslib: 2.6.2 dev: false - /react-remove-scroll@2.5.7(@types/react@18.2.73)(react@18.2.0): + /react-remove-scroll@2.5.7(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} engines: {node: '>=10'} peerDependencies: @@ -11477,81 +11080,81 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 - react-remove-scroll-bar: 2.3.5(@types/react@18.2.73)(react@18.2.0) - react-style-singleton: 2.2.1(@types/react@18.2.73)(react@18.2.0) + '@types/react': 18.3.1 + react: 18.3.1 + react-remove-scroll-bar: 2.3.5(@types/react@18.3.1)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.3.1) tslib: 2.6.2 - use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.3.1)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.1)(react@18.3.1) dev: false - /react-resizable-panels@2.0.16(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-UrnxmTZaTnbCl/xIOX38ig35RicqGfLuqt2x5fytpNlQvCRuxyXZwIBEhmF+pmrEGxfajyXFBoCplNxLvhF0CQ==} + /react-resizable-panels@2.0.19(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-v3E41kfKSuCPIvJVb4nL4mIZjjKIn/gh6YqZF/gDfQDolv/8XnhJBek4EiV2gOr3hhc5A3kOGOayk3DhanpaQw==} peerDependencies: react: ^16.14.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-rnd@10.4.10(react-dom@18.2.0)(react@18.2.0): + /react-rnd@10.4.10(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-YjQAgEeSbNUoOXSD9ZBvIiLVizFb+bNhpDk8DbIRHA557NW02CXbwsAeOTpJQnsdhEL+NP2I+Ssrwejqcodtjg==} peerDependencies: react: '>=16.3.0' react-dom: '>=16.3.0' dependencies: - re-resizable: 6.9.14(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-draggable: 4.4.6(react-dom@18.2.0)(react@18.2.0) + re-resizable: 6.9.14(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-draggable: 4.4.6(react-dom@18.3.1)(react@18.3.1) tslib: 2.6.2 dev: false - /react-select@5.7.7(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /react-select@5.7.7(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@floating-ui/dom': 1.6.3 + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@floating-ui/dom': 1.6.5 '@types/react-transition-group': 4.4.10 memoize-one: 6.0.0 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1)(react@18.3.1) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /react-select@5.8.0(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /react-select@5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@floating-ui/dom': 1.6.3 + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@floating-ui/dom': 1.6.5 '@types/react-transition-group': 4.4.10 memoize-one: 6.0.0 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1)(react@18.3.1) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /react-style-singleton@2.2.1(@types/react@18.2.73)(react@18.2.0): + /react-style-singleton@2.2.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} peerDependencies: @@ -11561,38 +11164,38 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 get-nonce: 1.0.1 invariant: 2.2.4 - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false - /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + /react-transition-group@4.4.5(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: react: '>=16.6.0' react-dom: '>=16.6.0' dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-universal-interface@0.6.2(react@18.2.0)(tslib@2.6.2): + /react-universal-interface@0.6.2(react@18.3.1)(tslib@2.6.2): resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} peerDependencies: react: '*' tslib: '*' dependencies: - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false - /react-use@17.5.0(react-dom@18.2.0)(react@18.2.0): + /react-use@17.5.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==} peerDependencies: react: '*' @@ -11604,10 +11207,10 @@ packages: fast-deep-equal: 3.1.3 fast-shallow-equal: 1.0.0 js-cookie: 2.2.1 - nano-css: 5.6.1(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-universal-interface: 0.6.2(react@18.2.0)(tslib@2.6.2) + nano-css: 5.6.1(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-universal-interface: 0.6.2(react@18.3.1)(tslib@2.6.2) resize-observer-polyfill: 1.5.1 screenfull: 5.2.0 set-harmonic-interval: 1.0.1 @@ -11616,50 +11219,42 @@ packages: tslib: 2.6.2 dev: false - /react-virtuoso@4.7.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-sYRQ1dHGiLCA/4ngq86U4fjO5SubEbbR53+mmcgcQZjzTK2E+9M300C3nXr54Zgr1ewZfdr9SKt6wpha0CsYUQ==} + /react-virtuoso@4.7.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-l+fnBf/G1Fp6pHCnhFq2Ra4lkZtT6c5XrS9rCS0OA6de7WGLZviCo0y61CUZZG79TeAw3L7O4czeNPiqh9CIrg==} engines: {node: '>=10'} peerDependencies: react: '>=16 || >=17 || >= 18' react-dom: '>=16 || >=17 || >= 18' dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + /react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - /reactflow@11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==} + /reactflow@11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-wusd1Xpn1wgsSEv7UIa4NNraCwH9syBtubBy4xVNXg3b+CDKM+sFaF3hnMx0tr0et4km9urIDdNvwm34QiZong==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/background': 11.3.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/controls': 11.2.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/minimap': 11.7.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-resizer': 2.2.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@reactflow/background': 11.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/controls': 11.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/minimap': 11.7.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/node-resizer': 2.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/node-toolbar': 1.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /read-package-json-fast@3.0.2: - resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - json-parse-even-better-errors: 3.0.1 - npm-normalize-package-bin: 3.0.1 - dev: true - /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -11760,10 +11355,10 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 get-intrinsic: 1.2.4 - globalthis: 1.0.3 + globalthis: 1.0.4 which-builtin-type: 1.1.3 dev: true @@ -11784,7 +11379,7 @@ packages: /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: true /regexp.prototype.flags@1.5.2: @@ -11837,14 +11432,6 @@ packages: unist-util-visit: 5.0.0 dev: true - /rename-overwrite@5.0.0: - resolution: {integrity: sha512-vSxE5Ww7Jnyotvaxi3Dj0vOMoojH8KMkBfs9xYeW/qNfJiLTcC1fmwTjrbGUq3mQSOCxkG0DbdcvwTUrpvBN4w==} - engines: {node: '>=12.10'} - dependencies: - '@zkochan/rimraf': 2.1.3 - fs-extra: 10.1.0 - dev: true - /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -11914,11 +11501,6 @@ packages: engines: {node: '>=0.12'} dev: false - /retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - dev: true - /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -11979,34 +11561,36 @@ packages: fsevents: 2.3.3 dev: true - /rollup@4.13.1: - resolution: {integrity: sha512-hFi+fU132IvJ2ZuihN56dwgpltpmLZHZWsx27rMCTZ2sYwrqlgL5sECGy1eeV2lAihD8EzChBVVhsXci0wD4Tg==} + /rollup@4.17.2: + resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.13.1 - '@rollup/rollup-android-arm64': 4.13.1 - '@rollup/rollup-darwin-arm64': 4.13.1 - '@rollup/rollup-darwin-x64': 4.13.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.13.1 - '@rollup/rollup-linux-arm64-gnu': 4.13.1 - '@rollup/rollup-linux-arm64-musl': 4.13.1 - '@rollup/rollup-linux-riscv64-gnu': 4.13.1 - '@rollup/rollup-linux-s390x-gnu': 4.13.1 - '@rollup/rollup-linux-x64-gnu': 4.13.1 - '@rollup/rollup-linux-x64-musl': 4.13.1 - '@rollup/rollup-win32-arm64-msvc': 4.13.1 - '@rollup/rollup-win32-ia32-msvc': 4.13.1 - '@rollup/rollup-win32-x64-msvc': 4.13.1 + '@rollup/rollup-android-arm-eabi': 4.17.2 + '@rollup/rollup-android-arm64': 4.17.2 + '@rollup/rollup-darwin-arm64': 4.17.2 + '@rollup/rollup-darwin-x64': 4.17.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.17.2 + '@rollup/rollup-linux-arm-musleabihf': 4.17.2 + '@rollup/rollup-linux-arm64-gnu': 4.17.2 + '@rollup/rollup-linux-arm64-musl': 4.17.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.17.2 + '@rollup/rollup-linux-riscv64-gnu': 4.17.2 + '@rollup/rollup-linux-s390x-gnu': 4.17.2 + '@rollup/rollup-linux-x64-gnu': 4.17.2 + '@rollup/rollup-linux-x64-musl': 4.17.2 + '@rollup/rollup-win32-arm64-msvc': 4.17.2 + '@rollup/rollup-win32-ia32-msvc': 4.17.2 + '@rollup/rollup-win32-x64-msvc': 4.17.2 fsevents: 2.3.3 dev: true /rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: false /run-parallel@1.2.0: @@ -12057,8 +11641,8 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true - /scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + /scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} dependencies: loose-envify: 1.4.0 @@ -12236,7 +11820,7 @@ packages: resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} engines: {node: '>=10.0.0'} dependencies: - '@socket.io/component-emitter': 3.1.0 + '@socket.io/component-emitter': 3.1.2 debug: 4.3.4 engine.io-client: 6.5.3 socket.io-parser: 4.2.4 @@ -12250,7 +11834,7 @@ packages: resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} engines: {node: '>=10.0.0'} dependencies: - '@socket.io/component-emitter': 3.1.0 + '@socket.io/component-emitter': 3.1.2 debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -12322,23 +11906,10 @@ packages: engines: {node: '>=12'} dev: false - /split2@3.2.2: - resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} - dependencies: - readable-stream: 3.6.2 - dev: true - /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true - /ssri@10.0.5: - resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 7.0.4 - dev: true - /stack-generator@2.0.10: resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} dependencies: @@ -12388,11 +11959,11 @@ packages: resolution: {integrity: sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==} dev: true - /storybook@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-FUr3Uc2dSAQ80jINH5fSXz7zD7Ncn08OthROjwRtHAH+jMf4wxyZ+RhF3heFy9xLot2/HXOLIWyHyzZZMtGhxg==} + /storybook@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-9/4oxISopLyr5xz7Du27mmQgcIfB7UTLlNzkK4IklWTiSgsOgYgZpsmIwymoXNtkrvh+QsqskdcUP1C7nNiEtw==} hasBin: true dependencies: - '@storybook/cli': 8.0.4(react-dom@18.2.0)(react@18.2.0) + '@storybook/cli': 8.0.10(react-dom@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@babel/preset-env' - bufferutil @@ -12440,7 +12011,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-object-atoms: 1.0.0 get-intrinsic: 1.2.4 @@ -12458,7 +12029,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-object-atoms: 1.0.0 dev: true @@ -12510,11 +12081,6 @@ packages: engines: {node: '>=4'} dev: true - /strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - dev: true - /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -12549,18 +12115,18 @@ packages: engines: {node: '>=14.16'} dev: true - /strip-literal@2.0.0: - resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} + /strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} dependencies: - js-tokens: 8.0.3 + js-tokens: 9.0.0 dev: true /stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} dev: false - /stylis@4.3.1: - resolution: {integrity: sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==} + /stylis@4.3.2: + resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} dev: false /summary@2.1.0: @@ -12677,21 +12243,15 @@ packages: xtend: 4.0.2 dev: true - /through2@4.0.2: - resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} - dependencies: - readable-stream: 3.6.2 - dev: true - /tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - /tinybench@2.6.0: - resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + /tinybench@2.8.0: + resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} dev: true - /tinypool@0.8.3: - resolution: {integrity: sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==} + /tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} dev: true @@ -12727,8 +12287,8 @@ packages: to-no-case: 1.0.2 dev: true - /tocbot@4.25.0: - resolution: {integrity: sha512-kE5wyCQJ40hqUaRVkyQ4z5+4juzYsv/eK+aqD97N62YH0TxFhzJvo22RUQQZdO3YnXAk42ZOfOpjVdy+Z0YokA==} + /tocbot@4.27.19: + resolution: {integrity: sha512-0yu8k0L3gCQ1OVNZnKqpbZp+kLd6qtlNEBxsb+e0G/bS0EXMl2tWqWi1Oy9knRX8rTPYfOxd/sI/OzAj3JowGg==} dev: true /toggle-selection@1.0.6: @@ -12748,13 +12308,13 @@ packages: hasBin: true dev: true - /ts-api-utils@1.3.0(typescript@5.4.3): + /ts-api-utils@1.3.0(typescript@5.4.5): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.4.3 + typescript: 5.4.5 dev: true /ts-dedent@2.2.0: @@ -12778,7 +12338,7 @@ packages: resolution: {integrity: sha512-gzkapsdbMNwBnTIjgO758GujLCj031IgHK/PKr2mrmkCSJMhSOR5FeOuSxKLMUoYc0vAA4RGEYYbjt/v6afD3g==} dev: true - /tsconfck@3.0.3(typescript@5.4.3): + /tsconfck@3.0.3(typescript@5.4.5): resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==} engines: {node: ^18 || >=20} hasBin: true @@ -12788,7 +12348,7 @@ packages: typescript: optional: true dependencies: - typescript: 5.4.3 + typescript: 5.4.5 dev: true /tsconfig-paths@3.15.0: @@ -12820,14 +12380,14 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsutils@3.21.0(typescript@5.4.3): + /tsutils@3.21.0(typescript@5.4.5): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.4.3 + typescript: 5.4.5 dev: true /type-check@0.4.0: @@ -12924,8 +12484,8 @@ packages: hasBin: true dev: true - /typescript@5.4.3: - resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} + /typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -12955,8 +12515,8 @@ packages: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true - /undici@5.28.3: - resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==} + /undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} dependencies: '@fastify/busboy': 2.1.1 @@ -13034,8 +12594,8 @@ packages: engines: {node: '>= 0.8'} dev: true - /unplugin@1.10.0: - resolution: {integrity: sha512-CuZtvvO8ua2Wl+9q2jEaqH6m3DoQ38N7pvBYQbbaeNlWGvK2l6GHiKi29aIHDPoSxdUzQ7Unevf1/ugil5X6Pg==} + /unplugin@1.10.1: + resolution: {integrity: sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg==} engines: {node: '>=14.0.0'} dependencies: acorn: 8.11.3 @@ -13049,8 +12609,8 @@ packages: engines: {node: '>=8'} dev: true - /update-browserslist-db@1.0.13(browserslist@4.23.0): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + /update-browserslist-db@1.0.15(browserslist@4.23.0): + resolution: {integrity: sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -13066,7 +12626,7 @@ packages: punycode: 2.3.1 dev: true - /use-callback-ref@1.3.1(@types/react@18.2.73)(react@18.2.0): + /use-callback-ref@1.3.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} engines: {node: '>=10'} peerDependencies: @@ -13076,39 +12636,39 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 + '@types/react': 18.3.1 + react: 18.3.1 tslib: 2.6.2 dev: false - /use-debounce@10.0.0(react@18.2.0): + /use-debounce@10.0.0(react@18.3.1): resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==} engines: {node: '>= 16.0.0'} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /use-device-pixel-ratio@1.1.2(react@18.2.0): + /use-device-pixel-ratio@1.1.2(react@18.3.1): resolution: {integrity: sha512-nFxV0HwLdRUt20kvIgqHYZe6PK/v4mU1X8/eLsT1ti5ck0l2ob0HDRziaJPx+YWzBo6dMm4cTac3mcyk68Gh+A==} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /use-image@1.1.1(react-dom@18.2.0)(react@18.2.0): + /use-image@1.1.1(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.73)(react@18.2.0): + /use-isomorphic-layout-effect@1.1.2(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} peerDependencies: '@types/react': '*' @@ -13117,11 +12677,11 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 + '@types/react': 18.3.1 + react: 18.3.1 dev: false - /use-sidecar@1.1.2(@types/react@18.2.73)(react@18.2.0): + /use-sidecar@1.1.2(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} peerDependencies: @@ -13131,18 +12691,26 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 detect-node-es: 1.1.0 - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false - /use-sync-external-store@1.2.0(react@18.2.0): + /use-sync-external-store@1.2.0(react@18.3.1): resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - react: 18.2.0 + react: 18.3.1 + dev: false + + /use-sync-external-store@1.2.2(react@18.3.1): + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.3.1 dev: false /util-deprecate@1.0.2: @@ -13175,20 +12743,6 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /validate-npm-package-name@4.0.0: - resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - builtins: 5.0.1 - dev: true - - /validate-npm-package-name@5.0.0: - resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - builtins: 5.0.1 - dev: true - /validator@13.11.0: resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} engines: {node: '>= 0.10'} @@ -13199,15 +12753,8 @@ packages: engines: {node: '>= 0.8'} dev: true - /version-selector-type@3.0.0: - resolution: {integrity: sha512-PSvMIZS7C1MuVNBXl/CDG2pZq8EXy/NW2dHIdm3bVP5N0PC8utDK8ttXLXj44Gn3J0lQE3U7Mpm1estAOd+eiA==} - engines: {node: '>=10.13'} - dependencies: - semver: 7.6.0 - dev: true - - /vite-node@1.4.0(@types/node@20.11.30): - resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} + /vite-node@1.6.0(@types/node@20.12.10): + resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -13215,7 +12762,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - '@types/node' - less @@ -13227,16 +12774,16 @@ packages: - terser dev: true - /vite-plugin-css-injected-by-js@3.5.0(vite@5.2.6): - resolution: {integrity: sha512-d0QaHH9kS93J25SwRqJNEfE29PSuQS5jn51y9N9i2Yoq0FRO7rjuTeLvjM5zwklZlRrIn6SUdtOEDKyHokgJZg==} + /vite-plugin-css-injected-by-js@3.5.1(vite@5.2.11): + resolution: {integrity: sha512-9ioqwDuEBxW55gNoWFEDhfLTrVKXEEZgl5adhWmmqa88EQGKfTmexy4v1Rh0pAS6RhKQs2bUYQArprB32JpUZQ==} peerDependencies: vite: '>2.0.0-0' dependencies: - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.11(@types/node@20.12.10) dev: true - /vite-plugin-dts@3.8.0(@types/node@20.11.30)(typescript@5.4.3)(vite@5.2.6): - resolution: {integrity: sha512-wt9ST1MwS5lkxHtA3M30+lSA3TO8RnaUu3YUPmGgY1iKm+vWZmB7KBss6qspyUlto9ynLNHYG2eJ09d2Q4/7Qg==} + /vite-plugin-dts@3.9.1(@types/node@20.12.10)(typescript@5.4.5)(vite@5.2.11): + resolution: {integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -13245,35 +12792,35 @@ packages: vite: optional: true dependencies: - '@microsoft/api-extractor': 7.43.0(@types/node@20.11.30) + '@microsoft/api-extractor': 7.43.0(@types/node@20.12.10) '@rollup/pluginutils': 5.1.0 - '@vue/language-core': 1.8.27(typescript@5.4.3) + '@vue/language-core': 1.8.27(typescript@5.4.5) debug: 4.3.4 kolorist: 1.8.0 - magic-string: 0.30.8 - typescript: 5.4.3 - vite: 5.2.6(@types/node@20.11.30) - vue-tsc: 1.8.27(typescript@5.4.3) + magic-string: 0.30.10 + typescript: 5.4.5 + vite: 5.2.11(@types/node@20.12.10) + vue-tsc: 1.8.27(typescript@5.4.5) transitivePeerDependencies: - '@types/node' - rollup - supports-color dev: true - /vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@5.2.6): + /vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@5.2.11): resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: eslint: '>=7' vite: '>=2' dependencies: '@rollup/pluginutils': 4.2.1 - '@types/eslint': 8.56.6 + '@types/eslint': 8.56.10 eslint: 8.57.0 rollup: 2.79.1 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.11(@types/node@20.12.10) dev: true - /vite-tsconfig-paths@4.3.2(typescript@5.4.3)(vite@5.2.6): + /vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.2.11): resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} peerDependencies: vite: '*' @@ -13283,15 +12830,15 @@ packages: dependencies: debug: 4.3.4 globrex: 0.1.2 - tsconfck: 3.0.3(typescript@5.4.3) - vite: 5.2.6(@types/node@20.11.30) + tsconfck: 3.0.3(typescript@5.4.5) + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@5.2.6(@types/node@20.11.30): - resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==} + /vite@5.2.11(@types/node@20.12.10): + resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -13318,23 +12865,23 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.10 esbuild: 0.20.2 postcss: 8.4.38 - rollup: 4.13.1 + rollup: 4.17.2 optionalDependencies: fsevents: 2.3.3 dev: true - /vitest@1.4.0(@types/node@20.11.30): - resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} + /vitest@1.6.0(@types/node@20.12.10): + resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.4.0 - '@vitest/ui': 1.4.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -13351,26 +12898,26 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.11.30 - '@vitest/expect': 1.4.0 - '@vitest/runner': 1.4.0 - '@vitest/snapshot': 1.4.0 - '@vitest/spy': 1.4.0 - '@vitest/utils': 1.4.0 + '@types/node': 20.12.10 + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 acorn-walk: 8.3.2 chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 local-pkg: 0.5.0 - magic-string: 0.30.8 + magic-string: 0.30.10 pathe: 1.1.2 picocolors: 1.0.0 std-env: 3.7.0 - strip-literal: 2.0.0 - tinybench: 2.6.0 - tinypool: 0.8.3 - vite: 5.2.6(@types/node@20.11.30) - vite-node: 1.4.0(@types/node@20.11.30) + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.2.11(@types/node@20.12.10) + vite-node: 1.6.0(@types/node@20.12.10) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -13398,16 +12945,16 @@ packages: he: 1.2.0 dev: true - /vue-tsc@1.8.27(typescript@5.4.3): + /vue-tsc@1.8.27(typescript@5.4.5): resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} hasBin: true peerDependencies: typescript: '*' dependencies: '@volar/typescript': 1.11.1 - '@vue/language-core': 1.8.27(typescript@5.4.3) + '@vue/language-core': 1.8.27(typescript@5.4.5) semver: 7.6.0 - typescript: 5.4.3 + typescript: 5.4.5 dev: true /watchpack@2.4.1: @@ -13499,14 +13046,6 @@ packages: isexe: 2.0.0 dev: true - /which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - dependencies: - isexe: 3.1.1 - dev: true - /why-is-node-running@2.2.2: resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} engines: {node: '>=8'} @@ -13516,6 +13055,11 @@ packages: stackback: 0.0.2 dev: true + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: true + /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: true @@ -13563,8 +13107,8 @@ packages: optional: true dev: false - /ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + /ws@8.17.0: + resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -13644,18 +13188,18 @@ packages: commander: 9.5.0 dev: true - /zod-validation-error@3.0.3(zod@3.22.4): - resolution: {integrity: sha512-cETTrcMq3Ze58vhdR0zD37uJm/694I6mAxcf/ei5bl89cC++fBNxrC2z8lkFze/8hVMPwrbtrwXHR2LB50fpHw==} + /zod-validation-error@3.2.0(zod@3.23.6): + resolution: {integrity: sha512-cYlPR6zuyrgmu2wRTdumEAJGuwI7eHVHGT+VyneAQxmRAKtGRL1/7pjz4wfLhz4J05f5qoSZc3rGacswgyTjjw==} engines: {node: '>=18.0.0'} peerDependencies: zod: ^3.18.0 dependencies: - zod: 3.22.4 + zod: 3.23.6 - /zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + /zod@3.23.6: + resolution: {integrity: sha512-RTHJlZhsRbuA8Hmp/iNL7jnfc4nZishjsanDAfEY1QpDQZCahUp3xDzl+zfweE9BklxMUcgBgS1b7Lvie/ZVwA==} - /zustand@4.5.2(@types/react@18.2.73)(react@18.2.0): + /zustand@4.5.2(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} engines: {node: '>=12.7.0'} peerDependencies: @@ -13670,7 +13214,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 - use-sync-external-store: 1.2.0(react@18.2.0) + '@types/react': 18.3.1 + react: 18.3.1 + use-sync-external-store: 1.2.0(react@18.3.1) dev: false From 67d6cf19c6d4d1e42ca5643c31bc1f7e5bf96e0f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 07:39:44 +1000 Subject: [PATCH 004/442] fix(ui): switch to viewer if auto-switch is enabled --- .../listeners/socketio/socketInvocationComplete.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 279f9aac5b..fb3a4a41c9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -2,7 +2,12 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { parseify } from 'common/util/serialize'; import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; -import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice'; +import { + boardIdSelected, + galleryViewChanged, + imageSelected, + isImageViewerOpenChanged, +} from 'features/gallery/store/gallerySlice'; import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; import { isImageOutput } from 'features/nodes/types/common'; import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; @@ -101,6 +106,7 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi } dispatch(imageSelected(imageDTO)); + dispatch(isImageViewerOpenChanged(true)); } } } From b6c19a8e47aa0c3a810f9a8870f59e553c71a44a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 07:43:54 +1000 Subject: [PATCH 005/442] feat(ui): close viewer when adding a RG layer --- .../frontend/web/src/features/gallery/store/gallerySlice.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 892c5c954d..16e0dd9770 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { uniqBy } from 'lodash-es'; import { boardsApi } from 'services/api/endpoints/boards'; @@ -89,6 +90,9 @@ export const gallerySlice = createSlice({ builder.addCase(setActiveTab, (state) => { state.isImageViewerOpen = false; }); + builder.addCase(rgLayerAdded, (state) => { + state.isImageViewerOpen = false; + }); builder.addMatcher(isAnyBoardDeleted, (state, action) => { const deletedBoardId = action.meta.arg.originalArgs; if (deletedBoardId === state.selectedBoardId) { From a826f8f8c54c8b22bf30cdcc09782202a7ea4527 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 08:05:05 +1000 Subject: [PATCH 006/442] fix(ui): show total layer count in control layers tab --- .../hooks/useControlLayersTitle.ts | 51 ------------------- .../components/ParametersPanelTextToImage.tsx | 11 ++-- 2 files changed, 8 insertions(+), 54 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts deleted file mode 100644 index bf0fa661a9..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isRegionalGuidanceLayer, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => { - let count = 0; - controlLayers.present.layers.forEach((l) => { - if (isRegionalGuidanceLayer(l)) { - const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasAtLeastOneImagePrompt = l.ipAdapters.filter((ipa) => Boolean(ipa.image)).length > 0; - if (hasTextPrompt || hasAtLeastOneImagePrompt) { - count += 1; - } - } - if (isControlAdapterLayer(l)) { - if (l.controlAdapter.image || l.controlAdapter.processedImage) { - count += 1; - } - } - if (isIPAdapterLayer(l)) { - if (l.ipAdapter.image) { - count += 1; - } - } - if (isInitialImageLayer(l)) { - if (l.image) { - count += 1; - } - } - }); - - return count; -}); - -export const useControlLayersTitle = () => { - const { t } = useTranslation(); - const validLayerCount = useAppSelector(selectValidLayerCount); - const title = useMemo(() => { - const suffix = validLayerCount > 0 ? ` (${validLayerCount})` : ''; - return `${t('controlLayers.controlLayers')}${suffix}`; - }, [t, validLayerCount]); - return title; -}; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx index 3e02e1e132..a7a401cde4 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx @@ -3,7 +3,6 @@ import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/u import { useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent'; -import { useControlLayersTitle } from 'features/controlLayers/hooks/useControlLayersTitle'; import { Prompts } from 'features/parameters/components/Prompts/Prompts'; import QueueControls from 'features/queue/components/QueueControls'; import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts'; @@ -16,7 +15,7 @@ import { RefinerSettingsAccordion } from 'features/settingsAccordions/components import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import type { CSSProperties } from 'react'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const overlayScrollbarsStyles: CSSProperties = { @@ -39,7 +38,13 @@ const selectedStyles: ChakraProps['sx'] = { const ParametersPanelTextToImage = () => { const { t } = useTranslation(); const activeTabName = useAppSelector(activeTabNameSelector); - const controlLayersTitle = useControlLayersTitle(); + const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length); + const controlLayersTitle = useMemo(() => { + if (controlLayersCount === 0) { + return t('controlLayers.controlLayers'); + } + return `${t('controlLayers.controlLayers')} (${controlLayersCount})`; + }, [controlLayersCount, t]); const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl'); return ( From 72ce2395925884c1d5e61debf7abb40424efd6f9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 08:06:19 +1000 Subject: [PATCH 007/442] revert(ui): remove floating viewer There are unresolved platform-specific issues with this component, and its utility is debatable. Should be easy to just revert this commit to add it back in the future if desired. --- invokeai/frontend/web/package.json | 1 - invokeai/frontend/web/pnpm-lock.yaml | 43 ---- .../frontend/web/src/app/components/App.tsx | 2 - .../ImageViewer/FloatingImageViewer.tsx | 190 ------------------ .../features/gallery/store/gallerySlice.ts | 5 - .../web/src/features/gallery/store/types.ts | 1 - .../src/features/ui/components/InvokeTabs.tsx | 2 - 7 files changed, 244 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index a598d0a2c7..78e8ca44ca 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -89,7 +89,6 @@ "react-konva": "^18.2.10", "react-redux": "9.1.2", "react-resizable-panels": "^2.0.19", - "react-rnd": "^10.4.10", "react-select": "5.8.0", "react-use": "^17.5.0", "react-virtuoso": "^4.7.10", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 2a3710ae9c..2703477200 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -122,9 +122,6 @@ dependencies: react-resizable-panels: specifier: ^2.0.19 version: 2.0.19(react-dom@18.3.1)(react@18.3.1) - react-rnd: - specifier: ^10.4.10 - version: 10.4.10(react-dom@18.3.1)(react@18.3.1) react-select: specifier: 5.8.0 version: 5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) @@ -7208,11 +7205,6 @@ packages: requiresBuild: true dev: true - /clsx@1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - dev: false - /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -10814,16 +10806,6 @@ packages: unpipe: 1.0.0 dev: true - /re-resizable@6.9.14(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-2UbPrpezMr6gkHKNCRA/N6QGGU237SKOZ78yMHId204A/oXWSAREAIuGZNQ9qlrJosewzcsv2CphZH3u7hC6ng==} - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - /react-clientside-effect@1.2.6(react@18.3.1): resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: @@ -10877,18 +10859,6 @@ packages: react: 18.3.1 scheduler: 0.23.2 - /react-draggable@4.4.6(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} - peerDependencies: - react: '>= 16.3.0' - react-dom: '>= 16.3.0' - dependencies: - clsx: 1.2.1 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - /react-dropzone@14.2.3(react@18.3.1): resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} engines: {node: '>= 10.13'} @@ -11099,19 +11069,6 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false - /react-rnd@10.4.10(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-YjQAgEeSbNUoOXSD9ZBvIiLVizFb+bNhpDk8DbIRHA557NW02CXbwsAeOTpJQnsdhEL+NP2I+Ssrwejqcodtjg==} - peerDependencies: - react: '>=16.3.0' - react-dom: '>=16.3.0' - dependencies: - re-resizable: 6.9.14(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-draggable: 4.4.6(react-dom@18.3.1)(react@18.3.1) - tslib: 2.6.2 - dev: false - /react-select@5.7.7(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==} peerDependencies: diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 03c854bb48..30d8f41200 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -12,7 +12,6 @@ import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal'; import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal'; import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal'; -import { FloatingImageViewer } from 'features/gallery/components/ImageViewer/FloatingImageViewer'; import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; import { configChanged } from 'features/system/store/configSlice'; import { languageSelector } from 'features/system/store/systemSelectors'; @@ -97,7 +96,6 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => { - ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx deleted file mode 100644 index 1d91dafd1c..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { Flex, IconButton, Spacer, Text, useShiftModifier } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview'; -import { isFloatingImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import { memo, useCallback, useLayoutEffect, useRef } from 'react'; -import { flushSync } from 'react-dom'; -import { useTranslation } from 'react-i18next'; -import { PiHourglassBold, PiXBold } from 'react-icons/pi'; -import { Rnd } from 'react-rnd'; - -const defaultDim = 256; -const maxDim = 512; -const defaultSize = { width: defaultDim, height: defaultDim + 24 }; -const maxSize = { width: maxDim, height: maxDim + 24 }; -const rndDefault = { x: 0, y: 0, ...defaultSize }; - -const rndStyles = { - zIndex: 11, -}; - -const enableResizing = { - top: false, - right: false, - bottom: false, - left: false, - topRight: false, - bottomRight: true, - bottomLeft: false, - topLeft: false, -}; - -const FloatingImageViewerComponent = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const shift = useShiftModifier(); - const rndRef = useRef(null); - const imagePreviewRef = useRef(null); - const onClose = useCallback(() => { - dispatch(isFloatingImageViewerOpenChanged(false)); - }, [dispatch]); - - const fitToScreen = useCallback(() => { - if (!imagePreviewRef.current || !rndRef.current) { - return; - } - const el = imagePreviewRef.current; - const rnd = rndRef.current; - - const { top, right, bottom, left, width, height } = el.getBoundingClientRect(); - const { innerWidth, innerHeight } = window; - - const newPosition = rnd.getDraggablePosition(); - - if (top < 0) { - newPosition.y = 0; - } - if (left < 0) { - newPosition.x = 0; - } - if (bottom > innerHeight) { - newPosition.y = innerHeight - height; - } - if (right > innerWidth) { - newPosition.x = innerWidth - width; - } - rnd.updatePosition(newPosition); - }, []); - - const onDoubleClick = useCallback(() => { - if (!rndRef.current || !imagePreviewRef.current) { - return; - } - const { width, height } = imagePreviewRef.current.getBoundingClientRect(); - if (width === defaultSize.width && height === defaultSize.height) { - rndRef.current.updateSize(maxSize); - } else { - rndRef.current.updateSize(defaultSize); - } - flushSync(fitToScreen); - }, [fitToScreen]); - - useLayoutEffect(() => { - window.addEventListener('resize', fitToScreen); - return () => { - window.removeEventListener('resize', fitToScreen); - }; - }, [fitToScreen]); - - useLayoutEffect(() => { - // Set the initial position - if (!imagePreviewRef.current || !rndRef.current) { - return; - } - - const { width, height } = imagePreviewRef.current.getBoundingClientRect(); - - const initialPosition = { - // 54 = width of left-hand vertical bar of tab icons - // 430 = width of parameters panel - x: 54 + 430 / 2 - width / 2, - // 16 = just a reasonable bottom padding - y: window.innerHeight - height - 16, - }; - - rndRef.current.updatePosition(initialPosition); - }, [fitToScreen]); - - return ( - - - - - {t('common.viewer')} - - - } size="sm" variant="link" onClick={onClose} /> - - - - - - - ); -}); - -FloatingImageViewerComponent.displayName = 'FloatingImageViewerComponent'; - -export const FloatingImageViewer = memo(() => { - const isOpen = useAppSelector((s) => s.gallery.isFloatingImageViewerOpen); - - if (!isOpen) { - return null; - } - - return ; -}); - -FloatingImageViewer.displayName = 'FloatingImageViewer'; - -export const ToggleFloatingImageViewerButton = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isOpen = useAppSelector((s) => s.gallery.isFloatingImageViewerOpen); - - const onToggle = useCallback(() => { - dispatch(isFloatingImageViewerOpenChanged(!isOpen)); - }, [dispatch, isOpen]); - - return ( - } - size="sm" - onClick={onToggle} - variant="link" - colorScheme={isOpen ? 'invokeBlue' : 'base'} - boxSize={8} - /> - ); -}); - -ToggleFloatingImageViewerButton.displayName = 'ToggleFloatingImageViewerButton'; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 16e0dd9770..744dc09f3f 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -24,7 +24,6 @@ const initialGalleryState: GalleryState = { limit: INITIAL_IMAGE_LIMIT, offset: 0, isImageViewerOpen: false, - isFloatingImageViewerOpen: false, }; export const gallerySlice = createSlice({ @@ -82,9 +81,6 @@ export const gallerySlice = createSlice({ isImageViewerOpenChanged: (state, action: PayloadAction) => { state.isImageViewerOpen = action.payload; }, - isFloatingImageViewerOpenChanged: (state, action: PayloadAction) => { - state.isFloatingImageViewerOpen = action.payload; - }, }, extraReducers: (builder) => { builder.addCase(setActiveTab, (state) => { @@ -129,7 +125,6 @@ export const { moreImagesLoaded, alwaysShowImageSizeBadgeChanged, isImageViewerOpenChanged, - isFloatingImageViewerOpenChanged, } = gallerySlice.actions; const isAnyBoardDeleted = isAnyOf( diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts index 9c258060c9..0e86d2d4be 100644 --- a/invokeai/frontend/web/src/features/gallery/store/types.ts +++ b/invokeai/frontend/web/src/features/gallery/store/types.ts @@ -21,5 +21,4 @@ export type GalleryState = { limit: number; alwaysShowImageSizeBadge: boolean; isImageViewerOpen: boolean; - isFloatingImageViewerOpen: boolean; }; diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 4152f4065b..42df03872c 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -4,7 +4,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { $customNavComponent } from 'app/store/nanostores/customNavComponent'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent'; -import { ToggleFloatingImageViewerButton } from 'features/gallery/components/ImageViewer/FloatingImageViewer'; import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup'; import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent'; @@ -224,7 +223,6 @@ const InvokeTabs = () => { - {customNavComponent ? customNavComponent : } Date: Mon, 6 May 2024 18:38:33 +1000 Subject: [PATCH 008/442] Update invokeai_version.py --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index afcedcd6bb..b6bcb5a14c 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.0b1" +__version__ = "4.2.0b2" From 886f5c90a39997dc75d26afacb52ddde72443fb3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 09:47:21 +1000 Subject: [PATCH 009/442] feat(ui): move img2img strength out of advanced on canvas --- .../ImageSettingsAccordion/ImageSettingsAccordion.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index e9a9263605..853d7f3a78 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -85,7 +85,10 @@ export const ImageSettingsAccordion = memo(() => { onToggle={onToggleAccordion} > - {activeTabName === 'canvas' ? : } + + {activeTabName === 'canvas' ? : } + {activeTabName === 'canvas' && } + @@ -93,7 +96,6 @@ export const ImageSettingsAccordion = memo(() => { - {activeTabName === 'canvas' && } {activeTabName === 'generation' && !isSDXL && } {activeTabName === 'canvas' && ( <> From e8d60e8d8358a6417b5198cd243b0da27111e271 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 09:50:54 +1000 Subject: [PATCH 010/442] fix(ui): image metadata viewer stuck when spamming hotkey --- .../ImageViewer/CurrentImagePreview.tsx | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx index 8e6eccbe73..f40ecfca32 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -103,24 +103,11 @@ const CurrentImagePreview = ({ dataTestId="image-preview" /> )} - - {shouldShowImageDetails && imageDTO && withMetadata && ( - - - - )} - + {shouldShowImageDetails && imageDTO && withMetadata && ( + + + + )} {withNextPrevButtons && shouldShowNextPrevButtons && imageDTO && ( Date: Tue, 7 May 2024 10:10:03 +1000 Subject: [PATCH 011/442] feat(ui): move strength to init image layer This further splits the control layers state into its own thing. --- .../components/IILayer/IILayer.tsx | 10 +++++++- .../controlLayers/store/controlLayersSlice.ts | 13 +++++++++- .../src/features/controlLayers/store/types.ts | 3 ++- .../graph/addInitialImageToLinearGraph.ts | 11 +++++---- .../Canvas/ParamImageToImageStrength.tsx | 20 ++++++++++++++++ .../ImageToImage/ImageToImageStrength.tsx | 24 +++++++++---------- .../ImageSettingsAccordion.tsx | 4 ++-- 7 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx index 772dbd7332..c6efd041ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -8,6 +8,7 @@ import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerT import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { + iiLayerDenoisingStrengthChanged, iiLayerImageChanged, layerSelected, selectIILayerOrThrow, @@ -36,6 +37,13 @@ export const IILayer = memo(({ layerId }: Props) => { [dispatch, layerId] ); + const onChangeDenoisingStrength = useCallback( + (denoisingStrength: number) => { + dispatch(iiLayerDenoisingStrengthChanged({ layerId, denoisingStrength })); + }, + [dispatch, layerId] + ); + const droppableData = useMemo( () => ({ actionType: 'SET_II_LAYER_IMAGE', @@ -67,7 +75,7 @@ export const IILayer = memo(({ layerId }: Props) => { {isOpen && ( - + ) => { + const { layerId, denoisingStrength } = action.payload; + const layer = selectIILayerOrThrow(state, layerId); + layer.denoisingStrength = denoisingStrength; + }, //#endregion //#region Globals @@ -841,6 +847,7 @@ export const { iiLayerAdded, iiLayerImageChanged, iiLayerOpacityChanged, + iiLayerDenoisingStrengthChanged, // Globals positivePromptChanged, negativePromptChanged, @@ -860,6 +867,10 @@ export const selectControlLayersSlice = (state: RootState) => state.controlLayer /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateControlLayersState = (state: any): any => { + if (state._version === 1) { + // Reset state for users on v1 (e.g. beta users), some changes could cause + return deepClone(initialControlLayersState); + } return state; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index afb04aae37..d469506c60 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -79,12 +79,13 @@ export type InitialImageLayer = RenderableLayerBase & { type: 'initial_image_layer'; opacity: number; image: ImageWithDims | null; + denoisingStrength: number; }; export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer | InitialImageLayer; export type ControlLayersState = { - _version: 1; + _version: 2; selectedLayerId: string | null; layers: Layer[]; brushSize: number; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts index eae45acc5b..54cf0ee59e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts @@ -15,13 +15,13 @@ export const addInitialImageToLinearGraph = ( denoiseNodeId: string ): boolean => { // Remove Existing UNet Connections - const { img2imgStrength, vaePrecision, model } = state.generation; + const { vaePrecision, model } = state.generation; const { refinerModel, refinerStart } = state.sdxl; const { width, height } = state.controlLayers.present.size; const initialImageLayer = state.controlLayers.present.layers.find(isInitialImageLayer); const initialImage = initialImageLayer?.isEnabled ? initialImageLayer?.image : null; - if (!initialImage) { + if (!initialImage || !initialImageLayer) { return false; } @@ -31,7 +31,10 @@ export const addInitialImageToLinearGraph = ( const denoiseNode = graph.nodes[denoiseNodeId]; assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`); - denoiseNode.denoising_start = useRefinerStartEnd ? Math.min(refinerStart, 1 - img2imgStrength) : 1 - img2imgStrength; + const { denoisingStrength } = initialImageLayer; + denoiseNode.denoising_start = useRefinerStartEnd + ? Math.min(refinerStart, 1 - denoisingStrength) + : 1 - denoisingStrength; denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1; // We conditionally hook the image in depending on if a resize is needed @@ -122,7 +125,7 @@ export const addInitialImageToLinearGraph = ( upsertMetadata(graph, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img', - strength: img2imgStrength, + strength: denoisingStrength, init_image: initialImage.imageName, }); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx new file mode 100644 index 0000000000..2519f9dad0 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx @@ -0,0 +1,20 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; +import { setImg2imgStrength } from 'features/parameters/store/generationSlice'; +import { memo, useCallback } from 'react'; + +const ParamImageToImageStrength = () => { + const img2imgStrength = useAppSelector((s) => s.generation.img2imgStrength); + const dispatch = useAppDispatch(); + + const onChange = useCallback( + (v: number) => { + dispatch(setImg2imgStrength(v)); + }, + [dispatch] + ); + + return ; +}; + +export default memo(ParamImageToImageStrength); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx index e92831ecd9..c56fef1903 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx @@ -1,14 +1,17 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setImg2imgStrength } from 'features/parameters/store/generationSlice'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const marks = [0, 0.5, 1]; -const ImageToImageStrength = () => { - const img2imgStrength = useAppSelector((s) => s.generation.img2imgStrength); +type Props = { + value: number; + onChange: (v: number) => void; +}; + +const ImageToImageStrength = ({ value, onChange }: Props) => { const initial = useAppSelector((s) => s.config.sd.img2imgStrength.initial); const sliderMin = useAppSelector((s) => s.config.sd.img2imgStrength.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.img2imgStrength.sliderMax); @@ -16,11 +19,8 @@ const ImageToImageStrength = () => { const numberInputMax = useAppSelector((s) => s.config.sd.img2imgStrength.numberInputMax); const coarseStep = useAppSelector((s) => s.config.sd.img2imgStrength.coarseStep); const fineStep = useAppSelector((s) => s.config.sd.img2imgStrength.fineStep); - const dispatch = useAppDispatch(); const { t } = useTranslation(); - const handleChange = useCallback((v: number) => dispatch(setImg2imgStrength(v)), [dispatch]); - return ( @@ -31,8 +31,8 @@ const ImageToImageStrength = () => { fineStep={fineStep} min={sliderMin} max={sliderMax} - onChange={handleChange} - value={img2imgStrength} + onChange={onChange} + value={value} defaultValue={initial} marks={marks} /> @@ -41,8 +41,8 @@ const ImageToImageStrength = () => { fineStep={fineStep} min={numberInputMin} max={numberInputMax} - onChange={handleChange} - value={img2imgStrength} + onChange={onChange} + value={value} defaultValue={initial} /> diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 853d7f3a78..47392cdb8c 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -9,7 +9,7 @@ import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing'; import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight'; import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth'; -import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; +import ParamImageToImageStrength from 'features/parameters/components/Canvas/ParamImageToImageStrength'; import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput'; import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize'; import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; @@ -87,7 +87,7 @@ export const ImageSettingsAccordion = memo(() => { {activeTabName === 'canvas' ? : } - {activeTabName === 'canvas' && } + {activeTabName === 'canvas' && } From a7aa529b99e78aea96709699a03eae431fb023b4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 11:16:06 +1000 Subject: [PATCH 012/442] tidy(ui): "imageName" -> "name" --- .../listeners/imageDeleted.ts | 10 +++--- .../ControlAdapterImagePreview.tsx | 4 +-- .../IPAdapterImagePreview.tsx | 2 +- .../IILayer/InitialImagePreview.tsx | 2 +- .../controlLayers/util/controlAdapters.ts | 32 +++++++++---------- .../features/controlLayers/util/renderers.ts | 8 ++--- .../deleteImageModal/store/selectors.ts | 8 ++--- .../util/graph/addControlLayersToGraph.ts | 22 ++++++------- .../graph/addInitialImageToLinearGraph.ts | 6 ++-- 9 files changed, 47 insertions(+), 47 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index 95d17da653..501f71db70 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -73,25 +73,25 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { state.controlLayers.present.layers.forEach((l) => { if (isRegionalGuidanceLayer(l)) { - if (l.ipAdapters.some((ipa) => ipa.image?.imageName === imageDTO.image_name)) { + if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) { dispatch(layerDeleted(l.id)); } } if (isControlAdapterLayer(l)) { if ( - l.controlAdapter.image?.imageName === imageDTO.image_name || - l.controlAdapter.processedImage?.imageName === imageDTO.image_name + l.controlAdapter.image?.name === imageDTO.image_name || + l.controlAdapter.processedImage?.name === imageDTO.image_name ) { dispatch(layerDeleted(l.id)); } } if (isIPAdapterLayer(l)) { - if (l.ipAdapter.image?.imageName === imageDTO.image_name) { + if (l.ipAdapter.image?.name === imageDTO.image_name) { dispatch(layerDeleted(l.id)); } } if (isInitialImageLayer(l)) { - if (l.image?.imageName === imageDTO.image_name) { + if (l.image?.name === imageDTO.image_name) { dispatch(layerDeleted(l.id)); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx index e6c6aae286..c1da425186 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -42,10 +42,10 @@ export const ControlAdapterImagePreview = memo( const [isMouseOverImage, setIsMouseOverImage] = useState(false); const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - controlAdapter.image?.imageName ?? skipToken + controlAdapter.image?.name ?? skipToken ); const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - controlAdapter.processedImage?.imageName ?? skipToken + controlAdapter.processedImage?.name ?? skipToken ); const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx index 83dd250cd0..e2ea215314 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx @@ -35,7 +35,7 @@ export const IPAdapterImagePreview = memo( const shift = useShiftModifier(); const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - image?.imageName ?? skipToken + image?.name ?? skipToken ); const handleResetControlImage = useCallback(() => { onChangeImage(null); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx index e355d5db86..9053c0c123 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx @@ -32,7 +32,7 @@ export const InitialImagePreview = memo(({ image, onChangeImage, droppableData, const optimalDimension = useAppSelector(selectOptimalDimension); const shift = useShiftModifier(); - const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.imageName ?? skipToken); + const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken); const onReset = useCallback(() => { onChangeImage(null); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 2964a2eb6c..617b527475 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -79,7 +79,7 @@ export type ProcessorConfig = | ZoeDepthProcessorConfig; export type ImageWithDims = { - imageName: string; + name: string; width: number; height: number; }; @@ -190,7 +190,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { buildNode: (image, config) => ({ ...config, type: 'canny_image_processor', - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -207,7 +207,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { buildNode: (image, config) => ({ ...config, type: 'color_map_image_processor', - image: { image_name: image.imageName }, + image: { image_name: image.name }, }), }, content_shuffle_image_processor: { @@ -223,7 +223,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -239,7 +239,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, resolution: minDim(image), }), }, @@ -254,7 +254,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -269,7 +269,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -285,7 +285,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -302,7 +302,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -319,7 +319,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -336,7 +336,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -351,7 +351,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -369,7 +369,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, image_resolution: minDim(image), }), }, @@ -385,7 +385,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -400,7 +400,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, }), }, }; @@ -462,7 +462,7 @@ export const buildControlAdapterProcessorV2 = ( }; export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({ - imageName: image_name, + name: image_name, width, height, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index f58b1e3b74..559d82aa5c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -510,7 +510,7 @@ const updateInitialImageLayerImageSource = async ( reduxLayer: InitialImageLayer ) => { if (reduxLayer.image) { - const { imageName } = reduxLayer.image; + const imageName = reduxLayer.image.name; const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); const imageDTO = await req.unwrap(); req.unsubscribe(); @@ -543,7 +543,7 @@ const renderInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLay let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { const image = reduxLayer.image; - if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) { + if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) { imageSourceNeedsUpdate = true; } else if (!image) { imageSourceNeedsUpdate = true; @@ -585,7 +585,7 @@ const updateControlNetLayerImageSource = async ( ) => { const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image; if (image) { - const { imageName } = image; + const imageName = image.name; const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); const imageDTO = await req.unwrap(); req.unsubscribe(); @@ -653,7 +653,7 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image; - if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) { + if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) { imageSourceNeedsUpdate = true; } else if (!image) { imageSourceNeedsUpdate = true; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index ce989de7b1..7e2605c6cf 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -46,18 +46,18 @@ export const getImageUsage = ( const isControlLayerImage = controlLayers.layers.some((l) => { if (isRegionalGuidanceLayer(l)) { - return l.ipAdapters.some((ipa) => ipa.image?.imageName === image_name); + return l.ipAdapters.some((ipa) => ipa.image?.name === image_name); } if (isControlAdapterLayer(l)) { return ( - l.controlAdapter.image?.imageName === image_name || l.controlAdapter.processedImage?.imageName === image_name + l.controlAdapter.image?.name === image_name || l.controlAdapter.processedImage?.name === image_name ); } if (isIPAdapterLayer(l)) { - return l.ipAdapter.image?.imageName === image_name; + return l.ipAdapter.image?.name === image_name; } if (isInitialImageLayer(l)) { - return l.image?.imageName === image_name; + return l.image?.name === image_name; } return false; }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index 30c15fae10..e1836a3dc6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts @@ -56,12 +56,12 @@ const buildControlImage = ( if (processedImage && processorConfig) { // We've processed the image in the app - use it for the control image. return { - image_name: processedImage.imageName, + image_name: processedImage.name, }; } else if (image) { // No processor selected, and we have an image - the user provided a processed image, use it for the control image. return { - image_name: image.imageName, + image_name: image.name, }; } assert(false, 'Attempted to add unprocessed control image'); @@ -76,7 +76,7 @@ const buildControlNetMetadata = (controlNet: ControlNetConfigV2): S['ControlNetM const processed_image = processedImage && processorConfig ? { - image_name: processedImage.imageName, + image_name: processedImage.name, } : null; @@ -88,7 +88,7 @@ const buildControlNetMetadata = (controlNet: ControlNetConfigV2): S['ControlNetM end_step_percent: beginEndStepPct[1], resize_mode: 'just_resize', image: { - image_name: image.imageName, + image_name: image.name, }, processed_image, }; @@ -169,7 +169,7 @@ const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfigV2): S['T2IAdapterM const processed_image = processedImage && processorConfig ? { - image_name: processedImage.imageName, + image_name: processedImage.name, } : null; @@ -180,7 +180,7 @@ const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfigV2): S['T2IAdapterM end_step_percent: beginEndStepPct[1], resize_mode: 'just_resize', image: { - image_name: image.imageName, + image_name: image.name, }, processed_image, }; @@ -266,7 +266,7 @@ const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfigV2): S['IPAdapterMetad begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: image.imageName, + image_name: image.name, }, }; }; @@ -319,7 +319,7 @@ const addGlobalIPAdaptersToGraph = async ( begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: image.imageName, + image_name: image.name, }, }; @@ -402,7 +402,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab // Only layers with prompts get added to the graph .filter((l) => { const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasIPAdapter = l.ipAdapters.length !== 0; + const hasIPAdapter = l.ipAdapters.filter((ipa) => ipa.image).length > 0; return hasTextPrompt || hasIPAdapter; }); @@ -648,7 +648,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: image.imageName, + image_name: image.name, }, }; @@ -673,7 +673,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { if (layer.uploadedMaskImage) { - const imageDTO = await getImageDTO(layer.uploadedMaskImage.imageName); + const imageDTO = await getImageDTO(layer.uploadedMaskImage.name); if (imageDTO) { return imageDTO; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts index 54cf0ee59e..2460b187a4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts @@ -66,7 +66,7 @@ export const addInitialImageToLinearGraph = ( id: RESIZE, type: 'img_resize', image: { - image_name: initialImage.imageName, + image_name: initialImage.name, }, is_intermediate: true, width, @@ -103,7 +103,7 @@ export const addInitialImageToLinearGraph = ( } else { // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly i2lNode.image = { - image_name: initialImage.imageName, + image_name: initialImage.name, }; // Pass the image's dimensions to the `NOISE` node @@ -126,7 +126,7 @@ export const addInitialImageToLinearGraph = ( upsertMetadata(graph, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img', strength: denoisingStrength, - init_image: initialImage.imageName, + init_image: initialImage.name, }); return true; From 8342f32f2e2a293ff4b839bb7ae66391fb08117b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 15:14:47 +1000 Subject: [PATCH 013/442] refactor(ui): rewrite all types as zod schemas This change prepares for safe metadata recall. --- .../src/features/controlLayers/store/types.ts | 177 +++++++----- .../util/controlAdapters.test.ts | 68 ++++- .../controlLayers/util/controlAdapters.ts | 268 +++++++++++++----- .../parameters/types/parameterSchemas.ts | 8 +- 4 files changed, 361 insertions(+), 160 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index d469506c60..11266c8049 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,88 +1,121 @@ -import type { - ControlNetConfigV2, - ImageWithDims, - IPAdapterConfigV2, - T2IAdapterConfigV2, +import { + zControlNetConfigV2, + zImageWithDims, + zIPAdapterConfigV2, + zT2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { - ParameterAutoNegative, - ParameterHeight, - ParameterNegativePrompt, - ParameterNegativeStylePromptSDXL, - ParameterPositivePrompt, - ParameterPositiveStylePromptSDXL, - ParameterWidth, +import { + type ParameterHeight, + type ParameterNegativePrompt, + type ParameterNegativeStylePromptSDXL, + type ParameterPositivePrompt, + type ParameterPositiveStylePromptSDXL, + type ParameterWidth, + zAutoNegative, + zParameterNegativePrompt, + zParameterPositivePrompt, + zParameterStrength, } from 'features/parameters/types/parameterSchemas'; -import type { IRect } from 'konva/lib/types'; -import type { RgbColor } from 'react-colorful'; +import { z } from 'zod'; -export type DrawingTool = 'brush' | 'eraser'; +export const zTool = z.enum(['brush', 'eraser', 'move', 'rect']); +export type Tool = z.infer; +export const zDrawingTool = zTool.extract(['brush', 'eraser']); +export type DrawingTool = z.infer; -export type Tool = DrawingTool | 'move' | 'rect'; +const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, { + message: 'Must have an even number of points', +}); +export const zVectorMaskLine = z.object({ + id: z.string(), + type: z.literal('vector_mask_line'), + tool: zDrawingTool, + strokeWidth: z.number().min(1), + points: zPoints, +}); +export type VectorMaskLine = z.infer; -export type VectorMaskLine = { - id: string; - type: 'vector_mask_line'; - tool: DrawingTool; - strokeWidth: number; - points: number[]; -}; +export const zVectorMaskRect = z.object({ + id: z.string(), + type: z.literal('vector_mask_rect'), + x: z.number(), + y: z.number(), + width: z.number().min(1), + height: z.number().min(1), +}); +export type VectorMaskRect = z.infer; -export type VectorMaskRect = { - id: string; - type: 'vector_mask_rect'; - x: number; - y: number; - width: number; - height: number; -}; +const zLayerBase = z.object({ + id: z.string(), + isEnabled: z.boolean(), +}); -type LayerBase = { - id: string; - isEnabled: boolean; -}; +const zRect = z.object({ + x: z.number(), + y: z.number(), + width: z.number().min(1), + height: z.number().min(1), +}); +const zRenderableLayerBase = zLayerBase.extend({ + x: z.number(), + y: z.number(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), + isSelected: z.boolean(), +}); -type RenderableLayerBase = LayerBase & { - x: number; - y: number; - bbox: IRect | null; - bboxNeedsUpdate: boolean; - isSelected: boolean; -}; +const zControlAdapterLayer = zRenderableLayerBase.extend({ + type: z.literal('control_adapter_layer'), + opacity: z.number().gte(0).lte(1), + isFilterEnabled: z.boolean(), + controlAdapter: z.discriminatedUnion('type', [zControlNetConfigV2, zT2IAdapterConfigV2]), +}); +export type ControlAdapterLayer = z.infer; -export type ControlAdapterLayer = RenderableLayerBase & { - type: 'control_adapter_layer'; // technically, also t2i adapter layer - opacity: number; - isFilterEnabled: boolean; - controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; -}; +const zIPAdapterLayer = zLayerBase.extend({ + type: z.literal('ip_adapter_layer'), + ipAdapter: zIPAdapterConfigV2, +}); +export type IPAdapterLayer = z.infer; -export type IPAdapterLayer = LayerBase & { - type: 'ip_adapter_layer'; - ipAdapter: IPAdapterConfigV2; -}; +const zRgbColor = z.object({ + r: z.number().int().min(0).max(255), + g: z.number().int().min(0).max(255), + b: z.number().int().min(0).max(255), +}); +const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ + type: z.literal('regional_guidance_layer'), + maskObjects: z.array(z.discriminatedUnion('type', [zVectorMaskLine, zVectorMaskRect])), + positivePrompt: zParameterPositivePrompt.nullable(), + negativePrompt: zParameterNegativePrompt.nullable(), + ipAdapters: z.array(zIPAdapterConfigV2), + previewColor: zRgbColor, + autoNegative: zAutoNegative, + needsPixelBbox: z + .boolean() + .describe( + 'Whether the layer needs the slower pixel-based bbox calculation. Set to true when an there is an eraser object.' + ), + uploadedMaskImage: zImageWithDims.nullable(), +}); +export type RegionalGuidanceLayer = z.infer; -export type RegionalGuidanceLayer = RenderableLayerBase & { - type: 'regional_guidance_layer'; - maskObjects: (VectorMaskLine | VectorMaskRect)[]; - positivePrompt: ParameterPositivePrompt | null; - negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask - ipAdapters: IPAdapterConfigV2[]; // Any number of image prompts - previewColor: RgbColor; - autoNegative: ParameterAutoNegative; - needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object - uploadedMaskImage: ImageWithDims | null; -}; +const zInitialImageLayer = zRenderableLayerBase.extend({ + type: z.literal('initial_image_layer'), + opacity: z.number().gte(0).lte(1), + image: zImageWithDims.nullable(), + denoisingStrength: zParameterStrength, +}); +export type InitialImageLayer = z.infer; -export type InitialImageLayer = RenderableLayerBase & { - type: 'initial_image_layer'; - opacity: number; - image: ImageWithDims | null; - denoisingStrength: number; -}; - -export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer | InitialImageLayer; +export const zLayer = z.discriminatedUnion('type', [ + zRegionalGuidanceLayer, + zControlAdapterLayer, + zIPAdapterLayer, + zInitialImageLayer, +]); +export type Layer = z.infer; export type ControlLayersState = { _version: 2; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts index 880514bf7c..31eb54e730 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts @@ -4,20 +4,74 @@ import { assert } from 'tsafe'; import { describe, test } from 'vitest'; import type { + _CannyProcessorConfig, + _ColorMapProcessorConfig, + _ContentShuffleProcessorConfig, + _DepthAnythingProcessorConfig, + _DWOpenposeProcessorConfig, + _HedProcessorConfig, + _LineartAnimeProcessorConfig, + _LineartProcessorConfig, + _MediapipeFaceProcessorConfig, + _MidasDepthProcessorConfig, + _MlsdProcessorConfig, + _NormalbaeProcessorConfig, + _PidiProcessorConfig, + _ZoeDepthProcessorConfig, + CannyProcessorConfig, CLIPVisionModelV2, + ColorMapProcessorConfig, + ContentShuffleProcessorConfig, ControlModeV2, DepthAnythingModelSize, + DepthAnythingProcessorConfig, + DWOpenposeProcessorConfig, + HedProcessorConfig, IPMethodV2, + LineartAnimeProcessorConfig, + LineartProcessorConfig, + MediapipeFaceProcessorConfig, + MidasDepthProcessorConfig, + MlsdProcessorConfig, + NormalbaeProcessorConfig, + PidiProcessorConfig, ProcessorConfig, ProcessorTypeV2, + ZoeDepthProcessorConfig, } from './controlAdapters'; describe('Control Adapter Types', () => { - test('ProcessorType', () => assert>()); - test('IP Adapter Method', () => assert, IPMethodV2>>()); - test('CLIP Vision Model', () => - assert, CLIPVisionModelV2>>()); - test('Control Mode', () => assert, ControlModeV2>>()); - test('DepthAnything Model Size', () => - assert, DepthAnythingModelSize>>()); + test('ProcessorType', () => { + assert>(); + }); + test('IP Adapter Method', () => { + assert, IPMethodV2>>(); + }); + test('CLIP Vision Model', () => { + assert, CLIPVisionModelV2>>(); + }); + test('Control Mode', () => { + assert, ControlModeV2>>(); + }); + test('DepthAnything Model Size', () => { + assert, DepthAnythingModelSize>>(); + }); + test('Processor Configs', () => { + // The processor configs are manually modeled zod schemas. This test ensures that the inferred types are correct. + // The types prefixed with `_` are types generated from OpenAPI, while the types without the prefix are manually modeled. + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + }); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 617b527475..9e885c56e2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -1,9 +1,5 @@ import { deepClone } from 'common/util/deepClone'; -import type { - ParameterControlNetModel, - ParameterIPAdapterModel, - ParameterT2IAdapterModel, -} from 'features/parameters/types/parameterSchemas'; +import { zModelIdentifierField } from 'features/nodes/types/common'; import { merge, omit } from 'lodash-es'; import type { BaseModelType, @@ -28,90 +24,207 @@ import type { } from 'services/api/types'; import { z } from 'zod'; +const zId = z.string().min(1); + +const zCannyProcessorConfig = z.object({ + id: zId, + type: z.literal('canny_image_processor'), + low_threshold: z.number().int().gte(0).lte(255), + high_threshold: z.number().int().gte(0).lte(255), +}); +export type _CannyProcessorConfig = Required< + Pick +>; +export type CannyProcessorConfig = z.infer; + +const zColorMapProcessorConfig = z.object({ + id: zId, + type: z.literal('color_map_image_processor'), + color_map_tile_size: z.number().int().gte(1), +}); +export type _ColorMapProcessorConfig = Required< + Pick +>; +export type ColorMapProcessorConfig = z.infer; + +const zContentShuffleProcessorConfig = z.object({ + id: zId, + type: z.literal('content_shuffle_image_processor'), + w: z.number().int().gte(0), + h: z.number().int().gte(0), + f: z.number().int().gte(0), +}); +export type _ContentShuffleProcessorConfig = Required< + Pick +>; +export type ContentShuffleProcessorConfig = z.infer; + const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']); export type DepthAnythingModelSize = z.infer; export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize => zDepthAnythingModelSize.safeParse(v).success; - -export type CannyProcessorConfig = Required< - Pick ->; -export type ColorMapProcessorConfig = Required< - Pick ->; -export type ContentShuffleProcessorConfig = Required< - Pick ->; -export type DepthAnythingProcessorConfig = Required< +const zDepthAnythingProcessorConfig = z.object({ + id: zId, + type: z.literal('depth_anything_image_processor'), + model_size: zDepthAnythingModelSize, +}); +export type _DepthAnythingProcessorConfig = Required< Pick >; -export type HedProcessorConfig = Required>; -type LineartAnimeProcessorConfig = Required>; -export type LineartProcessorConfig = Required>; -export type MediapipeFaceProcessorConfig = Required< +export type DepthAnythingProcessorConfig = z.infer; + +const zHedProcessorConfig = z.object({ + id: zId, + type: z.literal('hed_image_processor'), + scribble: z.boolean(), +}); +export type _HedProcessorConfig = Required>; +export type HedProcessorConfig = z.infer; + +const zLineartAnimeProcessorConfig = z.object({ + id: zId, + type: z.literal('lineart_anime_image_processor'), +}); +export type _LineartAnimeProcessorConfig = Required>; +export type LineartAnimeProcessorConfig = z.infer; + +const zLineartProcessorConfig = z.object({ + id: zId, + type: z.literal('lineart_image_processor'), + coarse: z.boolean(), +}); +export type _LineartProcessorConfig = Required>; +export type LineartProcessorConfig = z.infer; + +const zMediapipeFaceProcessorConfig = z.object({ + id: zId, + type: z.literal('mediapipe_face_processor'), + max_faces: z.number().int().gte(1), + min_confidence: z.number().gte(0).lte(1), +}); +export type _MediapipeFaceProcessorConfig = Required< Pick >; -export type MidasDepthProcessorConfig = Required< +export type MediapipeFaceProcessorConfig = z.infer; + +const zMidasDepthProcessorConfig = z.object({ + id: zId, + type: z.literal('midas_depth_image_processor'), + a_mult: z.number().gte(0), + bg_th: z.number().gte(0), +}); +export type _MidasDepthProcessorConfig = Required< Pick >; -export type MlsdProcessorConfig = Required>; -type NormalbaeProcessorConfig = Required>; -export type DWOpenposeProcessorConfig = Required< +export type MidasDepthProcessorConfig = z.infer; + +const zMlsdProcessorConfig = z.object({ + id: zId, + type: z.literal('mlsd_image_processor'), + thr_v: z.number().gte(0), + thr_d: z.number().gte(0), +}); +export type _MlsdProcessorConfig = Required>; +export type MlsdProcessorConfig = z.infer; + +const zNormalbaeProcessorConfig = z.object({ + id: zId, + type: z.literal('normalbae_image_processor'), +}); +export type _NormalbaeProcessorConfig = Required>; +export type NormalbaeProcessorConfig = z.infer; + +const zDWOpenposeProcessorConfig = z.object({ + id: zId, + type: z.literal('dw_openpose_image_processor'), + draw_body: z.boolean(), + draw_face: z.boolean(), + draw_hands: z.boolean(), +}); +export type _DWOpenposeProcessorConfig = Required< Pick >; -export type PidiProcessorConfig = Required>; -type ZoeDepthProcessorConfig = Required>; +export type DWOpenposeProcessorConfig = z.infer; -export type ProcessorConfig = - | CannyProcessorConfig - | ColorMapProcessorConfig - | ContentShuffleProcessorConfig - | DepthAnythingProcessorConfig - | HedProcessorConfig - | LineartAnimeProcessorConfig - | LineartProcessorConfig - | MediapipeFaceProcessorConfig - | MidasDepthProcessorConfig - | MlsdProcessorConfig - | NormalbaeProcessorConfig - | DWOpenposeProcessorConfig - | PidiProcessorConfig - | ZoeDepthProcessorConfig; +const zPidiProcessorConfig = z.object({ + id: zId, + type: z.literal('pidi_image_processor'), + safe: z.boolean(), + scribble: z.boolean(), +}); +export type _PidiProcessorConfig = Required>; +export type PidiProcessorConfig = z.infer; -export type ImageWithDims = { - name: string; - width: number; - height: number; -}; +const zZoeDepthProcessorConfig = z.object({ + id: zId, + type: z.literal('zoe_depth_image_processor'), +}); +export type _ZoeDepthProcessorConfig = Required>; +export type ZoeDepthProcessorConfig = z.infer; -type ControlAdapterBase = { - id: string; - weight: number; - image: ImageWithDims | null; - processedImage: ImageWithDims | null; - isProcessingImage: boolean; - processorConfig: ProcessorConfig | null; - beginEndStepPct: [number, number]; -}; +export const zProcessorConfig = z.discriminatedUnion('type', [ + zCannyProcessorConfig, + zColorMapProcessorConfig, + zContentShuffleProcessorConfig, + zDepthAnythingProcessorConfig, + zHedProcessorConfig, + zLineartAnimeProcessorConfig, + zLineartProcessorConfig, + zMediapipeFaceProcessorConfig, + zMidasDepthProcessorConfig, + zMlsdProcessorConfig, + zNormalbaeProcessorConfig, + zDWOpenposeProcessorConfig, + zPidiProcessorConfig, + zZoeDepthProcessorConfig, +]); +export type ProcessorConfig = z.infer; + +export const zImageWithDims = z.object({ + name: z.string(), + width: z.number().int().positive(), + height: z.number().int().positive(), +}); +export type ImageWithDims = z.infer; + +const zBeginEndStepPct = z + .tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)]) + .refine(([begin, end]) => begin < end, { + message: 'Begin must be less than end', + }); + +const zControlAdapterBase = z.object({ + id: zId, + weight: z.number().gte(0).lte(0), + image: zImageWithDims.nullable(), + processedImage: zImageWithDims.nullable(), + isProcessingImage: z.boolean(), + processorConfig: zProcessorConfig.nullable(), + beginEndStepPct: zBeginEndStepPct, +}); const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); export type ControlModeV2 = z.infer; export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success; -export type ControlNetConfigV2 = ControlAdapterBase & { - type: 'controlnet'; - model: ParameterControlNetModel | null; - controlMode: ControlModeV2; -}; -export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 => - ca.type === 'controlnet'; +export const zControlNetConfigV2 = zControlAdapterBase.extend({ + type: z.literal('controlnet'), + model: zModelIdentifierField.nullable(), + controlMode: zControlModeV2, +}); +export type ControlNetConfigV2 = z.infer; + +export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 => + zControlNetConfigV2.safeParse(ca).success; + +export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({ + type: z.literal('t2i_adapter'), + model: zModelIdentifierField.nullable(), +}); +export type T2IAdapterConfigV2 = z.infer; -export type T2IAdapterConfigV2 = ControlAdapterBase & { - type: 't2i_adapter'; - model: ParameterT2IAdapterModel | null; -}; export const isT2IAdapterConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is T2IAdapterConfigV2 => - ca.type === 't2i_adapter'; + zT2IAdapterConfigV2.safeParse(ca).success; const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); export type CLIPVisionModelV2 = z.infer; @@ -121,16 +234,17 @@ const zIPMethodV2 = z.enum(['full', 'style', 'composition']); export type IPMethodV2 = z.infer; export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success; -export type IPAdapterConfigV2 = { - id: string; - type: 'ip_adapter'; - weight: number; - method: IPMethodV2; - image: ImageWithDims | null; - model: ParameterIPAdapterModel | null; - clipVisionModel: CLIPVisionModelV2; - beginEndStepPct: [number, number]; -}; +export const zIPAdapterConfigV2 = z.object({ + id: zId, + type: z.literal('ip_adapter'), + weight: z.number().gte(0).lte(0), + method: zIPMethodV2, + image: zImageWithDims.nullable(), + model: zModelIdentifierField.nullable(), + clipVisionModel: zCLIPVisionModelV2, + beginEndStepPct: zBeginEndStepPct, +}); +export type IPAdapterConfigV2 = z.infer; const zProcessorTypeV2 = z.enum([ 'canny_image_processor', diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts index a18cc7f86d..8a808ed0c5 100644 --- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts +++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts @@ -16,14 +16,14 @@ import { z } from 'zod'; */ // #region Positive prompt -const zParameterPositivePrompt = z.string(); +export const zParameterPositivePrompt = z.string(); export type ParameterPositivePrompt = z.infer; export const isParameterPositivePrompt = (val: unknown): val is ParameterPositivePrompt => zParameterPositivePrompt.safeParse(val).success; // #endregion // #region Negative prompt -const zParameterNegativePrompt = z.string(); +export const zParameterNegativePrompt = z.string(); export type ParameterNegativePrompt = z.infer; export const isParameterNegativePrompt = (val: unknown): val is ParameterNegativePrompt => zParameterNegativePrompt.safeParse(val).success; @@ -127,7 +127,7 @@ export type ParameterT2IAdapterModel = z.infer // #endregion // #region Strength (l2l strength) -const zParameterStrength = z.number().min(0).max(1); +export const zParameterStrength = z.number().min(0).max(1); export type ParameterStrength = z.infer; export const isParameterStrength = (val: unknown): val is ParameterStrength => zParameterStrength.safeParse(val).success; @@ -198,6 +198,6 @@ export const isParameterLoRAWeight = (val: unknown): val is ParameterLoRAWeight // #endregion // #region Regional Prompts AutoNegative -const zAutoNegative = z.enum(['off', 'invert']); +export const zAutoNegative = z.enum(['off', 'invert']); export type ParameterAutoNegative = z.infer; // #endregion From e840de27ed069641bf191426648e3010ecfc09a4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 16:45:07 +1000 Subject: [PATCH 014/442] feat(ui): extend zod with a `is` typeguard` method Feels dangerous, but it's very handy. --- invokeai/frontend/web/src/extend-zod.ts | 8 ++++++++ invokeai/frontend/web/src/main.tsx | 2 ++ invokeai/frontend/web/src/zod-extensions.d.ts | 8 ++++++++ 3 files changed, 18 insertions(+) create mode 100644 invokeai/frontend/web/src/extend-zod.ts create mode 100644 invokeai/frontend/web/src/zod-extensions.d.ts diff --git a/invokeai/frontend/web/src/extend-zod.ts b/invokeai/frontend/web/src/extend-zod.ts new file mode 100644 index 0000000000..b1c155062d --- /dev/null +++ b/invokeai/frontend/web/src/extend-zod.ts @@ -0,0 +1,8 @@ +import { assert } from 'tsafe'; +import { z } from 'zod'; + +assert(!Object.hasOwn(z.ZodType.prototype, 'is')); + +z.ZodType.prototype.is = function (val: unknown): val is z.infer { + return this.safeParse(val).success; +}; diff --git a/invokeai/frontend/web/src/main.tsx b/invokeai/frontend/web/src/main.tsx index acf9491778..129d1bc9e5 100644 --- a/invokeai/frontend/web/src/main.tsx +++ b/invokeai/frontend/web/src/main.tsx @@ -1,3 +1,5 @@ +import 'extend-zod'; + import ReactDOM from 'react-dom/client'; import InvokeAIUI from './app/components/InvokeAIUI'; diff --git a/invokeai/frontend/web/src/zod-extensions.d.ts b/invokeai/frontend/web/src/zod-extensions.d.ts new file mode 100644 index 0000000000..0abab07a19 --- /dev/null +++ b/invokeai/frontend/web/src/zod-extensions.d.ts @@ -0,0 +1,8 @@ +import 'zod'; + +declare module 'zod' { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + export interface ZodType { + is(val: unknown): val is Output; + } +} From e47629cbe7aa5668a5cf86d2cc55bfc1d9e297c9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 16:49:10 +1000 Subject: [PATCH 015/442] feat(ui): add zod schema for layers array --- invokeai/frontend/web/src/features/controlLayers/store/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 11266c8049..1a3f94debc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -116,6 +116,7 @@ export const zLayer = z.discriminatedUnion('type', [ zInitialImageLayer, ]); export type Layer = z.infer; +export const zLayers = z.array(zLayer); export type ControlLayersState = { _version: 2; From 6e8b7f9421d2ebbf8a0a029ee56ec1b5928ce529 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 17:04:53 +1000 Subject: [PATCH 016/442] feat(ui): write layers to metadata --- .../util/graph/addControlLayersToGraph.ts | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index e1836a3dc6..9aa82fdb92 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts @@ -6,7 +6,7 @@ import { isRegionalGuidanceLayer, rgLayerMaskImageUploaded, } from 'features/controlLayers/store/controlLayersSlice'; -import type { RegionalGuidanceLayer } from 'features/controlLayers/store/types'; +import type { Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { type ControlNetConfigV2, type ImageWithDims, @@ -344,57 +344,45 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab assert(mainModel, 'Missing main model when building graph'); const isSDXL = mainModel.base === 'sdxl'; + const layersMetadata: Layer[] = []; + // Add global control adapters - const globalControlNets = state.controlLayers.present.layers - // Must be a CA layer + const validControlAdapterLayers = state.controlLayers.present.layers + // Must be a Control Adapter layer .filter(isControlAdapterLayer) // Must be enabled .filter((l) => l.isEnabled) - // We want the CAs themselves - .map((l) => l.controlAdapter) - // Must be a ControlNet - .filter(isControlNetConfigV2) - .filter((ca) => { + .filter((l) => { + const ca = l.controlAdapter; + // Must be have a model that matches the current base and must have a control image const hasModel = Boolean(ca.model); const modelMatchesBase = ca.model?.base === mainModel.base; const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig); return hasModel && modelMatchesBase && hasControlImage; }); - addGlobalControlNetsToGraph(globalControlNets, graph, denoiseNodeId); + const validControlNets = validControlAdapterLayers.map((l) => l.controlAdapter).filter(isControlNetConfigV2); + addGlobalControlNetsToGraph(validControlNets, graph, denoiseNodeId); - const globalT2IAdapters = state.controlLayers.present.layers - // Must be a CA layer - .filter(isControlAdapterLayer) - // Must be enabled - .filter((l) => l.isEnabled) - // We want the CAs themselves - .map((l) => l.controlAdapter) - // Must have a ControlNet CA - .filter(isT2IAdapterConfigV2) - .filter((ca) => { - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === mainModel.base; - const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig); - return hasModel && modelMatchesBase && hasControlImage; - }); - addGlobalT2IAdaptersToGraph(globalT2IAdapters, graph, denoiseNodeId); + const validT2IAdapters = validControlAdapterLayers.map((l) => l.controlAdapter).filter(isT2IAdapterConfigV2); + addGlobalT2IAdaptersToGraph(validT2IAdapters, graph, denoiseNodeId); - const globalIPAdapters = state.controlLayers.present.layers + const validIPAdapterLayers = state.controlLayers.present.layers // Must be an IP Adapter layer .filter(isIPAdapterLayer) // Must be enabled .filter((l) => l.isEnabled) // We want the IP Adapters themselves - .map((l) => l.ipAdapter) - .filter((ca) => { - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === mainModel.base; - const hasControlImage = Boolean(ca.image); - return hasModel && modelMatchesBase && hasControlImage; + .filter((l) => { + const ipa = l.ipAdapter; + const hasModel = Boolean(ipa.model); + const modelMatchesBase = ipa.model?.base === mainModel.base; + const hasImage = Boolean(ipa.image); + return hasModel && modelMatchesBase && hasImage; }); - addGlobalIPAdaptersToGraph(globalIPAdapters, graph, denoiseNodeId); + const validIPAdapters = validIPAdapterLayers.map((l) => l.ipAdapter); + addGlobalIPAdaptersToGraph(validIPAdapters, graph, denoiseNodeId); - const rgLayers = state.controlLayers.present.layers + const validRGLayers = state.controlLayers.present.layers // Only RG layers are get masks .filter(isRegionalGuidanceLayer) // Only visible layers are rendered on the canvas @@ -406,6 +394,8 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab return hasTextPrompt || hasIPAdapter; }); + layersMetadata.push(...validRGLayers, ...validControlAdapterLayers, ...validIPAdapterLayers); + // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing // the existing conditioning nodes. @@ -468,11 +458,11 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab }, }); - const layerIds = rgLayers.map((l) => l.id); + const layerIds = validRGLayers.map((l) => l.id); const blobs = await getRegionalPromptLayerBlobs(layerIds); assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); - for (const layer of rgLayers) { + for (const layer of validRGLayers) { const blob = blobs[layer.id]; assert(blob, `Blob for layer ${layer.id} not found`); // Upload the mask image, or get the cached image if it exists @@ -669,6 +659,8 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab }); } } + + upsertMetadata(graph, { layers: layersMetadata }); }; const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { From bfad814862c770c6a129a6bf28d9eee57f2cb4ef Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 17:37:45 +1000 Subject: [PATCH 017/442] fix(ui): fix IPAdapterConfigV2 schema weight --- .../web/src/features/controlLayers/util/controlAdapters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 9e885c56e2..0fbcaa6c2b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -237,7 +237,7 @@ export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safePar export const zIPAdapterConfigV2 = z.object({ id: zId, type: z.literal('ip_adapter'), - weight: z.number().gte(0).lte(0), + weight: z.number().gte(0).lte(1), method: zIPMethodV2, image: zImageWithDims.nullable(), model: zModelIdentifierField.nullable(), From ccd399e277e6caf4a24d26af2a6f2c9294c0e8ce Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 17:44:50 +1000 Subject: [PATCH 018/442] feat(ui): add `getIsVisible` to metadata handlers --- .../metadata/components/MetadataItem.tsx | 5 +++++ .../frontend/web/src/features/metadata/types.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx index 66d101f458..7489b05158 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx @@ -3,6 +3,7 @@ import { MetadataItemView } from 'features/metadata/components/MetadataItemView' import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem'; import type { MetadataHandlers } from 'features/metadata/types'; import { MetadataParseFailedToken } from 'features/metadata/util/parsers'; +import { isSymbol } from 'lodash-es'; type MetadataItemProps = { metadata: unknown; @@ -17,6 +18,10 @@ const _MetadataItem = typedMemo(({ metadata, handlers, direction = 'row' }: return null; } + if (handlers.getIsVisible && !isSymbol(value) && !handlers.getIsVisible(value)) { + return null; + } + return ( = (metadata: unknown) => Promise; */ export type MetadataValidateFunc = (value: T) => Promise; +/** + * A function that determines whether a metadata item should be visible. + * + * @param value The value to check. + * @returns True if the item should be visible, false otherwise. + */ +export type MetadataGetIsVisibleFunc = (value: T) => boolean; + export type MetadataHandlers = { /** * Gets the label of the current metadata item as a string. @@ -111,6 +119,14 @@ export type MetadataHandlers = { * @returns The rendered item. */ renderItemValue?: MetadataRenderValueFunc; + /** + * Checks if a parsed metadata value should be visible. + * If not provided, the item is always visible. + * + * @param value The value to check. + * @returns True if the item should be visible, false otherwise. + */ + getIsVisible?: MetadataGetIsVisibleFunc; }; // TODO(psyche): The types for item handlers should be able to be inferred from the type of the value: @@ -127,6 +143,7 @@ type BuildMetadataHandlersArg = { getLabel: MetadataGetLabelFunc; renderValue?: MetadataRenderValueFunc; renderItemValue?: MetadataRenderValueFunc; + getIsVisible?: MetadataGetIsVisibleFunc; }; export type BuildMetadataHandlers = ( From e537de2f6d39f9c33dc289167a8e882ac8628d26 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 17:47:09 +1000 Subject: [PATCH 019/442] feat(ui): layers recall This still needs some finessing - needs logic depending on the tab... --- invokeai/frontend/web/public/locales/en.json | 4 ++- .../controlLayers/store/controlLayersSlice.ts | 14 +++++++++ .../ImageMetadataActions.tsx | 1 + .../src/features/metadata/util/handlers.ts | 31 +++++++++++++++++-- .../web/src/features/metadata/util/parsers.ts | 16 ++++++++++ .../src/features/metadata/util/recallers.ts | 24 ++++++++++++++ .../src/features/metadata/util/validators.ts | 25 +++++++++++++++ 7 files changed, 112 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 83e80e8a81..0c7c6cd6e1 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1559,7 +1559,9 @@ "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", "resetProcessor": "Reset Processor to Defaults", - "noLayersAdded": "No Layers Added" + "noLayersAdded": "No Layers Added", + "layers_one": "Layer", + "layers_other": "Layers" }, "ui": { "tabs": { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index bbe0464aa7..6f6176c242 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -255,6 +255,10 @@ export const controlLayersSlice = createSlice({ payload: { layerId: uuidv4(), controlAdapter }, }), }, + caLayerRecalled: (state, action: PayloadAction) => { + state.layers.push({ ...action.payload, isSelected: true }); + state.selectedLayerId = action.payload.id; + }, caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectCALayerOrThrow(state, layerId); @@ -368,6 +372,9 @@ export const controlLayersSlice = createSlice({ }, prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }), }, + ipaLayerRecalled: (state, action: PayloadAction) => { + state.layers.push(action.payload); + }, ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); @@ -462,6 +469,10 @@ export const controlLayersSlice = createSlice({ }, prepare: () => ({ payload: { layerId: uuidv4() } }), }, + rgLayerRecalled: (state, action: PayloadAction) => { + state.layers.push({ ...action.payload, isSelected: true }); + state.selectedLayerId = action.payload.id; + }, rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; const layer = selectRGLayerOrThrow(state, layerId); @@ -805,6 +816,7 @@ export const { allLayersDeleted, // CA Layers caLayerAdded, + caLayerRecalled, caLayerImageChanged, caLayerProcessedImageChanged, caLayerModelChanged, @@ -817,6 +829,7 @@ export const { caLayerT2IAdaptersDeleted, // IPA Layers ipaLayerAdded, + ipaLayerRecalled, ipaLayerImageChanged, ipaLayerMethodChanged, ipaLayerModelChanged, @@ -827,6 +840,7 @@ export const { caOrIPALayerBeginEndStepPctChanged, // RG Layers rgLayerAdded, + rgLayerRecalled, rgLayerPositivePromptChanged, rgLayerNegativePromptChanged, rgLayerPreviewColorChanged, diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index c73f5b1817..7dd2be55b0 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -51,6 +51,7 @@ const ImageMetadataActions = (props: Props) => { + {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 467f702cea..4cbe69668f 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -1,5 +1,6 @@ import { objectKeys } from 'common/util/objectKeys'; import { toast } from 'common/util/toast'; +import type { Layer } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { AnyControlAdapterConfigMetadata, @@ -52,6 +53,9 @@ const renderControlAdapterValueV2: MetadataRenderValueFunc = async (value) => { + return `${value.length} ${t('controlLayers.layers', { count: value.length })}`; +}; const parameterSetToast = (parameter: string, description?: string) => { toast({ @@ -171,6 +175,7 @@ const buildHandlers: BuildMetadataHandlers = ({ itemValidator, renderValue, renderItemValue, + getIsVisible, }) => ({ parse: buildParse({ parser, getLabel }), parseItem: itemParser ? buildParseItem({ itemParser, getLabel }) : undefined, @@ -179,6 +184,7 @@ const buildHandlers: BuildMetadataHandlers = ({ getLabel, renderValue: renderValue ?? resolveToString, renderItemValue: renderItemValue ?? resolveToString, + getIsVisible, }); export const handlers = { @@ -380,6 +386,14 @@ export const handlers = { itemValidator: validators.t2iAdapterV2, renderItemValue: renderControlAdapterValueV2, }), + layers: buildHandlers({ + getLabel: () => t('controlLayers.layers_other'), + parser: parsers.layers, + recaller: recallers.layers, + validator: validators.layers, + renderValue: renderLayersValue, + getIsVisible: (value) => value.length > 0, + }), } as const; export const parseAndRecallPrompts = async (metadata: unknown) => { @@ -435,9 +449,22 @@ export const parseAndRecallImageDimensions = async (metadata: unknown) => { }; // These handlers should be omitted when recalling to control layers -const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters']; +const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = [ + 'controlNets', + 'ipAdapters', + 't2iAdapters', + 'controlNetsV2', + 'ipAdaptersV2', + 't2iAdaptersV2', +]; // These handlers should be omitted when recalling to the rest of the app -const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNetsV2', 'ipAdaptersV2', 't2iAdaptersV2']; +const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = [ + 'controlNetsV2', + 'ipAdaptersV2', + 't2iAdaptersV2', + 'initialImage', + 'layers', +]; export const parseAndRecallAllMetadata = async ( metadata: unknown, diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 8641977b1f..25ab72536a 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -5,6 +5,8 @@ import { initialT2IAdapter, } from 'features/controlAdapters/util/buildControlAdapter'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; +import type { Layer } from 'features/controlLayers/store/types'; +import { zLayer } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA, imageDTOToImageWithDims, @@ -623,6 +625,19 @@ const parseIPAdapterV2: MetadataParseFunc = async (me return ipAdapter; }; +const parseLayers: MetadataParseFunc = async (metadata) => { + try { + const layersRaw = await getProperty(metadata, 'layers', isArray); + const parseResults = await Promise.allSettled(layersRaw.map((layerRaw) => zLayer.parseAsync(layerRaw))); + const layers = parseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + return layers; + } catch { + return []; + } +}; + const parseAllIPAdaptersV2: MetadataParseFunc = async (metadata) => { try { const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray); @@ -678,4 +693,5 @@ export const parsers = { t2iAdaptersV2: parseAllT2IAdaptersV2, ipAdapterV2: parseIPAdapterV2, ipAdaptersV2: parseAllIPAdaptersV2, + layers: parseLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index b29d937159..3782c789e0 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -6,19 +6,24 @@ import { t2iAdaptersReset, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { + allLayersDeleted, caLayerAdded, caLayerControlNetsDeleted, + caLayerRecalled, caLayerT2IAdaptersDeleted, heightChanged, iiLayerAdded, ipaLayerAdded, + ipaLayerRecalled, ipaLayersDeleted, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, positivePromptChanged, + rgLayerRecalled, widthChanged, } from 'features/controlLayers/store/controlLayersSlice'; +import type { Layer } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { LoRA } from 'features/lora/store/loraSlice'; import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice'; @@ -290,6 +295,24 @@ const recallIPAdaptersV2: MetadataRecallFunc = (ipA }); }; +const recallLayers: MetadataRecallFunc = (layers) => { + const { dispatch } = getStore(); + dispatch(allLayersDeleted()); + for (const l of layers) { + switch (l.type) { + case 'control_adapter_layer': + dispatch(caLayerRecalled(l)); + break; + case 'ip_adapter_layer': + dispatch(ipaLayerRecalled(l)); + break; + case 'regional_guidance_layer': + dispatch(rgLayerRecalled(l)); + break; + } + } +}; + export const recallers = { positivePrompt: recallPositivePrompt, negativePrompt: recallNegativePrompt, @@ -330,4 +353,5 @@ export const recallers = { t2iAdaptersV2: recallT2IAdaptersV2, ipAdapterV2: recallIPAdapterV2, ipAdaptersV2: recallIPAdaptersV2, + layers: recallLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index d09321003f..aca988f85a 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,4 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; +import type { Layer } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, @@ -165,6 +166,29 @@ const validateIPAdaptersV2: MetadataValidateFunc = return new Promise((resolve) => resolve(validatedIPAdapters)); }; +const validateLayers: MetadataValidateFunc = (layers) => { + const validatedLayers: Layer[] = []; + for (const l of layers) { + try { + if (l.type === 'control_adapter_layer') { + validateBaseCompatibility(l.controlAdapter.model?.base, 'Layer incompatible with currently-selected model'); + } + if (l.type === 'ip_adapter_layer') { + validateBaseCompatibility(l.ipAdapter.model?.base, 'Layer incompatible with currently-selected model'); + } + if (l.type === 'regional_guidance_layer') { + for (const ipa of l.ipAdapters) { + validateBaseCompatibility(ipa.model?.base, 'Layer incompatible with currently-selected model'); + } + } + validatedLayers.push(l); + } catch { + // This is a no-op - we want to continue validating the rest of the layers, and an empty list is valid. + } + } + return new Promise((resolve) => resolve(validatedLayers)); +}; + export const validators = { refinerModel: validateRefinerModel, vaeModel: validateVAEModel, @@ -182,4 +206,5 @@ export const validators = { t2iAdaptersV2: validateT2IAdaptersV2, ipAdapterV2: validateIPAdapterV2, ipAdaptersV2: validateIPAdaptersV2, + layers: validateLayers, } as const; From b43b2714cc0a48989538ab732da52d391d5d629a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 18:11:20 +1000 Subject: [PATCH 020/442] feat(ui): add `fracturedjsonjs` to pretty-serialize objects In use on the metadata viewer - makes it sooo much easier on the eyes. --- invokeai/frontend/web/package.json | 1 + invokeai/frontend/web/pnpm-lock.yaml | 7 +++++++ .../gallery/components/ImageMetadataViewer/DataViewer.tsx | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 78e8ca44ca..8f6f2c6038 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -65,6 +65,7 @@ "chakra-react-select": "^4.7.6", "compare-versions": "^6.1.0", "dateformat": "^5.0.3", + "fracturedjsonjs": "^4.0.1", "framer-motion": "^11.1.8", "i18next": "^23.11.3", "i18next-http-backend": "^2.5.1", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 2703477200..b5de9e6426 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -50,6 +50,9 @@ dependencies: dateformat: specifier: ^5.0.3 version: 5.0.3 + fracturedjsonjs: + specifier: ^4.0.1 + version: 4.0.1 framer-motion: specifier: ^11.1.8 version: 11.1.8(react-dom@18.3.1)(react@18.3.1) @@ -8691,6 +8694,10 @@ packages: engines: {node: '>= 0.6'} dev: true + /fracturedjsonjs@4.0.1: + resolution: {integrity: sha512-KMhSx7o45aPVj4w27dwdQyKJkNU8oBqw8UiK/s3VzsQB3+pKQ/3AqG/YOEQblV2BDuYE5dKp0OMf8RDsshrjTA==} + dev: false + /framer-motion@10.18.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==} peerDependencies: diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx index a6d0481b89..2cbf93b899 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx @@ -1,5 +1,6 @@ import { Box, Flex, IconButton, Tooltip, useShiftModifier } from '@invoke-ai/ui-library'; import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; +import { Formatter } from 'fracturedjsonjs'; import { isString } from 'lodash-es'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import type { CSSProperties } from 'react'; @@ -7,6 +8,8 @@ import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCopyBold, PiDownloadSimpleBold } from 'react-icons/pi'; +const formatter = new Formatter(); + type Props = { label: string; data: unknown; @@ -20,7 +23,7 @@ const overlayscrollbarsOptions = getOverlayScrollbarsParams('scroll', 'scroll'). const DataViewer = (props: Props) => { const { label, data, fileName, withDownload = true, withCopy = true, extraCopyActions } = props; - const dataString = useMemo(() => (isString(data) ? data : JSON.stringify(data, null, 2)), [data]); + const dataString = useMemo(() => (isString(data) ? data : formatter.Serialize(data)) ?? '', [data]); const shift = useShiftModifier(); const handleCopy = useCallback(() => { navigator.clipboard.writeText(dataString); From dfbd7eb1cfc8bd02eef63d6f3a0924513480f8bb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 09:58:34 +1000 Subject: [PATCH 021/442] feat(ui): individual layer recall --- .../controlLayers/store/controlLayersSlice.ts | 15 ++++ .../ImageMetadataActions.tsx | 3 +- .../metadata/components/MetadataLayers.tsx | 68 +++++++++++++++++++ .../src/features/metadata/util/handlers.ts | 24 ++++++- .../metadata/util/modelFetchingHelpers.ts | 19 ++++++ .../web/src/features/metadata/util/parsers.ts | 29 ++++---- .../src/features/metadata/util/recallers.ts | 32 ++++++--- .../src/features/metadata/util/validators.ts | 41 +++++++---- 8 files changed, 192 insertions(+), 39 deletions(-) create mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 6f6176c242..bc9f133075 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -124,6 +124,12 @@ const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => { const lastColor = rgLayers[rgLayers.length - 1]?.previewColor; return LayerColors.next(lastColor); }; +const deselectAllLayers = (state: ControlLayersState) => { + for (const layer of state.layers.filter(isRenderableLayer)) { + layer.isSelected = false; + } + state.selectedLayerId = null; +}; export const controlLayersSlice = createSlice({ name: 'controlLayers', @@ -256,6 +262,7 @@ export const controlLayersSlice = createSlice({ }), }, caLayerRecalled: (state, action: PayloadAction) => { + deselectAllLayers(state); state.layers.push({ ...action.payload, isSelected: true }); state.selectedLayerId = action.payload.id; }, @@ -470,6 +477,7 @@ export const controlLayersSlice = createSlice({ prepare: () => ({ payload: { layerId: uuidv4() } }), }, rgLayerRecalled: (state, action: PayloadAction) => { + deselectAllLayers(state); state.layers.push({ ...action.payload, isSelected: true }); state.selectedLayerId = action.payload.id; }, @@ -665,6 +673,12 @@ export const controlLayersSlice = createSlice({ }, prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: 'initial_image_layer', imageDTO } }), }, + iiLayerRecalled: (state, action: PayloadAction) => { + deselectAllLayers(state); + state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); + state.layers.push({ ...action.payload, isSelected: true }); + state.selectedLayerId = action.payload.id; + }, iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectIILayerOrThrow(state, layerId); @@ -859,6 +873,7 @@ export const { rgLayerIPAdapterCLIPVisionModelChanged, // II Layer iiLayerAdded, + iiLayerRecalled, iiLayerImageChanged, iiLayerOpacityChanged, iiLayerDenoisingStrengthChanged, diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index 7dd2be55b0..04e8fd2eca 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -4,6 +4,7 @@ import { MetadataControlNetsV2 } from 'features/metadata/components/MetadataCont import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters'; import { MetadataIPAdaptersV2 } from 'features/metadata/components/MetadataIPAdaptersV2'; import { MetadataItem } from 'features/metadata/components/MetadataItem'; +import { MetadataLayers } from 'features/metadata/components/MetadataLayers'; import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs'; import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters'; import { MetadataT2IAdaptersV2 } from 'features/metadata/components/MetadataT2IAdaptersV2'; @@ -51,8 +52,8 @@ const ImageMetadataActions = (props: Props) => { - + {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx new file mode 100644 index 0000000000..ab4ce03987 --- /dev/null +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx @@ -0,0 +1,68 @@ +import type { Layer } from 'features/controlLayers/store/types'; +import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; +import type { MetadataHandlers } from 'features/metadata/types'; +import { handlers } from 'features/metadata/util/handlers'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +type Props = { + metadata: unknown; +}; + +export const MetadataLayers = ({ metadata }: Props) => { + const [layers, setLayers] = useState([]); + + useEffect(() => { + const parse = async () => { + try { + const parsed = await handlers.layers.parse(metadata); + setLayers(parsed); + } catch (e) { + setLayers([]); + } + }; + parse(); + }, [metadata]); + + const label = useMemo(() => handlers.layers.getLabel(), []); + + return ( + <> + {layers.map((layer) => ( + + ))} + + ); +}; + +const MetadataViewLayer = ({ + label, + layer, + handlers, +}: { + label: string; + layer: Layer; + handlers: MetadataHandlers; +}) => { + const onRecall = useCallback(() => { + if (!handlers.recallItem) { + return; + } + handlers.recallItem(layer, true); + }, [handlers, layer]); + + const [renderedValue, setRenderedValue] = useState(null); + useEffect(() => { + const _renderValue = async () => { + if (!handlers.renderItemValue) { + setRenderedValue(null); + return; + } + const rendered = await handlers.renderItemValue(layer); + setRenderedValue(rendered); + }; + + _renderValue(); + }, [handlers, layer]); + + return ; +}; diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 4cbe69668f..a2ba3dfc22 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -17,6 +17,7 @@ import { fetchModelConfig } from 'features/metadata/util/modelFetchingHelpers'; import { validators } from 'features/metadata/util/validators'; import type { ModelIdentifierField } from 'features/nodes/types/common'; import { t } from 'i18next'; +import { assert } from 'tsafe'; import { parsers } from './parsers'; import { recallers } from './recallers'; @@ -53,8 +54,23 @@ const renderControlAdapterValueV2: MetadataRenderValueFunc = async (value) => { - return `${value.length} ${t('controlLayers.layers', { count: value.length })}`; +const renderLayerValue: MetadataRenderValueFunc = async (layer) => { + if (layer.type === 'initial_image_layer') { + return t('controlLayers.initialImageLayer'); + } + if (layer.type === 'control_adapter_layer') { + return t('controlLayers.controlAdapterLayer'); + } + if (layer.type === 'ip_adapter_layer') { + return t('controlLayers.ipAdapterLayer'); + } + if (layer.type === 'regional_guidance_layer') { + return t('controlLayers.regionalGuidanceLayer'); + } + assert(false, 'Unknown layer type'); +}; +const renderLayersValue: MetadataRenderValueFunc = async (layers) => { + return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; }; const parameterSetToast = (parameter: string, description?: string) => { @@ -389,8 +405,12 @@ export const handlers = { layers: buildHandlers({ getLabel: () => t('controlLayers.layers_other'), parser: parsers.layers, + itemParser: parsers.layer, recaller: recallers.layers, + itemRecaller: recallers.layer, validator: validators.layers, + itemValidator: validators.layer, + renderItemValue: renderLayerValue, renderValue: renderLayersValue, getIsVisible: (value) => value.length > 0, }), diff --git a/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts b/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts index a237582ed8..a2db414937 100644 --- a/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts @@ -1,4 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; +import type { ModelIdentifierField } from 'features/nodes/types/common'; import { isModelIdentifier, isModelIdentifierV2 } from 'features/nodes/types/common'; import { modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig, BaseModelType, ModelType } from 'services/api/types'; @@ -68,6 +69,24 @@ const fetchModelConfigByAttrs = async (name: string, base: BaseModelType, type: } }; +/** + * Fetches the model config given an identifier. First attempts to fetch by key, then falls back to fetching by attrs. + * @param identifier The model identifier. + * @returns A promise that resolves to the model config. + * @throws {ModelConfigNotFoundError} If the model config is unable to be fetched. + */ +export const fetchModelConfigByIdentifier = async (identifier: ModelIdentifierField): Promise => { + try { + return await fetchModelConfig(identifier.key); + } catch { + try { + return await fetchModelConfigByAttrs(identifier.name, identifier.base, identifier.type); + } catch { + throw new ModelConfigNotFoundError(`Unable to retrieve model config for identifier ${identifier}`); + } + } +}; + /** * Fetches the model config for a given model key and type, and ensures that the model config is of a specific type. * @param key The model key. diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 25ab72536a..f59bbc90c6 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -625,19 +625,6 @@ const parseIPAdapterV2: MetadataParseFunc = async (me return ipAdapter; }; -const parseLayers: MetadataParseFunc = async (metadata) => { - try { - const layersRaw = await getProperty(metadata, 'layers', isArray); - const parseResults = await Promise.allSettled(layersRaw.map((layerRaw) => zLayer.parseAsync(layerRaw))); - const layers = parseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map((result) => result.value); - return layers; - } catch { - return []; - } -}; - const parseAllIPAdaptersV2: MetadataParseFunc = async (metadata) => { try { const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray); @@ -651,6 +638,21 @@ const parseAllIPAdaptersV2: MetadataParseFunc = asy } }; +const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.parseAsync(metadataItem); + +const parseLayers: MetadataParseFunc = async (metadata) => { + try { + const layersRaw = await getProperty(metadata, 'layers', isArray); + const parseResults = await Promise.allSettled(layersRaw.map(parseLayer)); + const layers = parseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + return layers; + } catch { + return []; + } +}; + export const parsers = { createdBy: parseCreatedBy, generationMode: parseGenerationMode, @@ -693,5 +695,6 @@ export const parsers = { t2iAdaptersV2: parseAllT2IAdaptersV2, ipAdapterV2: parseIPAdapterV2, ipAdaptersV2: parseAllIPAdaptersV2, + layer: parseLayer, layers: parseLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 3782c789e0..390e840776 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -13,6 +13,7 @@ import { caLayerT2IAdaptersDeleted, heightChanged, iiLayerAdded, + iiLayerRecalled, ipaLayerAdded, ipaLayerRecalled, ipaLayersDeleted, @@ -295,21 +296,29 @@ const recallIPAdaptersV2: MetadataRecallFunc = (ipA }); }; +const recallLayer: MetadataRecallFunc = (layer) => { + const { dispatch } = getStore(); + switch (layer.type) { + case 'control_adapter_layer': + dispatch(caLayerRecalled(layer)); + break; + case 'ip_adapter_layer': + dispatch(ipaLayerRecalled(layer)); + break; + case 'regional_guidance_layer': + dispatch(rgLayerRecalled(layer)); + break; + case 'initial_image_layer': + dispatch(iiLayerRecalled(layer)); + break; + } +}; + const recallLayers: MetadataRecallFunc = (layers) => { const { dispatch } = getStore(); dispatch(allLayersDeleted()); for (const l of layers) { - switch (l.type) { - case 'control_adapter_layer': - dispatch(caLayerRecalled(l)); - break; - case 'ip_adapter_layer': - dispatch(ipaLayerRecalled(l)); - break; - case 'regional_guidance_layer': - dispatch(rgLayerRecalled(l)); - break; - } + recallLayer(l); } }; @@ -353,5 +362,6 @@ export const recallers = { t2iAdaptersV2: recallT2IAdaptersV2, ipAdapterV2: recallIPAdapterV2, ipAdaptersV2: recallIPAdaptersV2, + layer: recallLayer, layers: recallLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index aca988f85a..7381d7aee0 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -10,9 +10,10 @@ import type { T2IAdapterConfigMetadata, T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; -import { InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers'; +import { fetchModelConfigByIdentifier, InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers'; import type { ParameterSDXLRefinerModel, ParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import type { BaseModelType } from 'services/api/types'; +import { assert } from 'tsafe'; /** * Checks the given base model type against the currently-selected model's base type and throws an error if they are @@ -166,21 +167,36 @@ const validateIPAdaptersV2: MetadataValidateFunc = return new Promise((resolve) => resolve(validatedIPAdapters)); }; +const validateLayer: MetadataValidateFunc = async (layer) => { + if (layer.type === 'control_adapter_layer') { + const model = layer.controlAdapter.model; + assert(model, 'Control Adapter layer missing model'); + validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); + fetchModelConfigByIdentifier(model); + } + if (layer.type === 'ip_adapter_layer') { + const model = layer.ipAdapter.model; + assert(model, 'IP Adapter layer missing model'); + validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); + fetchModelConfigByIdentifier(model); + } + if (layer.type === 'regional_guidance_layer') { + for (const ipa of layer.ipAdapters) { + const model = ipa.model; + assert(model, 'IP Adapter layer missing model'); + validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); + fetchModelConfigByIdentifier(model); + } + } + + return layer; +}; + const validateLayers: MetadataValidateFunc = (layers) => { const validatedLayers: Layer[] = []; for (const l of layers) { try { - if (l.type === 'control_adapter_layer') { - validateBaseCompatibility(l.controlAdapter.model?.base, 'Layer incompatible with currently-selected model'); - } - if (l.type === 'ip_adapter_layer') { - validateBaseCompatibility(l.ipAdapter.model?.base, 'Layer incompatible with currently-selected model'); - } - if (l.type === 'regional_guidance_layer') { - for (const ipa of l.ipAdapters) { - validateBaseCompatibility(ipa.model?.base, 'Layer incompatible with currently-selected model'); - } - } + validateLayer(l); validatedLayers.push(l); } catch { // This is a no-op - we want to continue validating the rest of the layers, and an empty list is valid. @@ -206,5 +222,6 @@ export const validators = { t2iAdaptersV2: validateT2IAdaptersV2, ipAdapterV2: validateIPAdapterV2, ipAdaptersV2: validateIPAdaptersV2, + layer: validateLayer, layers: validateLayers, } as const; From 8b25c1a62e8ce4600188ad434aef9414565f6f60 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 10:12:57 +1000 Subject: [PATCH 022/442] tidy(ui): remove extraneous metadata handlers --- .../controlLayers/store/controlLayersSlice.ts | 12 - .../ImageMetadataActions.tsx | 7 - .../components/MetadataControlNetsV2.tsx | 72 ------ .../components/MetadataIPAdaptersV2.tsx | 72 ------ .../components/MetadataT2IAdaptersV2.tsx | 72 ------ .../web/src/features/metadata/types.ts | 13 -- .../src/features/metadata/util/handlers.ts | 82 +------ .../web/src/features/metadata/util/parsers.ts | 218 +----------------- .../src/features/metadata/util/recallers.ts | 68 +----- .../src/features/metadata/util/validators.ts | 63 ----- 10 files changed, 6 insertions(+), 673 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataControlNetsV2.tsx delete mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataIPAdaptersV2.tsx delete mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index bc9f133075..27b7e4c3fd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -357,12 +357,6 @@ export const controlLayersSlice = createSlice({ const layer = selectCALayerOrThrow(state, layerId); layer.controlAdapter.isProcessingImage = isProcessingImage; }, - caLayerControlNetsDeleted: (state) => { - state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 'controlnet'); - }, - caLayerT2IAdaptersDeleted: (state) => { - state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 't2i_adapter'); - }, //#endregion //#region IP Adapter Layers @@ -415,9 +409,6 @@ export const controlLayersSlice = createSlice({ const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.clipVisionModel = clipVisionModel; }, - ipaLayersDeleted: (state) => { - state.layers = state.layers.filter((l) => !isIPAdapterLayer(l)); - }, //#endregion //#region CA or IPA Layers @@ -839,8 +830,6 @@ export const { caLayerIsFilterEnabledChanged, caLayerOpacityChanged, caLayerIsProcessingImageChanged, - caLayerControlNetsDeleted, - caLayerT2IAdaptersDeleted, // IPA Layers ipaLayerAdded, ipaLayerRecalled, @@ -848,7 +837,6 @@ export const { ipaLayerMethodChanged, ipaLayerModelChanged, ipaLayerCLIPVisionModelChanged, - ipaLayersDeleted, // CA or IPA Layers caOrIPALayerWeightChanged, caOrIPALayerBeginEndStepPctChanged, diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index 04e8fd2eca..f8425182dd 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -1,13 +1,10 @@ import { useAppSelector } from 'app/store/storeHooks'; import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets'; -import { MetadataControlNetsV2 } from 'features/metadata/components/MetadataControlNetsV2'; import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters'; -import { MetadataIPAdaptersV2 } from 'features/metadata/components/MetadataIPAdaptersV2'; import { MetadataItem } from 'features/metadata/components/MetadataItem'; import { MetadataLayers } from 'features/metadata/components/MetadataLayers'; import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs'; import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters'; -import { MetadataT2IAdaptersV2 } from 'features/metadata/components/MetadataT2IAdaptersV2'; import { handlers } from 'features/metadata/util/handlers'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; @@ -40,7 +37,6 @@ const ImageMetadataActions = (props: Props) => { - @@ -57,9 +53,6 @@ const ImageMetadataActions = (props: Props) => { {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } - {activeTabName === 'generation' && } - {activeTabName === 'generation' && } - {activeTabName === 'generation' && } ); }; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataControlNetsV2.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataControlNetsV2.tsx deleted file mode 100644 index 5f4df78afc..0000000000 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataControlNetsV2.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; -import type { ControlNetConfigV2Metadata, MetadataHandlers } from 'features/metadata/types'; -import { handlers } from 'features/metadata/util/handlers'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -type Props = { - metadata: unknown; -}; - -export const MetadataControlNetsV2 = ({ metadata }: Props) => { - const [controlNets, setControlNets] = useState([]); - - useEffect(() => { - const parse = async () => { - try { - const parsed = await handlers.controlNetsV2.parse(metadata); - setControlNets(parsed); - } catch (e) { - setControlNets([]); - } - }; - parse(); - }, [metadata]); - - const label = useMemo(() => handlers.controlNetsV2.getLabel(), []); - - return ( - <> - {controlNets.map((controlNet) => ( - - ))} - - ); -}; - -const MetadataViewControlNet = ({ - label, - controlNet, - handlers, -}: { - label: string; - controlNet: ControlNetConfigV2Metadata; - handlers: MetadataHandlers; -}) => { - const onRecall = useCallback(() => { - if (!handlers.recallItem) { - return; - } - handlers.recallItem(controlNet, true); - }, [handlers, controlNet]); - - const [renderedValue, setRenderedValue] = useState(null); - useEffect(() => { - const _renderValue = async () => { - if (!handlers.renderItemValue) { - setRenderedValue(null); - return; - } - const rendered = await handlers.renderItemValue(controlNet); - setRenderedValue(rendered); - }; - - _renderValue(); - }, [handlers, controlNet]); - - return ; -}; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataIPAdaptersV2.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataIPAdaptersV2.tsx deleted file mode 100644 index 201ebc4cb4..0000000000 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataIPAdaptersV2.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; -import type { IPAdapterConfigV2Metadata, MetadataHandlers } from 'features/metadata/types'; -import { handlers } from 'features/metadata/util/handlers'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -type Props = { - metadata: unknown; -}; - -export const MetadataIPAdaptersV2 = ({ metadata }: Props) => { - const [ipAdapters, setIPAdapters] = useState([]); - - useEffect(() => { - const parse = async () => { - try { - const parsed = await handlers.ipAdaptersV2.parse(metadata); - setIPAdapters(parsed); - } catch (e) { - setIPAdapters([]); - } - }; - parse(); - }, [metadata]); - - const label = useMemo(() => handlers.ipAdaptersV2.getLabel(), []); - - return ( - <> - {ipAdapters.map((ipAdapter) => ( - - ))} - - ); -}; - -const MetadataViewIPAdapter = ({ - label, - ipAdapter, - handlers, -}: { - label: string; - ipAdapter: IPAdapterConfigV2Metadata; - handlers: MetadataHandlers; -}) => { - const onRecall = useCallback(() => { - if (!handlers.recallItem) { - return; - } - handlers.recallItem(ipAdapter, true); - }, [handlers, ipAdapter]); - - const [renderedValue, setRenderedValue] = useState(null); - useEffect(() => { - const _renderValue = async () => { - if (!handlers.renderItemValue) { - setRenderedValue(null); - return; - } - const rendered = await handlers.renderItemValue(ipAdapter); - setRenderedValue(rendered); - }; - - _renderValue(); - }, [handlers, ipAdapter]); - - return ; -}; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx deleted file mode 100644 index 42d3de2ec2..0000000000 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; -import type { MetadataHandlers, T2IAdapterConfigV2Metadata } from 'features/metadata/types'; -import { handlers } from 'features/metadata/util/handlers'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -type Props = { - metadata: unknown; -}; - -export const MetadataT2IAdaptersV2 = ({ metadata }: Props) => { - const [t2iAdapters, setT2IAdapters] = useState([]); - - useEffect(() => { - const parse = async () => { - try { - const parsed = await handlers.t2iAdaptersV2.parse(metadata); - setT2IAdapters(parsed); - } catch (e) { - setT2IAdapters([]); - } - }; - parse(); - }, [metadata]); - - const label = useMemo(() => handlers.t2iAdaptersV2.getLabel(), []); - - return ( - <> - {t2iAdapters.map((t2iAdapter) => ( - - ))} - - ); -}; - -const MetadataViewT2IAdapter = ({ - label, - t2iAdapter, - handlers, -}: { - label: string; - t2iAdapter: T2IAdapterConfigV2Metadata; - handlers: MetadataHandlers; -}) => { - const onRecall = useCallback(() => { - if (!handlers.recallItem) { - return; - } - handlers.recallItem(t2iAdapter, true); - }, [handlers, t2iAdapter]); - - const [renderedValue, setRenderedValue] = useState(null); - useEffect(() => { - const _renderValue = async () => { - if (!handlers.renderItemValue) { - setRenderedValue(null); - return; - } - const rendered = await handlers.renderItemValue(t2iAdapter); - setRenderedValue(rendered); - }; - - _renderValue(); - }, [handlers, t2iAdapter]); - - return ; -}; diff --git a/invokeai/frontend/web/src/features/metadata/types.ts b/invokeai/frontend/web/src/features/metadata/types.ts index b65f2ae5e8..1d87efaf2e 100644 --- a/invokeai/frontend/web/src/features/metadata/types.ts +++ b/invokeai/frontend/web/src/features/metadata/types.ts @@ -1,9 +1,4 @@ import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types'; -import type { - ControlNetConfigV2, - IPAdapterConfigV2, - T2IAdapterConfigV2, -} from 'features/controlLayers/util/controlAdapters'; import type { O } from 'ts-toolbelt'; /** @@ -157,11 +152,3 @@ export type AnyControlAdapterConfigMetadata = | ControlNetConfigMetadata | T2IAdapterConfigMetadata | IPAdapterConfigMetadata; - -export type ControlNetConfigV2Metadata = O.NonNullable; -export type T2IAdapterConfigV2Metadata = O.NonNullable; -export type IPAdapterConfigV2Metadata = O.NonNullable; -export type AnyControlAdapterConfigV2Metadata = - | ControlNetConfigV2Metadata - | T2IAdapterConfigV2Metadata - | IPAdapterConfigV2Metadata; diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index a2ba3dfc22..a4cceb5ec2 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -4,7 +4,6 @@ import type { Layer } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { AnyControlAdapterConfigMetadata, - AnyControlAdapterConfigV2Metadata, BuildMetadataHandlers, MetadataGetLabelFunc, MetadataHandlers, @@ -46,14 +45,6 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (value) => { - try { - const modelConfig = await fetchModelConfig(value.model.key ?? 'none'); - return `${modelConfig.name} (${modelConfig.base.toUpperCase()}) - ${value.weight}`; - } catch { - return `${value.model.key} (${value.model.base.toUpperCase()}) - ${value.weight}`; - } -}; const renderLayerValue: MetadataRenderValueFunc = async (layer) => { if (layer.type === 'initial_image_layer') { return t('controlLayers.initialImageLayer'); @@ -93,26 +84,6 @@ const parameterNotSetToast = (parameter: string, description?: string) => { }); }; -// const allParameterSetToast = (description?: string) => { -// toast({ -// title: t('toast.parametersSet'), -// status: 'info', -// description, -// duration: 2500, -// isClosable: true, -// }); -// }; - -// const allParameterNotSetToast = (description?: string) => { -// toast({ -// title: t('toast.parametersNotSet'), -// status: 'warning', -// description, -// duration: 2500, -// isClosable: true, -// }); -// }; - const buildParse = (arg: { parser: MetadataParseFunc; @@ -220,12 +191,6 @@ export const handlers = { recaller: recallers.cfgScale, }), height: buildHandlers({ getLabel: () => t('metadata.height'), parser: parsers.height, recaller: recallers.height }), - initialImage: buildHandlers({ - getLabel: () => t('metadata.initImage'), - parser: parsers.initialImage, - recaller: recallers.initialImage, - renderValue: async (imageDTO) => imageDTO.image_name, - }), negativePrompt: buildHandlers({ getLabel: () => t('metadata.negativePrompt'), parser: parsers.negativePrompt, @@ -372,36 +337,6 @@ export const handlers = { itemValidator: validators.t2iAdapter, renderItemValue: renderControlAdapterValue, }), - controlNetsV2: buildHandlers({ - getLabel: () => t('common.controlNet'), - parser: parsers.controlNetsV2, - itemParser: parsers.controlNetV2, - recaller: recallers.controlNetsV2, - itemRecaller: recallers.controlNetV2, - validator: validators.controlNetsV2, - itemValidator: validators.controlNetV2, - renderItemValue: renderControlAdapterValueV2, - }), - ipAdaptersV2: buildHandlers({ - getLabel: () => t('common.ipAdapter'), - parser: parsers.ipAdaptersV2, - itemParser: parsers.ipAdapterV2, - recaller: recallers.ipAdaptersV2, - itemRecaller: recallers.ipAdapterV2, - validator: validators.ipAdaptersV2, - itemValidator: validators.ipAdapterV2, - renderItemValue: renderControlAdapterValueV2, - }), - t2iAdaptersV2: buildHandlers({ - getLabel: () => t('common.t2iAdapter'), - parser: parsers.t2iAdaptersV2, - itemParser: parsers.t2iAdapterV2, - recaller: recallers.t2iAdaptersV2, - itemRecaller: recallers.t2iAdapterV2, - validator: validators.t2iAdaptersV2, - itemValidator: validators.t2iAdapterV2, - renderItemValue: renderControlAdapterValueV2, - }), layers: buildHandlers({ getLabel: () => t('controlLayers.layers_other'), parser: parsers.layers, @@ -469,22 +404,9 @@ export const parseAndRecallImageDimensions = async (metadata: unknown) => { }; // These handlers should be omitted when recalling to control layers -const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = [ - 'controlNets', - 'ipAdapters', - 't2iAdapters', - 'controlNetsV2', - 'ipAdaptersV2', - 't2iAdaptersV2', -]; +const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters']; // These handlers should be omitted when recalling to the rest of the app -const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = [ - 'controlNetsV2', - 'ipAdaptersV2', - 't2iAdaptersV2', - 'initialImage', - 'layers', -]; +const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['layers']; export const parseAndRecallAllMetadata = async ( metadata: unknown, diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index f59bbc90c6..c17463c986 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -7,24 +7,13 @@ import { import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; import type { Layer } from 'features/controlLayers/store/types'; import { zLayer } from 'features/controlLayers/store/types'; -import { - CA_PROCESSOR_DATA, - imageDTOToImageWithDims, - initialControlNetV2, - initialIPAdapterV2, - initialT2IAdapterV2, - isProcessorTypeV2, -} from 'features/controlLayers/util/controlAdapters'; import type { LoRA } from 'features/lora/store/loraSlice'; import { defaultLoRAConfig } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, - ControlNetConfigV2Metadata, IPAdapterConfigMetadata, - IPAdapterConfigV2Metadata, MetadataParseFunc, T2IAdapterConfigMetadata, - T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; import { fetchModelConfigWithTypeGuard, getModelKey } from 'features/metadata/util/modelFetchingHelpers'; import { zControlField, zIPAdapterField, zModelIdentifierField, zT2IAdapterField } from 'features/nodes/types/common'; @@ -71,7 +60,7 @@ import { isParameterWidth, } from 'features/parameters/types/parameterSchemas'; import { get, isArray, isString } from 'lodash-es'; -import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; +import { imagesApi } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { isControlNetModelConfig, @@ -441,203 +430,7 @@ const parseAllIPAdapters: MetadataParseFunc = async ( } }; -//#region V2/Control Layers -const parseControlNetV2: MetadataParseFunc = async (metadataItem) => { - const control_model = await getProperty(metadataItem, 'control_model'); - const key = await getModelKey(control_model, 'controlnet'); - const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); - const image = zControlField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'image')); - const processedImage = zControlField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'processed_image')); - const control_weight = zControlField.shape.control_weight - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'control_weight')); - const begin_step_percent = zControlField.shape.begin_step_percent - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'begin_step_percent')); - const end_step_percent = zControlField.shape.end_step_percent - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'end_step_percent')); - const control_mode = zControlField.shape.control_mode - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'control_mode')); - - const id = uuidv4(); - const defaultPreprocessor = controlNetModel.default_settings?.preprocessor; - const processorConfig = isProcessorTypeV2(defaultPreprocessor) - ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults() - : null; - const beginEndStepPct: [number, number] = [ - begin_step_percent ?? initialControlNetV2.beginEndStepPct[0], - end_step_percent ?? initialControlNetV2.beginEndStepPct[1], - ]; - const imageDTO = image ? await getImageDTO(image.image_name) : null; - const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null; - - const controlNet: ControlNetConfigV2Metadata = { - id, - type: 'controlnet', - model: zModelIdentifierField.parse(controlNetModel), - weight: typeof control_weight === 'number' ? control_weight : initialControlNetV2.weight, - beginEndStepPct, - controlMode: control_mode ?? initialControlNetV2.controlMode, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, - processorConfig, - isProcessingImage: false, - }; - - return controlNet; -}; - -const parseAllControlNetsV2: MetadataParseFunc = async (metadata) => { - try { - const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray || undefined); - const parseResults = await Promise.allSettled(controlNetsRaw.map((cn) => parseControlNetV2(cn))); - const controlNets = parseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map((result) => result.value); - return controlNets; - } catch { - return []; - } -}; - -const parseT2IAdapterV2: MetadataParseFunc = async (metadataItem) => { - const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model'); - const key = await getModelKey(t2i_adapter_model, 't2i_adapter'); - const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); - - const image = zT2IAdapterField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'image')); - const processedImage = zT2IAdapterField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'processed_image')); - const weight = zT2IAdapterField.shape.weight - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'weight')); - const begin_step_percent = zT2IAdapterField.shape.begin_step_percent - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'begin_step_percent')); - const end_step_percent = zT2IAdapterField.shape.end_step_percent - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'end_step_percent')); - - const id = uuidv4(); - const defaultPreprocessor = t2iAdapterModel.default_settings?.preprocessor; - const processorConfig = isProcessorTypeV2(defaultPreprocessor) - ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults() - : null; - const beginEndStepPct: [number, number] = [ - begin_step_percent ?? initialT2IAdapterV2.beginEndStepPct[0], - end_step_percent ?? initialT2IAdapterV2.beginEndStepPct[1], - ]; - const imageDTO = image ? await getImageDTO(image.image_name) : null; - const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null; - - const t2iAdapter: T2IAdapterConfigV2Metadata = { - id, - type: 't2i_adapter', - model: zModelIdentifierField.parse(t2iAdapterModel), - weight: typeof weight === 'number' ? weight : initialT2IAdapterV2.weight, - beginEndStepPct, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, - processorConfig, - isProcessingImage: false, - }; - - return t2iAdapter; -}; - -const parseAllT2IAdaptersV2: MetadataParseFunc = async (metadata) => { - try { - const t2iAdaptersRaw = await getProperty(metadata, 't2iAdapters', isArray); - const parseResults = await Promise.allSettled(t2iAdaptersRaw.map((t2iAdapter) => parseT2IAdapterV2(t2iAdapter))); - const t2iAdapters = parseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map((result) => result.value); - return t2iAdapters; - } catch { - return []; - } -}; - -const parseIPAdapterV2: MetadataParseFunc = async (metadataItem) => { - const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model'); - const key = await getModelKey(ip_adapter_model, 'ip_adapter'); - const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig); - - const image = zIPAdapterField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'image')); - const weight = zIPAdapterField.shape.weight - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'weight')); - const method = zIPAdapterField.shape.method - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'method')); - const begin_step_percent = zIPAdapterField.shape.begin_step_percent - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'begin_step_percent')); - const end_step_percent = zIPAdapterField.shape.end_step_percent - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'end_step_percent')); - - const id = uuidv4(); - const beginEndStepPct: [number, number] = [ - begin_step_percent ?? initialIPAdapterV2.beginEndStepPct[0], - end_step_percent ?? initialIPAdapterV2.beginEndStepPct[1], - ]; - const imageDTO = image ? await getImageDTO(image.image_name) : null; - - const ipAdapter: IPAdapterConfigV2Metadata = { - id, - type: 'ip_adapter', - model: zModelIdentifierField.parse(ipAdapterModel), - weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight, - beginEndStepPct, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... - method: method ?? initialIPAdapterV2.method, - }; - - return ipAdapter; -}; - -const parseAllIPAdaptersV2: MetadataParseFunc = async (metadata) => { - try { - const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray); - const parseResults = await Promise.allSettled(ipAdaptersRaw.map((ipAdapter) => parseIPAdapterV2(ipAdapter))); - const ipAdapters = parseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map((result) => result.value); - return ipAdapters; - } catch { - return []; - } -}; - +//#region Control Layers const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.parseAsync(metadataItem); const parseLayers: MetadataParseFunc = async (metadata) => { @@ -652,6 +445,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { return []; } }; +//#endregion export const parsers = { createdBy: parseCreatedBy, @@ -689,12 +483,6 @@ export const parsers = { t2iAdapters: parseAllT2IAdapters, ipAdapter: parseIPAdapter, ipAdapters: parseAllIPAdapters, - controlNetV2: parseControlNetV2, - controlNetsV2: parseAllControlNetsV2, - t2iAdapterV2: parseT2IAdapterV2, - t2iAdaptersV2: parseAllT2IAdaptersV2, - ipAdapterV2: parseIPAdapterV2, - ipAdaptersV2: parseAllIPAdaptersV2, layer: parseLayer, layers: parseLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 390e840776..09c405c3d6 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -7,16 +7,10 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import { allLayersDeleted, - caLayerAdded, - caLayerControlNetsDeleted, caLayerRecalled, - caLayerT2IAdaptersDeleted, heightChanged, - iiLayerAdded, iiLayerRecalled, - ipaLayerAdded, ipaLayerRecalled, - ipaLayersDeleted, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, @@ -30,12 +24,9 @@ import type { LoRA } from 'features/lora/store/loraSlice'; import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, - ControlNetConfigV2Metadata, IPAdapterConfigMetadata, - IPAdapterConfigV2Metadata, MetadataRecallFunc, T2IAdapterConfigMetadata, - T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; import { modelSelected } from 'features/parameters/store/actions'; import { @@ -78,7 +69,6 @@ import { setRefinerStart, setRefinerSteps, } from 'features/sdxl/store/sdxlSlice'; -import type { ImageDTO } from 'services/api/types'; const recallPositivePrompt: MetadataRecallFunc = (positivePrompt) => { getStore().dispatch(positivePromptChanged(positivePrompt)); @@ -112,10 +102,6 @@ const recallScheduler: MetadataRecallFunc = (scheduler) => { getStore().dispatch(setScheduler(scheduler)); }; -const recallInitialImage: MetadataRecallFunc = async (imageDTO) => { - getStore().dispatch(iiLayerAdded(imageDTO)); -}; - const setSizeOptions = { updateAspectRatio: true, clamp: true }; const recallWidth: MetadataRecallFunc = (width) => { @@ -250,52 +236,7 @@ const recallIPAdapters: MetadataRecallFunc = (ipAdapt }); }; -//#region V2/Control Layer -const recallControlNetV2: MetadataRecallFunc = (controlNet) => { - getStore().dispatch(caLayerAdded(controlNet)); -}; - -const recallControlNetsV2: MetadataRecallFunc = (controlNets) => { - const { dispatch } = getStore(); - dispatch(caLayerControlNetsDeleted()); - if (!controlNets.length) { - return; - } - controlNets.forEach((controlNet) => { - dispatch(caLayerAdded(controlNet)); - }); -}; - -const recallT2IAdapterV2: MetadataRecallFunc = (t2iAdapter) => { - getStore().dispatch(caLayerAdded(t2iAdapter)); -}; - -const recallT2IAdaptersV2: MetadataRecallFunc = (t2iAdapters) => { - const { dispatch } = getStore(); - dispatch(caLayerT2IAdaptersDeleted()); - if (!t2iAdapters.length) { - return; - } - t2iAdapters.forEach((t2iAdapters) => { - dispatch(caLayerAdded(t2iAdapters)); - }); -}; - -const recallIPAdapterV2: MetadataRecallFunc = (ipAdapter) => { - getStore().dispatch(ipaLayerAdded(ipAdapter)); -}; - -const recallIPAdaptersV2: MetadataRecallFunc = (ipAdapters) => { - const { dispatch } = getStore(); - dispatch(ipaLayersDeleted()); - if (!ipAdapters.length) { - return; - } - ipAdapters.forEach((ipAdapter) => { - dispatch(ipaLayerAdded(ipAdapter)); - }); -}; - +//#region Control Layers const recallLayer: MetadataRecallFunc = (layer) => { const { dispatch } = getStore(); switch (layer.type) { @@ -331,7 +272,6 @@ export const recallers = { cfgScale: recallCFGScale, cfgRescaleMultiplier: recallCFGRescaleMultiplier, scheduler: recallScheduler, - initialImage: recallInitialImage, width: recallWidth, height: recallHeight, steps: recallSteps, @@ -356,12 +296,6 @@ export const recallers = { t2iAdapter: recallT2IAdapter, ipAdapters: recallIPAdapters, ipAdapter: recallIPAdapter, - controlNetV2: recallControlNetV2, - controlNetsV2: recallControlNetsV2, - t2iAdapterV2: recallT2IAdapterV2, - t2iAdaptersV2: recallT2IAdaptersV2, - ipAdapterV2: recallIPAdapterV2, - ipAdaptersV2: recallIPAdaptersV2, layer: recallLayer, layers: recallLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 7381d7aee0..a308021a1e 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -3,12 +3,9 @@ import type { Layer } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, - ControlNetConfigV2Metadata, IPAdapterConfigMetadata, - IPAdapterConfigV2Metadata, MetadataValidateFunc, T2IAdapterConfigMetadata, - T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; import { fetchModelConfigByIdentifier, InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers'; import type { ParameterSDXLRefinerModel, ParameterVAEModel } from 'features/parameters/types/parameterSchemas'; @@ -113,60 +110,6 @@ const validateIPAdapters: MetadataValidateFunc = (ipA return new Promise((resolve) => resolve(validatedIPAdapters)); }; -const validateControlNetV2: MetadataValidateFunc = (controlNet) => { - validateBaseCompatibility(controlNet.model?.base, 'ControlNet incompatible with currently-selected model'); - return new Promise((resolve) => resolve(controlNet)); -}; - -const validateControlNetsV2: MetadataValidateFunc = (controlNets) => { - const validatedControlNets: ControlNetConfigV2Metadata[] = []; - controlNets.forEach((controlNet) => { - try { - validateBaseCompatibility(controlNet.model?.base, 'ControlNet incompatible with currently-selected model'); - validatedControlNets.push(controlNet); - } catch { - // This is a no-op - we want to continue validating the rest of the ControlNets, and an empty list is valid. - } - }); - return new Promise((resolve) => resolve(validatedControlNets)); -}; - -const validateT2IAdapterV2: MetadataValidateFunc = (t2iAdapter) => { - validateBaseCompatibility(t2iAdapter.model?.base, 'T2I Adapter incompatible with currently-selected model'); - return new Promise((resolve) => resolve(t2iAdapter)); -}; - -const validateT2IAdaptersV2: MetadataValidateFunc = (t2iAdapters) => { - const validatedT2IAdapters: T2IAdapterConfigV2Metadata[] = []; - t2iAdapters.forEach((t2iAdapter) => { - try { - validateBaseCompatibility(t2iAdapter.model?.base, 'T2I Adapter incompatible with currently-selected model'); - validatedT2IAdapters.push(t2iAdapter); - } catch { - // This is a no-op - we want to continue validating the rest of the T2I Adapters, and an empty list is valid. - } - }); - return new Promise((resolve) => resolve(validatedT2IAdapters)); -}; - -const validateIPAdapterV2: MetadataValidateFunc = (ipAdapter) => { - validateBaseCompatibility(ipAdapter.model?.base, 'IP Adapter incompatible with currently-selected model'); - return new Promise((resolve) => resolve(ipAdapter)); -}; - -const validateIPAdaptersV2: MetadataValidateFunc = (ipAdapters) => { - const validatedIPAdapters: IPAdapterConfigV2Metadata[] = []; - ipAdapters.forEach((ipAdapter) => { - try { - validateBaseCompatibility(ipAdapter.model?.base, 'IP Adapter incompatible with currently-selected model'); - validatedIPAdapters.push(ipAdapter); - } catch { - // This is a no-op - we want to continue validating the rest of the IP Adapters, and an empty list is valid. - } - }); - return new Promise((resolve) => resolve(validatedIPAdapters)); -}; - const validateLayer: MetadataValidateFunc = async (layer) => { if (layer.type === 'control_adapter_layer') { const model = layer.controlAdapter.model; @@ -216,12 +159,6 @@ export const validators = { t2iAdapters: validateT2IAdapters, ipAdapter: validateIPAdapter, ipAdapters: validateIPAdapters, - controlNetV2: validateControlNetV2, - controlNetsV2: validateControlNetsV2, - t2iAdapterV2: validateT2IAdapterV2, - t2iAdaptersV2: validateT2IAdaptersV2, - ipAdapterV2: validateIPAdapterV2, - ipAdaptersV2: validateIPAdaptersV2, layer: validateLayer, layers: validateLayers, } as const; From e36e5871a1ee8026c6053d98aae1d4b179ffc9da Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 10:15:16 +1000 Subject: [PATCH 023/442] chore(ui): lint --- .../ControlAndIPAdapter/IPAdapterImagePreview.tsx | 4 +--- .../web/src/features/controlLayers/store/types.ts | 9 ++++----- .../src/features/controlLayers/util/controlAdapters.ts | 8 ++++---- .../web/src/features/deleteImageModal/store/selectors.ts | 4 +--- invokeai/frontend/web/src/features/metadata/types.ts | 2 +- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx index e2ea215314..93d493bcbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx @@ -34,9 +34,7 @@ export const IPAdapterImagePreview = memo( const optimalDimension = useAppSelector(selectOptimalDimension); const shift = useShiftModifier(); - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - image?.name ?? skipToken - ); + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken); const handleResetControlImage = useCallback(() => { onChangeImage(null); }, [onChangeImage]); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 1a3f94debc..cd9db6e962 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -19,15 +19,15 @@ import { } from 'features/parameters/types/parameterSchemas'; import { z } from 'zod'; -export const zTool = z.enum(['brush', 'eraser', 'move', 'rect']); +const zTool = z.enum(['brush', 'eraser', 'move', 'rect']); export type Tool = z.infer; -export const zDrawingTool = zTool.extract(['brush', 'eraser']); +const zDrawingTool = zTool.extract(['brush', 'eraser']); export type DrawingTool = z.infer; const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, { message: 'Must have an even number of points', }); -export const zVectorMaskLine = z.object({ +const zVectorMaskLine = z.object({ id: z.string(), type: z.literal('vector_mask_line'), tool: zDrawingTool, @@ -36,7 +36,7 @@ export const zVectorMaskLine = z.object({ }); export type VectorMaskLine = z.infer; -export const zVectorMaskRect = z.object({ +const zVectorMaskRect = z.object({ id: z.string(), type: z.literal('vector_mask_rect'), x: z.number(), @@ -116,7 +116,6 @@ export const zLayer = z.discriminatedUnion('type', [ zInitialImageLayer, ]); export type Layer = z.infer; -export const zLayers = z.array(zLayer); export type ControlLayersState = { _version: 2; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 0fbcaa6c2b..f9dc114767 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -162,7 +162,7 @@ const zZoeDepthProcessorConfig = z.object({ export type _ZoeDepthProcessorConfig = Required>; export type ZoeDepthProcessorConfig = z.infer; -export const zProcessorConfig = z.discriminatedUnion('type', [ +const zProcessorConfig = z.discriminatedUnion('type', [ zCannyProcessorConfig, zColorMapProcessorConfig, zContentShuffleProcessorConfig, @@ -519,7 +519,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }, }; -export const initialControlNetV2: Omit = { +const initialControlNetV2: Omit = { type: 'controlnet', model: null, weight: 1, @@ -531,7 +531,7 @@ export const initialControlNetV2: Omit = { processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -export const initialT2IAdapterV2: Omit = { +const initialT2IAdapterV2: Omit = { type: 't2i_adapter', model: null, weight: 1, @@ -542,7 +542,7 @@ export const initialT2IAdapterV2: Omit = { processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -export const initialIPAdapterV2: Omit = { +const initialIPAdapterV2: Omit = { type: 'ip_adapter', image: null, model: null, diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 7e2605c6cf..a7934f72d2 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -49,9 +49,7 @@ export const getImageUsage = ( return l.ipAdapters.some((ipa) => ipa.image?.name === image_name); } if (isControlAdapterLayer(l)) { - return ( - l.controlAdapter.image?.name === image_name || l.controlAdapter.processedImage?.name === image_name - ); + return l.controlAdapter.image?.name === image_name || l.controlAdapter.processedImage?.name === image_name; } if (isIPAdapterLayer(l)) { return l.ipAdapter.image?.name === image_name; diff --git a/invokeai/frontend/web/src/features/metadata/types.ts b/invokeai/frontend/web/src/features/metadata/types.ts index 1d87efaf2e..dfc1e828c9 100644 --- a/invokeai/frontend/web/src/features/metadata/types.ts +++ b/invokeai/frontend/web/src/features/metadata/types.ts @@ -51,7 +51,7 @@ export type MetadataValidateFunc = (value: T) => Promise; * @param value The value to check. * @returns True if the item should be visible, false otherwise. */ -export type MetadataGetIsVisibleFunc = (value: T) => boolean; +type MetadataGetIsVisibleFunc = (value: T) => boolean; export type MetadataHandlers = { /** From de33d6e6470fd2d4334d0eb8c1b2c27e3745e80c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 10:41:52 +1000 Subject: [PATCH 024/442] fix(ui): metadata "Layers" -> "Layer" --- invokeai/frontend/web/src/features/metadata/util/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index a4cceb5ec2..da8150fff4 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -338,7 +338,7 @@ export const handlers = { renderItemValue: renderControlAdapterValue, }), layers: buildHandlers({ - getLabel: () => t('controlLayers.layers_other'), + getLabel: () => t('controlLayers.layers_one'), parser: parsers.layers, itemParser: parsers.layer, recaller: recallers.layers, From 6107e3d28131482a6c904dc103642777cb51108d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 11:01:20 +1000 Subject: [PATCH 025/442] fix(ui): fix zControlAdapterBase schema weight --- .../web/src/features/controlLayers/util/controlAdapters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index f9dc114767..b643e4a64d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -195,7 +195,7 @@ const zBeginEndStepPct = z const zControlAdapterBase = z.object({ id: zId, - weight: z.number().gte(0).lte(0), + weight: z.number().gte(0).lte(1), image: zImageWithDims.nullable(), processedImage: zImageWithDims.nullable(), isProcessingImage: z.boolean(), From f147f99befa572c82c23862220b36c08f2fd0a50 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 11:02:07 +1000 Subject: [PATCH 026/442] feat(ui): better metadata labels for layers --- invokeai/frontend/web/public/locales/en.json | 2 -- .../src/features/metadata/util/handlers.ts | 33 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0c7c6cd6e1..375f691ab2 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1547,8 +1547,6 @@ "addIPAdapter": "Add $t(common.ipAdapter)", "regionalGuidance": "Regional Guidance", "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", - "controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)", - "ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)", "opacity": "Opacity", "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index da8150fff4..bf5fa2eaec 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -47,16 +47,41 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (layer) => { if (layer.type === 'initial_image_layer') { - return t('controlLayers.initialImageLayer'); + let rendered = t('controlLayers.globalInitialImageLayer'); + if (layer.image) { + rendered += ` (${layer.image})`; + } + return rendered; } if (layer.type === 'control_adapter_layer') { - return t('controlLayers.controlAdapterLayer'); + let rendered = t('controlLayers.globalControlAdapterLayer'); + const model = layer.controlAdapter.model; + if (model) { + rendered += ` (${model.name} - ${model.base.toUpperCase()})`; + } + return rendered; } if (layer.type === 'ip_adapter_layer') { - return t('controlLayers.ipAdapterLayer'); + let rendered = t('controlLayers.globalIPAdapterLayer'); + const model = layer.ipAdapter.model; + if (model) { + rendered += ` (${model.name} - ${model.base.toUpperCase()})`; + } + return rendered; } if (layer.type === 'regional_guidance_layer') { - return t('controlLayers.regionalGuidanceLayer'); + const rendered = t('controlLayers.regionalGuidanceLayer'); + const items: string[] = []; + if (layer.positivePrompt) { + items.push(`Positive: ${layer.positivePrompt}`); + } + if (layer.negativePrompt) { + items.push(`Negative: ${layer.negativePrompt}`); + } + if (layer.ipAdapters.length > 0) { + items.push(`${layer.ipAdapters.length} IP Adapters`); + } + return `${rendered} (${items.join(', ')})`; } assert(false, 'Unknown layer type'); }; From 3f489c92c843e4ea39d0c9ecb05c74e236ba6f8b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 14:22:19 +1000 Subject: [PATCH 027/442] feat(ui): handle initial image layers in control layers helper --- .../util/graph/addControlLayersToGraph.ts | 239 +++++++++++------- .../util/graph/buildGenerationTabGraph.ts | 16 +- .../util/graph/buildGenerationTabSDXLGraph.ts | 9 +- 3 files changed, 157 insertions(+), 107 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index 9aa82fdb92..de3456f371 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts @@ -2,11 +2,12 @@ import { getStore } from 'app/store/nanostores/store'; import type { RootState } from 'app/store/store'; import { isControlAdapterLayer, + isInitialImageLayer, isIPAdapterLayer, isRegionalGuidanceLayer, rgLayerMaskImageUploaded, } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; +import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { type ControlNetConfigV2, type ImageWithDims, @@ -20,9 +21,11 @@ import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLaye import type { ImageField } from 'features/nodes/types/common'; import { CONTROL_NET_COLLECT, + IMAGE_TO_LATENTS, IP_ADAPTER_COLLECT, NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING_COLLECT, + NOISE, POSITIVE_CONDITIONING, POSITIVE_CONDITIONING_COLLECT, PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, @@ -30,6 +33,7 @@ import { PROMPT_REGION_NEGATIVE_COND_PREFIX, PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, PROMPT_REGION_POSITIVE_COND_PREFIX, + RESIZE, T2I_ADAPTER_COLLECT, } from 'features/nodes/util/graph/constants'; import { upsertMetadata } from 'features/nodes/util/graph/metadata'; @@ -38,9 +42,10 @@ import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; import type { CollectInvocation, ControlNetInvocation, - CoreMetadataInvocation, Edge, ImageDTO, + ImageResizeInvocation, + ImageToLatentsInvocation, IPAdapterInvocation, NonNullableGraph, S, @@ -67,33 +72,6 @@ const buildControlImage = ( assert(false, 'Attempted to add unprocessed control image'); }; -const buildControlNetMetadata = (controlNet: ControlNetConfigV2): S['ControlNetMetadataField'] => { - const { beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; - - assert(model, 'ControlNet model is required'); - assert(image, 'ControlNet image is required'); - - const processed_image = - processedImage && processorConfig - ? { - image_name: processedImage.name, - } - : null; - - return { - control_model: model, - control_weight: weight, - control_mode: controlMode, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - resize_mode: 'just_resize', - image: { - image_name: image.name, - }, - processed_image, - }; -}; - const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { if (graph.nodes[CONTROL_NET_COLLECT]) { // You see, we've already got one! @@ -123,7 +101,6 @@ const addGlobalControlNetsToGraph = async ( if (controlNets.length === 0) { return; } - const controlNetMetadata: CoreMetadataInvocation['controlnets'] = []; addControlNetCollectorSafe(graph, denoiseNodeId); for (const controlNet of controlNets) { @@ -147,8 +124,6 @@ const addGlobalControlNetsToGraph = async ( graph.nodes[controlNetNode.id] = controlNetNode; - controlNetMetadata.push(buildControlNetMetadata(controlNet)); - graph.edges.push({ source: { node_id: controlNetNode.id, field: 'control' }, destination: { @@ -157,33 +132,6 @@ const addGlobalControlNetsToGraph = async ( }, }); } - upsertMetadata(graph, { controlnets: controlNetMetadata }); -}; - -const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfigV2): S['T2IAdapterMetadataField'] => { - const { beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; - - assert(model, 'T2I Adapter model is required'); - assert(image, 'T2I Adapter image is required'); - - const processed_image = - processedImage && processorConfig - ? { - image_name: processedImage.name, - } - : null; - - return { - t2i_adapter_model: model, - weight, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - resize_mode: 'just_resize', - image: { - image_name: image.name, - }, - processed_image, - }; }; const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { @@ -215,7 +163,6 @@ const addGlobalT2IAdaptersToGraph = async ( if (t2iAdapters.length === 0) { return; } - const t2iAdapterMetadata: CoreMetadataInvocation['t2iAdapters'] = []; addT2IAdapterCollectorSafe(graph, denoiseNodeId); for (const t2iAdapter of t2iAdapters) { @@ -238,8 +185,6 @@ const addGlobalT2IAdaptersToGraph = async ( graph.nodes[t2iAdapterNode.id] = t2iAdapterNode; - t2iAdapterMetadata.push(buildT2IAdapterMetadata(t2iAdapter)); - graph.edges.push({ source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' }, destination: { @@ -248,27 +193,6 @@ const addGlobalT2IAdaptersToGraph = async ( }, }); } - - upsertMetadata(graph, { t2iAdapters: t2iAdapterMetadata }); -}; - -const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfigV2): S['IPAdapterMetadataField'] => { - const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; - - assert(model, 'IP Adapter model is required'); - assert(image, 'IP Adapter image is required'); - - return { - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - weight, - method, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.name, - }, - }; }; const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { @@ -300,7 +224,6 @@ const addGlobalIPAdaptersToGraph = async ( if (ipAdapters.length === 0) { return; } - const ipAdapterMetdata: CoreMetadataInvocation['ipAdapters'] = []; addIPAdapterCollectorSafe(graph, denoiseNodeId); for (const ipAdapter of ipAdapters) { @@ -325,8 +248,6 @@ const addGlobalIPAdaptersToGraph = async ( graph.nodes[ipAdapterNode.id] = ipAdapterNode; - ipAdapterMetdata.push(buildIPAdapterMetadata(ipAdapter)); - graph.edges.push({ source: { node_id: ipAdapterNode.id, field: 'ip_adapter' }, destination: { @@ -335,16 +256,131 @@ const addGlobalIPAdaptersToGraph = async ( }, }); } - - upsertMetadata(graph, { ipAdapters: ipAdapterMetdata }); }; -export const addControlLayersToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => { +const addInitialImageLayerToGraph = ( + state: RootState, + graph: NonNullableGraph, + denoiseNodeId: string, + layer: InitialImageLayer +) => { + const { vaePrecision, model } = state.generation; + const { refinerModel, refinerStart } = state.sdxl; + const { width, height } = state.controlLayers.present.size; + assert(layer.isEnabled, 'Initial image layer is not enabled'); + assert(layer.image, 'Initial image layer has no image'); + + const isSDXL = model?.base === 'sdxl'; + const useRefinerStartEnd = isSDXL && Boolean(refinerModel); + + const denoiseNode = graph.nodes[denoiseNodeId]; + assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`); + + const { denoisingStrength } = layer; + denoiseNode.denoising_start = useRefinerStartEnd + ? Math.min(refinerStart, 1 - denoisingStrength) + : 1 - denoisingStrength; + denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1; + + // We conditionally hook the image in depending on if a resize is needed + const i2lNode: ImageToLatentsInvocation = { + type: 'i2l', + id: IMAGE_TO_LATENTS, + is_intermediate: true, + use_cache: true, + fp32: vaePrecision === 'fp32', + }; + + graph.nodes[i2lNode.id] = i2lNode; + graph.edges.push({ + source: { + node_id: IMAGE_TO_LATENTS, + field: 'latents', + }, + destination: { + node_id: denoiseNode.id, + field: 'latents', + }, + }); + + if (layer.image.width !== width || layer.image.height !== height) { + // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` + + // Create a resize node, explicitly setting its image + const resizeNode: ImageResizeInvocation = { + id: RESIZE, + type: 'img_resize', + image: { + image_name: layer.image.name, + }, + is_intermediate: true, + width, + height, + }; + + graph.nodes[RESIZE] = resizeNode; + + // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` + graph.edges.push({ + source: { node_id: RESIZE, field: 'image' }, + destination: { + node_id: IMAGE_TO_LATENTS, + field: 'image', + }, + }); + + // The `RESIZE` node also passes its width and height to `NOISE` + graph.edges.push({ + source: { node_id: RESIZE, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + + graph.edges.push({ + source: { node_id: RESIZE, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } else { + // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly + i2lNode.image = { + image_name: layer.image.name, + }; + + // Pass the image's dimensions to the `NOISE` node + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } + + upsertMetadata(graph, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); +}; + +export const addControlLayersToGraph = async ( + state: RootState, + graph: NonNullableGraph, + denoiseNodeId: string +): Promise => { const mainModel = state.generation.model; assert(mainModel, 'Missing main model when building graph'); const isSDXL = mainModel.base === 'sdxl'; - const layersMetadata: Layer[] = []; + const validLayers: Layer[] = []; // Add global control adapters const validControlAdapterLayers = state.controlLayers.present.layers @@ -366,6 +402,8 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab const validT2IAdapters = validControlAdapterLayers.map((l) => l.controlAdapter).filter(isT2IAdapterConfigV2); addGlobalT2IAdaptersToGraph(validT2IAdapters, graph, denoiseNodeId); + validLayers.push(...validControlAdapterLayers); + const validIPAdapterLayers = state.controlLayers.present.layers // Must be an IP Adapter layer .filter(isIPAdapterLayer) @@ -381,6 +419,21 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab }); const validIPAdapters = validIPAdapterLayers.map((l) => l.ipAdapter); addGlobalIPAdaptersToGraph(validIPAdapters, graph, denoiseNodeId); + validLayers.push(...validIPAdapterLayers); + + const initialImageLayer = state.controlLayers.present.layers.filter(isInitialImageLayer).find((l) => { + if (!l.isEnabled) { + return false; + } + if (!l.image) { + return false; + } + return true; + }); + if (initialImageLayer) { + addInitialImageLayerToGraph(state, graph, denoiseNodeId, initialImageLayer); + validLayers.push(initialImageLayer); + } const validRGLayers = state.controlLayers.present.layers // Only RG layers are get masks @@ -393,8 +446,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab const hasIPAdapter = l.ipAdapters.filter((ipa) => ipa.image).length > 0; return hasTextPrompt || hasIPAdapter; }); - - layersMetadata.push(...validRGLayers, ...validControlAdapterLayers, ...validIPAdapterLayers); + validLayers.push(...validRGLayers); // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing // the existing conditioning nodes. @@ -660,7 +712,8 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab } } - upsertMetadata(graph, { layers: layersMetadata }); + upsertMetadata(graph, { layers: validLayers }); + return validLayers; }; const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts index 41f9f4f748..a1ee581736 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts @@ -1,8 +1,8 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; -import { addInitialImageToLinearGraph } from 'features/nodes/util/graph/addInitialImageToLinearGraph'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; @@ -232,24 +232,24 @@ export const buildGenerationTabGraph = async (state: RootState): Promise isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); // High resolution fix. - if (state.hrf.hrfEnabled && !didAddInitialImage) { + if (state.hrf.hrfEnabled && shouldUseHRF) { + console.log('HRFING'); addHrfToGraph(state, graph); } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts index 900e993602..fbf6b3848c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts @@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; -import { addInitialImageToLinearGraph } from 'features/nodes/util/graph/addInitialImageToLinearGraph'; import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; @@ -242,8 +241,6 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise Date: Wed, 8 May 2024 15:21:51 +1000 Subject: [PATCH 028/442] tidy(ui): clean up control layers graph builder --- .../util/graph/addControlLayersToGraph.ts | 768 +++++++++--------- 1 file changed, 370 insertions(+), 398 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index de3456f371..5abbce028b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts @@ -8,14 +8,12 @@ import { rgLayerMaskImageUploaded, } from 'features/controlLayers/store/controlLayersSlice'; import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import { - type ControlNetConfigV2, - type ImageWithDims, - type IPAdapterConfigV2, - isControlNetConfigV2, - isT2IAdapterConfigV2, - type ProcessorConfig, - type T2IAdapterConfigV2, +import type { + ControlNetConfigV2, + ImageWithDims, + IPAdapterConfigV2, + ProcessorConfig, + T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; import type { ImageField } from 'features/nodes/types/common'; @@ -40,6 +38,7 @@ import { upsertMetadata } from 'features/nodes/util/graph/metadata'; import { size } from 'lodash-es'; import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; import type { + BaseModelType, CollectInvocation, ControlNetInvocation, Edge, @@ -53,324 +52,6 @@ import type { } from 'services/api/types'; import { assert } from 'tsafe'; -const buildControlImage = ( - image: ImageWithDims | null, - processedImage: ImageWithDims | null, - processorConfig: ProcessorConfig | null -): ImageField => { - if (processedImage && processorConfig) { - // We've processed the image in the app - use it for the control image. - return { - image_name: processedImage.name, - }; - } else if (image) { - // No processor selected, and we have an image - the user provided a processed image, use it for the control image. - return { - image_name: image.name, - }; - } - assert(false, 'Attempted to add unprocessed control image'); -}; - -const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { - if (graph.nodes[CONTROL_NET_COLLECT]) { - // You see, we've already got one! - return; - } - // Add the ControlNet collector - const controlNetIterateNode: CollectInvocation = { - id: CONTROL_NET_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode; - graph.edges.push({ - source: { node_id: CONTROL_NET_COLLECT, field: 'collection' }, - destination: { - node_id: denoiseNodeId, - field: 'control', - }, - }); -}; - -const addGlobalControlNetsToGraph = async ( - controlNets: ControlNetConfigV2[], - graph: NonNullableGraph, - denoiseNodeId: string -) => { - if (controlNets.length === 0) { - return; - } - addControlNetCollectorSafe(graph, denoiseNodeId); - - for (const controlNet of controlNets) { - if (!controlNet.model) { - return; - } - const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; - - const controlNetNode: ControlNetInvocation = { - id: `control_net_${id}`, - type: 'controlnet', - is_intermediate: true, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - control_mode: controlMode, - resize_mode: 'just_resize', - control_model: model, - control_weight: weight, - image: buildControlImage(image, processedImage, processorConfig), - }; - - graph.nodes[controlNetNode.id] = controlNetNode; - - graph.edges.push({ - source: { node_id: controlNetNode.id, field: 'control' }, - destination: { - node_id: CONTROL_NET_COLLECT, - field: 'item', - }, - }); - } -}; - -const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { - if (graph.nodes[T2I_ADAPTER_COLLECT]) { - // You see, we've already got one! - return; - } - // Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect - const t2iAdapterCollectNode: CollectInvocation = { - id: T2I_ADAPTER_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[T2I_ADAPTER_COLLECT] = t2iAdapterCollectNode; - graph.edges.push({ - source: { node_id: T2I_ADAPTER_COLLECT, field: 'collection' }, - destination: { - node_id: denoiseNodeId, - field: 't2i_adapter', - }, - }); -}; - -const addGlobalT2IAdaptersToGraph = async ( - t2iAdapters: T2IAdapterConfigV2[], - graph: NonNullableGraph, - denoiseNodeId: string -) => { - if (t2iAdapters.length === 0) { - return; - } - addT2IAdapterCollectorSafe(graph, denoiseNodeId); - - for (const t2iAdapter of t2iAdapters) { - if (!t2iAdapter.model) { - return; - } - const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; - - const t2iAdapterNode: T2IAdapterInvocation = { - id: `t2i_adapter_${id}`, - type: 't2i_adapter', - is_intermediate: true, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - resize_mode: 'just_resize', - t2i_adapter_model: model, - weight: weight, - image: buildControlImage(image, processedImage, processorConfig), - }; - - graph.nodes[t2iAdapterNode.id] = t2iAdapterNode; - - graph.edges.push({ - source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' }, - destination: { - node_id: T2I_ADAPTER_COLLECT, - field: 'item', - }, - }); - } -}; - -const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { - if (graph.nodes[IP_ADAPTER_COLLECT]) { - // You see, we've already got one! - return; - } - - const ipAdapterCollectNode: CollectInvocation = { - id: IP_ADAPTER_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode; - graph.edges.push({ - source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' }, - destination: { - node_id: denoiseNodeId, - field: 'ip_adapter', - }, - }); -}; - -const addGlobalIPAdaptersToGraph = async ( - ipAdapters: IPAdapterConfigV2[], - graph: NonNullableGraph, - denoiseNodeId: string -) => { - if (ipAdapters.length === 0) { - return; - } - addIPAdapterCollectorSafe(graph, denoiseNodeId); - - for (const ipAdapter of ipAdapters) { - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; - assert(image, 'IP Adapter image is required'); - assert(model, 'IP Adapter model is required'); - - const ipAdapterNode: IPAdapterInvocation = { - id: `ip_adapter_${id}`, - type: 'ip_adapter', - is_intermediate: true, - weight, - method, - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.name, - }, - }; - - graph.nodes[ipAdapterNode.id] = ipAdapterNode; - - graph.edges.push({ - source: { node_id: ipAdapterNode.id, field: 'ip_adapter' }, - destination: { - node_id: IP_ADAPTER_COLLECT, - field: 'item', - }, - }); - } -}; - -const addInitialImageLayerToGraph = ( - state: RootState, - graph: NonNullableGraph, - denoiseNodeId: string, - layer: InitialImageLayer -) => { - const { vaePrecision, model } = state.generation; - const { refinerModel, refinerStart } = state.sdxl; - const { width, height } = state.controlLayers.present.size; - assert(layer.isEnabled, 'Initial image layer is not enabled'); - assert(layer.image, 'Initial image layer has no image'); - - const isSDXL = model?.base === 'sdxl'; - const useRefinerStartEnd = isSDXL && Boolean(refinerModel); - - const denoiseNode = graph.nodes[denoiseNodeId]; - assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`); - - const { denoisingStrength } = layer; - denoiseNode.denoising_start = useRefinerStartEnd - ? Math.min(refinerStart, 1 - denoisingStrength) - : 1 - denoisingStrength; - denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1; - - // We conditionally hook the image in depending on if a resize is needed - const i2lNode: ImageToLatentsInvocation = { - type: 'i2l', - id: IMAGE_TO_LATENTS, - is_intermediate: true, - use_cache: true, - fp32: vaePrecision === 'fp32', - }; - - graph.nodes[i2lNode.id] = i2lNode; - graph.edges.push({ - source: { - node_id: IMAGE_TO_LATENTS, - field: 'latents', - }, - destination: { - node_id: denoiseNode.id, - field: 'latents', - }, - }); - - if (layer.image.width !== width || layer.image.height !== height) { - // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` - - // Create a resize node, explicitly setting its image - const resizeNode: ImageResizeInvocation = { - id: RESIZE, - type: 'img_resize', - image: { - image_name: layer.image.name, - }, - is_intermediate: true, - width, - height, - }; - - graph.nodes[RESIZE] = resizeNode; - - // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` - graph.edges.push({ - source: { node_id: RESIZE, field: 'image' }, - destination: { - node_id: IMAGE_TO_LATENTS, - field: 'image', - }, - }); - - // The `RESIZE` node also passes its width and height to `NOISE` - graph.edges.push({ - source: { node_id: RESIZE, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - - graph.edges.push({ - source: { node_id: RESIZE, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } else { - // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly - i2lNode.image = { - image_name: layer.image.name, - }; - - // Pass the image's dimensions to the `NOISE` node - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } - - upsertMetadata(graph, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); -}; - export const addControlLayersToGraph = async ( state: RootState, graph: NonNullableGraph, @@ -380,74 +61,24 @@ export const addControlLayersToGraph = async ( assert(mainModel, 'Missing main model when building graph'); const isSDXL = mainModel.base === 'sdxl'; - const validLayers: Layer[] = []; + // Filter out layers with incompatible base model, missing control image + const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, mainModel.base)); - // Add global control adapters - const validControlAdapterLayers = state.controlLayers.present.layers - // Must be a Control Adapter layer - .filter(isControlAdapterLayer) - // Must be enabled - .filter((l) => l.isEnabled) - .filter((l) => { - const ca = l.controlAdapter; - // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === mainModel.base; - const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig); - return hasModel && modelMatchesBase && hasControlImage; - }); - const validControlNets = validControlAdapterLayers.map((l) => l.controlAdapter).filter(isControlNetConfigV2); - addGlobalControlNetsToGraph(validControlNets, graph, denoiseNodeId); - - const validT2IAdapters = validControlAdapterLayers.map((l) => l.controlAdapter).filter(isT2IAdapterConfigV2); - addGlobalT2IAdaptersToGraph(validT2IAdapters, graph, denoiseNodeId); - - validLayers.push(...validControlAdapterLayers); - - const validIPAdapterLayers = state.controlLayers.present.layers - // Must be an IP Adapter layer - .filter(isIPAdapterLayer) - // Must be enabled - .filter((l) => l.isEnabled) - // We want the IP Adapters themselves - .filter((l) => { - const ipa = l.ipAdapter; - const hasModel = Boolean(ipa.model); - const modelMatchesBase = ipa.model?.base === mainModel.base; - const hasImage = Boolean(ipa.image); - return hasModel && modelMatchesBase && hasImage; - }); - const validIPAdapters = validIPAdapterLayers.map((l) => l.ipAdapter); - addGlobalIPAdaptersToGraph(validIPAdapters, graph, denoiseNodeId); - validLayers.push(...validIPAdapterLayers); - - const initialImageLayer = state.controlLayers.present.layers.filter(isInitialImageLayer).find((l) => { - if (!l.isEnabled) { - return false; - } - if (!l.image) { - return false; - } - return true; - }); - if (initialImageLayer) { - addInitialImageLayerToGraph(state, graph, denoiseNodeId, initialImageLayer); - validLayers.push(initialImageLayer); + const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); + for (const ca of validControlAdapters) { + addGlobalControlAdapterToGraph(ca, graph, denoiseNodeId); } - const validRGLayers = state.controlLayers.present.layers - // Only RG layers are get masks - .filter(isRegionalGuidanceLayer) - // Only visible layers are rendered on the canvas - .filter((l) => l.isEnabled) - // Only layers with prompts get added to the graph - .filter((l) => { - const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasIPAdapter = l.ipAdapters.filter((ipa) => ipa.image).length > 0; - return hasTextPrompt || hasIPAdapter; - }); - validLayers.push(...validRGLayers); + const validIPAdapters = validLayers.filter(isIPAdapterLayer).map((l) => l.ipAdapter); + for (const ipAdapter of validIPAdapters) { + addGlobalIPAdapterToGraph(ipAdapter, graph, denoiseNodeId); + } + const initialImageLayers = validLayers.filter(isInitialImageLayer); + assert(initialImageLayers.length <= 1, 'Only one initial image layer allowed'); + if (initialImageLayers[0]) { + addInitialImageLayerToGraph(state, graph, denoiseNodeId, initialImageLayers[0]); + } // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing // the existing conditioning nodes. @@ -510,6 +141,7 @@ export const addControlLayersToGraph = async ( }, }); + const validRGLayers = validLayers.filter(isRegionalGuidanceLayer); const layerIds = validRGLayers.map((l) => l.id); const blobs = await getRegionalPromptLayerBlobs(layerIds); assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); @@ -665,15 +297,11 @@ export const addControlLayersToGraph = async ( } } - // TODO(psyche): For some reason, I have to explicitly annotate regionalIPAdapters here. Not sure why. - const regionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipAdapter) => { - const hasModel = Boolean(ipAdapter.model); - const modelMatchesBase = ipAdapter.model?.base === mainModel.base; - const hasControlImage = Boolean(ipAdapter.image); - return hasModel && modelMatchesBase && hasControlImage; - }); + const validRegionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipa) => + isValidIPAdapter(ipa, mainModel.base) + ); - for (const ipAdapter of regionalIPAdapters) { + for (const ipAdapter of validRegionalIPAdapters) { addIPAdapterCollectorSafe(graph, denoiseNodeId); const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; assert(model, 'IP Adapter model is required'); @@ -735,3 +363,347 @@ const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise { + if (processedImage && processorConfig) { + // We've processed the image in the app - use it for the control image. + return { + image_name: processedImage.name, + }; + } else if (image) { + // No processor selected, and we have an image - the user provided a processed image, use it for the control image. + return { + image_name: image.name, + }; + } + assert(false, 'Attempted to add unprocessed control image'); +}; + +const addGlobalControlAdapterToGraph = ( + controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2, + graph: NonNullableGraph, + denoiseNodeId: string +) => { + if (controlAdapter.type === 'controlnet') { + addGlobalControlNetToGraph(controlAdapter, graph, denoiseNodeId); + } + if (controlAdapter.type === 't2i_adapter') { + addGlobalT2IAdapterToGraph(controlAdapter, graph, denoiseNodeId); + } +}; + +const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { + if (graph.nodes[CONTROL_NET_COLLECT]) { + // You see, we've already got one! + return; + } + // Add the ControlNet collector + const controlNetIterateNode: CollectInvocation = { + id: CONTROL_NET_COLLECT, + type: 'collect', + is_intermediate: true, + }; + graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode; + graph.edges.push({ + source: { node_id: CONTROL_NET_COLLECT, field: 'collection' }, + destination: { + node_id: denoiseNodeId, + field: 'control', + }, + }); +}; + +const addGlobalControlNetToGraph = (controlNet: ControlNetConfigV2, graph: NonNullableGraph, denoiseNodeId: string) => { + const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; + assert(model, 'ControlNet model is required'); + const controlImage = buildControlImage(image, processedImage, processorConfig); + addControlNetCollectorSafe(graph, denoiseNodeId); + + const controlNetNode: ControlNetInvocation = { + id: `control_net_${id}`, + type: 'controlnet', + is_intermediate: true, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + control_mode: controlMode, + resize_mode: 'just_resize', + control_model: model, + control_weight: weight, + image: controlImage, + }; + + graph.nodes[controlNetNode.id] = controlNetNode; + + graph.edges.push({ + source: { node_id: controlNetNode.id, field: 'control' }, + destination: { + node_id: CONTROL_NET_COLLECT, + field: 'item', + }, + }); +}; + +const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { + if (graph.nodes[T2I_ADAPTER_COLLECT]) { + // You see, we've already got one! + return; + } + // Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect + const t2iAdapterCollectNode: CollectInvocation = { + id: T2I_ADAPTER_COLLECT, + type: 'collect', + is_intermediate: true, + }; + graph.nodes[T2I_ADAPTER_COLLECT] = t2iAdapterCollectNode; + graph.edges.push({ + source: { node_id: T2I_ADAPTER_COLLECT, field: 'collection' }, + destination: { + node_id: denoiseNodeId, + field: 't2i_adapter', + }, + }); +}; + +const addGlobalT2IAdapterToGraph = (t2iAdapter: T2IAdapterConfigV2, graph: NonNullableGraph, denoiseNodeId: string) => { + const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; + assert(model, 'T2I Adapter model is required'); + const controlImage = buildControlImage(image, processedImage, processorConfig); + addT2IAdapterCollectorSafe(graph, denoiseNodeId); + + const t2iAdapterNode: T2IAdapterInvocation = { + id: `t2i_adapter_${id}`, + type: 't2i_adapter', + is_intermediate: true, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + t2i_adapter_model: model, + weight: weight, + image: controlImage, + }; + + graph.nodes[t2iAdapterNode.id] = t2iAdapterNode; + + graph.edges.push({ + source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' }, + destination: { + node_id: T2I_ADAPTER_COLLECT, + field: 'item', + }, + }); +}; + +const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { + if (graph.nodes[IP_ADAPTER_COLLECT]) { + // You see, we've already got one! + return; + } + + const ipAdapterCollectNode: CollectInvocation = { + id: IP_ADAPTER_COLLECT, + type: 'collect', + is_intermediate: true, + }; + graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode; + graph.edges.push({ + source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' }, + destination: { + node_id: denoiseNodeId, + field: 'ip_adapter', + }, + }); +}; + +const addGlobalIPAdapterToGraph = (ipAdapter: IPAdapterConfigV2, graph: NonNullableGraph, denoiseNodeId: string) => { + addIPAdapterCollectorSafe(graph, denoiseNodeId); + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; + assert(image, 'IP Adapter image is required'); + assert(model, 'IP Adapter model is required'); + + const ipAdapterNode: IPAdapterInvocation = { + id: `ip_adapter_${id}`, + type: 'ip_adapter', + is_intermediate: true, + weight, + method, + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.name, + }, + }; + + graph.nodes[ipAdapterNode.id] = ipAdapterNode; + + graph.edges.push({ + source: { node_id: ipAdapterNode.id, field: 'ip_adapter' }, + destination: { + node_id: IP_ADAPTER_COLLECT, + field: 'item', + }, + }); +}; + +const addInitialImageLayerToGraph = ( + state: RootState, + graph: NonNullableGraph, + denoiseNodeId: string, + layer: InitialImageLayer +) => { + const { vaePrecision, model } = state.generation; + const { refinerModel, refinerStart } = state.sdxl; + const { width, height } = state.controlLayers.present.size; + assert(layer.isEnabled, 'Initial image layer is not enabled'); + assert(layer.image, 'Initial image layer has no image'); + + const isSDXL = model?.base === 'sdxl'; + const useRefinerStartEnd = isSDXL && Boolean(refinerModel); + + const denoiseNode = graph.nodes[denoiseNodeId]; + assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`); + + const { denoisingStrength } = layer; + denoiseNode.denoising_start = useRefinerStartEnd + ? Math.min(refinerStart, 1 - denoisingStrength) + : 1 - denoisingStrength; + denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1; + + const i2lNode: ImageToLatentsInvocation = { + type: 'i2l', + id: IMAGE_TO_LATENTS, + is_intermediate: true, + use_cache: true, + fp32: vaePrecision === 'fp32', + }; + + graph.nodes[i2lNode.id] = i2lNode; + graph.edges.push({ + source: { + node_id: IMAGE_TO_LATENTS, + field: 'latents', + }, + destination: { + node_id: denoiseNode.id, + field: 'latents', + }, + }); + + if (layer.image.width !== width || layer.image.height !== height) { + // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` + + // Create a resize node, explicitly setting its image + const resizeNode: ImageResizeInvocation = { + id: RESIZE, + type: 'img_resize', + image: { + image_name: layer.image.name, + }, + is_intermediate: true, + width, + height, + }; + + graph.nodes[RESIZE] = resizeNode; + + // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` + graph.edges.push({ + source: { node_id: RESIZE, field: 'image' }, + destination: { + node_id: IMAGE_TO_LATENTS, + field: 'image', + }, + }); + + // The `RESIZE` node also passes its width and height to `NOISE` + graph.edges.push({ + source: { node_id: RESIZE, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + + graph.edges.push({ + source: { node_id: RESIZE, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } else { + // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly + i2lNode.image = { + image_name: layer.image.name, + }; + + // Pass the image's dimensions to the `NOISE` node + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } + + upsertMetadata(graph, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); +}; + +const isValidControlAdapter = (ca: ControlNetConfigV2 | T2IAdapterConfigV2, base: BaseModelType): boolean => { + // Must be have a model that matches the current base and must have a control image + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === base; + const hasControlImage = Boolean(ca.image || (ca.processedImage && ca.processorConfig)); + return hasModel && modelMatchesBase && hasControlImage; +}; + +const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean => { + // Must be have a model that matches the current base and must have a control image + const hasModel = Boolean(ipa.model); + const modelMatchesBase = ipa.model?.base === base; + const hasImage = Boolean(ipa.image); + return hasModel && modelMatchesBase && hasImage; +}; + +const isValidLayer = (layer: Layer, base: BaseModelType) => { + if (isControlAdapterLayer(layer)) { + if (!layer.isEnabled) { + return false; + } + return isValidControlAdapter(layer.controlAdapter, base); + } + if (isIPAdapterLayer(layer)) { + if (!layer.isEnabled) { + return false; + } + return isValidIPAdapter(layer.ipAdapter, base); + } + if (isInitialImageLayer(layer)) { + if (!layer.isEnabled) { + return false; + } + if (!layer.image) { + return false; + } + return true; + } + if (isRegionalGuidanceLayer(layer)) { + const hasTextPrompt = Boolean(layer.positivePrompt || layer.negativePrompt); + const hasIPAdapter = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; + return hasTextPrompt || hasIPAdapter; + } + return false; +}; From 23ad6fb730013c40c83c87393ac191f74ff363c2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 15:49:19 +1000 Subject: [PATCH 029/442] feat(ui): handle missing images/models when recalling control layers --- .../src/features/metadata/util/recallers.ts | 89 ++++++++++++++++--- .../src/features/metadata/util/validators.ts | 11 +-- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 09c405c3d6..673e2187f2 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -1,4 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; +import { deepClone } from 'common/util/deepClone'; import { controlAdapterRecalled, controlNetsReset, @@ -28,6 +29,7 @@ import type { MetadataRecallFunc, T2IAdapterConfigMetadata, } from 'features/metadata/types'; +import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers'; import { modelSelected } from 'features/parameters/store/actions'; import { setCfgRescaleMultiplier, @@ -69,6 +71,7 @@ import { setRefinerStart, setRefinerSteps, } from 'features/sdxl/store/sdxlSlice'; +import { getImageDTO } from 'services/api/endpoints/images'; const recallPositivePrompt: MetadataRecallFunc = (positivePrompt) => { getStore().dispatch(positivePromptChanged(positivePrompt)); @@ -237,21 +240,79 @@ const recallIPAdapters: MetadataRecallFunc = (ipAdapt }; //#region Control Layers -const recallLayer: MetadataRecallFunc = (layer) => { +const recallLayer: MetadataRecallFunc = async (layer) => { const { dispatch } = getStore(); - switch (layer.type) { - case 'control_adapter_layer': - dispatch(caLayerRecalled(layer)); - break; - case 'ip_adapter_layer': - dispatch(ipaLayerRecalled(layer)); - break; - case 'regional_guidance_layer': - dispatch(rgLayerRecalled(layer)); - break; - case 'initial_image_layer': - dispatch(iiLayerRecalled(layer)); - break; + // We need to check for the existence of all images and models when recalling. If they do not exist, SMITE THEM! + if (layer.type === 'control_adapter_layer') { + const clone = deepClone(layer); + if (clone.controlAdapter.image) { + const imageDTO = await getImageDTO(clone.controlAdapter.image.name); + if (!imageDTO) { + clone.controlAdapter.image = null; + } + } + if (clone.controlAdapter.processedImage) { + const imageDTO = await getImageDTO(clone.controlAdapter.processedImage.name); + if (!imageDTO) { + clone.controlAdapter.processedImage = null; + } + } + if (clone.controlAdapter.model) { + try { + await fetchModelConfigByIdentifier(clone.controlAdapter.model); + } catch { + clone.controlAdapter.model = null; + } + } + dispatch(caLayerRecalled(clone)); + return; + } + if (layer.type === 'ip_adapter_layer') { + const clone = deepClone(layer); + if (clone.ipAdapter.image) { + const imageDTO = await getImageDTO(clone.ipAdapter.image.name); + if (!imageDTO) { + clone.ipAdapter.image = null; + } + } + if (clone.ipAdapter.model) { + try { + await fetchModelConfigByIdentifier(clone.ipAdapter.model); + } catch { + clone.ipAdapter.model = null; + } + } + dispatch(ipaLayerRecalled(clone)); + return; + } + + if (layer.type === 'regional_guidance_layer') { + const clone = deepClone(layer); + // Strip out the uploaded mask image property - this is an intermediate image + clone.uploadedMaskImage = null; + + for (const ipAdapter of clone.ipAdapters) { + if (ipAdapter.image) { + const imageDTO = await getImageDTO(ipAdapter.image.name); + if (!imageDTO) { + ipAdapter.image = null; + } + } + if (ipAdapter.model) { + try { + await fetchModelConfigByIdentifier(ipAdapter.model); + } catch { + ipAdapter.model = null; + } + } + } + dispatch(rgLayerRecalled(clone)); + return; + } + + if (layer.type === 'initial_image_layer') { + dispatch(iiLayerRecalled(layer)); + return; } }; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index a308021a1e..759e8ba561 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -7,7 +7,7 @@ import type { MetadataValidateFunc, T2IAdapterConfigMetadata, } from 'features/metadata/types'; -import { fetchModelConfigByIdentifier, InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers'; +import { InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers'; import type { ParameterSDXLRefinerModel, ParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import type { BaseModelType } from 'services/api/types'; import { assert } from 'tsafe'; @@ -115,32 +115,29 @@ const validateLayer: MetadataValidateFunc = async (layer) => { const model = layer.controlAdapter.model; assert(model, 'Control Adapter layer missing model'); validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); - fetchModelConfigByIdentifier(model); } if (layer.type === 'ip_adapter_layer') { const model = layer.ipAdapter.model; assert(model, 'IP Adapter layer missing model'); validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); - fetchModelConfigByIdentifier(model); } if (layer.type === 'regional_guidance_layer') { for (const ipa of layer.ipAdapters) { const model = ipa.model; assert(model, 'IP Adapter layer missing model'); validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); - fetchModelConfigByIdentifier(model); } } return layer; }; -const validateLayers: MetadataValidateFunc = (layers) => { +const validateLayers: MetadataValidateFunc = async (layers) => { const validatedLayers: Layer[] = []; for (const l of layers) { try { - validateLayer(l); - validatedLayers.push(l); + const validated = await validateLayer(l); + validatedLayers.push(validated); } catch { // This is a no-op - we want to continue validating the rest of the layers, and an empty list is valid. } From e9d2ffe3d77c969fba8deb0022bf34e6aee851fe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 15:49:51 +1000 Subject: [PATCH 030/442] fix(ui): process control image on recall if no processed image --- .../listeners/controlAdapterPreprocessor.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index 7d5aa27f20..92c3e7afa7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -8,6 +8,7 @@ import { caLayerModelChanged, caLayerProcessedImageChanged, caLayerProcessorConfigChanged, + caLayerRecalled, isControlAdapterLayer, } from 'features/controlLayers/store/controlLayersSlice'; import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; @@ -20,7 +21,7 @@ import { queueApi } from 'services/api/endpoints/queue'; import type { BatchConfig, ImageDTO } from 'services/api/types'; import { socketInvocationComplete } from 'services/events/actions'; -const matcher = isAnyOf(caLayerImageChanged, caLayerProcessorConfigChanged, caLayerModelChanged); +const matcher = isAnyOf(caLayerImageChanged, caLayerProcessorConfigChanged, caLayerModelChanged, caLayerRecalled); const DEBOUNCE_MS = 300; const log = logger('session'); @@ -29,7 +30,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni startAppListening({ matcher, effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take }) => { - const { layerId } = action.payload; + const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId; const precheckLayerOriginal = getOriginalState() .controlLayers.present.layers.filter(isControlAdapterLayer) .find((l) => l.id === layerId); From a3a6449786377578401f384574fee4d10c17732d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 16:00:16 +1000 Subject: [PATCH 031/442] feat(ui): versioned control layers metadata --- invokeai/frontend/web/src/features/metadata/util/parsers.ts | 3 ++- .../src/features/nodes/util/graph/addControlLayersToGraph.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index c17463c986..213a4666fe 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -435,7 +435,8 @@ const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.pars const parseLayers: MetadataParseFunc = async (metadata) => { try { - const layersRaw = await getProperty(metadata, 'layers', isArray); + const control_layers = await getProperty(metadata, 'control_layers'); + const layersRaw = await getProperty(control_layers, 'layers', isArray); const parseResults = await Promise.allSettled(layersRaw.map(parseLayer)); const layers = parseResults .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index 5abbce028b..6f249fd522 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts @@ -340,7 +340,7 @@ export const addControlLayersToGraph = async ( } } - upsertMetadata(graph, { layers: validLayers }); + upsertMetadata(graph, { control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); return validLayers; }; From e8023c44b0eef64692af054d7a37e2348c6ebc52 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 16:01:34 +1000 Subject: [PATCH 032/442] chore(ui): lint --- .../controlLayers/util/controlAdapters.ts | 6 - .../graph/addInitialImageToLinearGraph.ts | 133 ------------------ 2 files changed, 139 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index b643e4a64d..070ca64b32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -214,18 +214,12 @@ export const zControlNetConfigV2 = zControlAdapterBase.extend({ }); export type ControlNetConfigV2 = z.infer; -export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 => - zControlNetConfigV2.safeParse(ca).success; - export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({ type: z.literal('t2i_adapter'), model: zModelIdentifierField.nullable(), }); export type T2IAdapterConfigV2 = z.infer; -export const isT2IAdapterConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is T2IAdapterConfigV2 => - zT2IAdapterConfigV2.safeParse(ca).success; - const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); export type CLIPVisionModelV2 = z.infer; export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts deleted file mode 100644 index 2460b187a4..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { isInitialImageLayer } from 'features/controlLayers/store/controlLayersSlice'; -import { upsertMetadata } from 'features/nodes/util/graph/metadata'; -import type { ImageResizeInvocation, ImageToLatentsInvocation, NonNullableGraph } from 'services/api/types'; -import { assert } from 'tsafe'; - -import { IMAGE_TO_LATENTS, NOISE, RESIZE } from './constants'; - -/** - * Returns true if an initial image was added, false if not. - */ -export const addInitialImageToLinearGraph = ( - state: RootState, - graph: NonNullableGraph, - denoiseNodeId: string -): boolean => { - // Remove Existing UNet Connections - const { vaePrecision, model } = state.generation; - const { refinerModel, refinerStart } = state.sdxl; - const { width, height } = state.controlLayers.present.size; - const initialImageLayer = state.controlLayers.present.layers.find(isInitialImageLayer); - const initialImage = initialImageLayer?.isEnabled ? initialImageLayer?.image : null; - - if (!initialImage || !initialImageLayer) { - return false; - } - - const isSDXL = model?.base === 'sdxl'; - const useRefinerStartEnd = isSDXL && Boolean(refinerModel); - - const denoiseNode = graph.nodes[denoiseNodeId]; - assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`); - - const { denoisingStrength } = initialImageLayer; - denoiseNode.denoising_start = useRefinerStartEnd - ? Math.min(refinerStart, 1 - denoisingStrength) - : 1 - denoisingStrength; - denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1; - - // We conditionally hook the image in depending on if a resize is needed - const i2lNode: ImageToLatentsInvocation = { - type: 'i2l', - id: IMAGE_TO_LATENTS, - is_intermediate: true, - use_cache: true, - fp32: vaePrecision === 'fp32', - }; - - graph.nodes[i2lNode.id] = i2lNode; - graph.edges.push({ - source: { - node_id: IMAGE_TO_LATENTS, - field: 'latents', - }, - destination: { - node_id: denoiseNode.id, - field: 'latents', - }, - }); - - if (initialImage.width !== width || initialImage.height !== height) { - // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` - - // Create a resize node, explicitly setting its image - const resizeNode: ImageResizeInvocation = { - id: RESIZE, - type: 'img_resize', - image: { - image_name: initialImage.name, - }, - is_intermediate: true, - width, - height, - }; - - graph.nodes[RESIZE] = resizeNode; - - // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` - graph.edges.push({ - source: { node_id: RESIZE, field: 'image' }, - destination: { - node_id: IMAGE_TO_LATENTS, - field: 'image', - }, - }); - - // The `RESIZE` node also passes its width and height to `NOISE` - graph.edges.push({ - source: { node_id: RESIZE, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - - graph.edges.push({ - source: { node_id: RESIZE, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } else { - // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly - i2lNode.image = { - image_name: initialImage.name, - }; - - // Pass the image's dimensions to the `NOISE` node - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } - - upsertMetadata(graph, { - generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img', - strength: denoisingStrength, - init_image: initialImage.name, - }); - - return true; -}; From e8e764be20aa5eb13ca987be335b97f669c2a55d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 17:38:18 +1000 Subject: [PATCH 033/442] feat(ui): revise image viewer - Viewer only exists on Generation tab - Viewer defaults to open - When clicking the Control Layers tab on the left panel, close the viewer (i.e. open the CL editor) - Do not switch to editor when adding layers (this is handled by clicking the Control Layers tab) - Do not open viewer when single-clicking images in gallery - _Do_ open viewer when _double_-clicking images in gallery - Do not change viewer state when switching between app tabs (this no longer makes sense; the viewer only exists on generation tab) - Change the button to a drop down menu that states what you are currently doing, e.g. Viewing vs Editing --- invokeai/frontend/web/public/locales/en.json | 11 +- .../listeners/galleryImageClicked.ts | 3 +- .../web/src/common/components/IAIDndImage.tsx | 2 + .../IAICanvasToolbar/IAICanvasToolbar.tsx | 177 ++++++++---------- .../components/ControlLayersToolbar.tsx | 4 +- .../components/ImageGrid/GalleryImage.tsx | 6 + .../components/ImageViewer/EditorButton.tsx | 39 ---- .../components/ImageViewer/ImageViewer.tsx | 4 +- .../ImageViewer/ImageViewerWorkflows.tsx | 45 +++++ .../components/ImageViewer/ViewerButton.tsx | 25 --- .../ImageViewer/ViewerToggleMenu.tsx | 67 +++++++ .../features/gallery/store/gallerySlice.ts | 10 +- .../flow/panels/TopPanel/TopPanel.tsx | 2 - .../src/features/ui/components/InvokeTabs.tsx | 4 +- .../components/ParametersPanelTextToImage.tsx | 24 ++- .../features/ui/components/tabs/NodesTab.tsx | 11 ++ .../ui/components/tabs/TextToImageTab.tsx | 2 + 17 files changed, 249 insertions(+), 187 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 375f691ab2..cfed635570 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -142,9 +142,11 @@ "blue": "Blue", "alpha": "Alpha", "selected": "Selected", - "viewer": "Viewer", "tab": "Tab", - "close": "Close" + "viewing": "Viewing", + "viewingDesc": "Review images in a large gallery view", + "editing": "Editing", + "editingDesc": "Edit on the Control Layers canvas" }, "controlnet": { "controlAdapter_one": "Control Adapter", @@ -365,10 +367,7 @@ "bulkDownloadRequestFailed": "Problem Preparing Download", "bulkDownloadFailed": "Download Failed", "problemDeletingImages": "Problem Deleting Images", - "problemDeletingImagesDesc": "One or more images could not be deleted", - "switchTo": "Switch to {{ tab }} (Z)", - "openFloatingViewer": "Open Floating Viewer", - "closeFloatingViewer": "Close Floating Viewer" + "problemDeletingImagesDesc": "One or more images could not be deleted" }, "hotkeys": { "searchHotkeys": "Search Hotkeys", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts index 6b8c9b4ea3..67c6d076ee 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts @@ -1,7 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; -import { isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; +import { selectionChanged } from 'features/gallery/store/gallerySlice'; import { imagesApi } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { imagesSelectors } from 'services/api/util'; @@ -62,7 +62,6 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen } else { dispatch(selectionChanged([imageDTO])); } - dispatch(isImageViewerOpenChanged(true)); }, }); }; diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index 01107c21b4..2712334e1e 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -70,6 +70,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { onMouseOver, onMouseOut, dataTestId, + ...rest } = props; const [isHovered, setIsHovered] = useState(false); @@ -138,6 +139,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { minH={minSize ? minSize : undefined} userSelect="none" cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'} + {...rest} > {imageDTO && ( { return ( - - - - - - - - - + + + + + - - + + - - } - isChecked={tool === 'move' || isStaging} - onClick={handleSelectMoveTool} - /> - : } - onClick={handleSetShouldShowBoundingBox} - isDisabled={isStaging} - /> - } - onClick={handleClickResetCanvasView} - /> - + + } + isChecked={tool === 'move' || isStaging} + onClick={handleSelectMoveTool} + /> + : } + onClick={handleSetShouldShowBoundingBox} + isDisabled={isStaging} + /> + } + onClick={handleClickResetCanvasView} + /> + - + + } + onClick={handleMergeVisible} + isDisabled={isStaging} + /> + } + onClick={handleSaveToGallery} + isDisabled={isStaging} + /> + {isClipboardAPIAvailable && ( } - onClick={handleMergeVisible} + aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`} + tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`} + icon={} + onClick={handleCopyImageToClipboard} isDisabled={isStaging} /> - } - onClick={handleSaveToGallery} - isDisabled={isStaging} - /> - {isClipboardAPIAvailable && ( - } - onClick={handleCopyImageToClipboard} - isDisabled={isStaging} - /> - )} - } - onClick={handleDownloadAsImage} - isDisabled={isStaging} - /> - - - - - + )} + } + onClick={handleDownloadAsImage} + isDisabled={isStaging} + /> + + + + + - - } - isDisabled={isStaging} - {...getUploadButtonProps()} - /> - - } - onClick={handleResetCanvas} - colorScheme="error" - isDisabled={isStaging} - /> - - - - - - - - - - + + } + isDisabled={isStaging} + {...getUploadButtonProps()} + /> + + } + onClick={handleResetCanvas} + colorScheme="error" + isDisabled={isStaging} + /> + + + + ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index b78910700d..b087d8dc70 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -4,7 +4,7 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; -import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton'; +import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import { memo } from 'react'; export const ControlLayersToolbar = memo(() => { @@ -21,7 +21,7 @@ export const ControlLayersToolbar = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index 2788b1095d..2c53599ba3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -11,6 +11,7 @@ import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggab import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId'; import { useMultiselect } from 'features/gallery/hooks/useMultiselect'; import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView'; +import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import type { MouseEvent } from 'react'; import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -102,6 +103,10 @@ const GalleryImage = (props: HoverableImageProps) => { setIsHovered(true); }, []); + const onDoubleClick = useCallback(() => { + dispatch(isImageViewerOpenChanged(true)); + }, [dispatch]); + const handleMouseOut = useCallback(() => { setIsHovered(false); }, []); @@ -143,6 +148,7 @@ const GalleryImage = (props: HoverableImageProps) => { > = { - generation: 'controlLayers.controlLayers', - canvas: 'ui.tabs.canvas', - workflows: 'ui.tabs.workflows', - models: 'ui.tabs.models', - queue: 'ui.tabs.queue', -}; - -export const EditorButton = () => { - const { t } = useTranslation(); - const { onClose } = useImageViewer(); - const activeTabName = useAppSelector(activeTabNameSelector); - const tooltip = useMemo( - () => t('gallery.switchTo', { tab: t(TAB_NAME_TO_TKEY_SHORT[activeTabName]) }), - [t, activeTabName] - ); - - return ( - - ); -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx index dcd4d4c304..7064e553dc 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -10,7 +10,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import CurrentImageButtons from './CurrentImageButtons'; import CurrentImagePreview from './CurrentImagePreview'; -import { EditorButton } from './EditorButton'; +import { ViewerToggleMenu } from './ViewerToggleMenu'; const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows']; @@ -60,7 +60,7 @@ export const ImageViewer = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx new file mode 100644 index 0000000000..fe09f11be6 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx @@ -0,0 +1,45 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton'; +import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; +import { memo } from 'react'; + +import CurrentImageButtons from './CurrentImageButtons'; +import CurrentImagePreview from './CurrentImagePreview'; + +export const ImageViewerWorkflows = memo(() => { + return ( + + + + + + + + + + + + + + + + + + ); +}); + +ImageViewerWorkflows.displayName = 'ImageViewerWorkflows'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx deleted file mode 100644 index edceb5099c..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button } from '@invoke-ai/ui-library'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowsDownUpBold } from 'react-icons/pi'; - -import { useImageViewer } from './useImageViewer'; - -export const ViewerButton = () => { - const { t } = useTranslation(); - const { onOpen } = useImageViewer(); - const tooltip = useMemo(() => t('gallery.switchTo', { tab: t('common.viewer') }), [t]); - - return ( - - ); -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx new file mode 100644 index 0000000000..c1277d142f --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx @@ -0,0 +1,67 @@ +import { + Button, + Flex, + Icon, + Popover, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, + Text, +} from '@invoke-ai/ui-library'; +import { useTranslation } from 'react-i18next'; +import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi'; + +import { useImageViewer } from './useImageViewer'; + +export const ViewerToggleMenu = () => { + const { t } = useTranslation(); + const { isOpen, onClose, onOpen } = useImageViewer(); + + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 744dc09f3f..af19017486 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,8 +1,6 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; -import { setActiveTab } from 'features/ui/store/uiSlice'; import { uniqBy } from 'lodash-es'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; @@ -23,7 +21,7 @@ const initialGalleryState: GalleryState = { boardSearchText: '', limit: INITIAL_IMAGE_LIMIT, offset: 0, - isImageViewerOpen: false, + isImageViewerOpen: true, }; export const gallerySlice = createSlice({ @@ -83,12 +81,6 @@ export const gallerySlice = createSlice({ }, }, extraReducers: (builder) => { - builder.addCase(setActiveTab, (state) => { - state.isImageViewerOpen = false; - }); - builder.addCase(rgLayerAdded, (state) => { - state.isImageViewerOpen = false; - }); builder.addMatcher(isAnyBoardDeleted, (state, action) => { const deletedBoardId = action.meta.arg.originalArgs; if (deletedBoardId === state.selectedBoardId) { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx index 2a08fb840e..93856a21c4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx @@ -1,6 +1,5 @@ import { Flex, Spacer } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton'; import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton'; import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton'; import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton'; @@ -23,7 +22,6 @@ const TopCenterPanel = () => { - ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 42df03872c..1968c64161 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -4,7 +4,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { $customNavComponent } from 'app/store/nanostores/customNavComponent'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent'; -import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup'; import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent'; import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu'; @@ -255,9 +254,8 @@ const InvokeTabs = () => { )} - + {tabPanels} - {shouldShowGalleryPanel && ( diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx index a7a401cde4..698be530f9 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx @@ -1,8 +1,9 @@ import type { ChakraProps } from '@invoke-ai/ui-library'; import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent'; +import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { Prompts } from 'features/parameters/components/Prompts/Prompts'; import QueueControls from 'features/queue/components/QueueControls'; import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts'; @@ -15,7 +16,7 @@ import { RefinerSettingsAccordion } from 'features/settingsAccordions/components import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import type { CSSProperties } from 'react'; -import { memo, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const overlayScrollbarsStyles: CSSProperties = { @@ -37,6 +38,7 @@ const selectedStyles: ChakraProps['sx'] = { const ParametersPanelTextToImage = () => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); const activeTabName = useAppSelector(activeTabNameSelector); const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length); const controlLayersTitle = useMemo(() => { @@ -46,6 +48,14 @@ const ParametersPanelTextToImage = () => { return `${t('controlLayers.controlLayers')} (${controlLayersCount})`; }, [controlLayersCount, t]); const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl'); + const onChangeTabs = useCallback( + (i: number) => { + if (i === 1) { + dispatch(isImageViewerOpenChanged(false)); + } + }, + [dispatch] + ); return ( @@ -55,7 +65,15 @@ const ParametersPanelTextToImage = () => { {isSDXL ? : } - + {t('common.settingsLabel')} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx index 2ee21bfadf..b4f473ae03 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx @@ -1,9 +1,20 @@ import { Box } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { ImageViewerWorkflows } from 'features/gallery/components/ImageViewer/ImageViewerWorkflows'; import NodeEditor from 'features/nodes/components/NodeEditor'; import { memo } from 'react'; import { ReactFlowProvider } from 'reactflow'; const NodesTab = () => { + const mode = useAppSelector((s) => s.workflow.mode); + if (mode === 'view') { + return ( + + + + ); + } + return ( diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx index 74845a9ca9..1c1c9c24a4 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx @@ -1,11 +1,13 @@ import { Box } from '@invoke-ai/ui-library'; import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor'; +import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; import { memo } from 'react'; const TextToImageTab = () => { return ( + ); }; From 6c1fd584d29b5aefe45a4859214b8306d3640227 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 19:06:04 +1000 Subject: [PATCH 034/442] feat(ui): pre-CL control adapter metadata recall --- .../controlLayers/store/controlLayersSlice.ts | 7 +- .../controlLayers/util/controlAdapters.ts | 6 +- .../ImageMetadataActions.tsx | 4 +- .../src/features/metadata/util/handlers.ts | 2 +- .../web/src/features/metadata/util/parsers.ts | 310 ++++++++++++++++-- 5 files changed, 300 insertions(+), 29 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 27b7e4c3fd..fb890f7d45 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -662,7 +662,7 @@ export const controlLayersSlice = createSlice({ } } }, - prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: 'initial_image_layer', imageDTO } }), + prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: INITIAL_IMAGE_LAYER_ID, imageDTO } }), }, iiLayerRecalled: (state, action: PayloadAction) => { deselectAllLayers(state); @@ -914,6 +914,7 @@ export const RG_LAYER_NAME = 'regional_guidance_layer'; export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line'; export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group'; export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect'; +export const INITIAL_IMAGE_LAYER_ID = 'singleton_initial_image_layer'; export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer'; export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image'; export const LAYER_BBOX_NAME = 'layer.bbox'; @@ -925,10 +926,10 @@ const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_$ const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`; -const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; +export const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; -const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; +export const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; export const controlLayersPersistConfig: PersistConfig = { name: controlLayersSlice.name, diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 070ca64b32..589c61b855 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -513,7 +513,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }, }; -const initialControlNetV2: Omit = { +export const initialControlNetV2: Omit = { type: 'controlnet', model: null, weight: 1, @@ -525,7 +525,7 @@ const initialControlNetV2: Omit = { processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -const initialT2IAdapterV2: Omit = { +export const initialT2IAdapterV2: Omit = { type: 't2i_adapter', model: null, weight: 1, @@ -536,7 +536,7 @@ const initialT2IAdapterV2: Omit = { processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -const initialIPAdapterV2: Omit = { +export const initialIPAdapterV2: Omit = { type: 'ip_adapter', image: null, model: null, diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index f8425182dd..a192ff4fbb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -37,7 +37,7 @@ const ImageMetadataActions = (props: Props) => { - + {activeTabName !== 'generation' && } @@ -49,7 +49,7 @@ const ImageMetadataActions = (props: Props) => { - + {activeTabName === 'generation' && } {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index bf5fa2eaec..b0d0e22688 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -429,7 +429,7 @@ export const parseAndRecallImageDimensions = async (metadata: unknown) => { }; // These handlers should be omitted when recalling to control layers -const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters']; +const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters', 'strength']; // These handlers should be omitted when recalling to the rest of the app const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['layers']; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 213a4666fe..1848152d2b 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,12 +1,20 @@ -import { getStore } from 'app/store/nanostores/store'; import { initialControlNet, initialIPAdapter, initialT2IAdapter, } from 'features/controlAdapters/util/buildControlAdapter'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; -import type { Layer } from 'features/controlLayers/store/types'; +import { getCALayerId, getIPALayerId, INITIAL_IMAGE_LAYER_ID } from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, Layer } from 'features/controlLayers/store/types'; import { zLayer } from 'features/controlLayers/store/types'; +import { + CA_PROCESSOR_DATA, + imageDTOToImageWithDims, + initialControlNetV2, + initialIPAdapterV2, + initialT2IAdapterV2, + isProcessorTypeV2, +} from 'features/controlLayers/util/controlAdapters'; import type { LoRA } from 'features/lora/store/loraSlice'; import { defaultLoRAConfig } from 'features/lora/store/loraSlice'; import type { @@ -60,8 +68,7 @@ import { isParameterWidth, } from 'features/parameters/types/parameterSchemas'; import { get, isArray, isString } from 'lodash-es'; -import { imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; +import { getImageDTO } from 'services/api/endpoints/images'; import { isControlNetModelConfig, isIPAdapterModelConfig, @@ -71,6 +78,7 @@ import { isT2IAdapterModelConfig, isVAEModelConfig, } from 'services/api/types'; +import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; export const MetadataParsePendingToken = Symbol('pending'); @@ -140,14 +148,6 @@ const parseCFGRescaleMultiplier: MetadataParseFunc = (metadata) => getProperty(metadata, 'scheduler', isParameterScheduler); -const parseInitialImage: MetadataParseFunc = async (metadata) => { - const imageName = await getProperty(metadata, 'init_image', isString); - const imageDTORequest = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); - const imageDTO = await imageDTORequest.unwrap(); - imageDTORequest.unsubscribe(); - return imageDTO; -}; - const parseWidth: MetadataParseFunc = (metadata) => getProperty(metadata, 'width', isParameterWidth); const parseHeight: MetadataParseFunc = (metadata) => @@ -300,7 +300,7 @@ const parseControlNet: MetadataParseFunc = async (meta const parseAllControlNets: MetadataParseFunc = async (metadata) => { try { - const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray || undefined); + const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray); const parseResults = await Promise.allSettled(controlNetsRaw.map((cn) => parseControlNet(cn))); const controlNets = parseResults .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') @@ -434,18 +434,286 @@ const parseAllIPAdapters: MetadataParseFunc = async ( const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.parseAsync(metadataItem); const parseLayers: MetadataParseFunc = async (metadata) => { + // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles + // taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For + // example, CL Control Adapters don't support resize mode, so we simply omit that property. + try { - const control_layers = await getProperty(metadata, 'control_layers'); - const layersRaw = await getProperty(control_layers, 'layers', isArray); - const parseResults = await Promise.allSettled(layersRaw.map(parseLayer)); - const layers = parseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map((result) => result.value); + const layers: Layer[] = []; + + try { + const control_layers = await getProperty(metadata, 'control_layers'); + const controlLayersRaw = await getProperty(control_layers, 'layers', isArray); + const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer)); + const controlLayers = controlLayersParseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + layers.push(...controlLayers); + } catch { + // no-op + } + + try { + const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray); + console.log('controlNetsRaw', controlNetsRaw); + const controlNetsParseResults = await Promise.allSettled( + controlNetsRaw.map(async (cn) => await parseControlNetToControlAdapterLayer(cn)) + ); + console.log('controlNetsParseResults', controlNetsParseResults); + const controlNetsAsLayers = controlNetsParseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + layers.push(...controlNetsAsLayers); + } catch { + // no-op + } + + try { + const t2iAdaptersRaw = await getProperty(metadata, 't2iAdapters', isArray); + console.log('t2iAdaptersRaw', t2iAdaptersRaw); + const t2iAdaptersParseResults = await Promise.allSettled( + t2iAdaptersRaw.map(async (cn) => await parseT2IAdapterToControlAdapterLayer(cn)) + ); + console.log('t2iAdaptersParseResults', t2iAdaptersParseResults); + const t2iAdaptersAsLayers = t2iAdaptersParseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + layers.push(...t2iAdaptersAsLayers); + } catch { + // no-op + } + + try { + const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray); + console.log('ipAdaptersRaw', ipAdaptersRaw); + const ipAdaptersParseResults = await Promise.allSettled( + ipAdaptersRaw.map(async (cn) => await parseIPAdapterToIPAdapterLayer(cn)) + ); + console.log('ipAdaptersParseResults', ipAdaptersParseResults); + const ipAdaptersAsLayers = ipAdaptersParseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + layers.push(...ipAdaptersAsLayers); + } catch { + // no-op + } + + try { + const initialImageLayer = await parseInitialImageToInitialImageLayer(metadata); + layers.push(initialImageLayer); + } catch { + // no-op + } + return layers; } catch { return []; } }; + +const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { + const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength); + const imageName = await getProperty(metadata, 'init_image', isString); + const imageDTO = await getImageDTO(imageName); + assert(imageDTO, 'ImageDTO is null'); + const layer: InitialImageLayer = { + id: INITIAL_IMAGE_LAYER_ID, + type: 'initial_image_layer', + bbox: null, + bboxNeedsUpdate: true, + x: 0, + y: 0, + isEnabled: true, + opacity: 1, + image: imageDTOToImageWithDims(imageDTO), + isSelected: true, + denoisingStrength, + }; + return layer; +}; + +const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { + const control_model = await getProperty(metadataItem, 'control_model'); + const key = await getModelKey(control_model, 'controlnet'); + const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); + const image = zControlField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'image')); + const processedImage = zControlField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'processed_image')); + const control_weight = zControlField.shape.control_weight + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'control_weight')); + const begin_step_percent = zControlField.shape.begin_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'begin_step_percent')); + const end_step_percent = zControlField.shape.end_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'end_step_percent')); + const control_mode = zControlField.shape.control_mode + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'control_mode')); + + const defaultPreprocessor = controlNetModel.default_settings?.preprocessor; + const processorConfig = isProcessorTypeV2(defaultPreprocessor) + ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults() + : null; + const beginEndStepPct: [number, number] = [ + begin_step_percent ?? initialControlNetV2.beginEndStepPct[0], + end_step_percent ?? initialControlNetV2.beginEndStepPct[1], + ]; + const imageDTO = image ? await getImageDTO(image.image_name) : null; + const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null; + + const layer: ControlAdapterLayer = { + id: getCALayerId(uuidv4()), + bbox: null, + bboxNeedsUpdate: true, + isEnabled: true, + isFilterEnabled: true, + isSelected: true, + opacity: 1, + type: 'control_adapter_layer', + x: 0, + y: 0, + controlAdapter: { + id: uuidv4(), + type: 'controlnet', + model: zModelIdentifierField.parse(controlNetModel), + weight: typeof control_weight === 'number' ? control_weight : initialControlNetV2.weight, + beginEndStepPct, + controlMode: control_mode ?? initialControlNetV2.controlMode, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, + processorConfig, + isProcessingImage: false, + }, + }; + + return layer; +}; + +const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { + const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model'); + const key = await getModelKey(t2i_adapter_model, 't2i_adapter'); + const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); + + const image = zT2IAdapterField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'image')); + const processedImage = zT2IAdapterField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'processed_image')); + const weight = zT2IAdapterField.shape.weight + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'weight')); + const begin_step_percent = zT2IAdapterField.shape.begin_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'begin_step_percent')); + const end_step_percent = zT2IAdapterField.shape.end_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'end_step_percent')); + + const defaultPreprocessor = t2iAdapterModel.default_settings?.preprocessor; + const processorConfig = isProcessorTypeV2(defaultPreprocessor) + ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults() + : null; + const beginEndStepPct: [number, number] = [ + begin_step_percent ?? initialT2IAdapterV2.beginEndStepPct[0], + end_step_percent ?? initialT2IAdapterV2.beginEndStepPct[1], + ]; + const imageDTO = image ? await getImageDTO(image.image_name) : null; + const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null; + + const layer: ControlAdapterLayer = { + id: getCALayerId(uuidv4()), + bbox: null, + bboxNeedsUpdate: true, + isEnabled: true, + isFilterEnabled: true, + isSelected: true, + opacity: 1, + type: 'control_adapter_layer', + x: 0, + y: 0, + controlAdapter: { + id: uuidv4(), + type: 't2i_adapter', + model: zModelIdentifierField.parse(t2iAdapterModel), + weight: typeof weight === 'number' ? weight : initialT2IAdapterV2.weight, + beginEndStepPct, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, + processorConfig, + isProcessingImage: false, + }, + }; + + return layer; +}; + +const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async (metadataItem) => { + const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model'); + const key = await getModelKey(ip_adapter_model, 'ip_adapter'); + const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig); + + const image = zIPAdapterField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'image')); + const weight = zIPAdapterField.shape.weight + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'weight')); + const method = zIPAdapterField.shape.method + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'method')); + const begin_step_percent = zIPAdapterField.shape.begin_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'begin_step_percent')); + const end_step_percent = zIPAdapterField.shape.end_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'end_step_percent')); + + const beginEndStepPct: [number, number] = [ + begin_step_percent ?? initialIPAdapterV2.beginEndStepPct[0], + end_step_percent ?? initialIPAdapterV2.beginEndStepPct[1], + ]; + const imageDTO = image ? await getImageDTO(image.image_name) : null; + + const layer: IPAdapterLayer = { + id: getIPALayerId(uuidv4()), + isEnabled: true, + type: 'ip_adapter_layer', + ipAdapter: { + id: uuidv4(), + type: 'ip_adapter', + model: zModelIdentifierField.parse(ipAdapterModel), + weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight, + beginEndStepPct, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... + method: method ?? initialIPAdapterV2.method, + }, + }; + + return layer; +}; //#endregion export const parsers = { @@ -459,7 +727,6 @@ export const parsers = { cfgScale: parseCFGScale, cfgRescaleMultiplier: parseCFGRescaleMultiplier, scheduler: parseScheduler, - initialImage: parseInitialImage, width: parseWidth, height: parseHeight, steps: parseSteps, @@ -484,6 +751,9 @@ export const parsers = { t2iAdapters: parseAllT2IAdapters, ipAdapter: parseIPAdapter, ipAdapters: parseAllIPAdapters, + controlNetToControlLayer: parseControlNetToControlAdapterLayer, + t2iAdapterToControlAdapterLayer: parseT2IAdapterToControlAdapterLayer, + ipAdapterToIPAdapterLayer: parseIPAdapterToIPAdapterLayer, layer: parseLayer, layers: parseLayers, } as const; From d8557d573b8b79aa481891225e1b44b3f8599283 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 19:15:37 +1000 Subject: [PATCH 035/442] Revert "feat(ui): extend zod with a `is` typeguard` method" This reverts commit 0f45933791fc9ea66dac8eda404c4ffef8e049d0. --- invokeai/frontend/web/src/extend-zod.ts | 8 -------- invokeai/frontend/web/src/main.tsx | 2 -- invokeai/frontend/web/src/zod-extensions.d.ts | 8 -------- 3 files changed, 18 deletions(-) delete mode 100644 invokeai/frontend/web/src/extend-zod.ts delete mode 100644 invokeai/frontend/web/src/zod-extensions.d.ts diff --git a/invokeai/frontend/web/src/extend-zod.ts b/invokeai/frontend/web/src/extend-zod.ts deleted file mode 100644 index b1c155062d..0000000000 --- a/invokeai/frontend/web/src/extend-zod.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { assert } from 'tsafe'; -import { z } from 'zod'; - -assert(!Object.hasOwn(z.ZodType.prototype, 'is')); - -z.ZodType.prototype.is = function (val: unknown): val is z.infer { - return this.safeParse(val).success; -}; diff --git a/invokeai/frontend/web/src/main.tsx b/invokeai/frontend/web/src/main.tsx index 129d1bc9e5..acf9491778 100644 --- a/invokeai/frontend/web/src/main.tsx +++ b/invokeai/frontend/web/src/main.tsx @@ -1,5 +1,3 @@ -import 'extend-zod'; - import ReactDOM from 'react-dom/client'; import InvokeAIUI from './app/components/InvokeAIUI'; diff --git a/invokeai/frontend/web/src/zod-extensions.d.ts b/invokeai/frontend/web/src/zod-extensions.d.ts deleted file mode 100644 index 0abab07a19..0000000000 --- a/invokeai/frontend/web/src/zod-extensions.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import 'zod'; - -declare module 'zod' { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - export interface ZodType { - is(val: unknown): val is Output; - } -} From d20695260d2600c47d883ffeb96437f000d12a30 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 19:37:45 +1000 Subject: [PATCH 036/442] feat(ui): open viewer on enqueue from generation tab --- .../listeners/enqueueRequestedLinear.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 557220c449..a3f8f34249 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,5 +1,6 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph'; import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; @@ -11,6 +12,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) enqueueRequested.match(action) && action.payload.tabName === 'generation', effect: async (action, { getState, dispatch }) => { const state = getState(); + const { shouldShowProgressInViewer } = state.ui; const model = state.generation.model; const { prepend } = action.payload; @@ -29,7 +31,14 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) fixedCacheKey: 'enqueueBatch', }) ); - req.reset(); + try { + req.unwrap(); + if (shouldShowProgressInViewer) { + dispatch(isImageViewerOpenChanged(true)); + } + } finally { + req.reset(); + } }, }); }; From 5ca794b94f3ee58ed8592c6e18d7e30e9e097b64 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 19:41:39 +1000 Subject: [PATCH 037/442] feat(ui): show progress toggle on control layers toolbar --- .../controlLayers/components/ControlLayersToolbar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index b087d8dc70..8cc3aa93fe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -4,6 +4,7 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; +import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import { memo } from 'react'; @@ -11,7 +12,9 @@ export const ControlLayersToolbar = memo(() => { return ( - + + + From 6c768bfe7ed6e55e2fab4a04aa7437d91c4bc77d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 19:42:03 +1000 Subject: [PATCH 038/442] fix(ui): viewer toggle prevents progress toggle interaction --- .../gallery/components/ImageViewer/ViewerToggleMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx index c1277d142f..dd4268b208 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx @@ -19,7 +19,7 @@ export const ViewerToggleMenu = () => { const { isOpen, onClose, onOpen } = useImageViewer(); return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index d0d693a5f2..16bf4aa121 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -233,7 +233,14 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { return ( - + ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx index a298ebda56..4bf55116db 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx @@ -35,6 +35,7 @@ export const ToggleMetadataViewerButton = memo(() => { isDisabled={!imageDTO} variant="outline" colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'} + data-testid="toggle-show-metadata-button" /> ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx index 994a8bf10e..ee698130fb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx @@ -22,6 +22,7 @@ export const ToggleProgressButton = memo(() => { onClick={onClick} variant="outline" colorScheme={shouldShowProgressInViewer ? 'invokeBlue' : 'base'} + data-testid="toggle-show-progress-button" /> ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx index dd4268b208..3552c28a5b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx @@ -21,7 +21,7 @@ export const ViewerToggleMenu = () => { return ( - + + + ); }); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index f63e96c45f..c865f3a60f 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -1,10 +1,11 @@ -import { Divider, Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library'; +import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue'; import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; +import type { PropsWithChildren } from 'react'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useEnqueueBatchMutation } from 'services/api/endpoints/queue'; @@ -21,7 +22,15 @@ type Props = { prepend?: boolean; }; -export const QueueButtonTooltip = memo(({ prepend = false }: Props) => { +export const QueueButtonTooltip = (props: PropsWithChildren) => { + return ( + } maxW={512}> + {props.children} + + ); +}; + +const TooltipContent = memo(({ prepend = false }: Props) => { const { t } = useTranslation(); const { isReady, reasons } = useIsReadyToEnqueue(); const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading); @@ -64,8 +73,15 @@ export const QueueButtonTooltip = memo(({ prepend = false }: Props) => { {reasons.map((reason, i) => ( - - {reason} + + + {reason.prefix && ( + + {reason.prefix}:{' '} + + )} + {reason.content} + ))} @@ -82,4 +98,4 @@ export const QueueButtonTooltip = memo(({ prepend = false }: Props) => { ); }); -QueueButtonTooltip.displayName = 'QueueButtonTooltip'; +TooltipContent.displayName = 'QueueButtonTooltipContent'; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueFrontButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueFrontButton.tsx index 07ad0f5b3c..eb0e72950f 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueFrontButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueFrontButton.tsx @@ -10,15 +10,16 @@ const QueueFrontButton = () => { const { t } = useTranslation(); const { queueFront, isLoading, isDisabled } = useQueueFront(); return ( - } - icon={} - size="lg" - /> + + } + size="lg" + /> + ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx index 32611b2354..5a8273b7fc 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx @@ -63,16 +63,17 @@ const FloatingSidePanelButtons = (props: Props) => { sx={floatingButtonStyles} icon={} /> - } - sx={floatingButtonStyles} - /> + + + From 6ff1c7d54182995fd7c005ba0ceea6804640a292 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 10 May 2024 18:23:37 +1000 Subject: [PATCH 066/442] feat(ui): add group by base & type to useGroupedModelCombobox hook This allows comboboxes for models to have more granular groupings. For example, Control Adapter models can be grouped by base model & model type. Before: - `SD-1` - `SDXL` After: - `SD-1 / ControlNet` - `SD-1 / T2I Adapter` - `SDXL / ControlNet` - `SDXL / T2I Adapter` --- .../web/src/common/hooks/useGroupedModelCombobox.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index 55887eb3be..5b57fcd2bb 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -13,6 +13,7 @@ type UseGroupedModelComboboxArg = { onChange: (value: T | null) => void; getIsDisabled?: (model: T) => boolean; isLoading?: boolean; + groupByType?: boolean; }; type UseGroupedModelComboboxReturn = { @@ -23,17 +24,21 @@ type UseGroupedModelComboboxReturn = { noOptionsMessage: () => string; }; +const groupByBaseFunc = (model: T) => model.base.toUpperCase(); +const groupByBaseAndTypeFunc = (model: T) => + `${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`; + export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg ): UseGroupedModelComboboxReturn => { const { t } = useTranslation(); const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl'); - const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading } = arg; + const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg; const options = useMemo[]>(() => { if (!modelConfigs) { return []; } - const groupedModels = groupBy(modelConfigs, 'base'); + const groupedModels = groupBy(modelConfigs, groupByType ? groupByBaseAndTypeFunc : groupByBaseFunc); const _options = reduce( groupedModels, (acc, val, label) => { @@ -49,9 +54,9 @@ export const useGroupedModelCombobox = ( }, [] as GroupBase[] ); - _options.sort((a) => (a.label === base_model ? -1 : 1)); + _options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base_model) ? -1 : 1)); return _options; - }, [getIsDisabled, modelConfigs, base_model]); + }, [modelConfigs, groupByType, getIsDisabled, base_model]); const value = useMemo( () => From 8dd0bfb06840c8406be24e2e05a970fa10331740 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 10 May 2024 18:23:56 +1000 Subject: [PATCH 067/442] feat(ui): use new model type grouping for control adapters in control layers --- .../ControlAndIPAdapter/ControlAdapterModelCombobox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx index a4b1d6b744..535f3067a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx @@ -42,6 +42,7 @@ export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeM selectedModel, getIsDisabled, isLoading, + groupByType: true, }); return ( From 4ea8416c68d67b562d96418c331e51d577afb6ef Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 07:44:33 +1000 Subject: [PATCH 068/442] fix(ui): use pluralization for invoke button tooltip --- invokeai/frontend/web/public/locales/en.json | 11 +++++++---- .../queue/components/QueueButtonTooltip.tsx | 17 +++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ed94dd09f4..7aa4b03b8c 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -261,7 +261,6 @@ "queue": "Queue", "queueFront": "Add to Front of Queue", "queueBack": "Add to Queue", - "queueCountPrediction": "{{promptsCount}} prompts \u00d7 {{iterations}} iterations -> {{count}} generations", "queueEmpty": "Queue Empty", "enqueueing": "Queueing Batch", "resume": "Resume", @@ -314,7 +313,13 @@ "batchFailedToQueue": "Failed to Queue Batch", "graphQueued": "Graph queued", "graphFailedToQueue": "Failed to queue graph", - "openQueue": "Open Queue" + "openQueue": "Open Queue", + "prompts_one": "Prompt", + "prompts_other": "Prompts", + "iterations_one": "Iteration", + "iterations_other": "Iterations", + "generations_one": "Generation", + "generations_other": "Generations" }, "invocationCache": { "invocationCache": "Invocation Cache", @@ -958,8 +963,6 @@ "positivePromptPlaceholder": "Positive Prompt", "globalPositivePromptPlaceholder": "Global Positive Prompt", "iterations": "Iterations", - "iterationsWithCount_one": "{{count}} Iteration", - "iterationsWithCount_other": "{{count}} Iterations", "scale": "Scale", "scaleBeforeProcessing": "Scale Before Processing", "scaledHeight": "Scaled H", diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index c865f3a60f..498414d377 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -35,12 +35,19 @@ const TooltipContent = memo(({ prepend = false }: Props) => { const { isReady, reasons } = useIsReadyToEnqueue(); const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading); const promptsCount = useAppSelector(selectPromptsCount); - const iterations = useAppSelector((s) => s.generation.iterations); + const iterationsCount = useAppSelector((s) => s.generation.iterations); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const autoAddBoardName = useBoardName(autoAddBoardId); const [_, { isLoading }] = useEnqueueBatchMutation({ fixedCacheKey: 'enqueueBatch', }); + const queueCountPredictionLabel = useMemo(() => { + const generationCount = Math.min(promptsCount * iterationsCount, 10000); + const prompts = t('queue.prompts', { count: promptsCount }); + const iterations = t('queue.iterations', { count: iterationsCount }); + const generations = t('queue.generations', { count: generationCount }); + return `${promptsCount} ${prompts} \u00d7 ${iterationsCount} ${iterations} -> ${generationCount} ${generations}`.toLowerCase(); + }, [iterationsCount, promptsCount, t]); const label = useMemo(() => { if (isLoading) { @@ -61,13 +68,7 @@ const TooltipContent = memo(({ prepend = false }: Props) => { return ( {label} - - {t('queue.queueCountPrediction', { - promptsCount, - iterations, - count: Math.min(promptsCount * iterations, 10000), - })} - + {queueCountPredictionLabel} {reasons.length > 0 && ( <> From 124d49f35e68731860c961c60b4fb2219e1acabe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 07:33:58 +1000 Subject: [PATCH 069/442] fix(ui): use translations for canvas layer select --- .../components/IAICanvasToolbar/IAICanvasToolbar.tsx | 12 +++++++++--- .../web/src/features/canvas/store/canvasTypes.ts | 5 ----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx index 686577b4a7..5ed5ffe573 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx @@ -21,7 +21,6 @@ import { setShouldShowBoundingBox, } from 'features/canvas/store/canvasSlice'; import type { CanvasLayer } from 'features/canvas/store/canvasTypes'; -import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes'; import { memo, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -216,13 +215,20 @@ const IAICanvasToolbar = () => { [dispatch, isMaskEnabled] ); - const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]); + const layerOptions = useMemo<{ label: string; value: CanvasLayer }[]>( + () => [ + { label: t('unifiedCanvas.base'), value: 'base' }, + { label: t('unifiedCanvas.mask'), value: 'mask' }, + ], + [t] + ); + const layerValue = useMemo(() => layerOptions.filter((o) => o.value === layer)[0] ?? null, [layer, layerOptions]); return ( - + diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts index 2d30e18760..c41c6f329f 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts @@ -5,11 +5,6 @@ import { z } from 'zod'; export type CanvasLayer = 'base' | 'mask'; -export const LAYER_NAMES_DICT: { label: string; value: CanvasLayer }[] = [ - { label: 'Base', value: 'base' }, - { label: 'Mask', value: 'mask' }, -]; - const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']); export type BoundingBoxScaleMethod = z.infer; export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => From 9cdb801c1c77ef2bca33c4ba5a167586031299e2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 10 May 2024 10:42:34 +1000 Subject: [PATCH 070/442] fix(api): add cover image to update model response Fixes a bug where the image _appears_ to be reset when editing a model. See: https://old.reddit.com/r/StableDiffusion/comments/1cnx40d/invoke_42_control_layers_regional_guidance_w_text/l3asdej/ --- invokeai/app/api/routers/model_manager.py | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index 7bb0f23dc8..2571c50507 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -6,7 +6,7 @@ import pathlib import shutil import traceback from copy import deepcopy -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Type from fastapi import Body, Path, Query, Response, UploadFile from fastapi.responses import FileResponse @@ -52,6 +52,13 @@ class ModelsList(BaseModel): model_config = ConfigDict(use_enum_values=True) +def add_cover_image_to_model_config(config: AnyModelConfig, dependencies: Type[ApiDependencies]) -> AnyModelConfig: + """Add a cover image URL to a model configuration.""" + cover_image = dependencies.invoker.services.model_images.get_url(config.key) + config.cover_image = cover_image + return config + + ############################################################################## # These are example inputs and outputs that are used in places where Swagger # is unable to generate a correct example. @@ -118,8 +125,7 @@ async def list_model_records( record_store.search_by_attr(model_type=model_type, model_name=model_name, model_format=model_format) ) for model in found_models: - cover_image = ApiDependencies.invoker.services.model_images.get_url(model.key) - model.cover_image = cover_image + model = add_cover_image_to_model_config(model, ApiDependencies) return ModelsList(models=found_models) @@ -160,12 +166,9 @@ async def get_model_record( key: str = Path(description="Key of the model record to fetch."), ) -> AnyModelConfig: """Get a model record""" - record_store = ApiDependencies.invoker.services.model_manager.store try: - config: AnyModelConfig = record_store.get_model(key) - cover_image = ApiDependencies.invoker.services.model_images.get_url(key) - config.cover_image = cover_image - return config + config = ApiDependencies.invoker.services.model_manager.store.get_model(key) + return add_cover_image_to_model_config(config, ApiDependencies) except UnknownModelException as e: raise HTTPException(status_code=404, detail=str(e)) @@ -294,14 +297,15 @@ async def update_model_record( installer = ApiDependencies.invoker.services.model_manager.install try: record_store.update_model(key, changes=changes) - model_response: AnyModelConfig = installer.sync_model_path(key) + config = installer.sync_model_path(key) + config = add_cover_image_to_model_config(config, ApiDependencies) logger.info(f"Updated model: {key}") except UnknownModelException as e: raise HTTPException(status_code=404, detail=str(e)) except ValueError as e: logger.error(str(e)) raise HTTPException(status_code=409, detail=str(e)) - return model_response + return config @model_manager_router.get( From 818d37f30469bc81fec634ce11fcd5eee6e12570 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 10 May 2024 11:17:35 +1000 Subject: [PATCH 071/442] fix(api): retain cover image when converting model to diffusers We need to retrieve and re-save the image, because a conversion to diffusers creates a new model record, with a new key. See: https://old.reddit.com/r/StableDiffusion/comments/1cnx40d/invoke_42_control_layers_regional_guidance_w_text/l3bv152/ --- invokeai/app/api/routers/model_manager.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index 2571c50507..1ba3e30e07 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -16,6 +16,7 @@ from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field from starlette.exceptions import HTTPException from typing_extensions import Annotated +from invokeai.app.services.model_images.model_images_common import ModelImageFileNotFoundException from invokeai.app.services.model_install import ModelInstallJob from invokeai.app.services.model_records import ( DuplicateModelException, @@ -652,6 +653,14 @@ async def convert_model( logger.error(str(e)) raise HTTPException(status_code=409, detail=str(e)) + # Update the model image if the model had one + try: + model_image = ApiDependencies.invoker.services.model_images.get(key) + ApiDependencies.invoker.services.model_images.save(model_image, new_key) + ApiDependencies.invoker.services.model_images.delete(key) + except ModelImageFileNotFoundException: + pass + # delete the original safetensors file installer.delete(key) @@ -659,7 +668,8 @@ async def convert_model( shutil.rmtree(cache_path) # return the config record for the new diffusers directory - new_config: AnyModelConfig = store.get_model(new_key) + new_config = store.get_model(new_key) + new_config = add_cover_image_to_model_config(new_config, ApiDependencies) return new_config From eb166baafe761189d7fee8414056ebe912841505 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 11:39:39 +1000 Subject: [PATCH 072/442] fix(ui): invoke button shows loading while queueing Make the Invoke button show a loading spinner while queueing. The queue mutations need to be awaited else the `isLoading` state doesn't work as expected. I feel like I should understand why, but I don't... --- .../listenerMiddleware/listeners/enqueueRequestedLinear.ts | 2 +- .../listenerMiddleware/listeners/enqueueRequestedNodes.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index a3f8f34249..2d267b92b2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -32,7 +32,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) }) ); try { - req.unwrap(); + await req.unwrap(); if (shouldShowProgressInViewer) { dispatch(isImageViewerOpenChanged(true)); } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 8d39daaef8..12741c52f5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -39,7 +39,11 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = fixedCacheKey: 'enqueueBatch', }) ); - req.reset(); + try { + await req.unwrap(); + } finally { + req.reset(); + } }, }); }; From b118a2565cbf937314930511cf58435da2f7f5a9 Mon Sep 17 00:00:00 2001 From: Riccardo Giovanetti Date: Mon, 13 May 2024 22:40:25 +0200 Subject: [PATCH 073/442] translationBot(ui): update translation (Italian) Currently translated at 96.0% (1138 of 1185 strings) translationBot(ui): update translation (Italian) Currently translated at 98.4% (1156 of 1174 strings) translationBot(ui): update translation (Italian) Currently translated at 98.3% (1155 of 1174 strings) translationBot(ui): update translation (Italian) Currently translated at 98.4% (1129 of 1147 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 54 +++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 491b31907b..363052e86a 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -74,7 +74,12 @@ "file": "File", "toResolve": "Da risolvere", "add": "Aggiungi", - "loglevel": "Livello di log" + "loglevel": "Livello di log", + "beta": "Beta", + "positivePrompt": "Prompt positivo", + "negativePrompt": "Prompt negativo", + "selected": "Selezionato", + "viewer": "Vista principale" }, "gallery": { "galleryImageSize": "Dimensione dell'immagine", @@ -534,7 +539,8 @@ "infillMosaicMinColor": "Colore minimo", "infillMosaicMaxColor": "Colore massimo", "infillMosaicTileHeight": "Altezza piastrella", - "infillColorValue": "Colore di riempimento" + "infillColorValue": "Colore di riempimento", + "globalSettings": "Impostazioni globali" }, "settings": { "models": "Modelli", @@ -795,7 +801,7 @@ "float": "In virgola mobile", "currentImageDescription": "Visualizza l'immagine corrente nell'editor dei nodi", "fieldTypesMustMatch": "I tipi di campo devono corrispondere", - "edge": "Bordo", + "edge": "Collegamento", "currentImage": "Immagine corrente", "integer": "Numero Intero", "inputMayOnlyHaveOneConnection": "L'ingresso può avere solo una connessione", @@ -845,7 +851,9 @@ "resetToDefaultValue": "Ripristina il valore predefinito", "noFieldsViewMode": "Questo flusso di lavoro non ha campi selezionati da visualizzare. Visualizza il flusso di lavoro completo per configurare i valori.", "edit": "Modifica", - "graph": "Grafico" + "graph": "Grafico", + "showEdgeLabelsHelp": "Mostra etichette sui collegamenti, che indicano i nodi collegati", + "showEdgeLabels": "Mostra le etichette del collegamento" }, "boards": { "autoAddBoard": "Aggiungi automaticamente bacheca", @@ -922,7 +930,7 @@ "colorMapTileSize": "Dimensione piastrella", "mediapipeFaceDescription": "Rilevamento dei volti tramite Mediapipe", "hedDescription": "Rilevamento dei bordi nidificati olisticamente", - "setControlImageDimensions": "Imposta le dimensioni dell'immagine di controllo su L/A", + "setControlImageDimensions": "Copia le dimensioni in L/A (ottimizza per il modello)", "maxFaces": "Numero massimo di volti", "addT2IAdapter": "Aggiungi $t(common.t2iAdapter)", "addControlNet": "Aggiungi $t(common.controlNet)", @@ -951,7 +959,13 @@ "mediapipeFace": "Mediapipe Volto", "ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))", "t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))", - "selectCLIPVisionModel": "Seleziona un modello CLIP Vision" + "selectCLIPVisionModel": "Seleziona un modello CLIP Vision", + "ipAdapterMethod": "Metodo", + "full": "Completo", + "composition": "Solo la composizione", + "style": "Solo lo stile", + "beginEndStepPercentShort": "Inizio/Fine %", + "setControlImageDimensionsForce": "Copia le dimensioni in L/A (ignora il modello)" }, "queue": { "queueFront": "Aggiungi all'inizio della coda", @@ -1394,6 +1408,12 @@ "paragraphs": [ "La dimensione del bordo del passaggio di coerenza." ] + }, + "ipAdapterMethod": { + "heading": "Metodo", + "paragraphs": [ + "Metodo con cui applicare l'adattatore IP corrente." + ] } }, "sdxl": { @@ -1522,5 +1542,27 @@ "compatibleEmbeddings": "Incorporamenti compatibili", "addPromptTrigger": "Aggiungi Trigger nel prompt", "noMatchingTriggers": "Nessun Trigger corrispondente" + }, + "regionalPrompts": { + "moveForward": "Sposta avanti", + "globalMaskOpacity": "Opacità globale maschera", + "autoNegative": "Auto Negativo", + "toggleVisibility": "Attiva/disattiva la visibilità del livello", + "deletePrompt": "Elimina il prompt", + "resetRegion": "Reimposta l'area", + "debugLayers": "Debug livelli", + "maskPreviewColor": "Colore anteprima maschera", + "addPositivePrompt": "Aggiungi $t(common.positivePrompt)", + "deleteAll": "Cancella tutto", + "addLayer": "Aggiungi livello", + "moveToFront": "Sposta in primo piano", + "moveToBack": "Sposta in fondo", + "moveBackward": "Sposta dietro", + "brushSize": "Dimensione del pennello", + "regionalControl": "Controllo Regionale (ALPHA)", + "enableRegionalPrompts": "Abilita $t(regionalPrompts.regionalPrompts)", + "rectangle": "Rettangolo", + "addIPAdapter": "Aggiungi $t(common.ipAdapter)", + "addNegativePrompt": "Aggiungi $t(common.negativePrompt)" } } From 1de704160ead5a7032f7dc6b6896620d3db3fa50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D1=8F=D0=BD=D0=B0=D1=82=D0=BE=D1=80?= Date: Mon, 13 May 2024 22:40:25 +0200 Subject: [PATCH 074/442] translationBot(ui): update translation (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 97.3% (1154 of 1185 strings) translationBot(ui): update translation (Russian) Currently translated at 100.0% (1174 of 1174 strings) translationBot(ui): update translation (Russian) Currently translated at 100.0% (1173 of 1173 strings) translationBot(ui): update translation (Russian) Currently translated at 100.0% (1166 of 1166 strings) translationBot(ui): update translation (Russian) Currently translated at 100.0% (1165 of 1165 strings) translationBot(ui): update translation (Russian) Currently translated at 100.0% (1149 of 1149 strings) translationBot(ui): update translation (Russian) Currently translated at 100.0% (1147 of 1147 strings) Co-authored-by: Васянатор Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/ru.json | 71 +++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index f254b7faa5..8c14381932 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -76,7 +76,12 @@ "localSystem": "Локальная система", "aboutDesc": "Используя Invoke для работы? Проверьте это:", "add": "Добавить", - "loglevel": "Уровень логов" + "loglevel": "Уровень логов", + "beta": "Бета", + "selected": "Выбрано", + "viewer": "Просмотрщик", + "positivePrompt": "Позитивный запрос", + "negativePrompt": "Негативный запрос" }, "gallery": { "galleryImageSize": "Размер изображений", @@ -87,8 +92,8 @@ "deleteImagePermanent": "Удаленные изображения невозможно восстановить.", "deleteImageBin": "Удаленные изображения будут отправлены в корзину вашей операционной системы.", "deleteImage_one": "Удалить изображение", - "deleteImage_few": "", - "deleteImage_many": "", + "deleteImage_few": "Удалить {{count}} изображения", + "deleteImage_many": "Удалить {{count}} изображений", "assets": "Ресурсы", "autoAssignBoardOnClick": "Авто-назначение доски по клику", "deleteSelection": "Удалить выделенное", @@ -541,7 +546,8 @@ "infillMosaicTileHeight": "Высота плиток", "infillMosaicMinColor": "Мин цвет", "infillMosaicMaxColor": "Макс цвет", - "infillColorValue": "Цвет заливки" + "infillColorValue": "Цвет заливки", + "globalSettings": "Глобальные настройки" }, "settings": { "models": "Модели", @@ -706,7 +712,9 @@ "coherenceModeBoxBlur": "коробчатое размытие", "discardCurrent": "Отбросить текущее", "invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти", - "initialFitImageSize": "Подогнать размер изображения при перебросе" + "initialFitImageSize": "Подогнать размер изображения при перебросе", + "hideBoundingBox": "Скрыть ограничительную рамку", + "showBoundingBox": "Показать ограничительную рамку" }, "accessibility": { "uploadImage": "Загрузить изображение", @@ -849,7 +857,10 @@ "editMode": "Открыть в редакторе узлов", "resetToDefaultValue": "Сбросить к стандартному значкнию", "edit": "Редактировать", - "noFieldsViewMode": "В этом рабочем процессе нет выбранных полей для отображения. Просмотрите полный рабочий процесс для настройки значений." + "noFieldsViewMode": "В этом рабочем процессе нет выбранных полей для отображения. Просмотрите полный рабочий процесс для настройки значений.", + "graph": "График", + "showEdgeLabels": "Показать метки на ребрах", + "showEdgeLabelsHelp": "Показать метки на ребрах, указывающие на соединенные узлы" }, "controlnet": { "amult": "a_mult", @@ -933,7 +944,17 @@ "small": "Маленький", "body": "Тело", "hands": "Руки", - "selectCLIPVisionModel": "Выбрать модель CLIP Vision" + "selectCLIPVisionModel": "Выбрать модель CLIP Vision", + "ipAdapterMethod": "Метод", + "full": "Всё", + "mlsd": "M-LSD", + "h": "H", + "style": "Только стиль", + "dwOpenpose": "DW Openpose", + "pidi": "PIDI", + "composition": "Только композиция", + "hed": "HED", + "beginEndStepPercentShort": "Начало/конец %" }, "boards": { "autoAddBoard": "Авто добавление Доски", @@ -1312,6 +1333,12 @@ "paragraphs": [ "Плавно укладывайте изображение вдоль вертикальной оси." ] + }, + "ipAdapterMethod": { + "heading": "Метод", + "paragraphs": [ + "Метод, с помощью которого применяется текущий IP-адаптер." + ] } }, "metadata": { @@ -1475,7 +1502,11 @@ "projectWorkflows": "Рабочие процессы проекта", "defaultWorkflows": "Стандартные рабочие процессы", "name": "Имя", - "noRecentWorkflows": "Нет последних рабочих процессов" + "noRecentWorkflows": "Нет последних рабочих процессов", + "loadWorkflow": "Рабочий процесс $t(common.load)", + "convertGraph": "Конвертировать график", + "loadFromGraph": "Загрузка рабочего процесса из графика", + "autoLayout": "Автоматическое расположение" }, "hrf": { "enableHrf": "Включить исправление высокого разрешения", @@ -1528,5 +1559,29 @@ "addPromptTrigger": "Добавить триггер запроса", "compatibleEmbeddings": "Совместимые встраивания", "noMatchingTriggers": "Нет соответствующих триггеров" + }, + "regionalPrompts": { + "deleteAll": "Удалить все", + "addLayer": "Добавить слой", + "moveToFront": "На передний план", + "moveBackward": "Назад", + "enableRegionalPrompts": "Включить $t(regionalPrompts.regionalPrompts)", + "debugLayers": "Слои отладки", + "moveToBack": "На задний план", + "moveForward": "Вперёд", + "brushSize": "Размер кисти", + "regionalPrompts": "Региональные запросы (Бета)", + "layerOpacity": "Непрозрачность слоя", + "autoNegative": "Автонегатив", + "toggleVisibility": "Переключение видимости слоя", + "resetRegion": "Сбросить регион", + "rectangle": "Прямоугольник", + "globalMaskOpacity": "Прозрачность глобальной маски", + "deletePrompt": "Удалить запрос", + "maskPreviewColor": "Цвет предпросмотра маски", + "addPositivePrompt": "Добавить $t(common.positivePrompt)", + "addNegativePrompt": "Добавить $t(common.negativePrompt)", + "addIPAdapter": "Добавить $t(common.ipAdapter)", + "regionalControl": "Региональный контроль (АЛЬФА)" } } From 63d7461510089339f84065ceda859d45ec93ced5 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Mon, 13 May 2024 22:40:26 +0200 Subject: [PATCH 075/442] translationBot(ui): update translation (German) Currently translated at 71.9% (839 of 1166 strings) Co-authored-by: Alexander Eichhorn Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 22 ++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 0a104c083b..15b99d6f3d 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -76,7 +76,10 @@ "aboutHeading": "Nutzen Sie Ihre kreative Energie", "toResolve": "Lösen", "add": "Hinzufügen", - "loglevel": "Protokoll Stufe" + "loglevel": "Protokoll Stufe", + "selected": "Ausgewählt", + "viewer": "Betrachter", + "beta": "Beta" }, "gallery": { "galleryImageSize": "Bildgröße", @@ -86,7 +89,7 @@ "noImagesInGallery": "Keine Bilder in der Galerie", "loading": "Lade", "deleteImage_one": "Lösche Bild", - "deleteImage_other": "", + "deleteImage_other": "Lösche {{count}} Bilder", "copy": "Kopieren", "download": "Runterladen", "setCurrentImage": "Setze aktuelle Bild", @@ -397,7 +400,14 @@ "cancel": "Stornieren", "defaultSettingsSaved": "Standardeinstellungen gespeichert", "addModels": "Model hinzufügen", - "deleteModelImage": "Lösche Model Bild" + "deleteModelImage": "Lösche Model Bild", + "hfTokenInvalidErrorMessage": "Falscher oder fehlender HuggingFace Schlüssel.", + "huggingFaceRepoID": "HuggingFace Repo ID", + "hfToken": "HuggingFace Schlüssel", + "hfTokenInvalid": "Falscher oder fehlender HF Schlüssel", + "huggingFacePlaceholder": "besitzer/model-name", + "hfTokenSaved": "HF Schlüssel gespeichert", + "hfTokenUnableToVerify": "Konnte den HF Schlüssel nicht validieren" }, "parameters": { "images": "Bilder", @@ -686,7 +696,11 @@ "hands": "Hände", "dwOpenpose": "DW Openpose", "dwOpenposeDescription": "Posenschätzung mit DW Openpose", - "selectCLIPVisionModel": "Wähle ein CLIP Vision Model aus" + "selectCLIPVisionModel": "Wähle ein CLIP Vision Model aus", + "ipAdapterMethod": "Methode", + "composition": "Nur Komposition", + "full": "Voll", + "style": "Nur Style" }, "queue": { "status": "Status", From f7834d7d59fb5b0cd263c018268f56209f9785e4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 13 May 2024 22:40:26 +0200 Subject: [PATCH 076/442] translationBot(ui): update translation files Updated by "Cleanup translation files" hook in Weblate. translationBot(ui): update translation files Updated by "Cleanup translation files" hook in Weblate. translationBot(ui): update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 22 ------------------ invokeai/frontend/web/public/locales/ru.json | 24 -------------------- 2 files changed, 46 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 363052e86a..b47ad876a0 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -1542,27 +1542,5 @@ "compatibleEmbeddings": "Incorporamenti compatibili", "addPromptTrigger": "Aggiungi Trigger nel prompt", "noMatchingTriggers": "Nessun Trigger corrispondente" - }, - "regionalPrompts": { - "moveForward": "Sposta avanti", - "globalMaskOpacity": "Opacità globale maschera", - "autoNegative": "Auto Negativo", - "toggleVisibility": "Attiva/disattiva la visibilità del livello", - "deletePrompt": "Elimina il prompt", - "resetRegion": "Reimposta l'area", - "debugLayers": "Debug livelli", - "maskPreviewColor": "Colore anteprima maschera", - "addPositivePrompt": "Aggiungi $t(common.positivePrompt)", - "deleteAll": "Cancella tutto", - "addLayer": "Aggiungi livello", - "moveToFront": "Sposta in primo piano", - "moveToBack": "Sposta in fondo", - "moveBackward": "Sposta dietro", - "brushSize": "Dimensione del pennello", - "regionalControl": "Controllo Regionale (ALPHA)", - "enableRegionalPrompts": "Abilita $t(regionalPrompts.regionalPrompts)", - "rectangle": "Rettangolo", - "addIPAdapter": "Aggiungi $t(common.ipAdapter)", - "addNegativePrompt": "Aggiungi $t(common.negativePrompt)" } } diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index 8c14381932..c886266e27 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -1559,29 +1559,5 @@ "addPromptTrigger": "Добавить триггер запроса", "compatibleEmbeddings": "Совместимые встраивания", "noMatchingTriggers": "Нет соответствующих триггеров" - }, - "regionalPrompts": { - "deleteAll": "Удалить все", - "addLayer": "Добавить слой", - "moveToFront": "На передний план", - "moveBackward": "Назад", - "enableRegionalPrompts": "Включить $t(regionalPrompts.regionalPrompts)", - "debugLayers": "Слои отладки", - "moveToBack": "На задний план", - "moveForward": "Вперёд", - "brushSize": "Размер кисти", - "regionalPrompts": "Региональные запросы (Бета)", - "layerOpacity": "Непрозрачность слоя", - "autoNegative": "Автонегатив", - "toggleVisibility": "Переключение видимости слоя", - "resetRegion": "Сбросить регион", - "rectangle": "Прямоугольник", - "globalMaskOpacity": "Прозрачность глобальной маски", - "deletePrompt": "Удалить запрос", - "maskPreviewColor": "Цвет предпросмотра маски", - "addPositivePrompt": "Добавить $t(common.positivePrompt)", - "addNegativePrompt": "Добавить $t(common.negativePrompt)", - "addIPAdapter": "Добавить $t(common.ipAdapter)", - "regionalControl": "Региональный контроль (АЛЬФА)" } } From fa832a8ac6820a6431ebfe7a1059b5f07c44735b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D1=8F=D0=BD=D0=B0=D1=82=D0=BE=D1=80?= Date: Mon, 13 May 2024 22:40:26 +0200 Subject: [PATCH 077/442] translationBot(ui): update translation (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (1209 of 1209 strings) translationBot(ui): update translation (Russian) Currently translated at 100.0% (1209 of 1209 strings) translationBot(ui): update translation (Russian) Currently translated at 100.0% (1188 of 1188 strings) translationBot(ui): update translation (Russian) Currently translated at 100.0% (1185 of 1185 strings) Co-authored-by: Васянатор Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/ru.json | 76 ++++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index c886266e27..c764fdfec0 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -81,7 +81,10 @@ "selected": "Выбрано", "viewer": "Просмотрщик", "positivePrompt": "Позитивный запрос", - "negativePrompt": "Негативный запрос" + "negativePrompt": "Негативный запрос", + "editor": "Редактор", + "goTo": "Перейти к", + "tab": "Вкладка" }, "gallery": { "galleryImageSize": "Размер изображений", @@ -118,7 +121,8 @@ "bulkDownloadRequested": "Подготовка к скачиванию", "bulkDownloadRequestedDesc": "Ваш запрос на скачивание готовится. Это может занять несколько минут.", "bulkDownloadRequestFailed": "Возникла проблема при подготовке скачивания", - "alwaysShowImageSizeBadge": "Всегда показывать значок размера изображения" + "alwaysShowImageSizeBadge": "Всегда показывать значок размера изображения", + "switchTo": "Переключится на {{ tab }} (Z)" }, "hotkeys": { "keyboardShortcuts": "Горячие клавиши", @@ -341,6 +345,14 @@ "remixImage": { "desc": "Используйте все параметры, кроме сида из текущего изображения", "title": "Ремикс изображения" + }, + "backToEditor": { + "title": "Вернуться к редактору", + "desc": "Закрывает просмотрщик изображений и показывает окно редактора (только на вкладке \"Текст в изображение\")" + }, + "openImageViewer": { + "title": "Открыть просмотрщик изображений", + "desc": "Открывает просмотрщик изображений (только на вкладке \"Текст в изображение\")" } }, "modelManager": { @@ -517,7 +529,8 @@ "missingNodeTemplate": "Отсутствует шаблон узла", "missingFieldTemplate": "Отсутствует шаблон поля", "addingImagesTo": "Добавление изображений в", - "invoke": "Создать" + "invoke": "Создать", + "imageNotProcessedForControlAdapter": "Изображение адаптера контроля №{{number}} не обрабатывается" }, "isAllowedToUpscale": { "useX2Model": "Изображение слишком велико для увеличения с помощью модели x4. Используйте модель x2", @@ -928,8 +941,8 @@ "lineartAnime": "Контурный рисунок в стиле аниме", "mediapipeFaceDescription": "Обнаружение лиц с помощью Mediapipe", "hedDescription": "Целостное обнаружение границ", - "setControlImageDimensions": "Установите размеры контрольного изображения на Ш/В", - "scribble": "каракули", + "setControlImageDimensions": "Скопируйте размер в Ш/В (оптимизируйте для модели)", + "scribble": "Штрихи", "maxFaces": "Макс Лица", "mlsdDescription": "Минималистичный детектор отрезков линии", "resizeSimple": "Изменить размер (простой)", @@ -954,7 +967,8 @@ "pidi": "PIDI", "composition": "Только композиция", "hed": "HED", - "beginEndStepPercentShort": "Начало/конец %" + "beginEndStepPercentShort": "Начало/конец %", + "setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)" }, "boards": { "autoAddBoard": "Авто добавление Доски", @@ -1559,5 +1573,55 @@ "addPromptTrigger": "Добавить триггер запроса", "compatibleEmbeddings": "Совместимые встраивания", "noMatchingTriggers": "Нет соответствующих триггеров" + }, + "controlLayers": { + "moveToBack": "На задний план", + "moveForward": "Переместить вперёд", + "moveBackward": "Переместить назад", + "brushSize": "Размер кисти", + "controlLayers": "Слои управления", + "globalMaskOpacity": "Глобальная непрозрачность маски", + "autoNegative": "Авто негатив", + "deletePrompt": "Удалить запрос", + "resetRegion": "Сбросить регион", + "debugLayers": "Слои отладки", + "rectangle": "Прямоугольник", + "maskPreviewColor": "Цвет предпросмотра маски", + "addNegativePrompt": "Добавить $t(common.negativePrompt)", + "regionalGuidance": "Региональная точность", + "controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)", + "ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)", + "opacity": "Непрозрачность", + "globalControlAdapter": "Глобальный $t(controlnet.controlAdapter_one)", + "globalControlAdapterLayer": "Глобальный $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", + "globalIPAdapter": "Глобальный $t(common.ipAdapter)", + "globalIPAdapterLayer": "Глобальный $t(common.ipAdapter) $t(unifiedCanvas.layer)", + "opacityFilter": "Фильтр непрозрачности", + "deleteAll": "Удалить всё", + "addLayer": "Добавить слой", + "moveToFront": "На передний план", + "toggleVisibility": "Переключить видимость слоя", + "addPositivePrompt": "Добавить $t(common.positivePrompt)", + "addIPAdapter": "Добавить $t(common.ipAdapter)", + "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", + "resetProcessor": "Сброс процессора по умолчанию", + "clearProcessor": "Чистый процессор", + "globalInitialImage": "Глобальное исходное изображение", + "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", + "noLayersAdded": "Без слоев" + }, + "ui": { + "tabs": { + "generation": "Генерация", + "canvas": "Холст", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "models": "Модели", + "generationTab": "$t(ui.tabs.generation) $t(common.tab)", + "workflows": "Рабочие процессы", + "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", + "queueTab": "$t(ui.tabs.queue) $t(common.tab)", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "queue": "Очередь" + } } } From 60e77e4ed6ba152266720ddc394d7ba172e17cda Mon Sep 17 00:00:00 2001 From: flower_elf Date: Mon, 13 May 2024 22:40:26 +0200 Subject: [PATCH 078/442] translationBot(ui): update translation (Chinese (Simplified)) Currently translated at 77.8% (922 of 1185 strings) Co-authored-by: flower_elf Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/zh_CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/public/locales/zh_CN.json b/invokeai/frontend/web/public/locales/zh_CN.json index 8aff73d2a1..3b0cd4f81b 100644 --- a/invokeai/frontend/web/public/locales/zh_CN.json +++ b/invokeai/frontend/web/public/locales/zh_CN.json @@ -66,7 +66,7 @@ "saveAs": "保存为", "ai": "ai", "or": "或", - "aboutDesc": "使用 Invoke 工作?查看:", + "aboutDesc": "使用 Invoke 工作?来看看:", "add": "添加", "loglevel": "日志级别", "copy": "复制", From 15c9a3a4b6cc62f4570f542118250da182aa39cb Mon Sep 17 00:00:00 2001 From: Riccardo Giovanetti Date: Mon, 13 May 2024 22:40:26 +0200 Subject: [PATCH 079/442] translationBot(ui): update translation (Italian) Currently translated at 98.3% (1189 of 1209 strings) translationBot(ui): update translation (Italian) Currently translated at 98.3% (1189 of 1209 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 65 +++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index b47ad876a0..9cf0250a82 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -79,7 +79,10 @@ "positivePrompt": "Prompt positivo", "negativePrompt": "Prompt negativo", "selected": "Selezionato", - "viewer": "Vista principale" + "viewer": "Visualizzatore", + "goTo": "Vai a", + "editor": "Editor", + "tab": "Scheda" }, "gallery": { "galleryImageSize": "Dimensione dell'immagine", @@ -116,7 +119,8 @@ "bulkDownloadRequestedDesc": "La tua richiesta di download è in preparazione. L'operazione potrebbe richiedere alcuni istanti.", "bulkDownloadRequestFailed": "Problema durante la preparazione del download", "bulkDownloadFailed": "Scaricamento fallito", - "alwaysShowImageSizeBadge": "Mostra sempre le dimensioni dell'immagine" + "alwaysShowImageSizeBadge": "Mostra sempre le dimensioni dell'immagine", + "switchTo": "Passa a {{ tab }} (Z)" }, "hotkeys": { "keyboardShortcuts": "Tasti di scelta rapida", @@ -339,6 +343,10 @@ "remixImage": { "desc": "Utilizza tutti i parametri tranne il seme dell'immagine corrente", "title": "Remixa l'immagine" + }, + "backToEditor": { + "desc": "Chiude il Visualizzatore immagini e mostra la vista Editor corrente", + "title": "Torna all'editor corrente" } }, "modelManager": { @@ -513,7 +521,8 @@ "incompatibleBaseModelForControlAdapter": "Il modello dell'adattatore di controllo #{{number}} non è compatibile con il modello principale.", "missingNodeTemplate": "Modello di nodo mancante", "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} ingresso mancante", - "missingFieldTemplate": "Modello di campo mancante" + "missingFieldTemplate": "Modello di campo mancante", + "imageNotProcessedForControlAdapter": "L'immagine dell'adattatore di controllo #{{number}} non è stata elaborata" }, "useCpuNoise": "Usa la CPU per generare rumore", "iterations": "Iterazioni", @@ -1542,5 +1551,55 @@ "compatibleEmbeddings": "Incorporamenti compatibili", "addPromptTrigger": "Aggiungi Trigger nel prompt", "noMatchingTriggers": "Nessun Trigger corrispondente" + }, + "controlLayers": { + "opacityFilter": "Filtro opacità", + "deleteAll": "Cancella tutto", + "addLayer": "Aggiungi Livello", + "moveToFront": "Sposta in primo piano", + "moveToBack": "Sposta in fondo", + "moveForward": "Sposta in avanti", + "moveBackward": "Sposta indietro", + "brushSize": "Dimensioni del pennello", + "globalMaskOpacity": "Opacità globale della maschera", + "autoNegative": "Auto Negativo", + "toggleVisibility": "Attiva/disattiva la visibilità dei livelli", + "deletePrompt": "Cancella il prompt", + "debugLayers": "Debug dei Livelli", + "rectangle": "Rettangolo", + "maskPreviewColor": "Colore anteprima maschera", + "addPositivePrompt": "Aggiungi $t(common.positivePrompt)", + "addNegativePrompt": "Aggiungi $t(common.negativePrompt)", + "addIPAdapter": "Aggiungi $t(common.ipAdapter)", + "regionalGuidance": "Guida regionale", + "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", + "controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)", + "ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)", + "opacity": "Opacità", + "globalControlAdapter": "$t(controlnet.controlAdapter_one) Globale", + "globalControlAdapterLayer": "$t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer) Globale", + "globalIPAdapter": "$t(common.ipAdapter) Globale", + "globalIPAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer) Globale", + "globalInitialImage": "Immagine iniziale globale", + "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", + "clearProcessor": "Cancella processore", + "resetProcessor": "Ripristina il processore alle impostazioni predefinite", + "noLayersAdded": "Nessun livello aggiunto", + "resetRegion": "Reimposta la regione", + "controlLayers": "Livelli di controllo" + }, + "ui": { + "tabs": { + "generation": "Generazione", + "generationTab": "$t(ui.tabs.generation) $t(common.tab)", + "canvas": "Tela", + "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", + "workflows": "Flussi di lavoro", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "models": "Modelli", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "queue": "Coda", + "queueTab": "$t(ui.tabs.queue) $t(common.tab)" + } } } From 3b495659b0aa37c3baa954775eb4c25779eba1d6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 13 May 2024 22:40:27 +0200 Subject: [PATCH 080/442] translationBot(ui): update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 4 ---- invokeai/frontend/web/public/locales/ru.json | 8 -------- 2 files changed, 12 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 9cf0250a82..76c7047b44 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -343,10 +343,6 @@ "remixImage": { "desc": "Utilizza tutti i parametri tranne il seme dell'immagine corrente", "title": "Remixa l'immagine" - }, - "backToEditor": { - "desc": "Chiude il Visualizzatore immagini e mostra la vista Editor corrente", - "title": "Torna all'editor corrente" } }, "modelManager": { diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index c764fdfec0..7cc876391a 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -345,14 +345,6 @@ "remixImage": { "desc": "Используйте все параметры, кроме сида из текущего изображения", "title": "Ремикс изображения" - }, - "backToEditor": { - "title": "Вернуться к редактору", - "desc": "Закрывает просмотрщик изображений и показывает окно редактора (только на вкладке \"Текст в изображение\")" - }, - "openImageViewer": { - "title": "Открыть просмотрщик изображений", - "desc": "Открывает просмотрщик изображений (только на вкладке \"Текст в изображение\")" } }, "modelManager": { From 11d88dae7faa6eccb82f0d7519e6deed86612ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D1=8F=D0=BD=D0=B0=D1=82=D0=BE=D1=80?= Date: Mon, 13 May 2024 22:40:27 +0200 Subject: [PATCH 081/442] translationBot(ui): update translation (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (1210 of 1210 strings) Co-authored-by: Васянатор Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/ru.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index 7cc876391a..a7f05e2a7a 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -84,7 +84,8 @@ "negativePrompt": "Негативный запрос", "editor": "Редактор", "goTo": "Перейти к", - "tab": "Вкладка" + "tab": "Вкладка", + "close": "Закрыть" }, "gallery": { "galleryImageSize": "Размер изображений", @@ -122,7 +123,9 @@ "bulkDownloadRequestedDesc": "Ваш запрос на скачивание готовится. Это может занять несколько минут.", "bulkDownloadRequestFailed": "Возникла проблема при подготовке скачивания", "alwaysShowImageSizeBadge": "Всегда показывать значок размера изображения", - "switchTo": "Переключится на {{ tab }} (Z)" + "switchTo": "Переключится на {{ tab }} (Z)", + "openFloatingViewer": "Открыть плавающий просмотрщик", + "closeFloatingViewer": "Закрыть плавающий просмотрщик" }, "hotkeys": { "keyboardShortcuts": "Горячие клавиши", @@ -345,6 +348,10 @@ "remixImage": { "desc": "Используйте все параметры, кроме сида из текущего изображения", "title": "Ремикс изображения" + }, + "toggleViewer": { + "title": "Переключить просмотр изображений", + "desc": "Переключение между средством просмотра изображений и рабочей областью для текущей вкладки." } }, "modelManager": { From c5fd08125dc85af268082f17f4c7576f8b097991 Mon Sep 17 00:00:00 2001 From: Riccardo Giovanetti Date: Mon, 13 May 2024 22:40:27 +0200 Subject: [PATCH 082/442] translationBot(ui): update translation (Italian) Currently translated at 98.5% (1192 of 1210 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 76c7047b44..15a69e7549 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -82,7 +82,8 @@ "viewer": "Visualizzatore", "goTo": "Vai a", "editor": "Editor", - "tab": "Scheda" + "tab": "Scheda", + "close": "Chiudi" }, "gallery": { "galleryImageSize": "Dimensione dell'immagine", @@ -120,7 +121,9 @@ "bulkDownloadRequestFailed": "Problema durante la preparazione del download", "bulkDownloadFailed": "Scaricamento fallito", "alwaysShowImageSizeBadge": "Mostra sempre le dimensioni dell'immagine", - "switchTo": "Passa a {{ tab }} (Z)" + "switchTo": "Passa a {{ tab }} (Z)", + "openFloatingViewer": "Apri il visualizzatore mobile", + "closeFloatingViewer": "Chiudi il visualizzatore mobile" }, "hotkeys": { "keyboardShortcuts": "Tasti di scelta rapida", @@ -343,6 +346,10 @@ "remixImage": { "desc": "Utilizza tutti i parametri tranne il seme dell'immagine corrente", "title": "Remixa l'immagine" + }, + "toggleViewer": { + "title": "Attiva/disattiva il visualizzatore di immagini", + "desc": "Passa dal Visualizzatore immagini all'area di lavoro per la scheda corrente." } }, "modelManager": { From 92658413846187a9754838dbe24b3ed50158e961 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 13 May 2024 22:40:27 +0200 Subject: [PATCH 083/442] translationBot(ui): update translation files Updated by "Cleanup translation files" hook in Weblate. translationBot(ui): update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 2 -- invokeai/frontend/web/public/locales/it.json | 15 ++------------- invokeai/frontend/web/public/locales/ja.json | 1 - invokeai/frontend/web/public/locales/ko.json | 1 - invokeai/frontend/web/public/locales/nl.json | 3 --- invokeai/frontend/web/public/locales/ru.json | 15 ++------------- invokeai/frontend/web/public/locales/zh_CN.json | 2 -- 7 files changed, 4 insertions(+), 35 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 15b99d6f3d..1db283aabd 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -78,7 +78,6 @@ "add": "Hinzufügen", "loglevel": "Protokoll Stufe", "selected": "Ausgewählt", - "viewer": "Betrachter", "beta": "Beta" }, "gallery": { @@ -731,7 +730,6 @@ "resume": "Wieder aufnehmen", "item": "Auftrag", "notReady": "Warteschlange noch nicht bereit", - "queueCountPrediction": "{{promptsCount}} Prompts × {{iterations}} Iterationen -> {{count}} Generationen", "clearQueueAlertDialog": "\"Die Warteschlange leeren\" stoppt den aktuellen Prozess und leert die Warteschlange komplett.", "completedIn": "Fertig in", "cancelBatchSucceeded": "Stapel abgebrochen", diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 15a69e7549..d6fd6c7d04 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -79,11 +79,9 @@ "positivePrompt": "Prompt positivo", "negativePrompt": "Prompt negativo", "selected": "Selezionato", - "viewer": "Visualizzatore", "goTo": "Vai a", "editor": "Editor", - "tab": "Scheda", - "close": "Chiudi" + "tab": "Scheda" }, "gallery": { "galleryImageSize": "Dimensione dell'immagine", @@ -120,10 +118,7 @@ "bulkDownloadRequestedDesc": "La tua richiesta di download è in preparazione. L'operazione potrebbe richiedere alcuni istanti.", "bulkDownloadRequestFailed": "Problema durante la preparazione del download", "bulkDownloadFailed": "Scaricamento fallito", - "alwaysShowImageSizeBadge": "Mostra sempre le dimensioni dell'immagine", - "switchTo": "Passa a {{ tab }} (Z)", - "openFloatingViewer": "Apri il visualizzatore mobile", - "closeFloatingViewer": "Chiudi il visualizzatore mobile" + "alwaysShowImageSizeBadge": "Mostra sempre le dimensioni dell'immagine" }, "hotkeys": { "keyboardShortcuts": "Tasti di scelta rapida", @@ -529,9 +524,6 @@ }, "useCpuNoise": "Usa la CPU per generare rumore", "iterations": "Iterazioni", - "iterationsWithCount_one": "{{count}} Iterazione", - "iterationsWithCount_many": "{{count}} Iterazioni", - "iterationsWithCount_other": "{{count}} Iterazioni", "isAllowedToUpscale": { "useX2Model": "L'immagine è troppo grande per l'ampliamento con il modello x4, utilizza il modello x2", "tooLarge": "L'immagine è troppo grande per l'ampliamento, seleziona un'immagine più piccola" @@ -982,7 +974,6 @@ "queue": { "queueFront": "Aggiungi all'inizio della coda", "queueBack": "Aggiungi alla coda", - "queueCountPrediction": "{{promptsCount}} prompt × {{iterations}} iterazioni -> {{count}} generazioni", "queue": "Coda", "status": "Stato", "pruneSucceeded": "Rimossi {{item_count}} elementi completati dalla coda", @@ -1576,8 +1567,6 @@ "addIPAdapter": "Aggiungi $t(common.ipAdapter)", "regionalGuidance": "Guida regionale", "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", - "controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)", - "ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)", "opacity": "Opacità", "globalControlAdapter": "$t(controlnet.controlAdapter_one) Globale", "globalControlAdapterLayer": "$t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer) Globale", diff --git a/invokeai/frontend/web/public/locales/ja.json b/invokeai/frontend/web/public/locales/ja.json index 264593153a..e953944c44 100644 --- a/invokeai/frontend/web/public/locales/ja.json +++ b/invokeai/frontend/web/public/locales/ja.json @@ -570,7 +570,6 @@ "pauseSucceeded": "処理が一時停止されました", "queueFront": "キューの先頭へ追加", "queueBack": "キューに追加", - "queueCountPrediction": "{{promptsCount}} プロンプト × {{iterations}} イテレーション -> {{count}} 枚生成", "pause": "一時停止", "queue": "キュー", "pauseTooltip": "処理を一時停止", diff --git a/invokeai/frontend/web/public/locales/ko.json b/invokeai/frontend/web/public/locales/ko.json index 1c02d86105..db9cd0ca67 100644 --- a/invokeai/frontend/web/public/locales/ko.json +++ b/invokeai/frontend/web/public/locales/ko.json @@ -505,7 +505,6 @@ "completed": "완성된", "queueBack": "Queue에 추가", "cancelFailed": "항목 취소 중 발생한 문제", - "queueCountPrediction": "Queue에 {{predicted}} 추가", "batchQueued": "Batch Queued", "pauseFailed": "프로세서 중지 중 발생한 문제", "clearFailed": "Queue 제거 중 발생한 문제", diff --git a/invokeai/frontend/web/public/locales/nl.json b/invokeai/frontend/web/public/locales/nl.json index 29ceb3227b..76377bd215 100644 --- a/invokeai/frontend/web/public/locales/nl.json +++ b/invokeai/frontend/web/public/locales/nl.json @@ -383,8 +383,6 @@ "useCpuNoise": "Gebruik CPU-ruis", "imageActions": "Afbeeldingshandeling", "iterations": "Iteraties", - "iterationsWithCount_one": "{{count}} iteratie", - "iterationsWithCount_other": "{{count}} iteraties", "coherenceMode": "Modus" }, "settings": { @@ -940,7 +938,6 @@ "completed": "Voltooid", "queueBack": "Voeg toe aan wachtrij", "cancelFailed": "Fout bij annuleren onderdeel", - "queueCountPrediction": "Voeg {{predicted}} toe aan wachtrij", "batchQueued": "Reeks in wachtrij geplaatst", "pauseFailed": "Fout bij onderbreken verwerker", "clearFailed": "Fout bij wissen van wachtrij", diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index a7f05e2a7a..44864e6624 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -79,13 +79,11 @@ "loglevel": "Уровень логов", "beta": "Бета", "selected": "Выбрано", - "viewer": "Просмотрщик", "positivePrompt": "Позитивный запрос", "negativePrompt": "Негативный запрос", "editor": "Редактор", "goTo": "Перейти к", - "tab": "Вкладка", - "close": "Закрыть" + "tab": "Вкладка" }, "gallery": { "galleryImageSize": "Размер изображений", @@ -122,10 +120,7 @@ "bulkDownloadRequested": "Подготовка к скачиванию", "bulkDownloadRequestedDesc": "Ваш запрос на скачивание готовится. Это может занять несколько минут.", "bulkDownloadRequestFailed": "Возникла проблема при подготовке скачивания", - "alwaysShowImageSizeBadge": "Всегда показывать значок размера изображения", - "switchTo": "Переключится на {{ tab }} (Z)", - "openFloatingViewer": "Открыть плавающий просмотрщик", - "closeFloatingViewer": "Закрыть плавающий просмотрщик" + "alwaysShowImageSizeBadge": "Всегда показывать значок размера изображения" }, "hotkeys": { "keyboardShortcuts": "Горячие клавиши", @@ -540,9 +535,6 @@ "useCpuNoise": "Использовать шум CPU", "imageActions": "Действия с изображениями", "iterations": "Кол-во", - "iterationsWithCount_one": "{{count}} Интеграция", - "iterationsWithCount_few": "{{count}} Итерации", - "iterationsWithCount_many": "{{count}} Итераций", "useSize": "Использовать размер", "coherenceMode": "Режим", "aspect": "Соотношение", @@ -1399,7 +1391,6 @@ "completed": "Выполнено", "queueBack": "Добавить в очередь", "cancelFailed": "Проблема с отменой элемента", - "queueCountPrediction": "{{promptsCount}} запросов × {{iterations}} изображений -> {{count}} генераций", "batchQueued": "Пакетная очередь", "pauseFailed": "Проблема с приостановкой рендеринга", "clearFailed": "Проблема с очисткой очереди", @@ -1588,8 +1579,6 @@ "maskPreviewColor": "Цвет предпросмотра маски", "addNegativePrompt": "Добавить $t(common.negativePrompt)", "regionalGuidance": "Региональная точность", - "controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)", - "ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)", "opacity": "Непрозрачность", "globalControlAdapter": "Глобальный $t(controlnet.controlAdapter_one)", "globalControlAdapterLayer": "Глобальный $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", diff --git a/invokeai/frontend/web/public/locales/zh_CN.json b/invokeai/frontend/web/public/locales/zh_CN.json index 3b0cd4f81b..45bab5c6da 100644 --- a/invokeai/frontend/web/public/locales/zh_CN.json +++ b/invokeai/frontend/web/public/locales/zh_CN.json @@ -445,7 +445,6 @@ "useX2Model": "图像太大,无法使用 x4 模型,使用 x2 模型作为替代", "tooLarge": "图像太大无法进行放大,请选择更小的图像" }, - "iterationsWithCount_other": "{{count}} 次迭代生成", "cfgRescaleMultiplier": "CFG 重缩放倍数", "useSize": "使用尺寸", "setToOptimalSize": "优化模型大小", @@ -853,7 +852,6 @@ "pruneSucceeded": "从队列修剪 {{item_count}} 个已完成的项目", "notReady": "无法排队", "batchFailedToQueue": "批次加入队列失败", - "queueCountPrediction": "{{promptsCount}} 提示词 × {{iterations}} 迭代次数 -> {{count}} 次生成", "batchQueued": "加入队列的批次", "front": "前", "pruneTooltip": "修剪 {{item_count}} 个已完成的项目", From ab1817477491be83958642218f60d12defb60206 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 13 May 2024 22:40:27 +0200 Subject: [PATCH 084/442] translationBot(ui): update translation (Spanish) Currently translated at 31.3% (379 of 1208 strings) Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/es/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/es.json | 90 +++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/public/locales/es.json b/invokeai/frontend/web/public/locales/es.json index 6b410cd0bf..dbdda8e209 100644 --- a/invokeai/frontend/web/public/locales/es.json +++ b/invokeai/frontend/web/public/locales/es.json @@ -25,7 +25,24 @@ "areYouSure": "¿Estas seguro?", "batch": "Administrador de lotes", "modelManager": "Administrador de modelos", - "communityLabel": "Comunidad" + "communityLabel": "Comunidad", + "direction": "Dirección", + "ai": "Ia", + "add": "Añadir", + "auto": "Automático", + "copyError": "Error $t(gallery.copy)", + "details": "Detalles", + "or": "o", + "checkpoint": "Punto de control", + "controlNet": "ControlNet", + "aboutHeading": "Sea dueño de su poder creativo", + "advanced": "Avanzado", + "data": "Fecha", + "delete": "Borrar", + "copy": "Copiar", + "beta": "Beta", + "on": "En", + "aboutDesc": "¿Utilizas Invoke para trabajar? Mira aquí:" }, "gallery": { "galleryImageSize": "Tamaño de la imagen", @@ -443,7 +460,13 @@ "previousImage": "Imagen anterior", "nextImage": "Siguiente imagen", "showOptionsPanel": "Mostrar el panel lateral", - "menu": "Menú" + "menu": "Menú", + "showGalleryPanel": "Mostrar panel de galería", + "loadMore": "Cargar más", + "about": "Acerca de", + "createIssue": "Crear un problema", + "resetUI": "Interfaz de usuario $t(accessibility.reset)", + "mode": "Modo" }, "nodes": { "zoomInNodes": "Acercar", @@ -456,5 +479,68 @@ "reloadNodeTemplates": "Recargar las plantillas de nodos", "loadWorkflow": "Cargar el flujo de trabajo", "downloadWorkflow": "Descargar el flujo de trabajo en un archivo JSON" + }, + "boards": { + "autoAddBoard": "Agregar panel automáticamente", + "changeBoard": "Cambiar el panel", + "clearSearch": "Borrar la búsqueda", + "deleteBoard": "Borrar el panel", + "selectBoard": "Seleccionar un panel", + "uncategorized": "Sin categoría", + "cancel": "Cancelar", + "addBoard": "Agregar un panel", + "movingImagesToBoard_one": "Moviendo {{count}} imagen al panel:", + "movingImagesToBoard_many": "Moviendo {{count}} imágenes al panel:", + "movingImagesToBoard_other": "Moviendo {{count}} imágenes al panel:", + "bottomMessage": "Al eliminar este panel y las imágenes que contiene, se restablecerán las funciones que los estén utilizando actualmente.", + "deleteBoardAndImages": "Borrar el panel y las imágenes", + "loading": "Cargando...", + "deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar", + "move": "Mover", + "menuItemAutoAdd": "Agregar automáticamente a este panel", + "searchBoard": "Buscando paneles…", + "topMessage": "Este panel contiene imágenes utilizadas en las siguientes funciones:", + "downloadBoard": "Descargar panel", + "deleteBoardOnly": "Borrar solo el panel", + "myBoard": "Mi panel", + "noMatching": "No hay paneles que coincidan" + }, + "accordions": { + "compositing": { + "title": "Composición", + "infillTab": "Relleno" + }, + "generation": { + "title": "Generación" + }, + "image": { + "title": "Imagen" + }, + "control": { + "title": "Control" + }, + "advanced": { + "options": "$t(accordions.advanced.title) opciones", + "title": "Avanzado" + } + }, + "ui": { + "tabs": { + "generationTab": "$t(ui.tabs.generation) $t(common.tab)", + "canvas": "Lienzo", + "generation": "Generación", + "queue": "Cola", + "queueTab": "$t(ui.tabs.queue) $t(common.tab)", + "workflows": "Flujos de trabajo", + "models": "Modelos", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)" + } + }, + "controlLayers": { + "layers_one": "Capa", + "layers_many": "Capas", + "layers_other": "Capas" } } From e375d9f7879c55afd936f541507ce57877e767a7 Mon Sep 17 00:00:00 2001 From: Riccardo Giovanetti Date: Mon, 13 May 2024 22:40:28 +0200 Subject: [PATCH 085/442] translationBot(ui): update translation (Italian) Currently translated at 98.5% (1192 of 1210 strings) translationBot(ui): update translation (Italian) Currently translated at 98.5% (1192 of 1210 strings) translationBot(ui): update translation (Italian) Currently translated at 98.5% (1192 of 1210 strings) translationBot(ui): update translation (Italian) Currently translated at 98.5% (1192 of 1210 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 45 ++++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index d6fd6c7d04..f365b43e10 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -5,7 +5,7 @@ "reportBugLabel": "Segnala un errore", "settingsLabel": "Impostazioni", "img2img": "Immagine a Immagine", - "unifiedCanvas": "Tela unificata", + "unifiedCanvas": "Tela", "nodes": "Flussi di lavoro", "upload": "Caricamento", "load": "Carica", @@ -81,7 +81,11 @@ "selected": "Selezionato", "goTo": "Vai a", "editor": "Editor", - "tab": "Scheda" + "tab": "Scheda", + "viewing": "Visualizza", + "viewingDesc": "Rivedi le immagini in un'ampia vista della galleria", + "editing": "Modifica", + "editingDesc": "Modifica nell'area Livelli di controllo" }, "gallery": { "galleryImageSize": "Dimensione dell'immagine", @@ -187,8 +191,8 @@ "desc": "Mostra le informazioni sui metadati dell'immagine corrente" }, "sendToImageToImage": { - "title": "Invia a Immagine a Immagine", - "desc": "Invia l'immagine corrente a da Immagine a Immagine" + "title": "Invia a Generazione da immagine", + "desc": "Invia l'immagine corrente a Generazione da immagine" }, "deleteImage": { "title": "Elimina immagine", @@ -482,8 +486,8 @@ "scaledHeight": "Altezza ridimensionata", "infillMethod": "Metodo di riempimento", "tileSize": "Dimensione piastrella", - "sendToImg2Img": "Invia a Immagine a Immagine", - "sendToUnifiedCanvas": "Invia a Tela Unificata", + "sendToImg2Img": "Invia a Generazione da immagine", + "sendToUnifiedCanvas": "Invia alla Tela", "downloadImage": "Scarica l'immagine", "usePrompt": "Usa Prompt", "useSeed": "Usa Seme", @@ -544,7 +548,9 @@ "infillMosaicMaxColor": "Colore massimo", "infillMosaicTileHeight": "Altezza piastrella", "infillColorValue": "Colore di riempimento", - "globalSettings": "Impostazioni globali" + "globalSettings": "Impostazioni globali", + "globalPositivePromptPlaceholder": "Prompt positivo globale", + "globalNegativePromptPlaceholder": "Prompt negativo globale" }, "settings": { "models": "Modelli", @@ -569,7 +575,7 @@ "intermediatesCleared_one": "Cancellata {{count}} immagine intermedia", "intermediatesCleared_many": "Cancellate {{count}} immagini intermedie", "intermediatesCleared_other": "Cancellate {{count}} immagini intermedie", - "clearIntermediatesDesc1": "La cancellazione delle immagini intermedie ripristinerà lo stato di Tela Unificata e ControlNet.", + "clearIntermediatesDesc1": "La cancellazione delle immagini intermedie ripristinerà lo stato della Tela e degli Adattatori di Controllo.", "intermediatesClearedFailed": "Problema con la cancellazione delle immagini intermedie", "clearIntermediatesWithCount_one": "Cancella {{count}} immagine intermedia", "clearIntermediatesWithCount_many": "Cancella {{count}} immagini intermedie", @@ -585,8 +591,8 @@ "imageCopied": "Immagine copiata", "imageNotLoadedDesc": "Impossibile trovare l'immagine", "canvasMerged": "Tela unita", - "sentToImageToImage": "Inviato a Immagine a Immagine", - "sentToUnifiedCanvas": "Inviato a Tela Unificata", + "sentToImageToImage": "Inviato a Generazione da immagine", + "sentToUnifiedCanvas": "Inviato alla Tela", "parametersNotSet": "Parametri non impostati", "metadataLoadFailed": "Impossibile caricare i metadati", "serverError": "Errore del Server", @@ -1010,7 +1016,7 @@ "cancelBatchSucceeded": "Lotto annullato", "clearTooltip": "Annulla e cancella tutti gli elementi", "current": "Attuale", - "pauseTooltip": "Sospende l'elaborazione", + "pauseTooltip": "Sospendi l'elaborazione", "failed": "Falliti", "cancelItem": "Annulla l'elemento", "next": "Prossimo", @@ -1552,7 +1558,7 @@ "addLayer": "Aggiungi Livello", "moveToFront": "Sposta in primo piano", "moveToBack": "Sposta in fondo", - "moveForward": "Sposta in avanti", + "moveForward": "Sposta avanti", "moveBackward": "Sposta indietro", "brushSize": "Dimensioni del pennello", "globalMaskOpacity": "Opacità globale della maschera", @@ -1566,19 +1572,22 @@ "addNegativePrompt": "Aggiungi $t(common.negativePrompt)", "addIPAdapter": "Aggiungi $t(common.ipAdapter)", "regionalGuidance": "Guida regionale", - "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", + "regionalGuidanceLayer": "$t(unifiedCanvas.layer) $t(controlLayers.regionalGuidance)", "opacity": "Opacità", "globalControlAdapter": "$t(controlnet.controlAdapter_one) Globale", - "globalControlAdapterLayer": "$t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer) Globale", + "globalControlAdapterLayer": "$t(controlnet.controlAdapter_one) - $t(unifiedCanvas.layer) Globale", "globalIPAdapter": "$t(common.ipAdapter) Globale", - "globalIPAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer) Globale", - "globalInitialImage": "Immagine iniziale globale", - "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", + "globalIPAdapterLayer": "$t(common.ipAdapter) - $t(unifiedCanvas.layer) Globale", + "globalInitialImage": "Immagine iniziale", + "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) - $t(unifiedCanvas.layer) Globale", "clearProcessor": "Cancella processore", "resetProcessor": "Ripristina il processore alle impostazioni predefinite", "noLayersAdded": "Nessun livello aggiunto", "resetRegion": "Reimposta la regione", - "controlLayers": "Livelli di controllo" + "controlLayers": "Livelli di controllo", + "layers_one": "Livello", + "layers_many": "Livelli", + "layers_other": "Livelli" }, "ui": { "tabs": { From eef6fcf28699b84e5d7109415aa2b53eb9fa5f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D1=8F=D0=BD=D0=B0=D1=82=D0=BE=D1=80?= Date: Mon, 13 May 2024 22:40:28 +0200 Subject: [PATCH 086/442] translationBot(ui): update translation (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (1210 of 1210 strings) Co-authored-by: Васянатор Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/ru.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index 44864e6624..7fa1b73e7a 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -83,7 +83,11 @@ "negativePrompt": "Негативный запрос", "editor": "Редактор", "goTo": "Перейти к", - "tab": "Вкладка" + "tab": "Вкладка", + "viewing": "Просмотр", + "editing": "Редактирование", + "viewingDesc": "Просмотр изображений в режиме большой галереи", + "editingDesc": "Редактировать на холсте слоёв управления" }, "gallery": { "galleryImageSize": "Размер изображений", @@ -551,7 +555,9 @@ "infillMosaicMinColor": "Мин цвет", "infillMosaicMaxColor": "Макс цвет", "infillColorValue": "Цвет заливки", - "globalSettings": "Глобальные настройки" + "globalSettings": "Глобальные настройки", + "globalNegativePromptPlaceholder": "Глобальный негативный запрос", + "globalPositivePromptPlaceholder": "Глобальный запрос" }, "settings": { "models": "Модели", @@ -1596,7 +1602,10 @@ "clearProcessor": "Чистый процессор", "globalInitialImage": "Глобальное исходное изображение", "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", - "noLayersAdded": "Без слоев" + "noLayersAdded": "Без слоев", + "layers_one": "Слой", + "layers_few": "Слоя", + "layers_many": "Слоев" }, "ui": { "tabs": { From 9c819f0fd8e6b6760ea8a773e9495787573a94bd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:38:02 +1000 Subject: [PATCH 087/442] fix(nodes): fix nsfw checker model download --- invokeai/app/api/routers/app_info.py | 5 +- invokeai/backend/image_util/safety_checker.py | 48 ++++++++++++++----- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py index 21286ac2b0..c3bc98a038 100644 --- a/invokeai/app/api/routers/app_info.py +++ b/invokeai/app/api/routers/app_info.py @@ -13,7 +13,6 @@ from pydantic import BaseModel, Field from invokeai.app.invocations.upscale import ESRGAN_MODELS from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch -from invokeai.backend.image_util.safety_checker import SafetyChecker from invokeai.backend.util.logging import logging from invokeai.version import __version__ @@ -109,9 +108,7 @@ async def get_config() -> AppConfig: upscaling_models.append(str(Path(model).stem)) upscaler = Upscaler(upscaling_method="esrgan", upscaling_models=upscaling_models) - nsfw_methods = [] - if SafetyChecker.safety_checker_available(): - nsfw_methods.append("nsfw_checker") + nsfw_methods = ["nsfw_checker"] watermarking_methods = ["invisible_watermark"] diff --git a/invokeai/backend/image_util/safety_checker.py b/invokeai/backend/image_util/safety_checker.py index 60dcd93fcc..4e0bfe56e5 100644 --- a/invokeai/backend/image_util/safety_checker.py +++ b/invokeai/backend/image_util/safety_checker.py @@ -8,7 +8,7 @@ from pathlib import Path import numpy as np from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker -from PIL import Image +from PIL import Image, ImageFilter from transformers import AutoFeatureExtractor import invokeai.backend.util.logging as logger @@ -16,6 +16,7 @@ from invokeai.app.services.config.config_default import get_config from invokeai.backend.util.devices import TorchDevice from invokeai.backend.util.silence_warnings import SilenceWarnings +repo_id = "CompVis/stable-diffusion-safety-checker" CHECKER_PATH = "core/convert/stable-diffusion-safety-checker" @@ -24,30 +25,30 @@ class SafetyChecker: Wrapper around SafetyChecker model. """ - safety_checker = None feature_extractor = None - tried_load: bool = False + safety_checker = None @classmethod def _load_safety_checker(cls): - if cls.tried_load: + if cls.safety_checker is not None and cls.feature_extractor is not None: return try: - cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(get_config().models_path / CHECKER_PATH) - cls.feature_extractor = AutoFeatureExtractor.from_pretrained(get_config().models_path / CHECKER_PATH) + model_path = get_config().models_path / CHECKER_PATH + if model_path.exists(): + cls.feature_extractor = AutoFeatureExtractor.from_pretrained(model_path) + cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(model_path) + else: + model_path.mkdir(parents=True, exist_ok=True) + cls.feature_extractor = AutoFeatureExtractor.from_pretrained(repo_id) + cls.feature_extractor.save_pretrained(model_path, safe_serialization=True) + cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(repo_id) + cls.safety_checker.save_pretrained(model_path, safe_serialization=True) except Exception as e: logger.warning(f"Could not load NSFW checker: {str(e)}") - cls.tried_load = True - - @classmethod - def safety_checker_available(cls) -> bool: - return Path(get_config().models_path, CHECKER_PATH).exists() @classmethod def has_nsfw_concept(cls, image: Image.Image) -> bool: - if not cls.safety_checker_available() and cls.tried_load: - return False cls._load_safety_checker() if cls.safety_checker is None or cls.feature_extractor is None: return False @@ -60,3 +61,24 @@ class SafetyChecker: with SilenceWarnings(): checked_image, has_nsfw_concept = cls.safety_checker(images=x_image, clip_input=features.pixel_values) return has_nsfw_concept[0] + + @classmethod + def blur_if_nsfw(cls, image: Image.Image) -> Image.Image: + if cls.has_nsfw_concept(image): + logger.info("A potentially NSFW image has been detected. Image will be blurred.") + blurry_image = image.filter(filter=ImageFilter.GaussianBlur(radius=32)) + caution = cls._get_caution_img() + # Center the caution image on the blurred image + x = (blurry_image.width - caution.width) // 2 + y = (blurry_image.height - caution.height) // 2 + blurry_image.paste(caution, (x, y), caution) + image = blurry_image + + return image + + @classmethod + def _get_caution_img(cls) -> Image.Image: + import invokeai.app.assets.images as image_assets + + caution = Image.open(Path(image_assets.__path__[0]) / "caution.png") + return caution.resize((caution.width // 2, caution.height // 2)) From 93da75209c6519ac260e05dfe76933d7c3ebaf23 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 18:33:25 +1000 Subject: [PATCH 088/442] feat(nodes): use new `blur_if_nsfw` method --- invokeai/app/invocations/image.py | 16 ++-------------- invokeai/backend/image_util/safety_checker.py | 2 +- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 05ffc0d67b..65e7ce5e06 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1,6 +1,5 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from pathlib import Path from typing import Literal, Optional import cv2 @@ -504,7 +503,7 @@ class ImageInverseLerpInvocation(BaseInvocation, WithMetadata, WithBoard): title="Blur NSFW Image", tags=["image", "nsfw"], category="image", - version="1.2.2", + version="1.2.3", ) class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard): """Add blur to NSFW-flagged images""" @@ -516,23 +515,12 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard): logger = context.logger logger.debug("Running NSFW checker") - if SafetyChecker.has_nsfw_concept(image): - logger.info("A potentially NSFW image has been detected. Image will be blurred.") - blurry_image = image.filter(filter=ImageFilter.GaussianBlur(radius=32)) - caution = self._get_caution_img() - blurry_image.paste(caution, (0, 0), caution) - image = blurry_image + image = SafetyChecker.blur_if_nsfw(image) image_dto = context.images.save(image=image) return ImageOutput.build(image_dto) - def _get_caution_img(self) -> Image.Image: - import invokeai.app.assets.images as image_assets - - caution = Image.open(Path(image_assets.__path__[0]) / "caution.png") - return caution.resize((caution.width // 2, caution.height // 2)) - @invocation( "img_watermark", diff --git a/invokeai/backend/image_util/safety_checker.py b/invokeai/backend/image_util/safety_checker.py index 4e0bfe56e5..ab09a29619 100644 --- a/invokeai/backend/image_util/safety_checker.py +++ b/invokeai/backend/image_util/safety_checker.py @@ -65,7 +65,7 @@ class SafetyChecker: @classmethod def blur_if_nsfw(cls, image: Image.Image) -> Image.Image: if cls.has_nsfw_concept(image): - logger.info("A potentially NSFW image has been detected. Image will be blurred.") + logger.warning("A potentially NSFW image has been detected. Image will be blurred.") blurry_image = image.filter(filter=ImageFilter.GaussianBlur(radius=32)) caution = cls._get_caution_img() # Center the caution image on the blurred image From 2a9cea66897e3f8098ca4967a457e4265ddcf854 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 07:37:02 +1000 Subject: [PATCH 089/442] Update invokeai_version.py Bump to v4.2.1 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 0fd7811c0d..aef46acb47 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.0" +__version__ = "4.2.1" From e22211dac0f32402f9803c9ca3246ca573f3bfa9 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Wed, 15 May 2024 08:44:59 +0530 Subject: [PATCH 090/442] fix: Fix Outpaint not applying the expanded mask correctly In unscaled situations --- .../src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts | 4 ++-- .../features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts index 6b564f464e..6114cb5e5c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts @@ -528,8 +528,8 @@ export const buildCanvasOutpaintGraph = async ( }, { source: { - node_id: MASK_RESIZE_DOWN, - field: 'image', + node_id: INPAINT_CREATE_MASK, + field: 'expanded_mask_area', }, destination: { node_id: CANVAS_OUTPUT, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts index c5c40b695a..eca498bf79 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts @@ -584,8 +584,8 @@ export const buildCanvasSDXLOutpaintGraph = async ( }, { source: { - node_id: MASK_COMBINE, - field: 'image', + node_id: INPAINT_CREATE_MASK, + field: 'expanded_mask_area', }, destination: { node_id: CANVAS_OUTPUT, From fc6b214470d6940b4299ef435afb3786ae848c96 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 5 May 2024 08:11:26 +1000 Subject: [PATCH 091/442] tests(ui): set up vitest coverage --- invokeai/frontend/web/.gitignore | 3 +- invokeai/frontend/web/package.json | 3 + invokeai/frontend/web/pnpm-lock.yaml | 148 +++++++++++++++++++++++++- invokeai/frontend/web/vite.config.mts | 5 + 4 files changed, 155 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/.gitignore b/invokeai/frontend/web/.gitignore index 3e8a372bc7..757d6ebcc8 100644 --- a/invokeai/frontend/web/.gitignore +++ b/invokeai/frontend/web/.gitignore @@ -43,4 +43,5 @@ stats.html yalc.lock # vitest -tsconfig.vitest-temp.json \ No newline at end of file +tsconfig.vitest-temp.json +coverage/ \ No newline at end of file diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 8f6f2c6038..f2210e4c68 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -35,6 +35,7 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "test": "vitest", + "test:ui": "vitest --coverage --ui", "test:no-watch": "vitest --no-watch" }, "madge": { @@ -132,6 +133,8 @@ "@types/react-dom": "^18.3.0", "@types/uuid": "^9.0.8", "@vitejs/plugin-react-swc": "^3.6.0", + "@vitest/coverage-v8": "^1.5.0", + "@vitest/ui": "^1.5.0", "concurrently": "^8.2.2", "dpdm": "^3.14.0", "eslint": "^8.57.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index b5de9e6426..64189f0d82 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -229,6 +229,12 @@ devDependencies: '@vitejs/plugin-react-swc': specifier: ^3.6.0 version: 3.6.0(vite@5.2.11) + '@vitest/coverage-v8': + specifier: ^1.5.0 + version: 1.6.0(vitest@1.6.0) + '@vitest/ui': + specifier: ^1.5.0 + version: 1.6.0(vitest@1.6.0) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -288,7 +294,7 @@ devDependencies: version: 4.3.2(typescript@5.4.5)(vite@5.2.11) vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@20.12.10) + version: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) packages: @@ -1679,6 +1685,10 @@ packages: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} dev: true + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} peerDependencies: @@ -3635,6 +3645,11 @@ packages: wrap-ansi-cjs: /wrap-ansi@7.0.0 dev: true + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3822,6 +3837,10 @@ packages: dev: true optional: true + /@polka/url@1.0.0-next.25: + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + dev: true + /@popperjs/core@2.11.8: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false @@ -5146,7 +5165,7 @@ packages: dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - vitest: 1.6.0(@types/node@20.12.10) + vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) dev: true /@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4): @@ -5825,6 +5844,29 @@ packages: - '@swc/helpers' dev: true + /@vitest/coverage-v8@1.6.0(vitest@1.6.0): + resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==} + peerDependencies: + vitest: 1.6.0 + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.4 + istanbul-reports: 3.1.7 + magic-string: 0.30.10 + magicast: 0.3.4 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.1.0 + test-exclude: 6.0.0 + vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) + transitivePeerDependencies: + - supports-color + dev: true + /@vitest/expect@1.3.1: resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} dependencies: @@ -5869,6 +5911,21 @@ packages: tinyspy: 2.2.1 dev: true + /@vitest/ui@1.6.0(vitest@1.6.0): + resolution: {integrity: sha512-k3Lyo+ONLOgylctiGovRKy7V4+dIN2yxstX3eY5cWFXH6WP+ooVX79YSyi0GagdTQzLmT43BF27T0s6dOIPBXA==} + peerDependencies: + vitest: 1.6.0 + dependencies: + '@vitest/utils': 1.6.0 + fast-glob: 3.3.2 + fflate: 0.8.2 + flatted: 3.3.1 + pathe: 1.1.2 + picocolors: 1.0.0 + sirv: 2.0.4 + vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) + dev: true + /@vitest/utils@1.3.1: resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} dependencies: @@ -8521,6 +8578,10 @@ packages: resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==} dev: true + /fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + dev: true + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -9084,6 +9145,10 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + /html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} dependencies: @@ -9513,6 +9578,39 @@ packages: engines: {node: '>=0.10.0'} dev: true + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@5.0.4: + resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} + engines: {node: '>=10'} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + /iterable-lookahead@1.0.0: resolution: {integrity: sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ==} engines: {node: '>=4'} @@ -9912,6 +10010,14 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /magicast@0.3.4: + resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} + dependencies: + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 + source-map-js: 1.2.0 + dev: true + /make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -9927,6 +10033,13 @@ packages: semver: 6.3.1 dev: true + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + dev: true + /map-obj@2.0.0: resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==} engines: {node: '>=4'} @@ -10101,6 +10214,11 @@ packages: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} dev: false + /mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + dev: true + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true @@ -11766,6 +11884,15 @@ packages: engines: {node: '>=14'} dev: true + /sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + dev: true + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -12191,6 +12318,15 @@ packages: unique-string: 2.0.0 dev: true + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -12264,6 +12400,11 @@ packages: engines: {node: '>=0.6'} dev: true + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -12837,7 +12978,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.6.0(@types/node@20.12.10): + /vitest@1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0): resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -12867,6 +13008,7 @@ packages: '@vitest/runner': 1.6.0 '@vitest/snapshot': 1.6.0 '@vitest/spy': 1.6.0 + '@vitest/ui': 1.6.0(vitest@1.6.0) '@vitest/utils': 1.6.0 acorn-walk: 8.3.2 chai: 4.4.1 diff --git a/invokeai/frontend/web/vite.config.mts b/invokeai/frontend/web/vite.config.mts index 21edc076db..a40c515465 100644 --- a/invokeai/frontend/web/vite.config.mts +++ b/invokeai/frontend/web/vite.config.mts @@ -93,6 +93,11 @@ export default defineConfig(({ mode }) => { enabled: true, ignoreSourceErrors: true, }, + coverage: { + provider: 'v8', + all: false, + reporter: ['html'], + }, }, }; }); From 18b0977a31f77c2f2971a4a8196d4cfd0ae8eb4b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 5 May 2024 08:12:50 +1000 Subject: [PATCH 092/442] feat(api): add InvocationOutputMap to OpenAPI schema This dynamically generated schema object maps node types to their pydantic schemas. This makes it much simpler to infer node types in the UI. --- invokeai/app/api_app.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index ceaeb95147..062682f7d0 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -164,6 +164,12 @@ def custom_openapi() -> dict[str, Any]: for schema_key, schema_json in additional_schemas[1]["$defs"].items(): openapi_schema["components"]["schemas"][schema_key] = schema_json + openapi_schema["components"]["schemas"]["InvocationOutputMap"] = { + "type": "object", + "properties": {}, + "required": [], + } + # Add a reference to the output type to additionalProperties of the invoker schema for invoker in all_invocations: invoker_name = invoker.__name__ # type: ignore [attr-defined] # this is a valid attribute @@ -172,6 +178,8 @@ def custom_openapi() -> dict[str, Any]: invoker_schema = openapi_schema["components"]["schemas"][f"{invoker_name}"] outputs_ref = {"$ref": f"#/components/schemas/{output_type_title}"} invoker_schema["output"] = outputs_ref + openapi_schema["components"]["schemas"]["InvocationOutputMap"]["properties"][invoker.get_type()] = outputs_ref + openapi_schema["components"]["schemas"]["InvocationOutputMap"]["required"].append(invoker.get_type()) invoker_schema["class"] = "invocation" # This code no longer seems to be necessary? From 7901e4c082fca9d76ec2a28f19a3d4e0810351f3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 5 May 2024 08:13:07 +1000 Subject: [PATCH 093/442] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 140 +++++++++++++++++- 1 file changed, 138 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 27a3c670da..9b6cd5b020 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -4189,7 +4189,7 @@ export type components = { * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["NoiseInvocation"]; + [key: string]: components["schemas"]["RandomFloatInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CreateGradientMaskInvocation"]; }; /** * Edges @@ -4226,7 +4226,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["ImageOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["String2Output"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["CLIPOutput"]; + [key: string]: components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["String2Output"] | components["schemas"]["BooleanOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["UNetOutput"]; }; /** * Errors @@ -11460,6 +11460,142 @@ export type components = { * @enum {string} */ UIType: "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "T2IAdapterModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict"; + InvocationOutputMap: { + rand_float: components["schemas"]["FloatOutput"]; + freeu: components["schemas"]["UNetOutput"]; + tile_image_processor: components["schemas"]["ImageOutput"]; + string_split_neg: components["schemas"]["StringPosNegOutput"]; + float_range: components["schemas"]["FloatCollectionOutput"]; + mask_combine: components["schemas"]["ImageOutput"]; + add: components["schemas"]["IntegerOutput"]; + pidi_image_processor: components["schemas"]["ImageOutput"]; + scheduler: components["schemas"]["SchedulerOutput"]; + lblend: components["schemas"]["LatentsOutput"]; + blank_image: components["schemas"]["ImageOutput"]; + content_shuffle_image_processor: components["schemas"]["ImageOutput"]; + infill_tile: components["schemas"]["ImageOutput"]; + img_channel_multiply: components["schemas"]["ImageOutput"]; + face_off: components["schemas"]["FaceOffOutput"]; + lineart_anime_image_processor: components["schemas"]["ImageOutput"]; + heuristic_resize: components["schemas"]["ImageOutput"]; + esrgan: components["schemas"]["ImageOutput"]; + img_resize: components["schemas"]["ImageOutput"]; + vae_loader: components["schemas"]["VAEOutput"]; + l2i: components["schemas"]["ImageOutput"]; + controlnet: components["schemas"]["ControlOutput"]; + show_image: components["schemas"]["ImageOutput"]; + infill_lama: components["schemas"]["ImageOutput"]; + image: components["schemas"]["ImageOutput"]; + zoe_depth_image_processor: components["schemas"]["ImageOutput"]; + merge_tiles_to_image: components["schemas"]["ImageOutput"]; + depth_anything_image_processor: components["schemas"]["ImageOutput"]; + latents_collection: components["schemas"]["LatentsCollectionOutput"]; + boolean_collection: components["schemas"]["BooleanCollectionOutput"]; + dynamic_prompt: components["schemas"]["StringCollectionOutput"]; + float: components["schemas"]["FloatOutput"]; + float_to_int: components["schemas"]["IntegerOutput"]; + integer: components["schemas"]["IntegerOutput"]; + unsharp_mask: components["schemas"]["ImageOutput"]; + calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; + normalbae_image_processor: components["schemas"]["ImageOutput"]; + img_paste: components["schemas"]["ImageOutput"]; + face_mask_detection: components["schemas"]["FaceMaskOutput"]; + step_param_easing: components["schemas"]["FloatCollectionOutput"]; + random_range: components["schemas"]["IntegerCollectionOutput"]; + leres_image_processor: components["schemas"]["ImageOutput"]; + string_replace: components["schemas"]["StringOutput"]; + sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; + main_model_loader: components["schemas"]["ModelLoaderOutput"]; + rectangle_mask: components["schemas"]["MaskOutput"]; + mask_edge: components["schemas"]["ImageOutput"]; + mask_from_id: components["schemas"]["ImageOutput"]; + img_crop: components["schemas"]["ImageOutput"]; + save_image: components["schemas"]["ImageOutput"]; + string_join: components["schemas"]["StringOutput"]; + seamless: components["schemas"]["SeamlessModeOutput"]; + img_pad_crop: components["schemas"]["ImageOutput"]; + range_of_size: components["schemas"]["IntegerCollectionOutput"]; + range: components["schemas"]["IntegerCollectionOutput"]; + prompt_from_file: components["schemas"]["StringCollectionOutput"]; + compel: components["schemas"]["ConditioningOutput"]; + face_identifier: components["schemas"]["ImageOutput"]; + img_ilerp: components["schemas"]["ImageOutput"]; + color_map_image_processor: components["schemas"]["ImageOutput"]; + lscale: components["schemas"]["LatentsOutput"]; + image_collection: components["schemas"]["ImageCollectionOutput"]; + color_correct: components["schemas"]["ImageOutput"]; + integer_math: components["schemas"]["IntegerOutput"]; + string_join_three: components["schemas"]["StringOutput"]; + merge_metadata: components["schemas"]["MetadataOutput"]; + rand_int: components["schemas"]["IntegerOutput"]; + metadata: components["schemas"]["MetadataOutput"]; + sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; + mlsd_image_processor: components["schemas"]["ImageOutput"]; + t2i_adapter: components["schemas"]["T2IAdapterOutput"]; + hed_image_processor: components["schemas"]["ImageOutput"]; + conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; + float_math: components["schemas"]["FloatOutput"]; + collect: components["schemas"]["CollectInvocationOutput"]; + canvas_paste_back: components["schemas"]["ImageOutput"]; + sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + latents: components["schemas"]["LatentsOutput"]; + conditioning: components["schemas"]["ConditioningOutput"]; + sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; + img_hue_adjust: components["schemas"]["ImageOutput"]; + img_lerp: components["schemas"]["ImageOutput"]; + div: components["schemas"]["IntegerOutput"]; + clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; + img_conv: components["schemas"]["ImageOutput"]; + crop_latents: components["schemas"]["LatentsOutput"]; + img_mul: components["schemas"]["ImageOutput"]; + string_split: components["schemas"]["String2Output"]; + integer_collection: components["schemas"]["IntegerCollectionOutput"]; + string_collection: components["schemas"]["StringCollectionOutput"]; + infill_cv2: components["schemas"]["ImageOutput"]; + float_collection: components["schemas"]["FloatCollectionOutput"]; + alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; + mediapipe_face_processor: components["schemas"]["ImageOutput"]; + invert_tensor_mask: components["schemas"]["MaskOutput"]; + mul: components["schemas"]["IntegerOutput"]; + ideal_size: components["schemas"]["IdealSizeOutput"]; + metadata_item: components["schemas"]["MetadataItemOutput"]; + color: components["schemas"]["ColorOutput"]; + sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; + infill_patchmatch: components["schemas"]["ImageOutput"]; + noise: components["schemas"]["NoiseOutput"]; + sub: components["schemas"]["IntegerOutput"]; + img_chan: components["schemas"]["ImageOutput"]; + infill_rgba: components["schemas"]["ImageOutput"]; + midas_depth_image_processor: components["schemas"]["ImageOutput"]; + segment_anything_processor: components["schemas"]["ImageOutput"]; + cv_inpaint: components["schemas"]["ImageOutput"]; + lineart_image_processor: components["schemas"]["ImageOutput"]; + calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; + create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; + boolean: components["schemas"]["BooleanOutput"]; + img_nsfw: components["schemas"]["ImageOutput"]; + tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; + denoise_latents: components["schemas"]["LatentsOutput"]; + ip_adapter: components["schemas"]["IPAdapterOutput"]; + pair_tile_image: components["schemas"]["PairTileImageOutput"]; + image_mask_to_tensor: components["schemas"]["MaskOutput"]; + dw_openpose_image_processor: components["schemas"]["ImageOutput"]; + tomask: components["schemas"]["ImageOutput"]; + lresize: components["schemas"]["LatentsOutput"]; + lora_loader: components["schemas"]["LoRALoaderOutput"]; + canny_image_processor: components["schemas"]["ImageOutput"]; + calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; + img_watermark: components["schemas"]["ImageOutput"]; + img_scale: components["schemas"]["ImageOutput"]; + round_float: components["schemas"]["FloatOutput"]; + img_channel_offset: components["schemas"]["ImageOutput"]; + core_metadata: components["schemas"]["MetadataOutput"]; + iterate: components["schemas"]["IterateInvocationOutput"]; + string: components["schemas"]["StringOutput"]; + img_blur: components["schemas"]["ImageOutput"]; + i2l: components["schemas"]["LatentsOutput"]; + create_gradient_mask: components["schemas"]["GradientMaskOutput"]; + }; }; responses: never; parameters: never; From 47b815372874b36f2e16272abdb790f28192f172 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 5 May 2024 08:13:41 +1000 Subject: [PATCH 094/442] build(ui): enable TS `strictPropertyInitialization` https://www.typescriptlang.org/tsconfig/#strictPropertyInitialization --- invokeai/frontend/web/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/tsconfig.json b/invokeai/frontend/web/tsconfig.json index 91906c9abe..b1e4ebfc0b 100644 --- a/invokeai/frontend/web/tsconfig.json +++ b/invokeai/frontend/web/tsconfig.json @@ -15,6 +15,7 @@ // "resolveJsonModule": true, "noUncheckedIndexedAccess": true, "strictNullChecks": true, + "strictPropertyInitialization": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", From e3289856c0320afcc16658aa2a95bb7437c74549 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 5 May 2024 08:20:31 +1000 Subject: [PATCH 095/442] feat(ui): add and use type helpers for invocations and invocation outputs --- .../util/controlAdapters.test.ts | 10 +- .../src/features/nodes/types/common.test-d.ts | 4 +- .../util/graph/addControlLayersToGraph.ts | 18 ++-- .../frontend/web/src/services/api/types.ts | 95 ++++++++++++------- 4 files changed, 76 insertions(+), 51 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts index 31eb54e730..fa617b0541 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts @@ -1,4 +1,4 @@ -import type { S } from 'services/api/types'; +import type { Invocation } from 'services/api/types'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; import { describe, test } from 'vitest'; @@ -45,16 +45,16 @@ describe('Control Adapter Types', () => { assert>(); }); test('IP Adapter Method', () => { - assert, IPMethodV2>>(); + assert['method']>, IPMethodV2>>(); }); test('CLIP Vision Model', () => { - assert, CLIPVisionModelV2>>(); + assert['clip_vision_model']>, CLIPVisionModelV2>>(); }); test('Control Mode', () => { - assert, ControlModeV2>>(); + assert['control_mode']>, ControlModeV2>>(); }); test('DepthAnything Model Size', () => { - assert, DepthAnythingModelSize>>(); + assert['model_size']>, DepthAnythingModelSize>>(); }); test('Processor Configs', () => { // The processor configs are manually modeled zod schemas. This test ensures that the inferred types are correct. diff --git a/invokeai/frontend/web/src/features/nodes/types/common.test-d.ts b/invokeai/frontend/web/src/features/nodes/types/common.test-d.ts index f2ebf94b06..bdf0b81fe5 100644 --- a/invokeai/frontend/web/src/features/nodes/types/common.test-d.ts +++ b/invokeai/frontend/web/src/features/nodes/types/common.test-d.ts @@ -11,7 +11,7 @@ import type { SchedulerField, T2IAdapterField, } from 'features/nodes/types/common'; -import type { S } from 'services/api/types'; +import type { Invocation, S } from 'services/api/types'; import type { Equals, Extends } from 'tsafe'; import { assert } from 'tsafe'; import { describe, test } from 'vitest'; @@ -26,7 +26,7 @@ describe('Common types', () => { test('ImageField', () => assert>()); test('BoardField', () => assert>()); test('ColorField', () => assert>()); - test('SchedulerField', () => assert>>()); + test('SchedulerField', () => assert['scheduler']>>>()); test('ControlField', () => assert>()); // @ts-expect-error TODO(psyche): fix types test('IPAdapterField', () => assert>()); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index a541d3948f..e48b9fb376 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts @@ -43,11 +43,9 @@ import type { ControlNetInvocation, Edge, ImageDTO, - ImageResizeInvocation, - ImageToLatentsInvocation, + Invocation, IPAdapterInvocation, NonNullableGraph, - S, T2IAdapterInvocation, } from 'services/api/types'; import { assert } from 'tsafe'; @@ -153,7 +151,7 @@ export const addControlLayersToGraph = async ( const { image_name } = await getMaskImage(layer, blob); // The main mask-to-tensor node - const maskToTensorNode: S['AlphaMaskToTensorInvocation'] = { + const maskToTensorNode: Invocation<'alpha_mask_to_tensor'> = { id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${layer.id}`, type: 'alpha_mask_to_tensor', image: { @@ -164,7 +162,7 @@ export const addControlLayersToGraph = async ( if (layer.positivePrompt) { // The main positive conditioning node - const regionalPositiveCondNode: S['SDXLCompelPromptInvocation'] | S['CompelInvocation'] = isSDXL + const regionalPositiveCondNode: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'> = isSDXL ? { type: 'sdxl_compel_prompt', id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, @@ -203,7 +201,7 @@ export const addControlLayersToGraph = async ( if (layer.negativePrompt) { // The main negative conditioning node - const regionalNegativeCondNode: S['SDXLCompelPromptInvocation'] | S['CompelInvocation'] = isSDXL + const regionalNegativeCondNode: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'> = isSDXL ? { type: 'sdxl_compel_prompt', id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, @@ -243,7 +241,7 @@ export const addControlLayersToGraph = async ( // If we are using the "invert" auto-negative setting, we need to add an additional negative conditioning node if (layer.autoNegative === 'invert' && layer.positivePrompt) { // We re-use the mask image, but invert it when converting to tensor - const invertTensorMaskNode: S['InvertTensorMaskInvocation'] = { + const invertTensorMaskNode: Invocation<'invert_tensor_mask'> = { id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${layer.id}`, type: 'invert_tensor_mask', }; @@ -263,7 +261,7 @@ export const addControlLayersToGraph = async ( // Create the conditioning node. It's going to be connected to the negative cond collector, but it uses the // positive prompt - const regionalPositiveCondInvertedNode: S['SDXLCompelPromptInvocation'] | S['CompelInvocation'] = isSDXL + const regionalPositiveCondInvertedNode: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'> = isSDXL ? { type: 'sdxl_compel_prompt', id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, @@ -574,7 +572,7 @@ const addInitialImageLayerToGraph = ( : 1 - denoisingStrength; denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1; - const i2lNode: ImageToLatentsInvocation = { + const i2lNode: Invocation<'i2l'> = { type: 'i2l', id: IMAGE_TO_LATENTS, is_intermediate: true, @@ -598,7 +596,7 @@ const addInitialImageLayerToGraph = ( // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` // Create a resize node, explicitly setting its image - const resizeNode: ImageResizeInvocation = { + const resizeNode: Invocation<'img_resize'> = { id: RESIZE, type: 'img_resize', image: { diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index a153780712..8d41fd6474 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -129,43 +129,70 @@ export type WorkflowRecordOrderBy = S['WorkflowRecordOrderBy']; export type SQLiteDirection = S['SQLiteDirection']; export type WorkflowRecordListItemDTO = S['WorkflowRecordListItemDTO']; +export type KeysOfUnion = T extends T ? keyof T : never; + +export type NonInputFields = 'id' | 'type' | 'is_intermediate' | 'use_cache'; +export type NonOutputFields = 'type'; +export type AnyInvocation = Graph['nodes'][string]; +export type AnyInvocationExcludeCoreMetata = Exclude; +export type InvocationType = AnyInvocation['type']; +export type InvocationTypeExcludeCoreMetadata = Exclude; + +export type InvocationOutputMap = S['InvocationOutputMap']; +export type AnyInvocationOutput = InvocationOutputMap[InvocationType]; + +export type Invocation = Extract; +export type InvocationExcludeCoreMetadata = Extract< + AnyInvocation, + { type: T } +>; +export type InvocationInputFields = Exclude< + keyof Invocation, + NonInputFields +>; +export type AnyInvocationInputField = Exclude, NonInputFields>; + +export type InvocationOutput = InvocationOutputMap[T]; +export type InvocationOutputFields = Exclude, NonOutputFields>; +export type AnyInvocationOutputField = Exclude, NonOutputFields>; + // General nodes -export type CollectInvocation = S['CollectInvocation']; -export type ImageResizeInvocation = S['ImageResizeInvocation']; -export type InfillPatchMatchInvocation = S['InfillPatchMatchInvocation']; -export type InfillTileInvocation = S['InfillTileInvocation']; -export type CreateGradientMaskInvocation = S['CreateGradientMaskInvocation']; -export type CanvasPasteBackInvocation = S['CanvasPasteBackInvocation']; -export type NoiseInvocation = S['NoiseInvocation']; -export type DenoiseLatentsInvocation = S['DenoiseLatentsInvocation']; -export type SDXLLoRALoaderInvocation = S['SDXLLoRALoaderInvocation']; -export type ImageToLatentsInvocation = S['ImageToLatentsInvocation']; -export type LatentsToImageInvocation = S['LatentsToImageInvocation']; -export type LoRALoaderInvocation = S['LoRALoaderInvocation']; -export type ESRGANInvocation = S['ESRGANInvocation']; -export type ImageNSFWBlurInvocation = S['ImageNSFWBlurInvocation']; -export type ImageWatermarkInvocation = S['ImageWatermarkInvocation']; -export type SeamlessModeInvocation = S['SeamlessModeInvocation']; -export type CoreMetadataInvocation = S['CoreMetadataInvocation']; +export type CollectInvocation = Invocation<'collect'>; +export type ImageResizeInvocation = Invocation<'img_resize'>; +export type InfillPatchMatchInvocation = Invocation<'infill_patchmatch'>; +export type InfillTileInvocation = Invocation<'infill_tile'>; +export type CreateGradientMaskInvocation = Invocation<'create_gradient_mask'>; +export type CanvasPasteBackInvocation = Invocation<'canvas_paste_back'>; +export type NoiseInvocation = Invocation<'noise'>; +export type DenoiseLatentsInvocation = Invocation<'denoise_latents'>; +export type SDXLLoRALoaderInvocation = Invocation<'sdxl_lora_loader'>; +export type ImageToLatentsInvocation = Invocation<'i2l'>; +export type LatentsToImageInvocation = Invocation<'l2i'>; +export type LoRALoaderInvocation = Invocation<'lora_loader'>; +export type ESRGANInvocation = Invocation<'esrgan'>; +export type ImageNSFWBlurInvocation = Invocation<'img_nsfw'>; +export type ImageWatermarkInvocation = Invocation<'img_watermark'>; +export type SeamlessModeInvocation = Invocation<'seamless'>; +export type CoreMetadataInvocation = Extract; // ControlNet Nodes -export type ControlNetInvocation = S['ControlNetInvocation']; -export type T2IAdapterInvocation = S['T2IAdapterInvocation']; -export type IPAdapterInvocation = S['IPAdapterInvocation']; -export type CannyImageProcessorInvocation = S['CannyImageProcessorInvocation']; -export type ColorMapImageProcessorInvocation = S['ColorMapImageProcessorInvocation']; -export type ContentShuffleImageProcessorInvocation = S['ContentShuffleImageProcessorInvocation']; -export type DepthAnythingImageProcessorInvocation = S['DepthAnythingImageProcessorInvocation']; -export type HedImageProcessorInvocation = S['HedImageProcessorInvocation']; -export type LineartAnimeImageProcessorInvocation = S['LineartAnimeImageProcessorInvocation']; -export type LineartImageProcessorInvocation = S['LineartImageProcessorInvocation']; -export type MediapipeFaceProcessorInvocation = S['MediapipeFaceProcessorInvocation']; -export type MidasDepthImageProcessorInvocation = S['MidasDepthImageProcessorInvocation']; -export type MlsdImageProcessorInvocation = S['MlsdImageProcessorInvocation']; -export type NormalbaeImageProcessorInvocation = S['NormalbaeImageProcessorInvocation']; -export type DWOpenposeImageProcessorInvocation = S['DWOpenposeImageProcessorInvocation']; -export type PidiImageProcessorInvocation = S['PidiImageProcessorInvocation']; -export type ZoeDepthImageProcessorInvocation = S['ZoeDepthImageProcessorInvocation']; +export type ControlNetInvocation = Invocation<'controlnet'>; +export type T2IAdapterInvocation = Invocation<'t2i_adapter'>; +export type IPAdapterInvocation = Invocation<'ip_adapter'>; +export type CannyImageProcessorInvocation = Invocation<'canny_image_processor'>; +export type ColorMapImageProcessorInvocation = Invocation<'color_map_image_processor'>; +export type ContentShuffleImageProcessorInvocation = Invocation<'content_shuffle_image_processor'>; +export type DepthAnythingImageProcessorInvocation = Invocation<'depth_anything_image_processor'>; +export type HedImageProcessorInvocation = Invocation<'hed_image_processor'>; +export type LineartAnimeImageProcessorInvocation = Invocation<'lineart_anime_image_processor'>; +export type LineartImageProcessorInvocation = Invocation<'lineart_image_processor'>; +export type MediapipeFaceProcessorInvocation = Invocation<'mediapipe_face_processor'>; +export type MidasDepthImageProcessorInvocation = Invocation<'midas_depth_image_processor'>; +export type MlsdImageProcessorInvocation = Invocation<'mlsd_image_processor'>; +export type NormalbaeImageProcessorInvocation = Invocation<'normalbae_image_processor'>; +export type DWOpenposeImageProcessorInvocation = Invocation<'dw_openpose_image_processor'>; +export type PidiImageProcessorInvocation = Invocation<'pidi_image_processor'>; +export type ZoeDepthImageProcessorInvocation = Invocation<'zoe_depth_image_processor'>; // Node Outputs export type ImageOutput = S['ImageOutput']; From 9d685da759afadaea32e9022cde8f78227cf1bb3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 5 May 2024 08:44:04 +1000 Subject: [PATCH 096/442] feat(ui): add stateful Graph class This stateful class provides abstractions for building a graph. It exposes graph methods like adding and removing nodes and edges. The methods are documented, tested, and strongly typed. --- .../features/nodes/util/graph/Graph.test.ts | 338 ++++++++++++++++ .../src/features/nodes/util/graph/Graph.ts | 366 ++++++++++++++++++ 2 files changed, 704 insertions(+) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts new file mode 100644 index 0000000000..6a38dbd218 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts @@ -0,0 +1,338 @@ +import { Graph } from 'features/nodes/util/graph/Graph'; +import type { Invocation } from 'services/api/types'; +import { assert, AssertionError, is } from 'tsafe'; +import { validate } from 'uuid'; +import { describe, expect, it } from 'vitest'; + +describe('Graph', () => { + describe('constructor', () => { + it('should create a new graph with the correct id', () => { + const g = new Graph('test-id'); + expect(g._graph.id).toBe('test-id'); + }); + it('should create a new graph with a uuid id if none is provided', () => { + const g = new Graph(); + expect(g._graph.id).not.toBeUndefined(); + expect(validate(g._graph.id)).toBeTruthy(); + }); + }); + + describe('addNode', () => { + const testNode = { + id: 'test-node', + type: 'add', + } as const; + it('should add a node to the graph', () => { + const g = new Graph(); + g.addNode(testNode); + expect(g._graph.nodes['test-node']).not.toBeUndefined(); + expect(g._graph.nodes['test-node']?.type).toBe('add'); + }); + it('should set is_intermediate to true if not provided', () => { + const g = new Graph(); + g.addNode(testNode); + expect(g._graph.nodes['test-node']?.is_intermediate).toBe(true); + }); + it('should not overwrite is_intermediate if provided', () => { + const g = new Graph(); + g.addNode({ + ...testNode, + is_intermediate: false, + }); + expect(g._graph.nodes['test-node']?.is_intermediate).toBe(false); + }); + it('should set use_cache to true if not provided', () => { + const g = new Graph(); + g.addNode(testNode); + expect(g._graph.nodes['test-node']?.use_cache).toBe(true); + }); + it('should not overwrite use_cache if provided', () => { + const g = new Graph(); + g.addNode({ + ...testNode, + use_cache: false, + }); + expect(g._graph.nodes['test-node']?.use_cache).toBe(false); + }); + it('should error if the node id is already in the graph', () => { + const g = new Graph(); + g.addNode(testNode); + expect(() => g.addNode(testNode)).toThrowError(AssertionError); + }); + it('should infer the types if provided', () => { + const g = new Graph(); + const node = g.addNode(testNode); + assert(is>(node)); + const g2 = new Graph(); + // @ts-expect-error The node object is an `add` type, but the generic is a `sub` type + g2.addNode<'sub'>(testNode); + }); + }); + + describe('updateNode', () => { + it('should update the node with the provided id', () => { + const g = new Graph(); + const node: Invocation<'add'> = { + id: 'test-node', + type: 'add', + a: 1, + }; + g.addNode(node); + const updatedNode = g.updateNode('test-node', 'add', { + a: 2, + }); + expect(g.getNode('test-node', 'add').a).toBe(2); + expect(node).toBe(updatedNode); + }); + it('should throw an error if the node is not found', () => { + expect(() => new Graph().updateNode('not-found', 'add', {})).toThrowError(AssertionError); + }); + it('should throw an error if the node is found but has the wrong type', () => { + const g = new Graph(); + g.addNode({ + id: 'test-node', + type: 'add', + a: 1, + }); + expect(() => g.updateNode('test-node', 'sub', {})).toThrowError(AssertionError); + }); + it('should infer types correctly when `type` is omitted', () => { + const g = new Graph(); + g.addNode({ + id: 'test-node', + type: 'add', + a: 1, + }); + const updatedNode = g.updateNode('test-node', 'add', { + a: 2, + }); + assert(is>(updatedNode)); + }); + it('should infer types correctly when `type` is provided', () => { + const g = new Graph(); + g.addNode({ + id: 'test-node', + type: 'add', + a: 1, + }); + const updatedNode = g.updateNode('test-node', 'add', { + a: 2, + }); + assert(is>(updatedNode)); + }); + }); + + describe('addEdge', () => { + it('should add an edge to the graph with the provided values', () => { + const g = new Graph(); + g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); + expect(g._graph.edges.length).toBe(1); + expect(g._graph.edges[0]).toEqual({ + source: { node_id: 'from-node', field: 'value' }, + destination: { node_id: 'to-node', field: 'b' }, + }); + }); + it('should throw an error if the edge already exists', () => { + const g = new Graph(); + g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); + expect(() => g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b')).toThrowError(AssertionError); + }); + it('should infer field names', () => { + const g = new Graph(); + // @ts-expect-error The first field must be a valid output field of the first type arg + g.addEdge<'add', 'sub'>('from-node', 'not-a-valid-field', 'to-node', 'a'); + // @ts-expect-error The second field must be a valid input field of the second type arg + g.addEdge<'add', 'sub'>('from-node-2', 'value', 'to-node-2', 'not-a-valid-field'); + // @ts-expect-error The first field must be any valid output field + g.addEdge('from-node-3', 'not-a-valid-field', 'to-node-3', 'a'); + // @ts-expect-error The second field must be any valid input field + g.addEdge('from-node-4', 'clip', 'to-node-4', 'not-a-valid-field'); + }); + }); + + describe('getNode', () => { + const g = new Graph(); + const node = g.addNode({ + id: 'test-node', + type: 'add', + }); + + it('should return the node with the provided id', () => { + const n = g.getNode('test-node'); + expect(n).toBe(node); + }); + it('should return the node with the provided id and type', () => { + const n = g.getNode('test-node', 'add'); + expect(n).toBe(node); + assert(is>(node)); + }); + it('should throw an error if the node is not found', () => { + expect(() => g.getNode('not-found')).toThrowError(AssertionError); + }); + it('should throw an error if the node is found but has the wrong type', () => { + expect(() => g.getNode('test-node', 'sub')).toThrowError(AssertionError); + }); + }); + + describe('getNodeSafe', () => { + const g = new Graph(); + const node = g.addNode({ + id: 'test-node', + type: 'add', + }); + it('should return the node if it is found', () => { + expect(g.getNodeSafe('test-node')).toBe(node); + }); + it('should return the node if it is found with the provided type', () => { + expect(g.getNodeSafe('test-node')).toBe(node); + assert(is>(node)); + }); + it("should return undefined if the node isn't found", () => { + expect(g.getNodeSafe('not-found')).toBeUndefined(); + }); + it('should return undefined if the node is found but has the wrong type', () => { + expect(g.getNodeSafe('test-node', 'sub')).toBeUndefined(); + }); + }); + + describe('hasNode', () => { + const g = new Graph(); + g.addNode({ + id: 'test-node', + type: 'add', + }); + + it('should return true if the node is in the graph', () => { + expect(g.hasNode('test-node')).toBe(true); + }); + it('should return false if the node is not in the graph', () => { + expect(g.hasNode('not-found')).toBe(false); + }); + }); + + describe('getEdge', () => { + const g = new Graph(); + g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); + it('should return the edge with the provided values', () => { + expect(g.getEdge('from-node', 'value', 'to-node', 'b')).toEqual({ + source: { node_id: 'from-node', field: 'value' }, + destination: { node_id: 'to-node', field: 'b' }, + }); + }); + it('should throw an error if the edge is not found', () => { + expect(() => g.getEdge('from-node', 'value', 'to-node', 'a')).toThrowError(AssertionError); + }); + }); + + describe('getEdgeSafe', () => { + const g = new Graph(); + g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); + it('should return the edge if it is found', () => { + expect(g.getEdgeSafe('from-node', 'value', 'to-node', 'b')).toEqual({ + source: { node_id: 'from-node', field: 'value' }, + destination: { node_id: 'to-node', field: 'b' }, + }); + }); + it('should return undefined if the edge is not found', () => { + expect(g.getEdgeSafe('from-node', 'value', 'to-node', 'a')).toBeUndefined(); + }); + }); + + describe('hasEdge', () => { + const g = new Graph(); + g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); + it('should return true if the edge is in the graph', () => { + expect(g.hasEdge('from-node', 'value', 'to-node', 'b')).toBe(true); + }); + it('should return false if the edge is not in the graph', () => { + expect(g.hasEdge('from-node', 'value', 'to-node', 'a')).toBe(false); + }); + }); + + describe('getGraph', () => { + it('should return the graph', () => { + const g = new Graph(); + expect(g.getGraph()).toBe(g._graph); + }); + it('should raise an error if the graph is invalid', () => { + const g = new Graph(); + g.addEdge('from-node', 'value', 'to-node', 'b'); + expect(() => g.getGraph()).toThrowError(AssertionError); + }); + }); + + describe('getGraphSafe', () => { + it('should return the graph even if it is invalid', () => { + const g = new Graph(); + g.addEdge('from-node', 'value', 'to-node', 'b'); + expect(g.getGraphSafe()).toBe(g._graph); + }); + }); + + describe('validate', () => { + it('should not throw an error if the graph is valid', () => { + const g = new Graph(); + expect(() => g.validate()).not.toThrow(); + }); + it('should throw an error if the graph is invalid', () => { + const g = new Graph(); + // edge from nowhere to nowhere + g.addEdge('from-node', 'value', 'to-node', 'b'); + expect(() => g.validate()).toThrowError(AssertionError); + }); + }); + + describe('traversal', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'add', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'alpha_mask_to_tensor', + }); + const n3 = g.addNode({ + id: 'n3', + type: 'add', + }); + const n4 = g.addNode({ + id: 'n4', + type: 'add', + }); + const n5 = g.addNode({ + id: 'n5', + type: 'add', + }); + const e1 = g.addEdge<'add', 'add'>(n1.id, 'value', n3.id, 'a'); + const e2 = g.addEdge<'alpha_mask_to_tensor', 'add'>(n2.id, 'height', n3.id, 'b'); + const e3 = g.addEdge<'add', 'add'>(n3.id, 'value', n4.id, 'a'); + const e4 = g.addEdge<'add', 'add'>(n3.id, 'value', n5.id, 'b'); + describe('getEdgesFrom', () => { + it('should return the edges that start at the provided node', () => { + expect(g.getEdgesFrom(n3.id)).toEqual([e3, e4]); + }); + it('should return the edges that start at the provided node and have the provided field', () => { + expect(g.getEdgesFrom(n2.id, 'height')).toEqual([e2]); + }); + }); + describe('getEdgesTo', () => { + it('should return the edges that end at the provided node', () => { + expect(g.getEdgesTo(n3.id)).toEqual([e1, e2]); + }); + it('should return the edges that end at the provided node and have the provided field', () => { + expect(g.getEdgesTo(n3.id, 'b')).toEqual([e2]); + }); + }); + describe('getIncomers', () => { + it('should return the nodes that have an edge to the provided node', () => { + expect(g.getIncomers(n3.id)).toEqual([n1, n2]); + }); + }); + describe('getOutgoers', () => { + it('should return the nodes that the provided node has an edge to', () => { + expect(g.getOutgoers(n3.id)).toEqual([n4, n5]); + }); + }); + }); +}); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts new file mode 100644 index 0000000000..ecade47b23 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts @@ -0,0 +1,366 @@ +import { isEqual } from 'lodash-es'; +import type { + AnyInvocation, + AnyInvocationInputField, + AnyInvocationOutputField, + Invocation, + InvocationInputFields, + InvocationOutputFields, + InvocationType, + S, +} from 'services/api/types'; +import type { O } from 'ts-toolbelt'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +type GraphType = O.NonNullable>; +type Edge = GraphType['edges'][number]; +type Never = Record; + +// The `core_metadata` node has very lax types, it accepts arbitrary field names. It must be excluded from edge utils +// to preview their types from being widened from a union of valid field names to `string | number | symbol`. +type EdgeNodeType = Exclude; + +type EdgeFromField = TFrom extends EdgeNodeType + ? InvocationOutputFields + : AnyInvocationOutputField; + +type EdgeToField = TTo extends EdgeNodeType + ? InvocationInputFields + : AnyInvocationInputField; + +export class Graph { + _graph: GraphType; + + constructor(id?: string) { + this._graph = { + id: id ?? Graph.uuid(), + nodes: {}, + edges: [], + }; + } + + //#region Node Operations + + /** + * Add a node to the graph. If a node with the same id already exists, an `AssertionError` is raised. + * The optional `is_intermediate` and `use_cache` fields are set to `true` and `true` respectively if not set on the node. + * @param node The node to add. + * @returns The added node. + * @raises `AssertionError` if a node with the same id already exists. + */ + addNode(node: Invocation): Invocation { + assert(this._graph.nodes[node.id] === undefined, Graph.getNodeAlreadyExistsMsg(node.id)); + if (node.is_intermediate === undefined) { + node.is_intermediate = true; + } + if (node.use_cache === undefined) { + node.use_cache = true; + } + this._graph.nodes[node.id] = node; + return node; + } + + /** + * Gets a node from the graph. + * @param id The id of the node to get. + * @param type The type of the node to get. If provided, the retrieved node is guaranteed to be of this type. + * @returns The node. + * @raises `AssertionError` if the node does not exist or if a `type` is provided but the node is not of the expected type. + */ + getNode(id: string, type?: T): Invocation { + const node = this._graph.nodes[id]; + assert(node !== undefined, Graph.getNodeNotFoundMsg(id)); + if (type) { + assert(node.type === type, Graph.getNodeNotOfTypeMsg(node, type)); + } + // We just asserted that the node type is correct, this is OK to cast + return node as Invocation; + } + + /** + * Gets a node from the graph without raising an error if the node does not exist or is not of the expected type. + * @param id The id of the node to get. + * @param type The type of the node to get. If provided, node is guaranteed to be of this type. + * @returns The node, if it exists and is of the correct type. Otherwise, `undefined`. + */ + getNodeSafe(id: string, type?: T): Invocation | undefined { + try { + return this.getNode(id, type); + } catch { + return undefined; + } + } + + /** + * Update a node in the graph. Properties are shallow-copied from `updates` to the node. + * @param id The id of the node to update. + * @param type The type of the node to update. If provided, node is guaranteed to be of this type. + * @param updates The fields to update on the node. + * @returns The updated node. + * @raises `AssertionError` if the node does not exist or its type doesn't match. + */ + updateNode(id: string, type: T, updates: Partial>): Invocation { + const node = this.getNode(id, type); + Object.assign(node, updates); + return node; + } + + /** + * Check if a node exists in the graph. + * @param id The id of the node to check. + */ + hasNode(id: string): boolean { + try { + this.getNode(id); + return true; + } catch { + return false; + } + } + + /** + * Get the immediate incomers of a node. + * @param nodeId The id of the node to get the incomers of. + * @returns The incoming nodes. + * @raises `AssertionError` if the node does not exist. + */ + getIncomers(nodeId: string): AnyInvocation[] { + return this.getEdgesTo(nodeId).map((edge) => this.getNode(edge.source.node_id)); + } + + /** + * Get the immediate outgoers of a node. + * @param nodeId The id of the node to get the outgoers of. + * @returns The outgoing nodes. + * @raises `AssertionError` if the node does not exist. + */ + getOutgoers(nodeId: string): AnyInvocation[] { + return this.getEdgesFrom(nodeId).map((edge) => this.getNode(edge.destination.node_id)); + } + //#endregion + + //#region Edge Operations + + /** + * Add an edge to the graph. If an edge with the same source and destination already exists, an `AssertionError` is raised. + * Provide the from and to node types as generics to get type hints for from and to field names. + * @param fromNodeId The id of the source node. + * @param fromField The field of the source node. + * @param toNodeId The id of the destination node. + * @param toField The field of the destination node. + * @returns The added edge. + * @raises `AssertionError` if an edge with the same source and destination already exists. + */ + addEdge( + fromNodeId: string, + fromField: EdgeFromField, + toNodeId: string, + toField: EdgeToField + ): Edge { + const edge = { + source: { node_id: fromNodeId, field: fromField }, + destination: { node_id: toNodeId, field: toField }, + }; + assert( + !this._graph.edges.some((e) => isEqual(e, edge)), + Graph.getEdgeAlreadyExistsMsg(fromNodeId, fromField, toNodeId, toField) + ); + this._graph.edges.push(edge); + return edge; + } + + /** + * Get an edge from the graph. If the edge does not exist, an `AssertionError` is raised. + * Provide the from and to node types as generics to get type hints for from and to field names. + * @param fromNodeId The id of the source node. + * @param fromField The field of the source node. + * @param toNodeId The id of the destination node. + * @param toField The field of the destination node. + * @returns The edge. + * @raises `AssertionError` if the edge does not exist. + */ + getEdge( + fromNode: string, + fromField: EdgeFromField, + toNode: string, + toField: EdgeToField + ): Edge { + const edge = this._graph.edges.find( + (e) => + e.source.node_id === fromNode && + e.source.field === fromField && + e.destination.node_id === toNode && + e.destination.field === toField + ); + assert(edge !== undefined, Graph.getEdgeNotFoundMsg(fromNode, fromField, toNode, toField)); + return edge; + } + + /** + * Get an edge from the graph, or undefined if it doesn't exist. + * Provide the from and to node types as generics to get type hints for from and to field names. + * @param fromNodeId The id of the source node. + * @param fromField The field of the source node. + * @param toNodeId The id of the destination node. + * @param toField The field of the destination node. + * @returns The edge, or undefined if it doesn't exist. + */ + getEdgeSafe( + fromNode: string, + fromField: EdgeFromField, + toNode: string, + toField: EdgeToField + ): Edge | undefined { + try { + return this.getEdge(fromNode, fromField, toNode, toField); + } catch { + return undefined; + } + } + + /** + * Check if a graph has an edge. + * Provide the from and to node types as generics to get type hints for from and to field names. + * @param fromNodeId The id of the source node. + * @param fromField The field of the source node. + * @param toNodeId The id of the destination node. + * @param toField The field of the destination node. + * @returns Whether the graph has the edge. + */ + + hasEdge( + fromNode: string, + fromField: EdgeFromField, + toNode: string, + toField: EdgeToField + ): boolean { + try { + this.getEdge(fromNode, fromField, toNode, toField); + return true; + } catch { + return false; + } + } + + /** + * Get all edges from a node. If `fromField` is provided, only edges from that field are returned. + * Provide the from node type as a generic to get type hints for from field names. + * @param fromNodeId The id of the source node. + * @param fromField The field of the source node (optional). + * @returns The edges. + */ + getEdgesFrom(fromNodeId: string, fromField?: EdgeFromField): Edge[] { + let edges = this._graph.edges.filter((edge) => edge.source.node_id === fromNodeId); + if (fromField) { + edges = edges.filter((edge) => edge.source.field === fromField); + } + return edges; + } + + /** + * Get all edges to a node. If `toField` is provided, only edges to that field are returned. + * Provide the to node type as a generic to get type hints for to field names. + * @param toNodeId The id of the destination node. + * @param toField The field of the destination node (optional). + * @returns The edges. + */ + getEdgesTo(toNodeId: string, toField?: EdgeToField): Edge[] { + let edges = this._graph.edges.filter((edge) => edge.destination.node_id === toNodeId); + if (toField) { + edges = edges.filter((edge) => edge.destination.field === toField); + } + return edges; + } + + /** + * Delete _all_ matching edges from the graph. Uses _.isEqual for comparison. + * @param edge The edge to delete + */ + private _deleteEdge(edge: Edge): void { + this._graph.edges = this._graph.edges.filter((e) => !isEqual(e, edge)); + } + + /** + * Delete all edges to a node. If `toField` is provided, only edges to that field are deleted. + * Provide the to node type as a generic to get type hints for to field names. + * @param toNodeId The id of the destination node. + * @param toField The field of the destination node (optional). + */ + deleteEdgesTo(toNodeId: string, toField?: EdgeToField): void { + for (const edge of this.getEdgesTo(toNodeId, toField)) { + this._deleteEdge(edge); + } + } + + /** + * Delete all edges from a node. If `fromField` is provided, only edges from that field are deleted. + * Provide the from node type as a generic to get type hints for from field names. + * @param toNodeId The id of the source node. + * @param toField The field of the source node (optional). + */ + deleteEdgesFrom(fromNodeId: string, fromField?: EdgeFromField): void { + for (const edge of this.getEdgesFrom(fromNodeId, fromField)) { + this._deleteEdge(edge); + } + } + //#endregion + + //#region Graph Ops + + /** + * Validate the graph. Checks that all edges have valid source and destination nodes. + * TODO(psyche): Add more validation checks - cycles, valid invocation types, etc. + * @raises `AssertionError` if an edge has an invalid source or destination node. + */ + validate(): void { + for (const edge of this._graph.edges) { + this.getNode(edge.source.node_id); + this.getNode(edge.destination.node_id); + } + } + + /** + * Gets the graph after validating it. + * @returns The graph. + * @raises `AssertionError` if the graph is invalid. + */ + getGraph(): GraphType { + this.validate(); + return this._graph; + } + + /** + * Gets the graph without validating it. + * @returns The graph. + */ + getGraphSafe(): GraphType { + return this._graph; + } + //#endregion + + //#region Util + + static getNodeNotFoundMsg(id: string): string { + return `Node ${id} not found`; + } + + static getNodeNotOfTypeMsg(node: AnyInvocation, expectedType: InvocationType): string { + return `Node ${node.id} is not of type ${expectedType}: ${node.type}`; + } + + static getNodeAlreadyExistsMsg(id: string): string { + return `Node ${id} already exists`; + } + + static getEdgeNotFoundMsg(fromNodeId: string, fromField: string, toNodeId: string, toField: string) { + return `Edge from ${fromNodeId}.${fromField} to ${toNodeId}.${toField} not found`; + } + + static getEdgeAlreadyExistsMsg(fromNodeId: string, fromField: string, toNodeId: string, toField: string) { + return `Edge from ${fromNodeId}.${fromField} to ${toNodeId}.${toField} already exists`; + } + + static uuid = uuidv4; + //#endregion +} From 4020bf47e22fb33697ca7fafc5e37fc144cdd2e2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 5 May 2024 10:00:14 +1000 Subject: [PATCH 097/442] feat(ui): add MetadataUtil class Provides methods for manipulating a graph's metadata. --- .../nodes/util/graph/MetadataUtil.test.ts | 88 +++++++++++++++++++ .../features/nodes/util/graph/MetadataUtil.ts | 57 ++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts new file mode 100644 index 0000000000..ba76e43632 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts @@ -0,0 +1,88 @@ +import { isModelIdentifier } from 'features/nodes/types/common'; +import { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import { pick } from 'lodash-es'; +import type { AnyModelConfig } from 'services/api/types'; +import { AssertionError } from 'tsafe'; +import { describe, expect, it } from 'vitest'; + +describe('MetadataUtil', () => { + describe('getNode', () => { + it('should return the metadata node if one exists', () => { + const g = new Graph(); + const metadataNode = g.addNode({ id: MetadataUtil.metadataNodeId, type: 'core_metadata' }); + expect(MetadataUtil.getNode(g)).toEqual(metadataNode); + }); + it('should raise an error if the metadata node does not exist', () => { + const g = new Graph(); + expect(() => MetadataUtil.getNode(g)).toThrowError(AssertionError); + }); + }); + + describe('add', () => { + const g = new Graph(); + it("should add metadata, creating the node if it doesn't exist", () => { + MetadataUtil.add(g, { foo: 'bar' }); + const metadataNode = MetadataUtil.getNode(g); + expect(metadataNode['type']).toBe('core_metadata'); + expect(metadataNode['foo']).toBe('bar'); + }); + it('should update existing metadata keys', () => { + const updatedMetadataNode = MetadataUtil.add(g, { foo: 'bananas', baz: 'qux' }); + expect(updatedMetadataNode['foo']).toBe('bananas'); + expect(updatedMetadataNode['baz']).toBe('qux'); + }); + }); + + describe('remove', () => { + it('should remove a single key', () => { + const g = new Graph(); + MetadataUtil.add(g, { foo: 'bar', baz: 'qux' }); + const updatedMetadataNode = MetadataUtil.remove(g, 'foo'); + expect(updatedMetadataNode['foo']).toBeUndefined(); + expect(updatedMetadataNode['baz']).toBe('qux'); + }); + it('should remove multiple keys', () => { + const g = new Graph(); + MetadataUtil.add(g, { foo: 'bar', baz: 'qux' }); + const updatedMetadataNode = MetadataUtil.remove(g, ['foo', 'baz']); + expect(updatedMetadataNode['foo']).toBeUndefined(); + expect(updatedMetadataNode['baz']).toBeUndefined(); + }); + }); + + describe('setMetadataReceivingNode', () => { + const g = new Graph(); + it('should add an edge from from metadata to the receiving node', () => { + const n = g.addNode({ id: 'my-node', type: 'img_resize' }); + MetadataUtil.add(g, { foo: 'bar' }); + MetadataUtil.setMetadataReceivingNode(g, n.id); + expect(g.hasEdge(MetadataUtil.metadataNodeId, 'metadata', n.id, 'metadata')).toBe(true); + }); + it('should remove existing metadata edges', () => { + const n2 = g.addNode({ id: 'my-other-node', type: 'img_resize' }); + MetadataUtil.setMetadataReceivingNode(g, n2.id); + expect(g.getIncomers(n2.id).length).toBe(1); + expect(g.hasEdge(MetadataUtil.metadataNodeId, 'metadata', n2.id, 'metadata')).toBe(true); + }); + }); + + describe('getModelMetadataField', () => { + it('should return a ModelIdentifierField', () => { + const model: AnyModelConfig = { + key: 'model_key', + type: 'main', + hash: 'model_hash', + base: 'sd-1', + format: 'diffusers', + name: 'my model', + path: '/some/path', + source: 'www.models.com', + source_type: 'url', + }; + const metadataField = MetadataUtil.getModelMetadataField(model); + expect(isModelIdentifier(metadataField)).toBe(true); + expect(pick(model, ['key', 'hash', 'name', 'base', 'type'])).toEqual(metadataField); + }); + }); +}); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts new file mode 100644 index 0000000000..a51cebd21e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts @@ -0,0 +1,57 @@ +import type { ModelIdentifierField } from 'features/nodes/types/common'; +import { METADATA } from 'features/nodes/util/graph/constants'; +import { isString, unset } from 'lodash-es'; +import type { AnyModelConfig, Invocation } from 'services/api/types'; + +import type { Graph } from './Graph'; + +export class MetadataUtil { + static metadataNodeId = METADATA; + + static getNode(graph: Graph): Invocation<'core_metadata'> { + return graph.getNode(this.metadataNodeId, 'core_metadata'); + } + + static add(graph: Graph, metadata: Partial>): Invocation<'core_metadata'> { + const metadataNode = graph.getNodeSafe(this.metadataNodeId, 'core_metadata'); + if (!metadataNode) { + return graph.addNode({ + id: this.metadataNodeId, + type: 'core_metadata', + ...metadata, + }); + } else { + return graph.updateNode(this.metadataNodeId, 'core_metadata', metadata); + } + } + + static remove(graph: Graph, key: string): Invocation<'core_metadata'>; + static remove(graph: Graph, keys: string[]): Invocation<'core_metadata'>; + static remove(graph: Graph, keyOrKeys: string | string[]): Invocation<'core_metadata'> { + const metadataNode = this.getNode(graph); + if (isString(keyOrKeys)) { + unset(metadataNode, keyOrKeys); + } else { + for (const key of keyOrKeys) { + unset(metadataNode, key); + } + } + return metadataNode; + } + + static setMetadataReceivingNode(graph: Graph, nodeId: string): void { + // We need to break the rules to update metadata - `addEdge` doesn't allow `core_metadata` as a node type + graph._graph.edges = graph._graph.edges.filter((edge) => edge.source.node_id !== this.metadataNodeId); + graph.addEdge(this.metadataNodeId, 'metadata', nodeId, 'metadata'); + } + + static getModelMetadataField({ key, hash, name, base, type }: AnyModelConfig): ModelIdentifierField { + return { + key, + hash, + name, + base, + type, + }; + } +} From 8f6078d007ffa52607ad79f11a5d59e45ef60d58 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 5 May 2024 16:14:54 +1000 Subject: [PATCH 098/442] feat(ui): refine graph building util Simpler types and API surface. --- .../features/nodes/util/graph/Graph.test.ts | 198 +++++++----------- .../src/features/nodes/util/graph/Graph.ts | 168 +++++---------- .../nodes/util/graph/MetadataUtil.test.ts | 13 +- .../features/nodes/util/graph/MetadataUtil.ts | 53 +++-- .../frontend/web/src/services/api/types.ts | 35 ++-- 5 files changed, 189 insertions(+), 278 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts index 6a38dbd218..71bcd9331c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts @@ -69,63 +69,20 @@ describe('Graph', () => { }); }); - describe('updateNode', () => { - it('should update the node with the provided id', () => { - const g = new Graph(); - const node: Invocation<'add'> = { - id: 'test-node', - type: 'add', - a: 1, - }; - g.addNode(node); - const updatedNode = g.updateNode('test-node', 'add', { - a: 2, - }); - expect(g.getNode('test-node', 'add').a).toBe(2); - expect(node).toBe(updatedNode); - }); - it('should throw an error if the node is not found', () => { - expect(() => new Graph().updateNode('not-found', 'add', {})).toThrowError(AssertionError); - }); - it('should throw an error if the node is found but has the wrong type', () => { - const g = new Graph(); - g.addNode({ - id: 'test-node', - type: 'add', - a: 1, - }); - expect(() => g.updateNode('test-node', 'sub', {})).toThrowError(AssertionError); - }); - it('should infer types correctly when `type` is omitted', () => { - const g = new Graph(); - g.addNode({ - id: 'test-node', - type: 'add', - a: 1, - }); - const updatedNode = g.updateNode('test-node', 'add', { - a: 2, - }); - assert(is>(updatedNode)); - }); - it('should infer types correctly when `type` is provided', () => { - const g = new Graph(); - g.addNode({ - id: 'test-node', - type: 'add', - a: 1, - }); - const updatedNode = g.updateNode('test-node', 'add', { - a: 2, - }); - assert(is>(updatedNode)); - }); - }); - describe('addEdge', () => { + const add: Invocation<'add'> = { + id: 'from-node', + type: 'add', + }; + const sub: Invocation<'sub'> = { + id: 'to-node', + type: 'sub', + }; it('should add an edge to the graph with the provided values', () => { const g = new Graph(); - g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); + g.addNode(add); + g.addNode(sub); + g.addEdge(add, 'value', sub, 'b'); expect(g._graph.edges.length).toBe(1); expect(g._graph.edges[0]).toEqual({ source: { node_id: 'from-node', field: 'value' }, @@ -134,19 +91,19 @@ describe('Graph', () => { }); it('should throw an error if the edge already exists', () => { const g = new Graph(); - g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); - expect(() => g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b')).toThrowError(AssertionError); + g.addEdge(add, 'value', sub, 'b'); + expect(() => g.addEdge(add, 'value', sub, 'b')).toThrowError(AssertionError); }); it('should infer field names', () => { const g = new Graph(); // @ts-expect-error The first field must be a valid output field of the first type arg - g.addEdge<'add', 'sub'>('from-node', 'not-a-valid-field', 'to-node', 'a'); + g.addEdge(add, 'not-a-valid-field', add, 'a'); // @ts-expect-error The second field must be a valid input field of the second type arg - g.addEdge<'add', 'sub'>('from-node-2', 'value', 'to-node-2', 'not-a-valid-field'); + g.addEdge(add, 'value', sub, 'not-a-valid-field'); // @ts-expect-error The first field must be any valid output field - g.addEdge('from-node-3', 'not-a-valid-field', 'to-node-3', 'a'); + g.addEdge(add, 'not-a-valid-field', sub, 'a'); // @ts-expect-error The second field must be any valid input field - g.addEdge('from-node-4', 'clip', 'to-node-4', 'not-a-valid-field'); + g.addEdge(add, 'clip', sub, 'not-a-valid-field'); }); }); @@ -161,38 +118,9 @@ describe('Graph', () => { const n = g.getNode('test-node'); expect(n).toBe(node); }); - it('should return the node with the provided id and type', () => { - const n = g.getNode('test-node', 'add'); - expect(n).toBe(node); - assert(is>(node)); - }); it('should throw an error if the node is not found', () => { expect(() => g.getNode('not-found')).toThrowError(AssertionError); }); - it('should throw an error if the node is found but has the wrong type', () => { - expect(() => g.getNode('test-node', 'sub')).toThrowError(AssertionError); - }); - }); - - describe('getNodeSafe', () => { - const g = new Graph(); - const node = g.addNode({ - id: 'test-node', - type: 'add', - }); - it('should return the node if it is found', () => { - expect(g.getNodeSafe('test-node')).toBe(node); - }); - it('should return the node if it is found with the provided type', () => { - expect(g.getNodeSafe('test-node')).toBe(node); - assert(is>(node)); - }); - it("should return undefined if the node isn't found", () => { - expect(g.getNodeSafe('not-found')).toBeUndefined(); - }); - it('should return undefined if the node is found but has the wrong type', () => { - expect(g.getNodeSafe('test-node', 'sub')).toBeUndefined(); - }); }); describe('hasNode', () => { @@ -212,40 +140,42 @@ describe('Graph', () => { describe('getEdge', () => { const g = new Graph(); - g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); + const add: Invocation<'add'> = { + id: 'from-node', + type: 'add', + }; + const sub: Invocation<'sub'> = { + id: 'to-node', + type: 'sub', + }; + g.addEdge(add, 'value', sub, 'b'); it('should return the edge with the provided values', () => { - expect(g.getEdge('from-node', 'value', 'to-node', 'b')).toEqual({ + expect(g.getEdge(add, 'value', sub, 'b')).toEqual({ source: { node_id: 'from-node', field: 'value' }, destination: { node_id: 'to-node', field: 'b' }, }); }); it('should throw an error if the edge is not found', () => { - expect(() => g.getEdge('from-node', 'value', 'to-node', 'a')).toThrowError(AssertionError); - }); - }); - - describe('getEdgeSafe', () => { - const g = new Graph(); - g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); - it('should return the edge if it is found', () => { - expect(g.getEdgeSafe('from-node', 'value', 'to-node', 'b')).toEqual({ - source: { node_id: 'from-node', field: 'value' }, - destination: { node_id: 'to-node', field: 'b' }, - }); - }); - it('should return undefined if the edge is not found', () => { - expect(g.getEdgeSafe('from-node', 'value', 'to-node', 'a')).toBeUndefined(); + expect(() => g.getEdge(add, 'value', sub, 'a')).toThrowError(AssertionError); }); }); describe('hasEdge', () => { const g = new Graph(); - g.addEdge<'add', 'sub'>('from-node', 'value', 'to-node', 'b'); + const add: Invocation<'add'> = { + id: 'from-node', + type: 'add', + }; + const sub: Invocation<'sub'> = { + id: 'to-node', + type: 'sub', + }; + g.addEdge(add, 'value', sub, 'b'); it('should return true if the edge is in the graph', () => { - expect(g.hasEdge('from-node', 'value', 'to-node', 'b')).toBe(true); + expect(g.hasEdge(add, 'value', sub, 'b')).toBe(true); }); it('should return false if the edge is not in the graph', () => { - expect(g.hasEdge('from-node', 'value', 'to-node', 'a')).toBe(false); + expect(g.hasEdge(add, 'value', sub, 'a')).toBe(false); }); }); @@ -256,7 +186,15 @@ describe('Graph', () => { }); it('should raise an error if the graph is invalid', () => { const g = new Graph(); - g.addEdge('from-node', 'value', 'to-node', 'b'); + const add: Invocation<'add'> = { + id: 'from-node', + type: 'add', + }; + const sub: Invocation<'sub'> = { + id: 'to-node', + type: 'sub', + }; + g.addEdge(add, 'value', sub, 'b'); expect(() => g.getGraph()).toThrowError(AssertionError); }); }); @@ -264,7 +202,15 @@ describe('Graph', () => { describe('getGraphSafe', () => { it('should return the graph even if it is invalid', () => { const g = new Graph(); - g.addEdge('from-node', 'value', 'to-node', 'b'); + const add: Invocation<'add'> = { + id: 'from-node', + type: 'add', + }; + const sub: Invocation<'sub'> = { + id: 'to-node', + type: 'sub', + }; + g.addEdge(add, 'value', sub, 'b'); expect(g.getGraphSafe()).toBe(g._graph); }); }); @@ -276,8 +222,16 @@ describe('Graph', () => { }); it('should throw an error if the graph is invalid', () => { const g = new Graph(); + const add: Invocation<'add'> = { + id: 'from-node', + type: 'add', + }; + const sub: Invocation<'sub'> = { + id: 'to-node', + type: 'sub', + }; // edge from nowhere to nowhere - g.addEdge('from-node', 'value', 'to-node', 'b'); + g.addEdge(add, 'value', sub, 'b'); expect(() => g.validate()).toThrowError(AssertionError); }); }); @@ -304,34 +258,34 @@ describe('Graph', () => { id: 'n5', type: 'add', }); - const e1 = g.addEdge<'add', 'add'>(n1.id, 'value', n3.id, 'a'); - const e2 = g.addEdge<'alpha_mask_to_tensor', 'add'>(n2.id, 'height', n3.id, 'b'); - const e3 = g.addEdge<'add', 'add'>(n3.id, 'value', n4.id, 'a'); - const e4 = g.addEdge<'add', 'add'>(n3.id, 'value', n5.id, 'b'); + const e1 = g.addEdge(n1, 'value', n3, 'a'); + const e2 = g.addEdge(n2, 'height', n3, 'b'); + const e3 = g.addEdge(n3, 'value', n4, 'a'); + const e4 = g.addEdge(n3, 'value', n5, 'b'); describe('getEdgesFrom', () => { it('should return the edges that start at the provided node', () => { - expect(g.getEdgesFrom(n3.id)).toEqual([e3, e4]); + expect(g.getEdgesFrom(n3)).toEqual([e3, e4]); }); it('should return the edges that start at the provided node and have the provided field', () => { - expect(g.getEdgesFrom(n2.id, 'height')).toEqual([e2]); + expect(g.getEdgesFrom(n2, 'height')).toEqual([e2]); }); }); describe('getEdgesTo', () => { it('should return the edges that end at the provided node', () => { - expect(g.getEdgesTo(n3.id)).toEqual([e1, e2]); + expect(g.getEdgesTo(n3)).toEqual([e1, e2]); }); it('should return the edges that end at the provided node and have the provided field', () => { - expect(g.getEdgesTo(n3.id, 'b')).toEqual([e2]); + expect(g.getEdgesTo(n3, 'b')).toEqual([e2]); }); }); describe('getIncomers', () => { it('should return the nodes that have an edge to the provided node', () => { - expect(g.getIncomers(n3.id)).toEqual([n1, n2]); + expect(g.getIncomers(n3)).toEqual([n1, n2]); }); }); describe('getOutgoers', () => { it('should return the nodes that the provided node has an edge to', () => { - expect(g.getOutgoers(n3.id)).toEqual([n4, n5]); + expect(g.getOutgoers(n3)).toEqual([n4, n5]); }); }); }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts index ecade47b23..e25cbaa78d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts @@ -3,31 +3,26 @@ import type { AnyInvocation, AnyInvocationInputField, AnyInvocationOutputField, + InputFields, Invocation, - InvocationInputFields, - InvocationOutputFields, InvocationType, - S, + OutputFields, } from 'services/api/types'; -import type { O } from 'ts-toolbelt'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -type GraphType = O.NonNullable>; -type Edge = GraphType['edges'][number]; -type Never = Record; +type Edge = { + source: { + node_id: string; + field: AnyInvocationOutputField; + }; + destination: { + node_id: string; + field: AnyInvocationInputField; + }; +}; -// The `core_metadata` node has very lax types, it accepts arbitrary field names. It must be excluded from edge utils -// to preview their types from being widened from a union of valid field names to `string | number | symbol`. -type EdgeNodeType = Exclude; - -type EdgeFromField = TFrom extends EdgeNodeType - ? InvocationOutputFields - : AnyInvocationOutputField; - -type EdgeToField = TTo extends EdgeNodeType - ? InvocationInputFields - : AnyInvocationInputField; +type GraphType = { id: string; nodes: Record; edges: Edge[] }; export class Graph { _graph: GraphType; @@ -64,45 +59,12 @@ export class Graph { /** * Gets a node from the graph. * @param id The id of the node to get. - * @param type The type of the node to get. If provided, the retrieved node is guaranteed to be of this type. * @returns The node. * @raises `AssertionError` if the node does not exist or if a `type` is provided but the node is not of the expected type. */ - getNode(id: string, type?: T): Invocation { + getNode(id: string): AnyInvocation { const node = this._graph.nodes[id]; assert(node !== undefined, Graph.getNodeNotFoundMsg(id)); - if (type) { - assert(node.type === type, Graph.getNodeNotOfTypeMsg(node, type)); - } - // We just asserted that the node type is correct, this is OK to cast - return node as Invocation; - } - - /** - * Gets a node from the graph without raising an error if the node does not exist or is not of the expected type. - * @param id The id of the node to get. - * @param type The type of the node to get. If provided, node is guaranteed to be of this type. - * @returns The node, if it exists and is of the correct type. Otherwise, `undefined`. - */ - getNodeSafe(id: string, type?: T): Invocation | undefined { - try { - return this.getNode(id, type); - } catch { - return undefined; - } - } - - /** - * Update a node in the graph. Properties are shallow-copied from `updates` to the node. - * @param id The id of the node to update. - * @param type The type of the node to update. If provided, node is guaranteed to be of this type. - * @param updates The fields to update on the node. - * @returns The updated node. - * @raises `AssertionError` if the node does not exist or its type doesn't match. - */ - updateNode(id: string, type: T, updates: Partial>): Invocation { - const node = this.getNode(id, type); - Object.assign(node, updates); return node; } @@ -125,8 +87,8 @@ export class Graph { * @returns The incoming nodes. * @raises `AssertionError` if the node does not exist. */ - getIncomers(nodeId: string): AnyInvocation[] { - return this.getEdgesTo(nodeId).map((edge) => this.getNode(edge.source.node_id)); + getIncomers(node: AnyInvocation): AnyInvocation[] { + return this.getEdgesTo(node).map((edge) => this.getNode(edge.source.node_id)); } /** @@ -135,8 +97,8 @@ export class Graph { * @returns The outgoing nodes. * @raises `AssertionError` if the node does not exist. */ - getOutgoers(nodeId: string): AnyInvocation[] { - return this.getEdgesFrom(nodeId).map((edge) => this.getNode(edge.destination.node_id)); + getOutgoers(node: AnyInvocation): AnyInvocation[] { + return this.getEdgesFrom(node).map((edge) => this.getNode(edge.destination.node_id)); } //#endregion @@ -144,28 +106,26 @@ export class Graph { /** * Add an edge to the graph. If an edge with the same source and destination already exists, an `AssertionError` is raised. - * Provide the from and to node types as generics to get type hints for from and to field names. - * @param fromNodeId The id of the source node. + * If providing node ids, provide the from and to node types as generics to get type hints for from and to field names. + * @param fromNode The source node or id of the source node. * @param fromField The field of the source node. - * @param toNodeId The id of the destination node. + * @param toNode The source node or id of the destination node. * @param toField The field of the destination node. * @returns The added edge. * @raises `AssertionError` if an edge with the same source and destination already exists. */ - addEdge( - fromNodeId: string, - fromField: EdgeFromField, - toNodeId: string, - toField: EdgeToField + addEdge( + fromNode: TFrom, + fromField: OutputFields, + toNode: TTo, + toField: InputFields ): Edge { - const edge = { - source: { node_id: fromNodeId, field: fromField }, - destination: { node_id: toNodeId, field: toField }, + const edge: Edge = { + source: { node_id: fromNode.id, field: fromField }, + destination: { node_id: toNode.id, field: toField }, }; - assert( - !this._graph.edges.some((e) => isEqual(e, edge)), - Graph.getEdgeAlreadyExistsMsg(fromNodeId, fromField, toNodeId, toField) - ); + const edgeAlreadyExists = this._graph.edges.some((e) => isEqual(e, edge)); + assert(!edgeAlreadyExists, Graph.getEdgeAlreadyExistsMsg(fromNode.id, fromField, toNode.id, toField)); this._graph.edges.push(edge); return edge; } @@ -180,45 +140,23 @@ export class Graph { * @returns The edge. * @raises `AssertionError` if the edge does not exist. */ - getEdge( - fromNode: string, - fromField: EdgeFromField, - toNode: string, - toField: EdgeToField + getEdge( + fromNode: TFrom, + fromField: OutputFields, + toNode: TTo, + toField: InputFields ): Edge { const edge = this._graph.edges.find( (e) => - e.source.node_id === fromNode && + e.source.node_id === fromNode.id && e.source.field === fromField && - e.destination.node_id === toNode && + e.destination.node_id === toNode.id && e.destination.field === toField ); - assert(edge !== undefined, Graph.getEdgeNotFoundMsg(fromNode, fromField, toNode, toField)); + assert(edge !== undefined, Graph.getEdgeNotFoundMsg(fromNode.id, fromField, toNode.id, toField)); return edge; } - /** - * Get an edge from the graph, or undefined if it doesn't exist. - * Provide the from and to node types as generics to get type hints for from and to field names. - * @param fromNodeId The id of the source node. - * @param fromField The field of the source node. - * @param toNodeId The id of the destination node. - * @param toField The field of the destination node. - * @returns The edge, or undefined if it doesn't exist. - */ - getEdgeSafe( - fromNode: string, - fromField: EdgeFromField, - toNode: string, - toField: EdgeToField - ): Edge | undefined { - try { - return this.getEdge(fromNode, fromField, toNode, toField); - } catch { - return undefined; - } - } - /** * Check if a graph has an edge. * Provide the from and to node types as generics to get type hints for from and to field names. @@ -229,11 +167,11 @@ export class Graph { * @returns Whether the graph has the edge. */ - hasEdge( - fromNode: string, - fromField: EdgeFromField, - toNode: string, - toField: EdgeToField + hasEdge( + fromNode: TFrom, + fromField: OutputFields, + toNode: TTo, + toField: InputFields ): boolean { try { this.getEdge(fromNode, fromField, toNode, toField); @@ -250,8 +188,8 @@ export class Graph { * @param fromField The field of the source node (optional). * @returns The edges. */ - getEdgesFrom(fromNodeId: string, fromField?: EdgeFromField): Edge[] { - let edges = this._graph.edges.filter((edge) => edge.source.node_id === fromNodeId); + getEdgesFrom(fromNode: T, fromField?: OutputFields): Edge[] { + let edges = this._graph.edges.filter((edge) => edge.source.node_id === fromNode.id); if (fromField) { edges = edges.filter((edge) => edge.source.field === fromField); } @@ -265,8 +203,8 @@ export class Graph { * @param toField The field of the destination node (optional). * @returns The edges. */ - getEdgesTo(toNodeId: string, toField?: EdgeToField): Edge[] { - let edges = this._graph.edges.filter((edge) => edge.destination.node_id === toNodeId); + getEdgesTo(toNode: T, toField?: InputFields): Edge[] { + let edges = this._graph.edges.filter((edge) => edge.destination.node_id === toNode.id); if (toField) { edges = edges.filter((edge) => edge.destination.field === toField); } @@ -284,11 +222,11 @@ export class Graph { /** * Delete all edges to a node. If `toField` is provided, only edges to that field are deleted. * Provide the to node type as a generic to get type hints for to field names. - * @param toNodeId The id of the destination node. + * @param toNode The destination node. * @param toField The field of the destination node (optional). */ - deleteEdgesTo(toNodeId: string, toField?: EdgeToField): void { - for (const edge of this.getEdgesTo(toNodeId, toField)) { + deleteEdgesTo(toNode: T, toField?: InputFields): void { + for (const edge of this.getEdgesTo(toNode, toField)) { this._deleteEdge(edge); } } @@ -299,8 +237,8 @@ export class Graph { * @param toNodeId The id of the source node. * @param toField The field of the source node (optional). */ - deleteEdgesFrom(fromNodeId: string, fromField?: EdgeFromField): void { - for (const edge of this.getEdgesFrom(fromNodeId, fromField)) { + deleteEdgesFrom(fromNode: T, fromField?: OutputFields): void { + for (const edge of this.getEdgesFrom(fromNode, fromField)) { this._deleteEdge(edge); } } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts index ba76e43632..69e3676641 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts @@ -10,6 +10,7 @@ describe('MetadataUtil', () => { describe('getNode', () => { it('should return the metadata node if one exists', () => { const g = new Graph(); + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing const metadataNode = g.addNode({ id: MetadataUtil.metadataNodeId, type: 'core_metadata' }); expect(MetadataUtil.getNode(g)).toEqual(metadataNode); }); @@ -56,14 +57,16 @@ describe('MetadataUtil', () => { it('should add an edge from from metadata to the receiving node', () => { const n = g.addNode({ id: 'my-node', type: 'img_resize' }); MetadataUtil.add(g, { foo: 'bar' }); - MetadataUtil.setMetadataReceivingNode(g, n.id); - expect(g.hasEdge(MetadataUtil.metadataNodeId, 'metadata', n.id, 'metadata')).toBe(true); + MetadataUtil.setMetadataReceivingNode(g, n); + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing + expect(g.hasEdge(MetadataUtil.getNode(g), 'metadata', n, 'metadata')).toBe(true); }); it('should remove existing metadata edges', () => { const n2 = g.addNode({ id: 'my-other-node', type: 'img_resize' }); - MetadataUtil.setMetadataReceivingNode(g, n2.id); - expect(g.getIncomers(n2.id).length).toBe(1); - expect(g.hasEdge(MetadataUtil.metadataNodeId, 'metadata', n2.id, 'metadata')).toBe(true); + MetadataUtil.setMetadataReceivingNode(g, n2); + expect(g.getIncomers(n2).length).toBe(1); + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing + expect(g.hasEdge(MetadataUtil.getNode(g), 'metadata', n2, 'metadata')).toBe(true); }); }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts index a51cebd21e..38e57a5e65 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts @@ -1,34 +1,50 @@ import type { ModelIdentifierField } from 'features/nodes/types/common'; import { METADATA } from 'features/nodes/util/graph/constants'; import { isString, unset } from 'lodash-es'; -import type { AnyModelConfig, Invocation } from 'services/api/types'; +import type { + AnyInvocation, + AnyInvocationIncMetadata, + AnyModelConfig, + CoreMetadataInvocation, + S, +} from 'services/api/types'; +import { assert } from 'tsafe'; import type { Graph } from './Graph'; +const isCoreMetadata = (node: S['Graph']['nodes'][string]): node is CoreMetadataInvocation => + node.type === 'core_metadata'; + export class MetadataUtil { static metadataNodeId = METADATA; - static getNode(graph: Graph): Invocation<'core_metadata'> { - return graph.getNode(this.metadataNodeId, 'core_metadata'); + static getNode(g: Graph): CoreMetadataInvocation { + const node = g.getNode(this.metadataNodeId) as AnyInvocationIncMetadata; + assert(isCoreMetadata(node)); + return node; } - static add(graph: Graph, metadata: Partial>): Invocation<'core_metadata'> { - const metadataNode = graph.getNodeSafe(this.metadataNodeId, 'core_metadata'); - if (!metadataNode) { - return graph.addNode({ + static add(g: Graph, metadata: Partial): CoreMetadataInvocation { + try { + const node = g.getNode(this.metadataNodeId) as AnyInvocationIncMetadata; + assert(isCoreMetadata(node)); + Object.assign(node, metadata); + return node; + } catch { + const metadataNode: CoreMetadataInvocation = { id: this.metadataNodeId, type: 'core_metadata', ...metadata, - }); - } else { - return graph.updateNode(this.metadataNodeId, 'core_metadata', metadata); + }; + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing + return g.addNode(metadataNode); } } - static remove(graph: Graph, key: string): Invocation<'core_metadata'>; - static remove(graph: Graph, keys: string[]): Invocation<'core_metadata'>; - static remove(graph: Graph, keyOrKeys: string | string[]): Invocation<'core_metadata'> { - const metadataNode = this.getNode(graph); + static remove(g: Graph, key: string): CoreMetadataInvocation; + static remove(g: Graph, keys: string[]): CoreMetadataInvocation; + static remove(g: Graph, keyOrKeys: string | string[]): CoreMetadataInvocation { + const metadataNode = this.getNode(g); if (isString(keyOrKeys)) { unset(metadataNode, keyOrKeys); } else { @@ -39,10 +55,11 @@ export class MetadataUtil { return metadataNode; } - static setMetadataReceivingNode(graph: Graph, nodeId: string): void { - // We need to break the rules to update metadata - `addEdge` doesn't allow `core_metadata` as a node type - graph._graph.edges = graph._graph.edges.filter((edge) => edge.source.node_id !== this.metadataNodeId); - graph.addEdge(this.metadataNodeId, 'metadata', nodeId, 'metadata'); + static setMetadataReceivingNode(g: Graph, node: AnyInvocation): void { + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing + g.deleteEdgesFrom(this.getNode(g)); + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing + g.addEdge(this.getNode(g), 'metadata', node, 'metadata'); } static getModelMetadataField({ key, hash, name, base, type }: AnyModelConfig): ModelIdentifierField { diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 8d41fd6474..e22f73ed9e 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -131,30 +131,29 @@ export type WorkflowRecordListItemDTO = S['WorkflowRecordListItemDTO']; export type KeysOfUnion = T extends T ? keyof T : never; -export type NonInputFields = 'id' | 'type' | 'is_intermediate' | 'use_cache'; -export type NonOutputFields = 'type'; -export type AnyInvocation = Graph['nodes'][string]; -export type AnyInvocationExcludeCoreMetata = Exclude; -export type InvocationType = AnyInvocation['type']; -export type InvocationTypeExcludeCoreMetadata = Exclude; +export type AnyInvocation = Exclude< + Graph['nodes'][string], + S['CoreMetadataInvocation'] | S['MetadataInvocation'] | S['MetadataItemInvocation'] | S['MergeMetadataInvocation'] +>; +export type AnyInvocationIncMetadata = S['Graph']['nodes'][string]; +export type InvocationType = AnyInvocation['type']; export type InvocationOutputMap = S['InvocationOutputMap']; export type AnyInvocationOutput = InvocationOutputMap[InvocationType]; export type Invocation = Extract; -export type InvocationExcludeCoreMetadata = Extract< - AnyInvocation, - { type: T } ->; -export type InvocationInputFields = Exclude< - keyof Invocation, - NonInputFields ->; -export type AnyInvocationInputField = Exclude, NonInputFields>; - export type InvocationOutput = InvocationOutputMap[T]; -export type InvocationOutputFields = Exclude, NonOutputFields>; -export type AnyInvocationOutputField = Exclude, NonOutputFields>; + +export type NonInputFields = 'id' | 'type' | 'is_intermediate' | 'use_cache' | 'board' | 'metadata'; +export type AnyInvocationInputField = Exclude>, NonInputFields>; +export type InputFields = Extract; + +export type NonOutputFields = 'type'; +export type AnyInvocationOutputField = Exclude>, NonOutputFields>; +export type OutputFields = Extract< + keyof InvocationOutputMap[T['type']], + AnyInvocationOutputField +>; // General nodes export type CollectInvocation = Invocation<'collect'>; From dbe22be5984fd0be13c36db75d2c503ddcc60ff7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 5 May 2024 16:26:30 +1000 Subject: [PATCH 099/442] feat(ui): use graph utils in builders (wip) --- .../addInitialImageToGenerationTabGraph.ts | 79 +++++++++ .../graph/addSeamlessToGenerationTabGraph.ts | 70 ++++++++ .../util/graph/buildGenerationTabGraph2.ts | 167 ++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToGenerationTabGraph.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToGenerationTabGraph.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToGenerationTabGraph.ts new file mode 100644 index 0000000000..e0cbea810f --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToGenerationTabGraph.ts @@ -0,0 +1,79 @@ +import type { RootState } from 'app/store/store'; +import { isInitialImageLayer } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageField } from 'features/nodes/types/common'; +import type { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import type { Invocation } from 'services/api/types'; + +import { IMAGE_TO_LATENTS, RESIZE } from './constants'; + +/** + * Adds the initial image to the graph and connects it to the denoise and noise nodes. + * @param state The current Redux state + * @param g The graph to add the initial image to + * @param denoise The denoise node in the graph + * @param noise The noise node in the graph + * @returns Whether the initial image was added to the graph + */ +export const addInitialImageToGenerationTabGraph = ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + noise: Invocation<'noise'> +): boolean => { + // Remove Existing UNet Connections + const { img2imgStrength, vaePrecision, model } = state.generation; + const { refinerModel, refinerStart } = state.sdxl; + const { width, height } = state.controlLayers.present.size; + const initialImageLayer = state.controlLayers.present.layers.find(isInitialImageLayer); + const initialImage = initialImageLayer?.isEnabled ? initialImageLayer?.image : null; + + if (!initialImage) { + return false; + } + + const isSDXL = model?.base === 'sdxl'; + const useRefinerStartEnd = isSDXL && Boolean(refinerModel); + const image: ImageField = { + image_name: initialImage.imageName, + }; + + denoise.denoising_start = useRefinerStartEnd ? Math.min(refinerStart, 1 - img2imgStrength) : 1 - img2imgStrength; + denoise.denoising_end = useRefinerStartEnd ? refinerStart : 1; + + const i2l = g.addNode({ + type: 'i2l', + id: IMAGE_TO_LATENTS, + fp32: vaePrecision === 'fp32', + }); + g.addEdge(i2l, 'latents', denoise, 'latents'); + + if (initialImage.width !== width || initialImage.height !== height) { + // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` + const resize = g.addNode({ + id: RESIZE, + type: 'img_resize', + image, + width, + height, + }); + // The `RESIZE` node then passes its image, to `IMAGE_TO_LATENTS` + g.addEdge(resize, 'image', i2l, 'image'); + // The `RESIZE` node also passes its width and height to `NOISE` + g.addEdge(resize, 'width', noise, 'width'); + g.addEdge(resize, 'height', noise, 'height'); + } else { + // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly + i2l.image = image; + g.addEdge(i2l, 'width', noise, 'width'); + g.addEdge(i2l, 'height', noise, 'height'); + } + + MetadataUtil.add(g, { + generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img', + strength: img2imgStrength, + init_image: initialImage.imageName, + }); + + return true; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToGenerationTabGraph.ts new file mode 100644 index 0000000000..7434058f7a --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToGenerationTabGraph.ts @@ -0,0 +1,70 @@ +import type { RootState } from 'app/store/store'; +import type { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import type { Invocation } from 'services/api/types'; + +import { SEAMLESS, VAE_LOADER } from './constants'; + +/** + * Adds the seamless node to the graph and connects it to the model loader and denoise node. + * Because the seamless node may insert a VAE loader node between the model loader and itself, + * this function returns the terminal model loader node in the graph. + * @param state The current Redux state + * @param g The graph to add the seamless node to + * @param denoise The denoise node in the graph + * @param modelLoader The model loader node in the graph + * @returns The terminal model loader node in the graph + */ +export const addSeamlessToGenerationTabGraph = ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + modelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> +): Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> | Invocation<'seamless'> => { + const { seamlessXAxis, seamlessYAxis, vae } = state.generation; + + if (!seamlessXAxis && !seamlessYAxis) { + return modelLoader; + } + + const seamless = g.addNode({ + id: SEAMLESS, + type: 'seamless', + seamless_x: seamlessXAxis, + seamless_y: seamlessYAxis, + }); + + const vaeLoader = vae + ? g.addNode({ + type: 'vae_loader', + id: VAE_LOADER, + vae_model: vae, + }) + : null; + + let terminalModelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> | Invocation<'seamless'> = + modelLoader; + + if (seamlessXAxis) { + MetadataUtil.add(g, { + seamless_x: seamlessXAxis, + }); + terminalModelLoader = seamless; + } + if (seamlessYAxis) { + MetadataUtil.add(g, { + seamless_y: seamlessYAxis, + }); + terminalModelLoader = seamless; + } + + // Seamless slots into the graph between the model loader and the denoise node + g.deleteEdgesFrom(modelLoader, 'unet'); + g.deleteEdgesFrom(modelLoader, 'clip'); + + g.addEdge(modelLoader, 'unet', seamless, 'unet'); + g.addEdge(vaeLoader ?? modelLoader, 'vae', seamless, 'unet'); + g.addEdge(seamless, 'unet', denoise, 'unet'); + + return terminalModelLoader; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts new file mode 100644 index 0000000000..be530095a3 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts @@ -0,0 +1,167 @@ +import { logger } from 'app/logging/logger'; +import type { RootState } from 'app/store/store'; +import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; +import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; +import { addInitialImageToGenerationTabGraph } from 'features/nodes/util/graph/addInitialImageToGenerationTabGraph'; +import { addSeamlessToGenerationTabGraph } from 'features/nodes/util/graph/addSeamlessToGenerationTabGraph'; +import { Graph } from 'features/nodes/util/graph/Graph'; +import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; + +import { addHrfToGraph } from './addHrfToGraph'; +import { addLoRAsToGraph } from './addLoRAsToGraph'; +import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; +import { addVAEToGraph } from './addVAEToGraph'; +import { addWatermarkerToGraph } from './addWatermarkerToGraph'; +import { + CLIP_SKIP, + CONTROL_LAYERS_GRAPH, + DENOISE_LATENTS, + LATENTS_TO_IMAGE, + MAIN_MODEL_LOADER, + NEGATIVE_CONDITIONING, + NEGATIVE_CONDITIONING_COLLECT, + NOISE, + POSITIVE_CONDITIONING, + POSITIVE_CONDITIONING_COLLECT, +} from './constants'; +import { getModelMetadataField } from './metadata'; + +const log = logger('nodes'); +export const buildGenerationTabGraph = async (state: RootState): Promise => { + const { + model, + cfgScale: cfg_scale, + cfgRescaleMultiplier: cfg_rescale_multiplier, + scheduler, + steps, + clipSkip: skipped_layers, + shouldUseCpuNoise, + vaePrecision, + seamlessXAxis, + seamlessYAxis, + seed, + } = state.generation; + const { positivePrompt, negativePrompt } = state.controlLayers.present; + const { width, height } = state.controlLayers.present.size; + + if (!model) { + log.error('No model found in state'); + throw new Error('No model found in state'); + } + + const g = new Graph(CONTROL_LAYERS_GRAPH); + const modelLoader = g.addNode({ + type: 'main_model_loader', + id: MAIN_MODEL_LOADER, + model, + }); + const clipSkip = g.addNode({ + type: 'clip_skip', + id: CLIP_SKIP, + skipped_layers, + }); + const posCond = g.addNode({ + type: 'compel', + id: POSITIVE_CONDITIONING, + prompt: positivePrompt, + }); + const posCondCollect = g.addNode({ + type: 'collect', + id: POSITIVE_CONDITIONING_COLLECT, + }); + const negCond = g.addNode({ + type: 'compel', + id: NEGATIVE_CONDITIONING, + prompt: negativePrompt, + }); + const negCondCollect = g.addNode({ + type: 'collect', + id: NEGATIVE_CONDITIONING_COLLECT, + }); + const noise = g.addNode({ + type: 'noise', + id: NOISE, + seed, + width, + height, + use_cpu: shouldUseCpuNoise, + }); + const denoise = g.addNode({ + type: 'denoise_latents', + id: DENOISE_LATENTS, + cfg_scale, + cfg_rescale_multiplier, + scheduler, + steps, + denoising_start: 0, + denoising_end: 1, + }); + const l2i = g.addNode({ + type: 'l2i', + id: LATENTS_TO_IMAGE, + fp32: vaePrecision === 'fp32', + board: getBoardField(state), + // This is the terminal node and must always save to gallery. + is_intermediate: false, + use_cache: false, + }); + + g.addEdge(modelLoader, 'unet', denoise, 'unet'); + g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); + g.addEdge(clipSkip, 'clip', posCond, 'clip'); + g.addEdge(clipSkip, 'clip', negCond, 'clip'); + g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); + g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); + g.addEdge(noise, 'noise', denoise, 'noise'); + g.addEdge(denoise, 'latents', l2i, 'latents'); + + const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); + + MetadataUtil.add(g, { + generation_mode: 'txt2img', + cfg_scale, + cfg_rescale_multiplier, + height, + width, + positive_prompt: positivePrompt, + negative_prompt: negativePrompt, + model: getModelMetadataField(modelConfig), + seed, + steps, + rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda', + scheduler, + clip_skip: skipped_layers, + }); + MetadataUtil.setMetadataReceivingNode(g, l2i); + + const didAddInitialImage = addInitialImageToGenerationTabGraph(state, g, denoise, noise); + const terminalModelLoader = addSeamlessToGenerationTabGraph(state, g, denoise, modelLoader); + + // optionally add custom VAE + await addVAEToGraph(state, graph, modelLoaderNodeId); + + // add LoRA support + await addLoRAsToGraph(state, graph, DENOISE_LATENTS, modelLoaderNodeId); + + await addControlLayersToGraph(state, graph, DENOISE_LATENTS); + + // High resolution fix. + if (state.hrf.hrfEnabled && !didAddInitialImage) { + addHrfToGraph(state, graph); + } + + // NSFW & watermark - must be last thing added to graph + if (state.system.shouldUseNSFWChecker) { + // must add before watermarker! + addNSFWCheckerToGraph(state, graph); + } + + if (state.system.shouldUseWatermarker) { + // must add after nsfw checker! + addWatermarkerToGraph(state, graph); + } + + return graph; +}; From f8042ffb410a11723ec187364c092bf5afae48c7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 6 May 2024 15:31:08 +1000 Subject: [PATCH 100/442] WIP, sd1.5 works --- .../listeners/enqueueRequestedLinear.ts | 4 +- .../features/nodes/util/graph/Graph.test.ts | 76 +++ .../src/features/nodes/util/graph/Graph.ts | 51 +- .../graph/addGenerationTabControlLayers.ts | 539 ++++++++++++++++++ ...aph.ts => addGenerationTabInitialImage.ts} | 8 +- .../nodes/util/graph/addGenerationTabLoRAs.ts | 94 +++ ...abGraph.ts => addGenerationTabSeamless.ts} | 43 +- .../nodes/util/graph/addGenerationTabVAE.ts | 37 ++ .../util/graph/buildGenerationTabGraph2.ts | 41 +- 9 files changed, 840 insertions(+), 53 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts rename invokeai/frontend/web/src/features/nodes/util/graph/{addInitialImageToGenerationTabGraph.ts => addGenerationTabInitialImage.ts} (96%) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts rename invokeai/frontend/web/src/features/nodes/util/graph/{addSeamlessToGenerationTabGraph.ts => addGenerationTabSeamless.ts} (60%) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabVAE.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 2d267b92b2..bbb77c9ac5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,7 +1,7 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph'; +import { buildGenerationTabGraph2 } from 'features/nodes/util/graph/buildGenerationTabGraph2'; import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { queueApi } from 'services/api/endpoints/queue'; @@ -21,7 +21,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) if (model && model.base === 'sdxl') { graph = await buildGenerationTabSDXLGraph(state); } else { - graph = await buildGenerationTabGraph(state); + graph = await buildGenerationTabGraph2(state); } const batchConfig = prepareLinearUIBatch(state, graph, prepend); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts index 71bcd9331c..b11e16545f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts @@ -289,4 +289,80 @@ describe('Graph', () => { }); }); }); + + describe('deleteEdgesFrom', () => { + it('should delete edges from the provided node', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'img_resize', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'add', + }); + const _e1 = g.addEdge(n1, 'height', n2, 'a'); + const _e2 = g.addEdge(n1, 'width', n2, 'b'); + g.deleteEdgesFrom(n1); + expect(g.getEdgesFrom(n1)).toEqual([]); + }); + it('should delete edges from the provided node, with the provided field', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'img_resize', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'add', + }); + const n3 = g.addNode({ + id: 'n3', + type: 'add', + }); + const _e1 = g.addEdge(n1, 'height', n2, 'a'); + const e2 = g.addEdge(n1, 'width', n2, 'b'); + const e3 = g.addEdge(n1, 'width', n3, 'b'); + g.deleteEdgesFrom(n1, 'height'); + expect(g.getEdgesFrom(n1)).toEqual([e2, e3]); + }); + }); + + describe('deleteEdgesTo', () => { + it('should delete edges to the provided node', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'img_resize', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'add', + }); + const _e1 = g.addEdge(n1, 'height', n2, 'a'); + const _e2 = g.addEdge(n1, 'width', n2, 'b'); + g.deleteEdgesTo(n2); + expect(g.getEdgesTo(n2)).toEqual([]); + }); + it('should delete edges to the provided node, with the provided field', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'img_resize', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'img_resize', + }); + const n3 = g.addNode({ + id: 'n3', + type: 'add', + }); + const _e1 = g.addEdge(n1, 'height', n3, 'a'); + const e2 = g.addEdge(n1, 'width', n3, 'b'); + const _e3 = g.addEdge(n2, 'width', n3, 'a'); + g.deleteEdgesTo(n3, 'a'); + expect(g.getEdgesTo(n3)).toEqual([e2]); + }); + }); }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts index e25cbaa78d..b578c5b40a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts @@ -1,4 +1,4 @@ -import { isEqual } from 'lodash-es'; +import { forEach, groupBy, isEqual, values } from 'lodash-es'; import type { AnyInvocation, AnyInvocationInputField, @@ -22,7 +22,7 @@ type Edge = { }; }; -type GraphType = { id: string; nodes: Record; edges: Edge[] }; +export type GraphType = { id: string; nodes: Record; edges: Edge[] }; export class Graph { _graph: GraphType; @@ -130,6 +130,31 @@ export class Graph { return edge; } + /** + * Add an edge to the graph. If an edge with the same source and destination already exists, an `AssertionError` is raised. + * If providing node ids, provide the from and to node types as generics to get type hints for from and to field names. + * @param fromNode The source node or id of the source node. + * @param fromField The field of the source node. + * @param toNode The source node or id of the destination node. + * @param toField The field of the destination node. + * @returns The added edge. + * @raises `AssertionError` if an edge with the same source and destination already exists. + */ + addEdgeFromObj(edge: Edge): Edge { + const edgeAlreadyExists = this._graph.edges.some((e) => isEqual(e, edge)); + assert( + !edgeAlreadyExists, + Graph.getEdgeAlreadyExistsMsg( + edge.source.node_id, + edge.source.field, + edge.destination.node_id, + edge.destination.field + ) + ); + this._graph.edges.push(edge); + return edge; + } + /** * Get an edge from the graph. If the edge does not exist, an `AssertionError` is raised. * Provide the from and to node types as generics to get type hints for from and to field names. @@ -255,6 +280,24 @@ export class Graph { for (const edge of this._graph.edges) { this.getNode(edge.source.node_id); this.getNode(edge.destination.node_id); + assert( + !this._graph.edges.filter((e) => e !== edge).find((e) => isEqual(e, edge)), + `Duplicate edge: ${Graph.edgeToString(edge)}` + ); + } + for (const node of values(this._graph.nodes)) { + const edgesTo = this.getEdgesTo(node); + // Validate that no node has multiple incoming edges with the same field + forEach(groupBy(edgesTo, 'destination.field'), (group, field) => { + if (node.type === 'collect' && field === 'item') { + // Collectors' item field accepts multiple incoming edges + return; + } + assert( + group.length === 1, + `Node ${node.id} has multiple incoming edges with field ${field}: ${group.map(Graph.edgeToString).join(', ')}` + ); + }); } } @@ -299,6 +342,10 @@ export class Graph { return `Edge from ${fromNodeId}.${fromField} to ${toNodeId}.${toField} already exists`; } + static edgeToString(edge: Edge): string { + return `${edge.source.node_id}.${edge.source.field} -> ${edge.destination.node_id}.${edge.destination.field}`; + } + static uuid = uuidv4; //#endregion } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts new file mode 100644 index 0000000000..0bc907e5e1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -0,0 +1,539 @@ +import { getStore } from 'app/store/nanostores/store'; +import type { RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; +import { + isControlAdapterLayer, + isIPAdapterLayer, + isRegionalGuidanceLayer, + rgLayerMaskImageUploaded, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { RegionalGuidanceLayer } from 'features/controlLayers/store/types'; +import { + type ControlNetConfigV2, + type ImageWithDims, + type IPAdapterConfigV2, + isControlNetConfigV2, + isT2IAdapterConfigV2, + type ProcessorConfig, + type T2IAdapterConfigV2, +} from 'features/controlLayers/util/controlAdapters'; +import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; +import type { ImageField } from 'features/nodes/types/common'; +import { + CONTROL_NET_COLLECT, + IP_ADAPTER_COLLECT, + PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, + PROMPT_REGION_MASK_TO_TENSOR_PREFIX, + PROMPT_REGION_NEGATIVE_COND_PREFIX, + PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, + PROMPT_REGION_POSITIVE_COND_PREFIX, + T2I_ADAPTER_COLLECT, +} from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import { size } from 'lodash-es'; +import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; +import type { ImageDTO, Invocation, S } from 'services/api/types'; +import { assert } from 'tsafe'; + +const buildControlImage = ( + image: ImageWithDims | null, + processedImage: ImageWithDims | null, + processorConfig: ProcessorConfig | null +): ImageField => { + if (processedImage && processorConfig) { + // We've processed the image in the app - use it for the control image. + return { + image_name: processedImage.imageName, + }; + } else if (image) { + // No processor selected, and we have an image - the user provided a processed image, use it for the control image. + return { + image_name: image.imageName, + }; + } + assert(false, 'Attempted to add unprocessed control image'); +}; + +const buildControlNetMetadata = (controlNet: ControlNetConfigV2): S['ControlNetMetadataField'] => { + const { beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; + + assert(model, 'ControlNet model is required'); + assert(image, 'ControlNet image is required'); + + const processed_image = + processedImage && processorConfig + ? { + image_name: processedImage.imageName, + } + : null; + + return { + control_model: model, + control_weight: weight, + control_mode: controlMode, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + image: { + image_name: image.imageName, + }, + processed_image, + }; +}; + +const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // Attempt to retrieve the collector + const controlNetCollect = g.getNode(CONTROL_NET_COLLECT); + assert(controlNetCollect.type === 'collect'); + return controlNetCollect; + } catch { + // Add the ControlNet collector + const controlNetCollect = g.addNode({ + id: CONTROL_NET_COLLECT, + type: 'collect', + }); + g.addEdge(controlNetCollect, 'collection', denoise, 'control'); + return controlNetCollect; + } +}; + +const addGlobalControlNetsToGraph = ( + controlNetConfigs: ControlNetConfigV2[], + g: Graph, + denoise: Invocation<'denoise_latents'> +): void => { + if (controlNetConfigs.length === 0) { + return; + } + const controlNetMetadata: S['ControlNetMetadataField'][] = []; + const controlNetCollect = addControlNetCollectorSafe(g, denoise); + + for (const controlNetConfig of controlNetConfigs) { + if (!controlNetConfig.model) { + return; + } + const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = + controlNetConfig; + + const controlNet = g.addNode({ + id: `control_net_${id}`, + type: 'controlnet', + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + control_mode: controlMode, + resize_mode: 'just_resize', + control_model: model, + control_weight: weight, + image: buildControlImage(image, processedImage, processorConfig), + }); + + g.addEdge(controlNet, 'control', controlNetCollect, 'item'); + + controlNetMetadata.push(buildControlNetMetadata(controlNetConfig)); + } + MetadataUtil.add(g, { controlnets: controlNetMetadata }); +}; + +const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfigV2): S['T2IAdapterMetadataField'] => { + const { beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; + + assert(model, 'T2I Adapter model is required'); + assert(image, 'T2I Adapter image is required'); + + const processed_image = + processedImage && processorConfig + ? { + image_name: processedImage.imageName, + } + : null; + + return { + t2i_adapter_model: model, + weight, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + image: { + image_name: image.imageName, + }, + processed_image, + }; +}; + +const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // You see, we've already got one! + const t2iAdapterCollect = g.getNode(T2I_ADAPTER_COLLECT); + assert(t2iAdapterCollect.type === 'collect'); + return t2iAdapterCollect; + } catch { + const t2iAdapterCollect = g.addNode({ + id: T2I_ADAPTER_COLLECT, + type: 'collect', + }); + + g.addEdge(t2iAdapterCollect, 'collection', denoise, 't2i_adapter'); + + return t2iAdapterCollect; + } +}; + +const addGlobalT2IAdaptersToGraph = ( + t2iAdapterConfigs: T2IAdapterConfigV2[], + g: Graph, + denoise: Invocation<'denoise_latents'> +): void => { + if (t2iAdapterConfigs.length === 0) { + return; + } + const t2iAdapterMetadata: S['T2IAdapterMetadataField'][] = []; + const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); + + for (const t2iAdapterConfig of t2iAdapterConfigs) { + if (!t2iAdapterConfig.model) { + return; + } + const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapterConfig; + + const t2iAdapter = g.addNode({ + id: `t2i_adapter_${id}`, + type: 't2i_adapter', + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + t2i_adapter_model: model, + weight: weight, + image: buildControlImage(image, processedImage, processorConfig), + }); + + g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); + + t2iAdapterMetadata.push(buildT2IAdapterMetadata(t2iAdapterConfig)); + } + + MetadataUtil.add(g, { t2iAdapters: t2iAdapterMetadata }); +}; + +const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfigV2): S['IPAdapterMetadataField'] => { + const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; + + assert(model, 'IP Adapter model is required'); + assert(image, 'IP Adapter image is required'); + + return { + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + weight, + method, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.imageName, + }, + }; +}; + +const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // You see, we've already got one! + const ipAdapterCollect = g.getNode(IP_ADAPTER_COLLECT); + assert(ipAdapterCollect.type === 'collect'); + return ipAdapterCollect; + } catch { + const ipAdapterCollect = g.addNode({ + id: IP_ADAPTER_COLLECT, + type: 'collect', + }); + g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter'); + return ipAdapterCollect; + } +}; + +const addGlobalIPAdaptersToGraph = ( + ipAdapterConfigs: IPAdapterConfigV2[], + g: Graph, + denoise: Invocation<'denoise_latents'> +): void => { + if (ipAdapterConfigs.length === 0) { + return; + } + const ipAdapterMetdata: S['IPAdapterMetadataField'][] = []; + const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); + + for (const ipAdapterConfig of ipAdapterConfigs) { + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; + assert(image, 'IP Adapter image is required'); + assert(model, 'IP Adapter model is required'); + + const ipAdapter = g.addNode({ + id: `ip_adapter_${id}`, + type: 'ip_adapter', + weight, + method, + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.imageName, + }, + }); + g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); + ipAdapterMetdata.push(buildIPAdapterMetadata(ipAdapterConfig)); + } + + MetadataUtil.add(g, { ipAdapters: ipAdapterMetdata }); +}; + +export const addGenerationTabControlLayers = async ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + posCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, + negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, + posCondCollect: Invocation<'collect'>, + negCondCollect: Invocation<'collect'> +) => { + const mainModel = state.generation.model; + assert(mainModel, 'Missing main model when building graph'); + const isSDXL = mainModel.base === 'sdxl'; + + // Add global control adapters + const globalControlNetConfigs = state.controlLayers.present.layers + // Must be a CA layer + .filter(isControlAdapterLayer) + // Must be enabled + .filter((l) => l.isEnabled) + // We want the CAs themselves + .map((l) => l.controlAdapter) + // Must be a ControlNet + .filter(isControlNetConfigV2) + .filter((ca) => { + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === mainModel.base; + const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig); + return hasModel && modelMatchesBase && hasControlImage; + }); + addGlobalControlNetsToGraph(globalControlNetConfigs, g, denoise); + + const globalT2IAdapterConfigs = state.controlLayers.present.layers + // Must be a CA layer + .filter(isControlAdapterLayer) + // Must be enabled + .filter((l) => l.isEnabled) + // We want the CAs themselves + .map((l) => l.controlAdapter) + // Must have a ControlNet CA + .filter(isT2IAdapterConfigV2) + .filter((ca) => { + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === mainModel.base; + const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig); + return hasModel && modelMatchesBase && hasControlImage; + }); + addGlobalT2IAdaptersToGraph(globalT2IAdapterConfigs, g, denoise); + + const globalIPAdapterConfigs = state.controlLayers.present.layers + // Must be an IP Adapter layer + .filter(isIPAdapterLayer) + // Must be enabled + .filter((l) => l.isEnabled) + // We want the IP Adapters themselves + .map((l) => l.ipAdapter) + .filter((ca) => { + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === mainModel.base; + const hasControlImage = Boolean(ca.image); + return hasModel && modelMatchesBase && hasControlImage; + }); + addGlobalIPAdaptersToGraph(globalIPAdapterConfigs, g, denoise); + + const rgLayers = state.controlLayers.present.layers + // Only RG layers are get masks + .filter(isRegionalGuidanceLayer) + // Only visible layers are rendered on the canvas + .filter((l) => l.isEnabled) + // Only layers with prompts get added to the graph + .filter((l) => { + const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); + const hasIPAdapter = l.ipAdapters.length !== 0; + return hasTextPrompt || hasIPAdapter; + }); + + const layerIds = rgLayers.map((l) => l.id); + const blobs = await getRegionalPromptLayerBlobs(layerIds); + assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); + + for (const layer of rgLayers) { + const blob = blobs[layer.id]; + assert(blob, `Blob for layer ${layer.id} not found`); + // Upload the mask image, or get the cached image if it exists + const { image_name } = await getMaskImage(layer, blob); + + // The main mask-to-tensor node + const maskToTensor = g.addNode({ + id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${layer.id}`, + type: 'alpha_mask_to_tensor', + image: { + image_name, + }, + }); + + if (layer.positivePrompt) { + // The main positive conditioning node + const regionalPosCond = g.addNode( + isSDXL + ? { + type: 'sdxl_compel_prompt', + id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, + prompt: layer.positivePrompt, + style: layer.positivePrompt, // TODO: Should we put the positive prompt in both fields? + } + : { + type: 'compel', + id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, + prompt: layer.positivePrompt, + } + ); + // Connect the mask to the conditioning + g.addEdge(maskToTensor, 'mask', regionalPosCond, 'mask'); + // Connect the conditioning to the collector + g.addEdge(regionalPosCond, 'conditioning', posCondCollect, 'item'); + // Copy the connections to the "global" positive conditioning node to the regional cond + for (const edge of g.getEdgesTo(posCond)) { + console.log(edge); + if (edge.destination.field !== 'prompt') { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCond.id; + g.addEdgeFromObj(clone); + } + } + } + + if (layer.negativePrompt) { + // The main negative conditioning node + const regionalNegCond = g.addNode( + isSDXL + ? { + type: 'sdxl_compel_prompt', + id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, + prompt: layer.negativePrompt, + style: layer.negativePrompt, + } + : { + type: 'compel', + id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, + prompt: layer.negativePrompt, + } + ); + // Connect the mask to the conditioning + g.addEdge(maskToTensor, 'mask', regionalNegCond, 'mask'); + // Connect the conditioning to the collector + g.addEdge(regionalNegCond, 'conditioning', negCondCollect, 'item'); + // Copy the connections to the "global" negative conditioning node to the regional cond + for (const edge of g.getEdgesTo(negCond)) { + if (edge.destination.field !== 'prompt') { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalNegCond.id; + g.addEdgeFromObj(clone); + } + } + } + + // If we are using the "invert" auto-negative setting, we need to add an additional negative conditioning node + if (layer.autoNegative === 'invert' && layer.positivePrompt) { + // We re-use the mask image, but invert it when converting to tensor + const invertTensorMask = g.addNode({ + id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${layer.id}`, + type: 'invert_tensor_mask', + }); + // Connect the OG mask image to the inverted mask-to-tensor node + g.addEdge(maskToTensor, 'mask', invertTensorMask, 'mask'); + // Create the conditioning node. It's going to be connected to the negative cond collector, but it uses the positive prompt + const regionalPosCondInverted = g.addNode( + isSDXL + ? { + type: 'sdxl_compel_prompt', + id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, + prompt: layer.positivePrompt, + style: layer.positivePrompt, + } + : { + type: 'compel', + id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, + prompt: layer.positivePrompt, + } + ); + // Connect the inverted mask to the conditioning + g.addEdge(invertTensorMask, 'mask', regionalPosCondInverted, 'mask'); + // Connect the conditioning to the negative collector + g.addEdge(regionalPosCondInverted, 'conditioning', negCondCollect, 'item'); + // Copy the connections to the "global" positive conditioning node to our regional node + for (const edge of g.getEdgesTo(posCond)) { + if (edge.destination.field !== 'prompt') { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCondInverted.id; + g.addEdgeFromObj(clone); + } + } + } + + // TODO(psyche): For some reason, I have to explicitly annotate regionalIPAdapters here. Not sure why. + const regionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipAdapter) => { + const hasModel = Boolean(ipAdapter.model); + const modelMatchesBase = ipAdapter.model?.base === mainModel.base; + const hasControlImage = Boolean(ipAdapter.image); + return hasModel && modelMatchesBase && hasControlImage; + }); + + for (const ipAdapterConfig of regionalIPAdapters) { + const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; + assert(model, 'IP Adapter model is required'); + assert(image, 'IP Adapter image is required'); + + const ipAdapter = g.addNode({ + id: `ip_adapter_${id}`, + type: 'ip_adapter', + weight, + method, + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.imageName, + }, + }); + + // Connect the mask to the conditioning + g.addEdge(maskToTensor, 'mask', ipAdapter, 'mask'); + g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); + } + } +}; + +const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { + if (layer.uploadedMaskImage) { + const imageDTO = await getImageDTO(layer.uploadedMaskImage.imageName); + if (imageDTO) { + return imageDTO; + } + } + const { dispatch } = getStore(); + // No cached mask, or the cached image no longer exists - we need to upload the mask image + const file = new File([blob], `${layer.id}_mask.png`, { type: 'image/png' }); + const req = dispatch( + imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) + ); + req.reset(); + + const imageDTO = await req.unwrap(); + dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO })); + return imageDTO; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabInitialImage.ts similarity index 96% rename from invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToGenerationTabGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabInitialImage.ts index e0cbea810f..3a6b124b30 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabInitialImage.ts @@ -15,12 +15,12 @@ import { IMAGE_TO_LATENTS, RESIZE } from './constants'; * @param noise The noise node in the graph * @returns Whether the initial image was added to the graph */ -export const addInitialImageToGenerationTabGraph = ( +export const addGenerationTabInitialImage = ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, noise: Invocation<'noise'> -): boolean => { +): Invocation<'i2l'> | null => { // Remove Existing UNet Connections const { img2imgStrength, vaePrecision, model } = state.generation; const { refinerModel, refinerStart } = state.sdxl; @@ -29,7 +29,7 @@ export const addInitialImageToGenerationTabGraph = ( const initialImage = initialImageLayer?.isEnabled ? initialImageLayer?.image : null; if (!initialImage) { - return false; + return null; } const isSDXL = model?.base === 'sdxl'; @@ -75,5 +75,5 @@ export const addInitialImageToGenerationTabGraph = ( init_image: initialImage.imageName, }); - return true; + return i2l; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts new file mode 100644 index 0000000000..3cb43fd48d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts @@ -0,0 +1,94 @@ +import type { RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import { filter, size } from 'lodash-es'; +import type { Invocation, S } from 'services/api/types'; +import { assert } from 'tsafe'; + +import { LORA_LOADER } from './constants'; + +export const addGenerationTabLoRAs = ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + unetSource: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> | Invocation<'seamless'>, + clipSkip: Invocation<'clip_skip'>, + posCond: Invocation<'compel'>, + negCond: Invocation<'compel'> +): void => { + /** + * LoRA nodes get the UNet and CLIP models from the main model loader and apply the LoRA to them. + * They then output the UNet and CLIP models references on to either the next LoRA in the chain, + * or to the inference/conditioning nodes. + * + * So we need to inject a LoRA chain into the graph. + */ + + const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); + const loraCount = size(enabledLoRAs); + + if (loraCount === 0) { + return; + } + + // Remove modelLoaderNodeId unet connection to feed it to LoRAs + console.log(deepClone(g)._graph.edges.map((e) => Graph.edgeToString(e))); + g.deleteEdgesFrom(unetSource, 'unet'); + console.log(deepClone(g)._graph.edges.map((e) => Graph.edgeToString(e))); + if (clipSkip) { + // Remove CLIP_SKIP connections to conditionings to feed it through LoRAs + g.deleteEdgesFrom(clipSkip, 'clip'); + } + console.log(deepClone(g)._graph.edges.map((e) => Graph.edgeToString(e))); + + // we need to remember the last lora so we can chain from it + let lastLoRALoader: Invocation<'lora_loader'> | null = null; + let currentLoraIndex = 0; + const loraMetadata: S['LoRAMetadataField'][] = []; + + for (const lora of enabledLoRAs) { + const { weight } = lora; + const { key } = lora.model; + const currentLoraNodeId = `${LORA_LOADER}_${key}`; + const parsedModel = zModelIdentifierField.parse(lora.model); + + const currentLoRALoader = g.addNode({ + type: 'lora_loader', + id: currentLoraNodeId, + lora: parsedModel, + weight, + }); + + loraMetadata.push({ + model: parsedModel, + weight, + }); + + // add to graph + if (currentLoraIndex === 0) { + // first lora = start the lora chain, attach directly to model loader + g.addEdge(unetSource, 'unet', currentLoRALoader, 'unet'); + g.addEdge(clipSkip, 'clip', currentLoRALoader, 'clip'); + } else { + assert(lastLoRALoader !== null); + // we are in the middle of the lora chain, instead connect to the previous lora + g.addEdge(lastLoRALoader, 'unet', currentLoRALoader, 'unet'); + g.addEdge(lastLoRALoader, 'clip', currentLoRALoader, 'clip'); + } + + if (currentLoraIndex === loraCount - 1) { + // final lora, end the lora chain - we need to connect up to inference and conditioning nodes + g.addEdge(currentLoRALoader, 'unet', denoise, 'unet'); + g.addEdge(currentLoRALoader, 'clip', posCond, 'clip'); + g.addEdge(currentLoRALoader, 'clip', negCond, 'clip'); + } + + // increment the lora for the next one in the chain + lastLoRALoader = currentLoRALoader; + currentLoraIndex += 1; + } + + MetadataUtil.add(g, { loras: loraMetadata }); +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts similarity index 60% rename from invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToGenerationTabGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts index 7434058f7a..e56f37916c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts @@ -15,26 +15,28 @@ import { SEAMLESS, VAE_LOADER } from './constants'; * @param modelLoader The model loader node in the graph * @returns The terminal model loader node in the graph */ -export const addSeamlessToGenerationTabGraph = ( +export const addGenerationTabSeamless = ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, modelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> -): Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> | Invocation<'seamless'> => { - const { seamlessXAxis, seamlessYAxis, vae } = state.generation; +): Invocation<'seamless'> | null => { + const { seamlessXAxis: seamless_x, seamlessYAxis: seamless_y, vae } = state.generation; - if (!seamlessXAxis && !seamlessYAxis) { - return modelLoader; + if (!seamless_x && !seamless_y) { + return null; } const seamless = g.addNode({ id: SEAMLESS, type: 'seamless', - seamless_x: seamlessXAxis, - seamless_y: seamlessYAxis, + seamless_x, + seamless_y, }); - const vaeLoader = vae + // The VAE helper also adds the VAE loader - so we need to check if it's already there + const shouldAddVAELoader = !g.hasNode(VAE_LOADER) && vae; + const vaeLoader = shouldAddVAELoader ? g.addNode({ type: 'vae_loader', id: VAE_LOADER, @@ -42,29 +44,18 @@ export const addSeamlessToGenerationTabGraph = ( }) : null; - let terminalModelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> | Invocation<'seamless'> = - modelLoader; - - if (seamlessXAxis) { - MetadataUtil.add(g, { - seamless_x: seamlessXAxis, - }); - terminalModelLoader = seamless; - } - if (seamlessYAxis) { - MetadataUtil.add(g, { - seamless_y: seamlessYAxis, - }); - terminalModelLoader = seamless; - } + MetadataUtil.add(g, { + seamless_x: seamless_x || undefined, + seamless_y: seamless_y || undefined, + }); // Seamless slots into the graph between the model loader and the denoise node g.deleteEdgesFrom(modelLoader, 'unet'); - g.deleteEdgesFrom(modelLoader, 'clip'); + g.deleteEdgesFrom(modelLoader, 'vae'); g.addEdge(modelLoader, 'unet', seamless, 'unet'); - g.addEdge(vaeLoader ?? modelLoader, 'vae', seamless, 'unet'); + g.addEdge(vaeLoader ?? modelLoader, 'vae', seamless, 'vae'); g.addEdge(seamless, 'unet', denoise, 'unet'); - return terminalModelLoader; + return seamless; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabVAE.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabVAE.ts new file mode 100644 index 0000000000..037924d5cb --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabVAE.ts @@ -0,0 +1,37 @@ +import type { RootState } from 'app/store/store'; +import type { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import type { Invocation } from 'services/api/types'; + +import { VAE_LOADER } from './constants'; + +export const addGenerationTabVAE = ( + state: RootState, + g: Graph, + modelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'>, + l2i: Invocation<'l2i'>, + i2l: Invocation<'i2l'> | null, + seamless: Invocation<'seamless'> | null +): void => { + const { vae } = state.generation; + + // The seamless helper also adds the VAE loader... so we need to check if it's already there + const shouldAddVAELoader = !g.hasNode(VAE_LOADER) && vae; + const vaeLoader = shouldAddVAELoader + ? g.addNode({ + type: 'vae_loader', + id: VAE_LOADER, + vae_model: vae, + }) + : null; + + const vaeSource = seamless ? seamless : vaeLoader ? vaeLoader : modelLoader; + g.addEdge(vaeSource, 'vae', l2i, 'vae'); + if (i2l) { + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + } + + if (vae) { + MetadataUtil.add(g, { vae }); + } +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts index be530095a3..328cccb98a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts @@ -1,18 +1,19 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; -import { addInitialImageToGenerationTabGraph } from 'features/nodes/util/graph/addInitialImageToGenerationTabGraph'; -import { addSeamlessToGenerationTabGraph } from 'features/nodes/util/graph/addSeamlessToGenerationTabGraph'; +import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; +import { addGenerationTabInitialImage } from 'features/nodes/util/graph/addGenerationTabInitialImage'; +import { addGenerationTabLoRAs } from 'features/nodes/util/graph/addGenerationTabLoRAs'; +import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; +import { addGenerationTabVAE } from 'features/nodes/util/graph/addGenerationTabVAE'; +import type { GraphType } from 'features/nodes/util/graph/Graph'; import { Graph } from 'features/nodes/util/graph/Graph'; import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { addHrfToGraph } from './addHrfToGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { CLIP_SKIP, @@ -29,7 +30,7 @@ import { import { getModelMetadataField } from './metadata'; const log = logger('nodes'); -export const buildGenerationTabGraph = async (state: RootState): Promise => { +export const buildGenerationTabGraph2 = async (state: RootState): Promise => { const { model, cfgScale: cfg_scale, @@ -39,8 +40,6 @@ export const buildGenerationTabGraph = async (state: RootState): Promise clipSkip: skipped_layers, shouldUseCpuNoise, vaePrecision, - seamlessXAxis, - seamlessYAxis, seed, } = state.generation; const { positivePrompt, negativePrompt } = state.controlLayers.present; @@ -114,6 +113,8 @@ export const buildGenerationTabGraph = async (state: RootState): Promise g.addEdge(clipSkip, 'clip', negCond, 'clip'); g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); + g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); + g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning'); g.addEdge(noise, 'noise', denoise, 'noise'); g.addEdge(denoise, 'latents', l2i, 'latents'); @@ -135,20 +136,22 @@ export const buildGenerationTabGraph = async (state: RootState): Promise clip_skip: skipped_layers, }); MetadataUtil.setMetadataReceivingNode(g, l2i); + g.validate(); - const didAddInitialImage = addInitialImageToGenerationTabGraph(state, g, denoise, noise); - const terminalModelLoader = addSeamlessToGenerationTabGraph(state, g, denoise, modelLoader); + const i2l = addGenerationTabInitialImage(state, g, denoise, noise); + g.validate(); + const seamless = addGenerationTabSeamless(state, g, denoise, modelLoader); + g.validate(); + addGenerationTabVAE(state, g, modelLoader, l2i, i2l, seamless); + g.validate(); + addGenerationTabLoRAs(state, g, denoise, seamless ?? modelLoader, clipSkip, posCond, negCond); + g.validate(); - // optionally add custom VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add LoRA support - await addLoRAsToGraph(state, graph, DENOISE_LATENTS, modelLoaderNodeId); - - await addControlLayersToGraph(state, graph, DENOISE_LATENTS); + await addGenerationTabControlLayers(state, g, denoise, posCond, negCond, posCondCollect, negCondCollect); + g.validate(); // High resolution fix. - if (state.hrf.hrfEnabled && !didAddInitialImage) { + if (state.hrf.hrfEnabled && !i2l) { addHrfToGraph(state, graph); } @@ -163,5 +166,5 @@ export const buildGenerationTabGraph = async (state: RootState): Promise addWatermarkerToGraph(state, graph); } - return graph; + return g.getGraph(); }; From 008645d386e0ed1dc84a2bc2aa0eb2415c87af27 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 8 May 2024 20:51:45 +1000 Subject: [PATCH 101/442] fix(ui): work through merge conflicts (wip) --- .../util/graph/addControlLayersToGraph2.ts | 520 ++++++++++++++ .../graph/addGenerationTabControlLayers.ts | 647 +++++++++--------- .../util/graph/buildGenerationTabGraph2.ts | 18 +- 3 files changed, 847 insertions(+), 338 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts new file mode 100644 index 0000000000..16d7d74c27 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts @@ -0,0 +1,520 @@ +import { getStore } from 'app/store/nanostores/store'; +import type { RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; +import { + isControlAdapterLayer, + isInitialImageLayer, + isIPAdapterLayer, + isRegionalGuidanceLayer, + rgLayerMaskImageUploaded, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; +import type { + ControlNetConfigV2, + ImageWithDims, + IPAdapterConfigV2, + ProcessorConfig, + T2IAdapterConfigV2, +} from 'features/controlLayers/util/controlAdapters'; +import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; +import type { ImageField } from 'features/nodes/types/common'; +import { + CONTROL_NET_COLLECT, + IMAGE_TO_LATENTS, + IP_ADAPTER_COLLECT, + PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, + PROMPT_REGION_MASK_TO_TENSOR_PREFIX, + PROMPT_REGION_NEGATIVE_COND_PREFIX, + PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, + PROMPT_REGION_POSITIVE_COND_PREFIX, + RESIZE, + T2I_ADAPTER_COLLECT, +} from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import { size } from 'lodash-es'; +import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; +import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; +import { assert } from 'tsafe'; + +export const addControlLayersToGraph = async ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + posCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, + negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, + posCondCollect: Invocation<'collect'>, + negCondCollect: Invocation<'collect'>, + noise: Invocation<'noise'> +): Promise => { + const mainModel = state.generation.model; + assert(mainModel, 'Missing main model when building graph'); + const isSDXL = mainModel.base === 'sdxl'; + + // Filter out layers with incompatible base model, missing control image + const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, mainModel.base)); + + const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); + for (const ca of validControlAdapters) { + addGlobalControlAdapterToGraph(ca, g, denoise); + } + + const validIPAdapters = validLayers.filter(isIPAdapterLayer).map((l) => l.ipAdapter); + for (const ipAdapter of validIPAdapters) { + addGlobalIPAdapterToGraph(ipAdapter, g, denoise); + } + + const initialImageLayers = validLayers.filter(isInitialImageLayer); + assert(initialImageLayers.length <= 1, 'Only one initial image layer allowed'); + if (initialImageLayers[0]) { + addInitialImageLayerToGraph(state, g, denoise, noise, initialImageLayers[0]); + } + // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing + // the existing conditioning nodes. + + const validRGLayers = validLayers.filter(isRegionalGuidanceLayer); + const layerIds = validRGLayers.map((l) => l.id); + const blobs = await getRegionalPromptLayerBlobs(layerIds); + assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); + + for (const layer of validRGLayers) { + const blob = blobs[layer.id]; + assert(blob, `Blob for layer ${layer.id} not found`); + // Upload the mask image, or get the cached image if it exists + const { image_name } = await getMaskImage(layer, blob); + + // The main mask-to-tensor node + const maskToTensor = g.addNode({ + id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${layer.id}`, + type: 'alpha_mask_to_tensor', + image: { + image_name, + }, + }); + + if (layer.positivePrompt) { + // The main positive conditioning node + const regionalPosCond = g.addNode( + isSDXL + ? { + type: 'sdxl_compel_prompt', + id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, + prompt: layer.positivePrompt, + style: layer.positivePrompt, // TODO: Should we put the positive prompt in both fields? + } + : { + type: 'compel', + id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, + prompt: layer.positivePrompt, + } + ); + // Connect the mask to the conditioning + g.addEdge(maskToTensor, 'mask', regionalPosCond, 'mask'); + // Connect the conditioning to the collector + g.addEdge(regionalPosCond, 'conditioning', posCondCollect, 'item'); + // Copy the connections to the "global" positive conditioning node to the regional cond + for (const edge of g.getEdgesTo(posCond)) { + console.log(edge); + if (edge.destination.field !== 'prompt') { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCond.id; + g.addEdgeFromObj(clone); + } + } + } + + if (layer.negativePrompt) { + // The main negative conditioning node + const regionalNegCond = g.addNode( + isSDXL + ? { + type: 'sdxl_compel_prompt', + id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, + prompt: layer.negativePrompt, + style: layer.negativePrompt, + } + : { + type: 'compel', + id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, + prompt: layer.negativePrompt, + } + ); + // Connect the mask to the conditioning + g.addEdge(maskToTensor, 'mask', regionalNegCond, 'mask'); + // Connect the conditioning to the collector + g.addEdge(regionalNegCond, 'conditioning', negCondCollect, 'item'); + // Copy the connections to the "global" negative conditioning node to the regional cond + for (const edge of g.getEdgesTo(negCond)) { + if (edge.destination.field !== 'prompt') { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalNegCond.id; + g.addEdgeFromObj(clone); + } + } + } + + // If we are using the "invert" auto-negative setting, we need to add an additional negative conditioning node + if (layer.autoNegative === 'invert' && layer.positivePrompt) { + // We re-use the mask image, but invert it when converting to tensor + const invertTensorMask = g.addNode({ + id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${layer.id}`, + type: 'invert_tensor_mask', + }); + // Connect the OG mask image to the inverted mask-to-tensor node + g.addEdge(maskToTensor, 'mask', invertTensorMask, 'mask'); + // Create the conditioning node. It's going to be connected to the negative cond collector, but it uses the positive prompt + const regionalPosCondInverted = g.addNode( + isSDXL + ? { + type: 'sdxl_compel_prompt', + id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, + prompt: layer.positivePrompt, + style: layer.positivePrompt, + } + : { + type: 'compel', + id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, + prompt: layer.positivePrompt, + } + ); + // Connect the inverted mask to the conditioning + g.addEdge(invertTensorMask, 'mask', regionalPosCondInverted, 'mask'); + // Connect the conditioning to the negative collector + g.addEdge(regionalPosCondInverted, 'conditioning', negCondCollect, 'item'); + // Copy the connections to the "global" positive conditioning node to our regional node + for (const edge of g.getEdgesTo(posCond)) { + if (edge.destination.field !== 'prompt') { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCondInverted.id; + g.addEdgeFromObj(clone); + } + } + } + + const validRegionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipa) => + isValidIPAdapter(ipa, mainModel.base) + ); + + for (const ipAdapterConfig of validRegionalIPAdapters) { + const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; + assert(model, 'IP Adapter model is required'); + assert(image, 'IP Adapter image is required'); + + const ipAdapter = g.addNode({ + id: `ip_adapter_${id}`, + type: 'ip_adapter', + is_intermediate: true, + weight, + method, + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.name, + }, + }); + + // Connect the mask to the conditioning + g.addEdge(maskToTensor, 'mask', ipAdapter, 'mask'); + g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); + } + } + + MetadataUtil.add(g, { control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); + return validLayers; +}; + +const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { + if (layer.uploadedMaskImage) { + const imageDTO = await getImageDTO(layer.uploadedMaskImage.name); + if (imageDTO) { + return imageDTO; + } + } + const { dispatch } = getStore(); + // No cached mask, or the cached image no longer exists - we need to upload the mask image + const file = new File([blob], `${layer.id}_mask.png`, { type: 'image/png' }); + const req = dispatch( + imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) + ); + req.reset(); + + const imageDTO = await req.unwrap(); + dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO })); + return imageDTO; +}; + +const buildControlImage = ( + image: ImageWithDims | null, + processedImage: ImageWithDims | null, + processorConfig: ProcessorConfig | null +): ImageField => { + if (processedImage && processorConfig) { + // We've processed the image in the app - use it for the control image. + return { + image_name: processedImage.name, + }; + } else if (image) { + // No processor selected, and we have an image - the user provided a processed image, use it for the control image. + return { + image_name: image.name, + }; + } + assert(false, 'Attempted to add unprocessed control image'); +}; + +const addGlobalControlAdapterToGraph = ( + controlAdapterConfig: ControlNetConfigV2 | T2IAdapterConfigV2, + g: Graph, + denoise: Invocation<'denoise_latents'> +): void => { + if (controlAdapterConfig.type === 'controlnet') { + addGlobalControlNetToGraph(controlAdapterConfig, g, denoise); + } + if (controlAdapterConfig.type === 't2i_adapter') { + addGlobalT2IAdapterToGraph(controlAdapterConfig, g, denoise); + } +}; + +const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // Attempt to retrieve the collector + const controlNetCollect = g.getNode(CONTROL_NET_COLLECT); + assert(controlNetCollect.type === 'collect'); + return controlNetCollect; + } catch { + // Add the ControlNet collector + const controlNetCollect = g.addNode({ + id: CONTROL_NET_COLLECT, + type: 'collect', + }); + g.addEdge(controlNetCollect, 'collection', denoise, 'control'); + return controlNetCollect; + } +}; + +const addGlobalControlNetToGraph = ( + controlNetConfig: ControlNetConfigV2, + g: Graph, + denoise: Invocation<'denoise_latents'> +) => { + const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNetConfig; + assert(model, 'ControlNet model is required'); + const controlImage = buildControlImage(image, processedImage, processorConfig); + const controlNetCollect = addControlNetCollectorSafe(g, denoise); + + const controlNet = g.addNode({ + id: `control_net_${id}`, + type: 'controlnet', + is_intermediate: true, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + control_mode: controlMode, + resize_mode: 'just_resize', + control_model: model, + control_weight: weight, + image: controlImage, + }); + g.addEdge(controlNet, 'control', controlNetCollect, 'item'); +}; + +const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // You see, we've already got one! + const t2iAdapterCollect = g.getNode(T2I_ADAPTER_COLLECT); + assert(t2iAdapterCollect.type === 'collect'); + return t2iAdapterCollect; + } catch { + const t2iAdapterCollect = g.addNode({ + id: T2I_ADAPTER_COLLECT, + type: 'collect', + }); + + g.addEdge(t2iAdapterCollect, 'collection', denoise, 't2i_adapter'); + + return t2iAdapterCollect; + } +}; + +const addGlobalT2IAdapterToGraph = ( + t2iAdapterConfig: T2IAdapterConfigV2, + g: Graph, + denoise: Invocation<'denoise_latents'> +) => { + const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapterConfig; + assert(model, 'T2I Adapter model is required'); + const controlImage = buildControlImage(image, processedImage, processorConfig); + const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); + + const t2iAdapter = g.addNode({ + id: `t2i_adapter_${id}`, + type: 't2i_adapter', + is_intermediate: true, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + t2i_adapter_model: model, + weight: weight, + image: controlImage, + }); + + g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); +}; + +const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // You see, we've already got one! + const ipAdapterCollect = g.getNode(IP_ADAPTER_COLLECT); + assert(ipAdapterCollect.type === 'collect'); + return ipAdapterCollect; + } catch { + const ipAdapterCollect = g.addNode({ + id: IP_ADAPTER_COLLECT, + type: 'collect', + }); + g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter'); + return ipAdapterCollect; + } +}; + +const addGlobalIPAdapterToGraph = ( + ipAdapterConfig: IPAdapterConfigV2, + g: Graph, + denoise: Invocation<'denoise_latents'> +) => { + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; + assert(image, 'IP Adapter image is required'); + assert(model, 'IP Adapter model is required'); + const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); + + const ipAdapter = g.addNode({ + id: `ip_adapter_${id}`, + type: 'ip_adapter', + is_intermediate: true, + weight, + method, + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.name, + }, + }); + g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); +}; + +const addInitialImageLayerToGraph = ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + noise: Invocation<'noise'>, + layer: InitialImageLayer +) => { + const { vaePrecision, model } = state.generation; + const { refinerModel, refinerStart } = state.sdxl; + const { width, height } = state.controlLayers.present.size; + assert(layer.isEnabled, 'Initial image layer is not enabled'); + assert(layer.image, 'Initial image layer has no image'); + + const isSDXL = model?.base === 'sdxl'; + const useRefinerStartEnd = isSDXL && Boolean(refinerModel); + + const { denoisingStrength } = layer; + denoise.denoising_start = useRefinerStartEnd ? Math.min(refinerStart, 1 - denoisingStrength) : 1 - denoisingStrength; + denoise.denoising_end = useRefinerStartEnd ? refinerStart : 1; + + const i2l = g.addNode({ + type: 'i2l', + id: IMAGE_TO_LATENTS, + is_intermediate: true, + use_cache: true, + fp32: vaePrecision === 'fp32', + }); + + g.addEdge(i2l, 'latents', denoise, 'latents'); + + if (layer.image.width !== width || layer.image.height !== height) { + // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` + + // Create a resize node, explicitly setting its image + const resize = g.addNode({ + id: RESIZE, + type: 'img_resize', + image: { + image_name: layer.image.name, + }, + is_intermediate: true, + width, + height, + }); + + // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` + g.addEdge(resize, 'image', i2l, 'image'); + // The `RESIZE` node also passes its width and height to `NOISE` + g.addEdge(resize, 'width', noise, 'width'); + g.addEdge(resize, 'height', noise, 'height'); + } else { + // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly + i2l.image = { + image_name: layer.image.name, + }; + + // Pass the image's dimensions to the `NOISE` node + g.addEdge(i2l, 'width', noise, 'width'); + g.addEdge(i2l, 'height', noise, 'height'); + } + + MetadataUtil.add(g, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); +}; + +const isValidControlAdapter = (ca: ControlNetConfigV2 | T2IAdapterConfigV2, base: BaseModelType): boolean => { + // Must be have a model that matches the current base and must have a control image + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === base; + const hasControlImage = Boolean(ca.image || (ca.processedImage && ca.processorConfig)); + return hasModel && modelMatchesBase && hasControlImage; +}; + +const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean => { + // Must be have a model that matches the current base and must have a control image + const hasModel = Boolean(ipa.model); + const modelMatchesBase = ipa.model?.base === base; + const hasImage = Boolean(ipa.image); + return hasModel && modelMatchesBase && hasImage; +}; + +const isValidLayer = (layer: Layer, base: BaseModelType) => { + if (isControlAdapterLayer(layer)) { + if (!layer.isEnabled) { + return false; + } + return isValidControlAdapter(layer.controlAdapter, base); + } + if (isIPAdapterLayer(layer)) { + if (!layer.isEnabled) { + return false; + } + return isValidIPAdapter(layer.ipAdapter, base); + } + if (isInitialImageLayer(layer)) { + if (!layer.isEnabled) { + return false; + } + if (!layer.image) { + return false; + } + return true; + } + if (isRegionalGuidanceLayer(layer)) { + const hasTextPrompt = Boolean(layer.positivePrompt || layer.negativePrompt); + const hasIPAdapter = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; + return hasTextPrompt || hasIPAdapter; + } + return false; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts index 0bc907e5e1..d3b2788329 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -3,290 +3,40 @@ import type { RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { isControlAdapterLayer, + isInitialImageLayer, isIPAdapterLayer, isRegionalGuidanceLayer, rgLayerMaskImageUploaded, } from 'features/controlLayers/store/controlLayersSlice'; -import type { RegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import { - type ControlNetConfigV2, - type ImageWithDims, - type IPAdapterConfigV2, - isControlNetConfigV2, - isT2IAdapterConfigV2, - type ProcessorConfig, - type T2IAdapterConfigV2, +import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; +import type { + ControlNetConfigV2, + ImageWithDims, + IPAdapterConfigV2, + ProcessorConfig, + T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; import type { ImageField } from 'features/nodes/types/common'; import { CONTROL_NET_COLLECT, + IMAGE_TO_LATENTS, IP_ADAPTER_COLLECT, PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, PROMPT_REGION_MASK_TO_TENSOR_PREFIX, PROMPT_REGION_NEGATIVE_COND_PREFIX, PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, PROMPT_REGION_POSITIVE_COND_PREFIX, + RESIZE, T2I_ADAPTER_COLLECT, } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/Graph'; import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import { size } from 'lodash-es'; import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO, Invocation, S } from 'services/api/types'; +import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; import { assert } from 'tsafe'; -const buildControlImage = ( - image: ImageWithDims | null, - processedImage: ImageWithDims | null, - processorConfig: ProcessorConfig | null -): ImageField => { - if (processedImage && processorConfig) { - // We've processed the image in the app - use it for the control image. - return { - image_name: processedImage.imageName, - }; - } else if (image) { - // No processor selected, and we have an image - the user provided a processed image, use it for the control image. - return { - image_name: image.imageName, - }; - } - assert(false, 'Attempted to add unprocessed control image'); -}; - -const buildControlNetMetadata = (controlNet: ControlNetConfigV2): S['ControlNetMetadataField'] => { - const { beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; - - assert(model, 'ControlNet model is required'); - assert(image, 'ControlNet image is required'); - - const processed_image = - processedImage && processorConfig - ? { - image_name: processedImage.imageName, - } - : null; - - return { - control_model: model, - control_weight: weight, - control_mode: controlMode, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - resize_mode: 'just_resize', - image: { - image_name: image.imageName, - }, - processed_image, - }; -}; - -const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { - try { - // Attempt to retrieve the collector - const controlNetCollect = g.getNode(CONTROL_NET_COLLECT); - assert(controlNetCollect.type === 'collect'); - return controlNetCollect; - } catch { - // Add the ControlNet collector - const controlNetCollect = g.addNode({ - id: CONTROL_NET_COLLECT, - type: 'collect', - }); - g.addEdge(controlNetCollect, 'collection', denoise, 'control'); - return controlNetCollect; - } -}; - -const addGlobalControlNetsToGraph = ( - controlNetConfigs: ControlNetConfigV2[], - g: Graph, - denoise: Invocation<'denoise_latents'> -): void => { - if (controlNetConfigs.length === 0) { - return; - } - const controlNetMetadata: S['ControlNetMetadataField'][] = []; - const controlNetCollect = addControlNetCollectorSafe(g, denoise); - - for (const controlNetConfig of controlNetConfigs) { - if (!controlNetConfig.model) { - return; - } - const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = - controlNetConfig; - - const controlNet = g.addNode({ - id: `control_net_${id}`, - type: 'controlnet', - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - control_mode: controlMode, - resize_mode: 'just_resize', - control_model: model, - control_weight: weight, - image: buildControlImage(image, processedImage, processorConfig), - }); - - g.addEdge(controlNet, 'control', controlNetCollect, 'item'); - - controlNetMetadata.push(buildControlNetMetadata(controlNetConfig)); - } - MetadataUtil.add(g, { controlnets: controlNetMetadata }); -}; - -const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfigV2): S['T2IAdapterMetadataField'] => { - const { beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; - - assert(model, 'T2I Adapter model is required'); - assert(image, 'T2I Adapter image is required'); - - const processed_image = - processedImage && processorConfig - ? { - image_name: processedImage.imageName, - } - : null; - - return { - t2i_adapter_model: model, - weight, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - resize_mode: 'just_resize', - image: { - image_name: image.imageName, - }, - processed_image, - }; -}; - -const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { - try { - // You see, we've already got one! - const t2iAdapterCollect = g.getNode(T2I_ADAPTER_COLLECT); - assert(t2iAdapterCollect.type === 'collect'); - return t2iAdapterCollect; - } catch { - const t2iAdapterCollect = g.addNode({ - id: T2I_ADAPTER_COLLECT, - type: 'collect', - }); - - g.addEdge(t2iAdapterCollect, 'collection', denoise, 't2i_adapter'); - - return t2iAdapterCollect; - } -}; - -const addGlobalT2IAdaptersToGraph = ( - t2iAdapterConfigs: T2IAdapterConfigV2[], - g: Graph, - denoise: Invocation<'denoise_latents'> -): void => { - if (t2iAdapterConfigs.length === 0) { - return; - } - const t2iAdapterMetadata: S['T2IAdapterMetadataField'][] = []; - const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); - - for (const t2iAdapterConfig of t2iAdapterConfigs) { - if (!t2iAdapterConfig.model) { - return; - } - const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapterConfig; - - const t2iAdapter = g.addNode({ - id: `t2i_adapter_${id}`, - type: 't2i_adapter', - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - resize_mode: 'just_resize', - t2i_adapter_model: model, - weight: weight, - image: buildControlImage(image, processedImage, processorConfig), - }); - - g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); - - t2iAdapterMetadata.push(buildT2IAdapterMetadata(t2iAdapterConfig)); - } - - MetadataUtil.add(g, { t2iAdapters: t2iAdapterMetadata }); -}; - -const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfigV2): S['IPAdapterMetadataField'] => { - const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; - - assert(model, 'IP Adapter model is required'); - assert(image, 'IP Adapter image is required'); - - return { - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - weight, - method, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.imageName, - }, - }; -}; - -const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { - try { - // You see, we've already got one! - const ipAdapterCollect = g.getNode(IP_ADAPTER_COLLECT); - assert(ipAdapterCollect.type === 'collect'); - return ipAdapterCollect; - } catch { - const ipAdapterCollect = g.addNode({ - id: IP_ADAPTER_COLLECT, - type: 'collect', - }); - g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter'); - return ipAdapterCollect; - } -}; - -const addGlobalIPAdaptersToGraph = ( - ipAdapterConfigs: IPAdapterConfigV2[], - g: Graph, - denoise: Invocation<'denoise_latents'> -): void => { - if (ipAdapterConfigs.length === 0) { - return; - } - const ipAdapterMetdata: S['IPAdapterMetadataField'][] = []; - const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - - for (const ipAdapterConfig of ipAdapterConfigs) { - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; - assert(image, 'IP Adapter image is required'); - assert(model, 'IP Adapter model is required'); - - const ipAdapter = g.addNode({ - id: `ip_adapter_${id}`, - type: 'ip_adapter', - weight, - method, - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.imageName, - }, - }); - g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); - ipAdapterMetdata.push(buildIPAdapterMetadata(ipAdapterConfig)); - } - - MetadataUtil.add(g, { ipAdapters: ipAdapterMetdata }); -}; - export const addGenerationTabControlLayers = async ( state: RootState, g: Graph, @@ -294,79 +44,40 @@ export const addGenerationTabControlLayers = async ( posCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, posCondCollect: Invocation<'collect'>, - negCondCollect: Invocation<'collect'> -) => { + negCondCollect: Invocation<'collect'>, + noise: Invocation<'noise'> +): Promise => { const mainModel = state.generation.model; assert(mainModel, 'Missing main model when building graph'); const isSDXL = mainModel.base === 'sdxl'; - // Add global control adapters - const globalControlNetConfigs = state.controlLayers.present.layers - // Must be a CA layer - .filter(isControlAdapterLayer) - // Must be enabled - .filter((l) => l.isEnabled) - // We want the CAs themselves - .map((l) => l.controlAdapter) - // Must be a ControlNet - .filter(isControlNetConfigV2) - .filter((ca) => { - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === mainModel.base; - const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig); - return hasModel && modelMatchesBase && hasControlImage; - }); - addGlobalControlNetsToGraph(globalControlNetConfigs, g, denoise); + // Filter out layers with incompatible base model, missing control image + const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, mainModel.base)); - const globalT2IAdapterConfigs = state.controlLayers.present.layers - // Must be a CA layer - .filter(isControlAdapterLayer) - // Must be enabled - .filter((l) => l.isEnabled) - // We want the CAs themselves - .map((l) => l.controlAdapter) - // Must have a ControlNet CA - .filter(isT2IAdapterConfigV2) - .filter((ca) => { - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === mainModel.base; - const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig); - return hasModel && modelMatchesBase && hasControlImage; - }); - addGlobalT2IAdaptersToGraph(globalT2IAdapterConfigs, g, denoise); + const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); + for (const ca of validControlAdapters) { + addGlobalControlAdapterToGraph(ca, g, denoise); + } - const globalIPAdapterConfigs = state.controlLayers.present.layers - // Must be an IP Adapter layer - .filter(isIPAdapterLayer) - // Must be enabled - .filter((l) => l.isEnabled) - // We want the IP Adapters themselves - .map((l) => l.ipAdapter) - .filter((ca) => { - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === mainModel.base; - const hasControlImage = Boolean(ca.image); - return hasModel && modelMatchesBase && hasControlImage; - }); - addGlobalIPAdaptersToGraph(globalIPAdapterConfigs, g, denoise); + const validIPAdapters = validLayers.filter(isIPAdapterLayer).map((l) => l.ipAdapter); + for (const ipAdapter of validIPAdapters) { + addGlobalIPAdapterToGraph(ipAdapter, g, denoise); + } - const rgLayers = state.controlLayers.present.layers - // Only RG layers are get masks - .filter(isRegionalGuidanceLayer) - // Only visible layers are rendered on the canvas - .filter((l) => l.isEnabled) - // Only layers with prompts get added to the graph - .filter((l) => { - const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasIPAdapter = l.ipAdapters.length !== 0; - return hasTextPrompt || hasIPAdapter; - }); + const initialImageLayers = validLayers.filter(isInitialImageLayer); + assert(initialImageLayers.length <= 1, 'Only one initial image layer allowed'); + if (initialImageLayers[0]) { + addInitialImageLayerToGraph(state, g, denoise, noise, initialImageLayers[0]); + } + // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing + // the existing conditioning nodes. - const layerIds = rgLayers.map((l) => l.id); + const validRGLayers = validLayers.filter(isRegionalGuidanceLayer); + const layerIds = validRGLayers.map((l) => l.id); const blobs = await getRegionalPromptLayerBlobs(layerIds); assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); - for (const layer of rgLayers) { + for (const layer of validRGLayers) { const blob = blobs[layer.id]; assert(blob, `Blob for layer ${layer.id} not found`); // Upload the mask image, or get the cached image if it exists @@ -483,15 +194,11 @@ export const addGenerationTabControlLayers = async ( } } - // TODO(psyche): For some reason, I have to explicitly annotate regionalIPAdapters here. Not sure why. - const regionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipAdapter) => { - const hasModel = Boolean(ipAdapter.model); - const modelMatchesBase = ipAdapter.model?.base === mainModel.base; - const hasControlImage = Boolean(ipAdapter.image); - return hasModel && modelMatchesBase && hasControlImage; - }); + const validRegionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipa) => + isValidIPAdapter(ipa, mainModel.base) + ); - for (const ipAdapterConfig of regionalIPAdapters) { + for (const ipAdapterConfig of validRegionalIPAdapters) { const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; assert(model, 'IP Adapter model is required'); @@ -500,6 +207,7 @@ export const addGenerationTabControlLayers = async ( const ipAdapter = g.addNode({ id: `ip_adapter_${id}`, type: 'ip_adapter', + is_intermediate: true, weight, method, ip_adapter_model: model, @@ -507,7 +215,7 @@ export const addGenerationTabControlLayers = async ( begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: image.imageName, + image_name: image.name, }, }); @@ -516,11 +224,14 @@ export const addGenerationTabControlLayers = async ( g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); } } + + MetadataUtil.add(g, { control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); + return validLayers; }; const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { if (layer.uploadedMaskImage) { - const imageDTO = await getImageDTO(layer.uploadedMaskImage.imageName); + const imageDTO = await getImageDTO(layer.uploadedMaskImage.name); if (imageDTO) { return imageDTO; } @@ -537,3 +248,273 @@ const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise { + if (processedImage && processorConfig) { + // We've processed the image in the app - use it for the control image. + return { + image_name: processedImage.name, + }; + } else if (image) { + // No processor selected, and we have an image - the user provided a processed image, use it for the control image. + return { + image_name: image.name, + }; + } + assert(false, 'Attempted to add unprocessed control image'); +}; + +const addGlobalControlAdapterToGraph = ( + controlAdapterConfig: ControlNetConfigV2 | T2IAdapterConfigV2, + g: Graph, + denoise: Invocation<'denoise_latents'> +): void => { + if (controlAdapterConfig.type === 'controlnet') { + addGlobalControlNetToGraph(controlAdapterConfig, g, denoise); + } + if (controlAdapterConfig.type === 't2i_adapter') { + addGlobalT2IAdapterToGraph(controlAdapterConfig, g, denoise); + } +}; + +const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // Attempt to retrieve the collector + const controlNetCollect = g.getNode(CONTROL_NET_COLLECT); + assert(controlNetCollect.type === 'collect'); + return controlNetCollect; + } catch { + // Add the ControlNet collector + const controlNetCollect = g.addNode({ + id: CONTROL_NET_COLLECT, + type: 'collect', + }); + g.addEdge(controlNetCollect, 'collection', denoise, 'control'); + return controlNetCollect; + } +}; + +const addGlobalControlNetToGraph = ( + controlNetConfig: ControlNetConfigV2, + g: Graph, + denoise: Invocation<'denoise_latents'> +) => { + const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNetConfig; + assert(model, 'ControlNet model is required'); + const controlImage = buildControlImage(image, processedImage, processorConfig); + const controlNetCollect = addControlNetCollectorSafe(g, denoise); + + const controlNet = g.addNode({ + id: `control_net_${id}`, + type: 'controlnet', + is_intermediate: true, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + control_mode: controlMode, + resize_mode: 'just_resize', + control_model: model, + control_weight: weight, + image: controlImage, + }); + g.addEdge(controlNet, 'control', controlNetCollect, 'item'); +}; + +const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // You see, we've already got one! + const t2iAdapterCollect = g.getNode(T2I_ADAPTER_COLLECT); + assert(t2iAdapterCollect.type === 'collect'); + return t2iAdapterCollect; + } catch { + const t2iAdapterCollect = g.addNode({ + id: T2I_ADAPTER_COLLECT, + type: 'collect', + }); + + g.addEdge(t2iAdapterCollect, 'collection', denoise, 't2i_adapter'); + + return t2iAdapterCollect; + } +}; + +const addGlobalT2IAdapterToGraph = ( + t2iAdapterConfig: T2IAdapterConfigV2, + g: Graph, + denoise: Invocation<'denoise_latents'> +) => { + const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapterConfig; + assert(model, 'T2I Adapter model is required'); + const controlImage = buildControlImage(image, processedImage, processorConfig); + const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); + + const t2iAdapter = g.addNode({ + id: `t2i_adapter_${id}`, + type: 't2i_adapter', + is_intermediate: true, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + t2i_adapter_model: model, + weight: weight, + image: controlImage, + }); + + g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); +}; + +const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // You see, we've already got one! + const ipAdapterCollect = g.getNode(IP_ADAPTER_COLLECT); + assert(ipAdapterCollect.type === 'collect'); + return ipAdapterCollect; + } catch { + const ipAdapterCollect = g.addNode({ + id: IP_ADAPTER_COLLECT, + type: 'collect', + }); + g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter'); + return ipAdapterCollect; + } +}; + +const addGlobalIPAdapterToGraph = ( + ipAdapterConfig: IPAdapterConfigV2, + g: Graph, + denoise: Invocation<'denoise_latents'> +) => { + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; + assert(image, 'IP Adapter image is required'); + assert(model, 'IP Adapter model is required'); + const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); + + const ipAdapter = g.addNode({ + id: `ip_adapter_${id}`, + type: 'ip_adapter', + is_intermediate: true, + weight, + method, + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.name, + }, + }); + g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); +}; + +const addInitialImageLayerToGraph = ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + noise: Invocation<'noise'>, + layer: InitialImageLayer +) => { + const { vaePrecision, model } = state.generation; + const { refinerModel, refinerStart } = state.sdxl; + const { width, height } = state.controlLayers.present.size; + assert(layer.isEnabled, 'Initial image layer is not enabled'); + assert(layer.image, 'Initial image layer has no image'); + + const isSDXL = model?.base === 'sdxl'; + const useRefinerStartEnd = isSDXL && Boolean(refinerModel); + + const { denoisingStrength } = layer; + denoise.denoising_start = useRefinerStartEnd ? Math.min(refinerStart, 1 - denoisingStrength) : 1 - denoisingStrength; + denoise.denoising_end = useRefinerStartEnd ? refinerStart : 1; + + const i2l = g.addNode({ + type: 'i2l', + id: IMAGE_TO_LATENTS, + is_intermediate: true, + use_cache: true, + fp32: vaePrecision === 'fp32', + }); + + g.addEdge(i2l, 'latents', denoise, 'latents'); + + if (layer.image.width !== width || layer.image.height !== height) { + // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` + + // Create a resize node, explicitly setting its image + const resize = g.addNode({ + id: RESIZE, + type: 'img_resize', + image: { + image_name: layer.image.name, + }, + is_intermediate: true, + width, + height, + }); + + // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` + g.addEdge(resize, 'image', i2l, 'image'); + // The `RESIZE` node also passes its width and height to `NOISE` + g.addEdge(resize, 'width', noise, 'width'); + g.addEdge(resize, 'height', noise, 'height'); + } else { + // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly + i2l.image = { + image_name: layer.image.name, + }; + + // Pass the image's dimensions to the `NOISE` node + g.addEdge(i2l, 'width', noise, 'width'); + g.addEdge(i2l, 'height', noise, 'height'); + } + + MetadataUtil.add(g, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); +}; + +const isValidControlAdapter = (ca: ControlNetConfigV2 | T2IAdapterConfigV2, base: BaseModelType): boolean => { + // Must be have a model that matches the current base and must have a control image + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === base; + const hasControlImage = Boolean(ca.image || (ca.processedImage && ca.processorConfig)); + return hasModel && modelMatchesBase && hasControlImage; +}; + +const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean => { + // Must be have a model that matches the current base and must have a control image + const hasModel = Boolean(ipa.model); + const modelMatchesBase = ipa.model?.base === base; + const hasImage = Boolean(ipa.image); + return hasModel && modelMatchesBase && hasImage; +}; + +const isValidLayer = (layer: Layer, base: BaseModelType) => { + if (isControlAdapterLayer(layer)) { + if (!layer.isEnabled) { + return false; + } + return isValidControlAdapter(layer.controlAdapter, base); + } + if (isIPAdapterLayer(layer)) { + if (!layer.isEnabled) { + return false; + } + return isValidIPAdapter(layer.ipAdapter, base); + } + if (isInitialImageLayer(layer)) { + if (!layer.isEnabled) { + return false; + } + if (!layer.image) { + return false; + } + return true; + } + if (isRegionalGuidanceLayer(layer)) { + const hasTextPrompt = Boolean(layer.positivePrompt || layer.negativePrompt); + const hasIPAdapter = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; + return hasTextPrompt || hasIPAdapter; + } + return false; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts index 328cccb98a..38c7ba18d1 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts @@ -1,8 +1,8 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; -import { addGenerationTabInitialImage } from 'features/nodes/util/graph/addGenerationTabInitialImage'; import { addGenerationTabLoRAs } from 'features/nodes/util/graph/addGenerationTabLoRAs'; import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; import { addGenerationTabVAE } from 'features/nodes/util/graph/addGenerationTabVAE'; @@ -138,8 +138,6 @@ export const buildGenerationTabGraph2 = async (state: RootState): Promise isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); + if (state.hrf.hrfEnabled && !shouldUseHRF) { addHrfToGraph(state, graph); } From ef89c7e537fa7bcdc73ed60768fcdb2252feeef1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 9 May 2024 19:05:34 +1000 Subject: [PATCH 102/442] feat(nodes): add LoRASelectorInvocation, LoRACollectionLoader, SDXLLoRACollectionLoader These simplify loading multiple LoRAs. Instead of requiring chained lora loader nodes, configure each LoRA (model & weight) with a selector, collect them, then send the collection to the collection loader to apply all of the LoRAs to the UNet/CLIP models. The collection loaders accept a single lora or collection of loras. --- invokeai/app/invocations/model.py | 135 ++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index caa8a53540..245034c481 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -190,6 +190,75 @@ class LoRALoaderInvocation(BaseInvocation): return output +@invocation_output("lora_selector_output") +class LoRASelectorOutput(BaseInvocationOutput): + """Model loader output""" + + lora: LoRAField = OutputField(description="LoRA model and weight", title="LoRA") + + +@invocation("lora_selector", title="LoRA Selector", tags=["model"], category="model", version="1.0.0") +class LoRASelectorInvocation(BaseInvocation): + """Selects a LoRA model and weight.""" + + lora: ModelIdentifierField = InputField( + description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA", ui_type=UIType.LoRAModel + ) + weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) + + def invoke(self, context: InvocationContext) -> LoRASelectorOutput: + return LoRASelectorOutput(lora=LoRAField(lora=self.lora, weight=self.weight)) + + +@invocation("lora_collection_loader", title="LoRA Collection Loader", tags=["model"], category="model", version="1.0.0") +class LoRACollectionLoader(BaseInvocation): + """Applies a collection of LoRAs to the provided UNet and CLIP models.""" + + loras: LoRAField | list[LoRAField] = InputField( + description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs" + ) + unet: Optional[UNetField] = InputField( + default=None, + description=FieldDescriptions.unet, + input=Input.Connection, + title="UNet", + ) + clip: Optional[CLIPField] = InputField( + default=None, + description=FieldDescriptions.clip, + input=Input.Connection, + title="CLIP", + ) + + def invoke(self, context: InvocationContext) -> LoRALoaderOutput: + output = LoRALoaderOutput() + loras = self.loras if isinstance(self.loras, list) else [self.loras] + added_loras: list[str] = [] + + for lora in loras: + if lora.lora.key in added_loras: + continue + + if not context.models.exists(lora.lora.key): + raise Exception(f"Unknown lora: {lora.lora.key}!") + + assert lora.lora.base in (BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2) + + added_loras.append(lora.lora.key) + + if self.unet is not None: + if output.unet is None: + output.unet = self.unet.model_copy(deep=True) + output.unet.loras.append(lora) + + if self.clip is not None: + if output.clip is None: + output.clip = self.clip.model_copy(deep=True) + output.clip.loras.append(lora) + + return output + + @invocation_output("sdxl_lora_loader_output") class SDXLLoRALoaderOutput(BaseInvocationOutput): """SDXL LoRA Loader Output""" @@ -279,6 +348,72 @@ class SDXLLoRALoaderInvocation(BaseInvocation): return output +@invocation( + "sdxl_lora_collection_loader", + title="SDXL LoRA Collection Loader", + tags=["model"], + category="model", + version="1.0.0", +) +class SDXLLoRACollectionLoader(BaseInvocation): + """Applies a collection of SDXL LoRAs to the provided UNet and CLIP models.""" + + loras: LoRAField | list[LoRAField] = InputField( + description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs" + ) + unet: Optional[UNetField] = InputField( + default=None, + description=FieldDescriptions.unet, + input=Input.Connection, + title="UNet", + ) + clip: Optional[CLIPField] = InputField( + default=None, + description=FieldDescriptions.clip, + input=Input.Connection, + title="CLIP", + ) + clip2: Optional[CLIPField] = InputField( + default=None, + description=FieldDescriptions.clip, + input=Input.Connection, + title="CLIP 2", + ) + + def invoke(self, context: InvocationContext) -> SDXLLoRALoaderOutput: + output = SDXLLoRALoaderOutput() + loras = self.loras if isinstance(self.loras, list) else [self.loras] + added_loras: list[str] = [] + + for lora in loras: + if lora.lora.key in added_loras: + continue + + if not context.models.exists(lora.lora.key): + raise Exception(f"Unknown lora: {lora.lora.key}!") + + assert lora.lora.base is BaseModelType.StableDiffusionXL + + added_loras.append(lora.lora.key) + + if self.unet is not None: + if output.unet is None: + output.unet = self.unet.model_copy(deep=True) + output.unet.loras.append(lora) + + if self.clip is not None: + if output.clip is None: + output.clip = self.clip.model_copy(deep=True) + output.clip.loras.append(lora) + + if self.clip2 is not None: + if output.clip2 is None: + output.clip2 = self.clip2.model_copy(deep=True) + output.clip2.loras.append(lora) + + return output + + @invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.2") class VAELoaderInvocation(BaseInvocation): """Loads a VAE model, outputting a VaeLoaderOutput""" From de1869773fa6012a9816151cefbbd1074b4f9fb1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 9 May 2024 19:34:10 +1000 Subject: [PATCH 103/442] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 415 ++++++++++++------ 1 file changed, 284 insertions(+), 131 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 9b6cd5b020..b3d8a61870 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -4189,7 +4189,7 @@ export type components = { * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["RandomFloatInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CreateGradientMaskInvocation"]; + [key: string]: components["schemas"]["StringReplaceInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ResizeLatentsInvocation"]; }; /** * Edges @@ -4226,7 +4226,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["String2Output"] | components["schemas"]["BooleanOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["UNetOutput"]; + [key: string]: components["schemas"]["LatentsOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["String2Output"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["T2IAdapterOutput"]; }; /** * Errors @@ -6656,6 +6656,50 @@ export type components = { */ type: "lineart_image_processor"; }; + /** + * LoRA Collection Loader + * @description Applies a collection of LoRAs to the provided UNet and CLIP models. + */ + LoRACollectionLoader: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * LoRAs + * @description LoRA models and weights. May be a single LoRA or collection. + */ + loras?: components["schemas"]["LoRAField"] | components["schemas"]["LoRAField"][]; + /** + * UNet + * @description UNet (scheduler, LoRAs) + */ + unet?: components["schemas"]["UNetField"] | null; + /** + * CLIP + * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count + */ + clip?: components["schemas"]["CLIPField"] | null; + /** + * type + * @default lora_collection_loader + * @constant + */ + type: "lora_collection_loader"; + }; /** * LoRADiffusersConfig * @description Model config for LoRA/Diffusers models. @@ -6887,6 +6931,63 @@ export type components = { */ weight: number; }; + /** + * LoRA Selector + * @description Selects a LoRA model and weight. + */ + LoRASelectorInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * LoRA + * @description LoRA model to load + */ + lora: components["schemas"]["ModelIdentifierField"]; + /** + * Weight + * @description The weight at which the LoRA is applied to each model + * @default 0.75 + */ + weight?: number; + /** + * type + * @default lora_selector + * @constant + */ + type: "lora_selector"; + }; + /** + * LoRASelectorOutput + * @description Model loader output + */ + LoRASelectorOutput: { + /** + * LoRA + * @description LoRA model and weight + */ + lora: components["schemas"]["LoRAField"]; + /** + * type + * @default lora_selector_output + * @constant + */ + type: "lora_selector_output"; + }; /** * LocalModelSource * @description A local file or directory path. @@ -8897,6 +8998,55 @@ export type components = { */ type: "sdxl_compel_prompt"; }; + /** + * SDXL LoRA Collection Loader + * @description Applies a collection of SDXL LoRAs to the provided UNet and CLIP models. + */ + SDXLLoRACollectionLoader: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * LoRAs + * @description LoRA models and weights. May be a single LoRA or collection. + */ + loras?: components["schemas"]["LoRAField"] | components["schemas"]["LoRAField"][]; + /** + * UNet + * @description UNet (scheduler, LoRAs) + */ + unet?: components["schemas"]["UNetField"] | null; + /** + * CLIP + * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count + */ + clip?: components["schemas"]["CLIPField"] | null; + /** + * CLIP 2 + * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count + */ + clip2?: components["schemas"]["CLIPField"] | null; + /** + * type + * @default sdxl_lora_collection_loader + * @constant + */ + type: "sdxl_lora_collection_loader"; + }; /** * SDXL LoRA * @description Apply selected lora to unet and text_encoder. @@ -11461,140 +11611,143 @@ export type components = { */ UIType: "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "T2IAdapterModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict"; InvocationOutputMap: { - rand_float: components["schemas"]["FloatOutput"]; - freeu: components["schemas"]["UNetOutput"]; - tile_image_processor: components["schemas"]["ImageOutput"]; - string_split_neg: components["schemas"]["StringPosNegOutput"]; - float_range: components["schemas"]["FloatCollectionOutput"]; - mask_combine: components["schemas"]["ImageOutput"]; - add: components["schemas"]["IntegerOutput"]; - pidi_image_processor: components["schemas"]["ImageOutput"]; - scheduler: components["schemas"]["SchedulerOutput"]; - lblend: components["schemas"]["LatentsOutput"]; - blank_image: components["schemas"]["ImageOutput"]; - content_shuffle_image_processor: components["schemas"]["ImageOutput"]; - infill_tile: components["schemas"]["ImageOutput"]; - img_channel_multiply: components["schemas"]["ImageOutput"]; - face_off: components["schemas"]["FaceOffOutput"]; - lineart_anime_image_processor: components["schemas"]["ImageOutput"]; - heuristic_resize: components["schemas"]["ImageOutput"]; - esrgan: components["schemas"]["ImageOutput"]; - img_resize: components["schemas"]["ImageOutput"]; - vae_loader: components["schemas"]["VAEOutput"]; - l2i: components["schemas"]["ImageOutput"]; - controlnet: components["schemas"]["ControlOutput"]; - show_image: components["schemas"]["ImageOutput"]; - infill_lama: components["schemas"]["ImageOutput"]; - image: components["schemas"]["ImageOutput"]; - zoe_depth_image_processor: components["schemas"]["ImageOutput"]; - merge_tiles_to_image: components["schemas"]["ImageOutput"]; - depth_anything_image_processor: components["schemas"]["ImageOutput"]; - latents_collection: components["schemas"]["LatentsCollectionOutput"]; - boolean_collection: components["schemas"]["BooleanCollectionOutput"]; - dynamic_prompt: components["schemas"]["StringCollectionOutput"]; - float: components["schemas"]["FloatOutput"]; - float_to_int: components["schemas"]["IntegerOutput"]; - integer: components["schemas"]["IntegerOutput"]; - unsharp_mask: components["schemas"]["ImageOutput"]; - calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; - normalbae_image_processor: components["schemas"]["ImageOutput"]; - img_paste: components["schemas"]["ImageOutput"]; - face_mask_detection: components["schemas"]["FaceMaskOutput"]; - step_param_easing: components["schemas"]["FloatCollectionOutput"]; - random_range: components["schemas"]["IntegerCollectionOutput"]; - leres_image_processor: components["schemas"]["ImageOutput"]; string_replace: components["schemas"]["StringOutput"]; - sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; - main_model_loader: components["schemas"]["ModelLoaderOutput"]; - rectangle_mask: components["schemas"]["MaskOutput"]; - mask_edge: components["schemas"]["ImageOutput"]; - mask_from_id: components["schemas"]["ImageOutput"]; - img_crop: components["schemas"]["ImageOutput"]; - save_image: components["schemas"]["ImageOutput"]; - string_join: components["schemas"]["StringOutput"]; - seamless: components["schemas"]["SeamlessModeOutput"]; - img_pad_crop: components["schemas"]["ImageOutput"]; - range_of_size: components["schemas"]["IntegerCollectionOutput"]; - range: components["schemas"]["IntegerCollectionOutput"]; - prompt_from_file: components["schemas"]["StringCollectionOutput"]; - compel: components["schemas"]["ConditioningOutput"]; - face_identifier: components["schemas"]["ImageOutput"]; - img_ilerp: components["schemas"]["ImageOutput"]; - color_map_image_processor: components["schemas"]["ImageOutput"]; - lscale: components["schemas"]["LatentsOutput"]; - image_collection: components["schemas"]["ImageCollectionOutput"]; - color_correct: components["schemas"]["ImageOutput"]; - integer_math: components["schemas"]["IntegerOutput"]; - string_join_three: components["schemas"]["StringOutput"]; merge_metadata: components["schemas"]["MetadataOutput"]; - rand_int: components["schemas"]["IntegerOutput"]; - metadata: components["schemas"]["MetadataOutput"]; - sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; - mlsd_image_processor: components["schemas"]["ImageOutput"]; - t2i_adapter: components["schemas"]["T2IAdapterOutput"]; - hed_image_processor: components["schemas"]["ImageOutput"]; - conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; - float_math: components["schemas"]["FloatOutput"]; - collect: components["schemas"]["CollectInvocationOutput"]; - canvas_paste_back: components["schemas"]["ImageOutput"]; - sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - latents: components["schemas"]["LatentsOutput"]; - conditioning: components["schemas"]["ConditioningOutput"]; - sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; - img_hue_adjust: components["schemas"]["ImageOutput"]; - img_lerp: components["schemas"]["ImageOutput"]; - div: components["schemas"]["IntegerOutput"]; - clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; - img_conv: components["schemas"]["ImageOutput"]; - crop_latents: components["schemas"]["LatentsOutput"]; - img_mul: components["schemas"]["ImageOutput"]; - string_split: components["schemas"]["String2Output"]; - integer_collection: components["schemas"]["IntegerCollectionOutput"]; - string_collection: components["schemas"]["StringCollectionOutput"]; - infill_cv2: components["schemas"]["ImageOutput"]; - float_collection: components["schemas"]["FloatCollectionOutput"]; - alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; - mediapipe_face_processor: components["schemas"]["ImageOutput"]; - invert_tensor_mask: components["schemas"]["MaskOutput"]; - mul: components["schemas"]["IntegerOutput"]; - ideal_size: components["schemas"]["IdealSizeOutput"]; - metadata_item: components["schemas"]["MetadataItemOutput"]; - color: components["schemas"]["ColorOutput"]; - sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; - infill_patchmatch: components["schemas"]["ImageOutput"]; - noise: components["schemas"]["NoiseOutput"]; - sub: components["schemas"]["IntegerOutput"]; - img_chan: components["schemas"]["ImageOutput"]; - infill_rgba: components["schemas"]["ImageOutput"]; - midas_depth_image_processor: components["schemas"]["ImageOutput"]; - segment_anything_processor: components["schemas"]["ImageOutput"]; - cv_inpaint: components["schemas"]["ImageOutput"]; - lineart_image_processor: components["schemas"]["ImageOutput"]; - calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; - create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; boolean: components["schemas"]["BooleanOutput"]; - img_nsfw: components["schemas"]["ImageOutput"]; - tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; - denoise_latents: components["schemas"]["LatentsOutput"]; - ip_adapter: components["schemas"]["IPAdapterOutput"]; - pair_tile_image: components["schemas"]["PairTileImageOutput"]; - image_mask_to_tensor: components["schemas"]["MaskOutput"]; - dw_openpose_image_processor: components["schemas"]["ImageOutput"]; - tomask: components["schemas"]["ImageOutput"]; - lresize: components["schemas"]["LatentsOutput"]; - lora_loader: components["schemas"]["LoRALoaderOutput"]; - canny_image_processor: components["schemas"]["ImageOutput"]; - calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; - img_watermark: components["schemas"]["ImageOutput"]; - img_scale: components["schemas"]["ImageOutput"]; - round_float: components["schemas"]["FloatOutput"]; - img_channel_offset: components["schemas"]["ImageOutput"]; - core_metadata: components["schemas"]["MetadataOutput"]; - iterate: components["schemas"]["IterateInvocationOutput"]; - string: components["schemas"]["StringOutput"]; - img_blur: components["schemas"]["ImageOutput"]; + face_off: components["schemas"]["FaceOffOutput"]; + prompt_from_file: components["schemas"]["StringCollectionOutput"]; + color_map_image_processor: components["schemas"]["ImageOutput"]; + range_of_size: components["schemas"]["IntegerCollectionOutput"]; + infill_rgba: components["schemas"]["ImageOutput"]; + compel: components["schemas"]["ConditioningOutput"]; + midas_depth_image_processor: components["schemas"]["ImageOutput"]; + image: components["schemas"]["ImageOutput"]; + color: components["schemas"]["ColorOutput"]; + mediapipe_face_processor: components["schemas"]["ImageOutput"]; + cv_inpaint: components["schemas"]["ImageOutput"]; + pidi_image_processor: components["schemas"]["ImageOutput"]; i2l: components["schemas"]["LatentsOutput"]; + infill_lama: components["schemas"]["ImageOutput"]; + freeu: components["schemas"]["UNetOutput"]; + segment_anything_processor: components["schemas"]["ImageOutput"]; + canny_image_processor: components["schemas"]["ImageOutput"]; + mlsd_image_processor: components["schemas"]["ImageOutput"]; + conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; + round_float: components["schemas"]["FloatOutput"]; + crop_latents: components["schemas"]["LatentsOutput"]; + tile_image_processor: components["schemas"]["ImageOutput"]; + integer_collection: components["schemas"]["IntegerCollectionOutput"]; + float_range: components["schemas"]["FloatCollectionOutput"]; + img_conv: components["schemas"]["ImageOutput"]; + sub: components["schemas"]["IntegerOutput"]; + denoise_latents: components["schemas"]["LatentsOutput"]; + string_collection: components["schemas"]["StringCollectionOutput"]; + lora_loader: components["schemas"]["LoRALoaderOutput"]; + core_metadata: components["schemas"]["MetadataOutput"]; + t2i_adapter: components["schemas"]["T2IAdapterOutput"]; + boolean_collection: components["schemas"]["BooleanCollectionOutput"]; + canvas_paste_back: components["schemas"]["ImageOutput"]; + string_split_neg: components["schemas"]["StringPosNegOutput"]; + img_nsfw: components["schemas"]["ImageOutput"]; + random_range: components["schemas"]["IntegerCollectionOutput"]; + ideal_size: components["schemas"]["IdealSizeOutput"]; + img_scale: components["schemas"]["ImageOutput"]; + dw_openpose_image_processor: components["schemas"]["ImageOutput"]; + image_collection: components["schemas"]["ImageCollectionOutput"]; + tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; + save_image: components["schemas"]["ImageOutput"]; + img_channel_offset: components["schemas"]["ImageOutput"]; + float_collection: components["schemas"]["FloatCollectionOutput"]; + lineart_anime_image_processor: components["schemas"]["ImageOutput"]; + img_blur: components["schemas"]["ImageOutput"]; + img_watermark: components["schemas"]["ImageOutput"]; + lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; + rand_int: components["schemas"]["IntegerOutput"]; + pair_tile_image: components["schemas"]["PairTileImageOutput"]; + tomask: components["schemas"]["ImageOutput"]; + sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; + mask_combine: components["schemas"]["ImageOutput"]; create_gradient_mask: components["schemas"]["GradientMaskOutput"]; + face_identifier: components["schemas"]["ImageOutput"]; + string: components["schemas"]["StringOutput"]; + heuristic_resize: components["schemas"]["ImageOutput"]; + lineart_image_processor: components["schemas"]["ImageOutput"]; + image_mask_to_tensor: components["schemas"]["MaskOutput"]; + lscale: components["schemas"]["LatentsOutput"]; + img_channel_multiply: components["schemas"]["ImageOutput"]; + sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + infill_cv2: components["schemas"]["ImageOutput"]; + latents: components["schemas"]["LatentsOutput"]; + add: components["schemas"]["IntegerOutput"]; + sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; + img_mul: components["schemas"]["ImageOutput"]; + leres_image_processor: components["schemas"]["ImageOutput"]; + ip_adapter: components["schemas"]["IPAdapterOutput"]; + string_join: components["schemas"]["StringOutput"]; + img_resize: components["schemas"]["ImageOutput"]; + img_chan: components["schemas"]["ImageOutput"]; + blank_image: components["schemas"]["ImageOutput"]; + latents_collection: components["schemas"]["LatentsCollectionOutput"]; + conditioning: components["schemas"]["ConditioningOutput"]; + main_model_loader: components["schemas"]["ModelLoaderOutput"]; + img_pad_crop: components["schemas"]["ImageOutput"]; + rectangle_mask: components["schemas"]["MaskOutput"]; + create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; + vae_loader: components["schemas"]["VAEOutput"]; + calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; + unsharp_mask: components["schemas"]["ImageOutput"]; + scheduler: components["schemas"]["SchedulerOutput"]; + integer_math: components["schemas"]["IntegerOutput"]; + mul: components["schemas"]["IntegerOutput"]; + string_join_three: components["schemas"]["StringOutput"]; + rand_float: components["schemas"]["FloatOutput"]; + calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; + controlnet: components["schemas"]["ControlOutput"]; + img_crop: components["schemas"]["ImageOutput"]; + float_math: components["schemas"]["FloatOutput"]; + div: components["schemas"]["IntegerOutput"]; + integer: components["schemas"]["IntegerOutput"]; + img_paste: components["schemas"]["ImageOutput"]; + lora_selector: components["schemas"]["LoRASelectorOutput"]; + iterate: components["schemas"]["IterateInvocationOutput"]; + show_image: components["schemas"]["ImageOutput"]; + collect: components["schemas"]["CollectInvocationOutput"]; + depth_anything_image_processor: components["schemas"]["ImageOutput"]; + mask_edge: components["schemas"]["ImageOutput"]; + infill_patchmatch: components["schemas"]["ImageOutput"]; + lblend: components["schemas"]["LatentsOutput"]; + metadata_item: components["schemas"]["MetadataItemOutput"]; + content_shuffle_image_processor: components["schemas"]["ImageOutput"]; + img_ilerp: components["schemas"]["ImageOutput"]; + metadata: components["schemas"]["MetadataOutput"]; + normalbae_image_processor: components["schemas"]["ImageOutput"]; + sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; + merge_tiles_to_image: components["schemas"]["ImageOutput"]; + mask_from_id: components["schemas"]["ImageOutput"]; + string_split: components["schemas"]["String2Output"]; + esrgan: components["schemas"]["ImageOutput"]; + color_correct: components["schemas"]["ImageOutput"]; + alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; + float_to_int: components["schemas"]["IntegerOutput"]; + seamless: components["schemas"]["SeamlessModeOutput"]; + float: components["schemas"]["FloatOutput"]; + dynamic_prompt: components["schemas"]["StringCollectionOutput"]; + clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; + sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; + l2i: components["schemas"]["ImageOutput"]; + infill_tile: components["schemas"]["ImageOutput"]; + invert_tensor_mask: components["schemas"]["MaskOutput"]; + face_mask_detection: components["schemas"]["FaceMaskOutput"]; + zoe_depth_image_processor: components["schemas"]["ImageOutput"]; + range: components["schemas"]["IntegerCollectionOutput"]; + hed_image_processor: components["schemas"]["ImageOutput"]; + img_hue_adjust: components["schemas"]["ImageOutput"]; + step_param_easing: components["schemas"]["FloatCollectionOutput"]; + img_lerp: components["schemas"]["ImageOutput"]; + noise: components["schemas"]["NoiseOutput"]; + calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; + lresize: components["schemas"]["LatentsOutput"]; }; }; responses: never; From eb320df41d338b6de05b6148d2dd07b16e19e567 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 9 May 2024 19:35:16 +1000 Subject: [PATCH 104/442] feat(ui): use new lora loaders, simplify VAE loader, seamless --- .../util/graph/addControlLayersToGraph2.ts | 18 ++--- .../graph/addGenerationTabControlLayers.ts | 15 +++- .../nodes/util/graph/addGenerationTabLoRAs.ts | 81 +++++++------------ .../util/graph/addGenerationTabSeamless.ts | 17 +--- .../util/graph/buildGenerationTabGraph2.ts | 25 ++++-- 5 files changed, 74 insertions(+), 82 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts index 16d7d74c27..5721f31313 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts @@ -490,29 +490,27 @@ const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean }; const isValidLayer = (layer: Layer, base: BaseModelType) => { + if (!layer.isEnabled) { + return false; + } if (isControlAdapterLayer(layer)) { - if (!layer.isEnabled) { - return false; - } return isValidControlAdapter(layer.controlAdapter, base); } if (isIPAdapterLayer(layer)) { - if (!layer.isEnabled) { - return false; - } return isValidIPAdapter(layer.ipAdapter, base); } if (isInitialImageLayer(layer)) { - if (!layer.isEnabled) { - return false; - } if (!layer.image) { return false; } return true; } if (isRegionalGuidanceLayer(layer)) { - const hasTextPrompt = Boolean(layer.positivePrompt || layer.negativePrompt); + if (layer.maskObjects.length === 0) { + // Layer has no mask, meaning any guidance would be applied to an empty region. + return false; + } + const hasTextPrompt = Boolean(layer.positivePrompt) || Boolean(layer.negativePrompt); const hasIPAdapter = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; return hasTextPrompt || hasIPAdapter; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts index d3b2788329..7851d5c19d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -45,7 +45,12 @@ export const addGenerationTabControlLayers = async ( negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, posCondCollect: Invocation<'collect'>, negCondCollect: Invocation<'collect'>, - noise: Invocation<'noise'> + noise: Invocation<'noise'>, + vaeSource: + | Invocation<'seamless'> + | Invocation<'vae_loader'> + | Invocation<'main_model_loader'> + | Invocation<'sdxl_model_loader'> ): Promise => { const mainModel = state.generation.model; assert(mainModel, 'Missing main model when building graph'); @@ -67,7 +72,7 @@ export const addGenerationTabControlLayers = async ( const initialImageLayers = validLayers.filter(isInitialImageLayer); assert(initialImageLayers.length <= 1, 'Only one initial image layer allowed'); if (initialImageLayers[0]) { - addInitialImageLayerToGraph(state, g, denoise, noise, initialImageLayers[0]); + addInitialImageLayerToGraph(state, g, denoise, noise, vaeSource, initialImageLayers[0]); } // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing // the existing conditioning nodes. @@ -414,6 +419,11 @@ const addInitialImageLayerToGraph = ( g: Graph, denoise: Invocation<'denoise_latents'>, noise: Invocation<'noise'>, + vaeSource: + | Invocation<'seamless'> + | Invocation<'vae_loader'> + | Invocation<'main_model_loader'> + | Invocation<'sdxl_model_loader'>, layer: InitialImageLayer ) => { const { vaePrecision, model } = state.generation; @@ -438,6 +448,7 @@ const addInitialImageLayerToGraph = ( }); g.addEdge(i2l, 'latents', denoise, 'latents'); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); if (layer.image.width !== width || layer.image.height !== height) { // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts index 3cb43fd48d..5a7173f2d5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts @@ -1,11 +1,9 @@ import type { RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import { Graph } from 'features/nodes/util/graph/Graph'; +import type { Graph } from 'features/nodes/util/graph/Graph'; import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import { filter, size } from 'lodash-es'; import type { Invocation, S } from 'services/api/types'; -import { assert } from 'tsafe'; import { LORA_LOADER } from './constants'; @@ -13,19 +11,12 @@ export const addGenerationTabLoRAs = ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, - unetSource: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> | Invocation<'seamless'>, + modelLoader: Invocation<'main_model_loader'>, + seamless: Invocation<'seamless'> | null, clipSkip: Invocation<'clip_skip'>, posCond: Invocation<'compel'>, negCond: Invocation<'compel'> ): void => { - /** - * LoRA nodes get the UNet and CLIP models from the main model loader and apply the LoRA to them. - * They then output the UNet and CLIP models references on to either the next LoRA in the chain, - * or to the inference/conditioning nodes. - * - * So we need to inject a LoRA chain into the graph. - */ - const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); const loraCount = size(enabledLoRAs); @@ -33,30 +24,39 @@ export const addGenerationTabLoRAs = ( return; } - // Remove modelLoaderNodeId unet connection to feed it to LoRAs - console.log(deepClone(g)._graph.edges.map((e) => Graph.edgeToString(e))); - g.deleteEdgesFrom(unetSource, 'unet'); - console.log(deepClone(g)._graph.edges.map((e) => Graph.edgeToString(e))); - if (clipSkip) { - // Remove CLIP_SKIP connections to conditionings to feed it through LoRAs - g.deleteEdgesFrom(clipSkip, 'clip'); - } - console.log(deepClone(g)._graph.edges.map((e) => Graph.edgeToString(e))); - - // we need to remember the last lora so we can chain from it - let lastLoRALoader: Invocation<'lora_loader'> | null = null; - let currentLoraIndex = 0; const loraMetadata: S['LoRAMetadataField'][] = []; + // We will collect LoRAs into a single collection node, then pass them to the LoRA collection loader, which applies + // each LoRA to the UNet and CLIP. + const loraCollector = g.addNode({ + id: `${LORA_LOADER}_collect`, + type: 'collect', + }); + const loraCollectionLoader = g.addNode({ + id: LORA_LOADER, + type: 'lora_collection_loader', + }); + + g.addEdge(loraCollector, 'collection', loraCollectionLoader, 'loras'); + // Use seamless as UNet input if it exists, otherwise use the model loader + g.addEdge(seamless ?? modelLoader, 'unet', loraCollectionLoader, 'unet'); + g.addEdge(clipSkip, 'clip', loraCollectionLoader, 'clip'); + // Reroute UNet & CLIP connections through the LoRA collection loader + g.deleteEdgesTo(denoise, 'unet'); + g.deleteEdgesTo(posCond, 'clip'); + g.deleteEdgesTo(negCond, 'clip'); + g.addEdge(loraCollectionLoader, 'unet', denoise, 'unet'); + g.addEdge(loraCollectionLoader, 'clip', posCond, 'clip'); + g.addEdge(loraCollectionLoader, 'clip', negCond, 'clip'); + for (const lora of enabledLoRAs) { const { weight } = lora; const { key } = lora.model; - const currentLoraNodeId = `${LORA_LOADER}_${key}`; const parsedModel = zModelIdentifierField.parse(lora.model); - const currentLoRALoader = g.addNode({ - type: 'lora_loader', - id: currentLoraNodeId, + const loraSelector = g.addNode({ + type: 'lora_selector', + id: `${LORA_LOADER}_${key}`, lora: parsedModel, weight, }); @@ -66,28 +66,7 @@ export const addGenerationTabLoRAs = ( weight, }); - // add to graph - if (currentLoraIndex === 0) { - // first lora = start the lora chain, attach directly to model loader - g.addEdge(unetSource, 'unet', currentLoRALoader, 'unet'); - g.addEdge(clipSkip, 'clip', currentLoRALoader, 'clip'); - } else { - assert(lastLoRALoader !== null); - // we are in the middle of the lora chain, instead connect to the previous lora - g.addEdge(lastLoRALoader, 'unet', currentLoRALoader, 'unet'); - g.addEdge(lastLoRALoader, 'clip', currentLoRALoader, 'clip'); - } - - if (currentLoraIndex === loraCount - 1) { - // final lora, end the lora chain - we need to connect up to inference and conditioning nodes - g.addEdge(currentLoRALoader, 'unet', denoise, 'unet'); - g.addEdge(currentLoRALoader, 'clip', posCond, 'clip'); - g.addEdge(currentLoRALoader, 'clip', negCond, 'clip'); - } - - // increment the lora for the next one in the chain - lastLoRALoader = currentLoRALoader; - currentLoraIndex += 1; + g.addEdge(loraSelector, 'lora', loraCollector, 'item'); } MetadataUtil.add(g, { loras: loraMetadata }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts index e56f37916c..ef0b38291f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts @@ -3,7 +3,7 @@ import type { Graph } from 'features/nodes/util/graph/Graph'; import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import type { Invocation } from 'services/api/types'; -import { SEAMLESS, VAE_LOADER } from './constants'; +import { SEAMLESS } from './constants'; /** * Adds the seamless node to the graph and connects it to the model loader and denoise node. @@ -19,9 +19,10 @@ export const addGenerationTabSeamless = ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, - modelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> + modelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'>, + vaeLoader: Invocation<'vae_loader'> | null ): Invocation<'seamless'> | null => { - const { seamlessXAxis: seamless_x, seamlessYAxis: seamless_y, vae } = state.generation; + const { seamlessXAxis: seamless_x, seamlessYAxis: seamless_y } = state.generation; if (!seamless_x && !seamless_y) { return null; @@ -34,16 +35,6 @@ export const addGenerationTabSeamless = ( seamless_y, }); - // The VAE helper also adds the VAE loader - so we need to check if it's already there - const shouldAddVAELoader = !g.hasNode(VAE_LOADER) && vae; - const vaeLoader = shouldAddVAELoader - ? g.addNode({ - type: 'vae_loader', - id: VAE_LOADER, - vae_model: vae, - }) - : null; - MetadataUtil.add(g, { seamless_x: seamless_x || undefined, seamless_y: seamless_y || undefined, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts index 38c7ba18d1..2735103215 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts @@ -5,7 +5,6 @@ import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetch import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; import { addGenerationTabLoRAs } from 'features/nodes/util/graph/addGenerationTabLoRAs'; import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; -import { addGenerationTabVAE } from 'features/nodes/util/graph/addGenerationTabVAE'; import type { GraphType } from 'features/nodes/util/graph/Graph'; import { Graph } from 'features/nodes/util/graph/Graph'; import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; @@ -26,6 +25,7 @@ import { NOISE, POSITIVE_CONDITIONING, POSITIVE_CONDITIONING_COLLECT, + VAE_LOADER, } from './constants'; import { getModelMetadataField } from './metadata'; @@ -41,6 +41,7 @@ export const buildGenerationTabGraph2 = async (state: RootState): Promise Date: Mon, 13 May 2024 15:00:41 +1000 Subject: [PATCH 105/442] tidy(ui): remove extraneous `is_intermediate` node fields --- .../nodes/util/graph/addGenerationTabControlLayers.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts index 7851d5c19d..0647d52502 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -212,7 +212,6 @@ export const addGenerationTabControlLayers = async ( const ipAdapter = g.addNode({ id: `ip_adapter_${id}`, type: 'ip_adapter', - is_intermediate: true, weight, method, ip_adapter_model: model, @@ -316,7 +315,6 @@ const addGlobalControlNetToGraph = ( const controlNet = g.addNode({ id: `control_net_${id}`, type: 'controlnet', - is_intermediate: true, begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], control_mode: controlMode, @@ -359,7 +357,6 @@ const addGlobalT2IAdapterToGraph = ( const t2iAdapter = g.addNode({ id: `t2i_adapter_${id}`, type: 't2i_adapter', - is_intermediate: true, begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], resize_mode: 'just_resize', @@ -400,7 +397,6 @@ const addGlobalIPAdapterToGraph = ( const ipAdapter = g.addNode({ id: `ip_adapter_${id}`, type: 'ip_adapter', - is_intermediate: true, weight, method, ip_adapter_model: model, @@ -442,8 +438,6 @@ const addInitialImageLayerToGraph = ( const i2l = g.addNode({ type: 'i2l', id: IMAGE_TO_LATENTS, - is_intermediate: true, - use_cache: true, fp32: vaePrecision === 'fp32', }); @@ -460,7 +454,6 @@ const addInitialImageLayerToGraph = ( image: { image_name: layer.image.name, }, - is_intermediate: true, width, height, }); From b5d42fbc66b3da20f563bf66e731cad750467862 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 15:00:52 +1000 Subject: [PATCH 106/442] tidy(ui): remove unused graph helper --- .../util/graph/addControlLayersToGraph2.ts | 518 ------------------ 1 file changed, 518 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts deleted file mode 100644 index 5721f31313..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph2.ts +++ /dev/null @@ -1,518 +0,0 @@ -import { getStore } from 'app/store/nanostores/store'; -import type { RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isRegionalGuidanceLayer, - rgLayerMaskImageUploaded, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import type { - ControlNetConfigV2, - ImageWithDims, - IPAdapterConfigV2, - ProcessorConfig, - T2IAdapterConfigV2, -} from 'features/controlLayers/util/controlAdapters'; -import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; -import type { ImageField } from 'features/nodes/types/common'; -import { - CONTROL_NET_COLLECT, - IMAGE_TO_LATENTS, - IP_ADAPTER_COLLECT, - PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, - PROMPT_REGION_MASK_TO_TENSOR_PREFIX, - PROMPT_REGION_NEGATIVE_COND_PREFIX, - PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, - PROMPT_REGION_POSITIVE_COND_PREFIX, - RESIZE, - T2I_ADAPTER_COLLECT, -} from 'features/nodes/util/graph/constants'; -import type { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; -import { size } from 'lodash-es'; -import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; -import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; -import { assert } from 'tsafe'; - -export const addControlLayersToGraph = async ( - state: RootState, - g: Graph, - denoise: Invocation<'denoise_latents'>, - posCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, - negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, - posCondCollect: Invocation<'collect'>, - negCondCollect: Invocation<'collect'>, - noise: Invocation<'noise'> -): Promise => { - const mainModel = state.generation.model; - assert(mainModel, 'Missing main model when building graph'); - const isSDXL = mainModel.base === 'sdxl'; - - // Filter out layers with incompatible base model, missing control image - const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, mainModel.base)); - - const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); - for (const ca of validControlAdapters) { - addGlobalControlAdapterToGraph(ca, g, denoise); - } - - const validIPAdapters = validLayers.filter(isIPAdapterLayer).map((l) => l.ipAdapter); - for (const ipAdapter of validIPAdapters) { - addGlobalIPAdapterToGraph(ipAdapter, g, denoise); - } - - const initialImageLayers = validLayers.filter(isInitialImageLayer); - assert(initialImageLayers.length <= 1, 'Only one initial image layer allowed'); - if (initialImageLayers[0]) { - addInitialImageLayerToGraph(state, g, denoise, noise, initialImageLayers[0]); - } - // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing - // the existing conditioning nodes. - - const validRGLayers = validLayers.filter(isRegionalGuidanceLayer); - const layerIds = validRGLayers.map((l) => l.id); - const blobs = await getRegionalPromptLayerBlobs(layerIds); - assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); - - for (const layer of validRGLayers) { - const blob = blobs[layer.id]; - assert(blob, `Blob for layer ${layer.id} not found`); - // Upload the mask image, or get the cached image if it exists - const { image_name } = await getMaskImage(layer, blob); - - // The main mask-to-tensor node - const maskToTensor = g.addNode({ - id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${layer.id}`, - type: 'alpha_mask_to_tensor', - image: { - image_name, - }, - }); - - if (layer.positivePrompt) { - // The main positive conditioning node - const regionalPosCond = g.addNode( - isSDXL - ? { - type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - style: layer.positivePrompt, // TODO: Should we put the positive prompt in both fields? - } - : { - type: 'compel', - id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - } - ); - // Connect the mask to the conditioning - g.addEdge(maskToTensor, 'mask', regionalPosCond, 'mask'); - // Connect the conditioning to the collector - g.addEdge(regionalPosCond, 'conditioning', posCondCollect, 'item'); - // Copy the connections to the "global" positive conditioning node to the regional cond - for (const edge of g.getEdgesTo(posCond)) { - console.log(edge); - if (edge.destination.field !== 'prompt') { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCond.id; - g.addEdgeFromObj(clone); - } - } - } - - if (layer.negativePrompt) { - // The main negative conditioning node - const regionalNegCond = g.addNode( - isSDXL - ? { - type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.negativePrompt, - style: layer.negativePrompt, - } - : { - type: 'compel', - id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.negativePrompt, - } - ); - // Connect the mask to the conditioning - g.addEdge(maskToTensor, 'mask', regionalNegCond, 'mask'); - // Connect the conditioning to the collector - g.addEdge(regionalNegCond, 'conditioning', negCondCollect, 'item'); - // Copy the connections to the "global" negative conditioning node to the regional cond - for (const edge of g.getEdgesTo(negCond)) { - if (edge.destination.field !== 'prompt') { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalNegCond.id; - g.addEdgeFromObj(clone); - } - } - } - - // If we are using the "invert" auto-negative setting, we need to add an additional negative conditioning node - if (layer.autoNegative === 'invert' && layer.positivePrompt) { - // We re-use the mask image, but invert it when converting to tensor - const invertTensorMask = g.addNode({ - id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${layer.id}`, - type: 'invert_tensor_mask', - }); - // Connect the OG mask image to the inverted mask-to-tensor node - g.addEdge(maskToTensor, 'mask', invertTensorMask, 'mask'); - // Create the conditioning node. It's going to be connected to the negative cond collector, but it uses the positive prompt - const regionalPosCondInverted = g.addNode( - isSDXL - ? { - type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - style: layer.positivePrompt, - } - : { - type: 'compel', - id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - } - ); - // Connect the inverted mask to the conditioning - g.addEdge(invertTensorMask, 'mask', regionalPosCondInverted, 'mask'); - // Connect the conditioning to the negative collector - g.addEdge(regionalPosCondInverted, 'conditioning', negCondCollect, 'item'); - // Copy the connections to the "global" positive conditioning node to our regional node - for (const edge of g.getEdgesTo(posCond)) { - if (edge.destination.field !== 'prompt') { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCondInverted.id; - g.addEdgeFromObj(clone); - } - } - } - - const validRegionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipa) => - isValidIPAdapter(ipa, mainModel.base) - ); - - for (const ipAdapterConfig of validRegionalIPAdapters) { - const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; - assert(model, 'IP Adapter model is required'); - assert(image, 'IP Adapter image is required'); - - const ipAdapter = g.addNode({ - id: `ip_adapter_${id}`, - type: 'ip_adapter', - is_intermediate: true, - weight, - method, - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.name, - }, - }); - - // Connect the mask to the conditioning - g.addEdge(maskToTensor, 'mask', ipAdapter, 'mask'); - g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); - } - } - - MetadataUtil.add(g, { control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); - return validLayers; -}; - -const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { - if (layer.uploadedMaskImage) { - const imageDTO = await getImageDTO(layer.uploadedMaskImage.name); - if (imageDTO) { - return imageDTO; - } - } - const { dispatch } = getStore(); - // No cached mask, or the cached image no longer exists - we need to upload the mask image - const file = new File([blob], `${layer.id}_mask.png`, { type: 'image/png' }); - const req = dispatch( - imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) - ); - req.reset(); - - const imageDTO = await req.unwrap(); - dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO })); - return imageDTO; -}; - -const buildControlImage = ( - image: ImageWithDims | null, - processedImage: ImageWithDims | null, - processorConfig: ProcessorConfig | null -): ImageField => { - if (processedImage && processorConfig) { - // We've processed the image in the app - use it for the control image. - return { - image_name: processedImage.name, - }; - } else if (image) { - // No processor selected, and we have an image - the user provided a processed image, use it for the control image. - return { - image_name: image.name, - }; - } - assert(false, 'Attempted to add unprocessed control image'); -}; - -const addGlobalControlAdapterToGraph = ( - controlAdapterConfig: ControlNetConfigV2 | T2IAdapterConfigV2, - g: Graph, - denoise: Invocation<'denoise_latents'> -): void => { - if (controlAdapterConfig.type === 'controlnet') { - addGlobalControlNetToGraph(controlAdapterConfig, g, denoise); - } - if (controlAdapterConfig.type === 't2i_adapter') { - addGlobalT2IAdapterToGraph(controlAdapterConfig, g, denoise); - } -}; - -const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { - try { - // Attempt to retrieve the collector - const controlNetCollect = g.getNode(CONTROL_NET_COLLECT); - assert(controlNetCollect.type === 'collect'); - return controlNetCollect; - } catch { - // Add the ControlNet collector - const controlNetCollect = g.addNode({ - id: CONTROL_NET_COLLECT, - type: 'collect', - }); - g.addEdge(controlNetCollect, 'collection', denoise, 'control'); - return controlNetCollect; - } -}; - -const addGlobalControlNetToGraph = ( - controlNetConfig: ControlNetConfigV2, - g: Graph, - denoise: Invocation<'denoise_latents'> -) => { - const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNetConfig; - assert(model, 'ControlNet model is required'); - const controlImage = buildControlImage(image, processedImage, processorConfig); - const controlNetCollect = addControlNetCollectorSafe(g, denoise); - - const controlNet = g.addNode({ - id: `control_net_${id}`, - type: 'controlnet', - is_intermediate: true, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - control_mode: controlMode, - resize_mode: 'just_resize', - control_model: model, - control_weight: weight, - image: controlImage, - }); - g.addEdge(controlNet, 'control', controlNetCollect, 'item'); -}; - -const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { - try { - // You see, we've already got one! - const t2iAdapterCollect = g.getNode(T2I_ADAPTER_COLLECT); - assert(t2iAdapterCollect.type === 'collect'); - return t2iAdapterCollect; - } catch { - const t2iAdapterCollect = g.addNode({ - id: T2I_ADAPTER_COLLECT, - type: 'collect', - }); - - g.addEdge(t2iAdapterCollect, 'collection', denoise, 't2i_adapter'); - - return t2iAdapterCollect; - } -}; - -const addGlobalT2IAdapterToGraph = ( - t2iAdapterConfig: T2IAdapterConfigV2, - g: Graph, - denoise: Invocation<'denoise_latents'> -) => { - const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapterConfig; - assert(model, 'T2I Adapter model is required'); - const controlImage = buildControlImage(image, processedImage, processorConfig); - const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); - - const t2iAdapter = g.addNode({ - id: `t2i_adapter_${id}`, - type: 't2i_adapter', - is_intermediate: true, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - resize_mode: 'just_resize', - t2i_adapter_model: model, - weight: weight, - image: controlImage, - }); - - g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); -}; - -const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { - try { - // You see, we've already got one! - const ipAdapterCollect = g.getNode(IP_ADAPTER_COLLECT); - assert(ipAdapterCollect.type === 'collect'); - return ipAdapterCollect; - } catch { - const ipAdapterCollect = g.addNode({ - id: IP_ADAPTER_COLLECT, - type: 'collect', - }); - g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter'); - return ipAdapterCollect; - } -}; - -const addGlobalIPAdapterToGraph = ( - ipAdapterConfig: IPAdapterConfigV2, - g: Graph, - denoise: Invocation<'denoise_latents'> -) => { - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; - assert(image, 'IP Adapter image is required'); - assert(model, 'IP Adapter model is required'); - const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - - const ipAdapter = g.addNode({ - id: `ip_adapter_${id}`, - type: 'ip_adapter', - is_intermediate: true, - weight, - method, - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.name, - }, - }); - g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); -}; - -const addInitialImageLayerToGraph = ( - state: RootState, - g: Graph, - denoise: Invocation<'denoise_latents'>, - noise: Invocation<'noise'>, - layer: InitialImageLayer -) => { - const { vaePrecision, model } = state.generation; - const { refinerModel, refinerStart } = state.sdxl; - const { width, height } = state.controlLayers.present.size; - assert(layer.isEnabled, 'Initial image layer is not enabled'); - assert(layer.image, 'Initial image layer has no image'); - - const isSDXL = model?.base === 'sdxl'; - const useRefinerStartEnd = isSDXL && Boolean(refinerModel); - - const { denoisingStrength } = layer; - denoise.denoising_start = useRefinerStartEnd ? Math.min(refinerStart, 1 - denoisingStrength) : 1 - denoisingStrength; - denoise.denoising_end = useRefinerStartEnd ? refinerStart : 1; - - const i2l = g.addNode({ - type: 'i2l', - id: IMAGE_TO_LATENTS, - is_intermediate: true, - use_cache: true, - fp32: vaePrecision === 'fp32', - }); - - g.addEdge(i2l, 'latents', denoise, 'latents'); - - if (layer.image.width !== width || layer.image.height !== height) { - // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` - - // Create a resize node, explicitly setting its image - const resize = g.addNode({ - id: RESIZE, - type: 'img_resize', - image: { - image_name: layer.image.name, - }, - is_intermediate: true, - width, - height, - }); - - // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` - g.addEdge(resize, 'image', i2l, 'image'); - // The `RESIZE` node also passes its width and height to `NOISE` - g.addEdge(resize, 'width', noise, 'width'); - g.addEdge(resize, 'height', noise, 'height'); - } else { - // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly - i2l.image = { - image_name: layer.image.name, - }; - - // Pass the image's dimensions to the `NOISE` node - g.addEdge(i2l, 'width', noise, 'width'); - g.addEdge(i2l, 'height', noise, 'height'); - } - - MetadataUtil.add(g, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); -}; - -const isValidControlAdapter = (ca: ControlNetConfigV2 | T2IAdapterConfigV2, base: BaseModelType): boolean => { - // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === base; - const hasControlImage = Boolean(ca.image || (ca.processedImage && ca.processorConfig)); - return hasModel && modelMatchesBase && hasControlImage; -}; - -const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean => { - // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ipa.model); - const modelMatchesBase = ipa.model?.base === base; - const hasImage = Boolean(ipa.image); - return hasModel && modelMatchesBase && hasImage; -}; - -const isValidLayer = (layer: Layer, base: BaseModelType) => { - if (!layer.isEnabled) { - return false; - } - if (isControlAdapterLayer(layer)) { - return isValidControlAdapter(layer.controlAdapter, base); - } - if (isIPAdapterLayer(layer)) { - return isValidIPAdapter(layer.ipAdapter, base); - } - if (isInitialImageLayer(layer)) { - if (!layer.image) { - return false; - } - return true; - } - if (isRegionalGuidanceLayer(layer)) { - if (layer.maskObjects.length === 0) { - // Layer has no mask, meaning any guidance would be applied to an empty region. - return false; - } - const hasTextPrompt = Boolean(layer.positivePrompt) || Boolean(layer.negativePrompt); - const hasIPAdapter = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; - return hasTextPrompt || hasIPAdapter; - } - return false; -}; From 76e181fd4488976e58691d1529639f8580d209bb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 15:12:43 +1000 Subject: [PATCH 107/442] build(ui): add eslint `no-console` rule --- invokeai/frontend/web/.eslintrc.js | 2 ++ invokeai/frontend/web/scripts/typegen.js | 1 + invokeai/frontend/web/src/app/hooks/useSocketIO.ts | 4 ++++ .../web/src/app/store/middleware/debugLoggerMiddleware.ts | 3 +++ .../listeners/controlAdapterPreprocessor.ts | 1 - 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/.eslintrc.js b/invokeai/frontend/web/.eslintrc.js index 18a6e3a9b9..519e725fb4 100644 --- a/invokeai/frontend/web/.eslintrc.js +++ b/invokeai/frontend/web/.eslintrc.js @@ -10,6 +10,8 @@ module.exports = { 'path/no-relative-imports': ['error', { maxDepth: 0 }], // https://github.com/edvardchen/eslint-plugin-i18next/blob/HEAD/docs/rules/no-literal-string.md 'i18next/no-literal-string': 'error', + // https://eslint.org/docs/latest/rules/no-console + 'no-console': 'error', }, overrides: [ /** diff --git a/invokeai/frontend/web/scripts/typegen.js b/invokeai/frontend/web/scripts/typegen.js index 758a0ef4f5..fa2d791350 100644 --- a/invokeai/frontend/web/scripts/typegen.js +++ b/invokeai/frontend/web/scripts/typegen.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import fs from 'node:fs'; import openapiTS from 'openapi-typescript'; diff --git a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts b/invokeai/frontend/web/src/app/hooks/useSocketIO.ts index e1c4cebdb9..aaa3b8f6f2 100644 --- a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts +++ b/invokeai/frontend/web/src/app/hooks/useSocketIO.ts @@ -67,6 +67,8 @@ export const useSocketIO = () => { if ($isDebugging.get() || import.meta.env.MODE === 'development') { window.$socketOptions = $socketOptions; + // This is only enabled manually for debugging, console is allowed. + /* eslint-disable-next-line no-console */ console.log('Socket initialized', socket); } @@ -75,6 +77,8 @@ export const useSocketIO = () => { return () => { if ($isDebugging.get() || import.meta.env.MODE === 'development') { window.$socketOptions = undefined; + // This is only enabled manually for debugging, console is allowed. + /* eslint-disable-next-line no-console */ console.log('Socket teardown', socket); } socket.disconnect(); diff --git a/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts b/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts index b6df6dab94..89010275d1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts +++ b/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-console */ +// This is only enabled manually for debugging, console is allowed. + import type { Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; import { diff } from 'jsondiffpatch'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index a1f7ebcca1..2a59cc0317 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -148,7 +148,6 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni log.trace('Control Adapter preprocessor cancelled'); } else { // Some other error condition... - console.log(error); log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue')); if (error instanceof Object) { From 2be66b1546dd96665b92005b8258d123cf74aa8a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 15:40:20 +1000 Subject: [PATCH 108/442] feat(ui): add `deleteNode` and `getEdges` to graph util --- .../features/nodes/util/graph/Graph.test.ts | 49 +++++++++++++++++++ .../src/features/nodes/util/graph/Graph.ts | 21 ++++++++ 2 files changed, 70 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts index b11e16545f..412d886720 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts @@ -123,6 +123,34 @@ describe('Graph', () => { }); }); + describe('deleteNode', () => { + it('should delete the node with the provided id', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'add', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'add', + }); + const n3 = g.addNode({ + id: 'n3', + type: 'add', + }); + g.addEdge(n1, 'value', n2, 'a'); + g.addEdge(n2, 'value', n3, 'a'); + // This edge should not be deleted bc it doesn't touch n2 + g.addEdge(n1, 'value', n3, 'a'); + g.deleteNode(n2.id); + expect(g.hasNode(n1.id)).toBe(true); + expect(g.hasNode(n2.id)).toBe(false); + expect(g.hasNode(n3.id)).toBe(true); + // Should delete edges to and from the node + expect(g.getEdges().length).toBe(1); + }); + }); + describe('hasNode', () => { const g = new Graph(); g.addNode({ @@ -160,6 +188,27 @@ describe('Graph', () => { }); }); + describe('getEdges', () => { + it('should get all edges in the graph', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'add', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'add', + }); + const n3 = g.addNode({ + id: 'n3', + type: 'add', + }); + const e1 = g.addEdge(n1, 'value', n2, 'a'); + const e2 = g.addEdge(n2, 'value', n3, 'a'); + expect(g.getEdges()).toEqual([e1, e2]); + }); + }); + describe('hasEdge', () => { const g = new Graph(); const add: Invocation<'add'> = { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts index b578c5b40a..165e04ffe4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts @@ -68,6 +68,19 @@ export class Graph { return node; } + /** + * Deletes a node from the graph. All edges to and from the node are also deleted. + * @param id The id of the node to delete. + */ + deleteNode(id: string): void { + const node = this._graph.nodes[id]; + if (node) { + this.deleteEdgesFrom(node); + this.deleteEdgesTo(node); + delete this._graph.nodes[id]; + } + } + /** * Check if a node exists in the graph. * @param id The id of the node to check. @@ -182,6 +195,14 @@ export class Graph { return edge; } + /** + * Get all edges in the graph. + * @returns The edges. + */ + getEdges(): Edge[] { + return this._graph.edges; + } + /** * Check if a graph has an edge. * Provide the from and to node types as generics to get type hints for from and to field names. From e8d3a7c870c9f4d278cb9e8d66eebe26570dab54 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 16:07:34 +1000 Subject: [PATCH 109/442] feat(ui): support multiple fields for `getEdgesTo`, `getEdgesFrom`, `deleteEdgesTo`, `deleteEdgesFrom` --- .../features/nodes/util/graph/Graph.test.ts | 8 ++--- .../src/features/nodes/util/graph/Graph.ts | 29 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts index 412d886720..148bbf1ded 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts @@ -316,7 +316,7 @@ describe('Graph', () => { expect(g.getEdgesFrom(n3)).toEqual([e3, e4]); }); it('should return the edges that start at the provided node and have the provided field', () => { - expect(g.getEdgesFrom(n2, 'height')).toEqual([e2]); + expect(g.getEdgesFrom(n3, ['value'])).toEqual([e3, e4]); }); }); describe('getEdgesTo', () => { @@ -324,7 +324,7 @@ describe('Graph', () => { expect(g.getEdgesTo(n3)).toEqual([e1, e2]); }); it('should return the edges that end at the provided node and have the provided field', () => { - expect(g.getEdgesTo(n3, 'b')).toEqual([e2]); + expect(g.getEdgesTo(n3, ['b', 'a'])).toEqual([e1, e2]); }); }); describe('getIncomers', () => { @@ -372,7 +372,7 @@ describe('Graph', () => { const _e1 = g.addEdge(n1, 'height', n2, 'a'); const e2 = g.addEdge(n1, 'width', n2, 'b'); const e3 = g.addEdge(n1, 'width', n3, 'b'); - g.deleteEdgesFrom(n1, 'height'); + g.deleteEdgesFrom(n1, ['height']); expect(g.getEdgesFrom(n1)).toEqual([e2, e3]); }); }); @@ -410,7 +410,7 @@ describe('Graph', () => { const _e1 = g.addEdge(n1, 'height', n3, 'a'); const e2 = g.addEdge(n1, 'width', n3, 'b'); const _e3 = g.addEdge(n2, 'width', n3, 'a'); - g.deleteEdgesTo(n3, 'a'); + g.deleteEdgesTo(n3, ['a']); expect(g.getEdgesTo(n3)).toEqual([e2]); }); }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts index 165e04ffe4..7cc976bfb0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts @@ -231,13 +231,14 @@ export class Graph { * Get all edges from a node. If `fromField` is provided, only edges from that field are returned. * Provide the from node type as a generic to get type hints for from field names. * @param fromNodeId The id of the source node. - * @param fromField The field of the source node (optional). + * @param fromFields The field of the source node (optional). * @returns The edges. */ - getEdgesFrom(fromNode: T, fromField?: OutputFields): Edge[] { + getEdgesFrom(fromNode: T, fromFields?: OutputFields[]): Edge[] { let edges = this._graph.edges.filter((edge) => edge.source.node_id === fromNode.id); - if (fromField) { - edges = edges.filter((edge) => edge.source.field === fromField); + if (fromFields) { + // TODO(psyche): figure out how to satisfy TS here without casting - this is _not_ an unsafe cast + edges = edges.filter((edge) => (fromFields as AnyInvocationOutputField[]).includes(edge.source.field)); } return edges; } @@ -246,13 +247,13 @@ export class Graph { * Get all edges to a node. If `toField` is provided, only edges to that field are returned. * Provide the to node type as a generic to get type hints for to field names. * @param toNodeId The id of the destination node. - * @param toField The field of the destination node (optional). + * @param toFields The field of the destination node (optional). * @returns The edges. */ - getEdgesTo(toNode: T, toField?: InputFields): Edge[] { + getEdgesTo(toNode: T, toFields?: InputFields[]): Edge[] { let edges = this._graph.edges.filter((edge) => edge.destination.node_id === toNode.id); - if (toField) { - edges = edges.filter((edge) => edge.destination.field === toField); + if (toFields) { + edges = edges.filter((edge) => (toFields as AnyInvocationInputField[]).includes(edge.destination.field)); } return edges; } @@ -269,10 +270,10 @@ export class Graph { * Delete all edges to a node. If `toField` is provided, only edges to that field are deleted. * Provide the to node type as a generic to get type hints for to field names. * @param toNode The destination node. - * @param toField The field of the destination node (optional). + * @param toFields The field of the destination node (optional). */ - deleteEdgesTo(toNode: T, toField?: InputFields): void { - for (const edge of this.getEdgesTo(toNode, toField)) { + deleteEdgesTo(toNode: T, toFields?: InputFields[]): void { + for (const edge of this.getEdgesTo(toNode, toFields)) { this._deleteEdge(edge); } } @@ -281,10 +282,10 @@ export class Graph { * Delete all edges from a node. If `fromField` is provided, only edges from that field are deleted. * Provide the from node type as a generic to get type hints for from field names. * @param toNodeId The id of the source node. - * @param toField The field of the source node (optional). + * @param fromFields The field of the source node (optional). */ - deleteEdgesFrom(fromNode: T, fromField?: OutputFields): void { - for (const edge of this.getEdgesFrom(fromNode, fromField)) { + deleteEdgesFrom(fromNode: T, fromFields?: OutputFields[]): void { + for (const edge of this.getEdgesFrom(fromNode, fromFields)) { this._deleteEdge(edge); } } From c538ffea269b5731f3e327a1576ede1700d017ae Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 16:43:42 +1000 Subject: [PATCH 110/442] tidy(ui): remove console.log --- .../features/nodes/util/graph/addGenerationTabControlLayers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts index 0647d52502..1afa21ba9f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -119,7 +119,6 @@ export const addGenerationTabControlLayers = async ( g.addEdge(regionalPosCond, 'conditioning', posCondCollect, 'item'); // Copy the connections to the "global" positive conditioning node to the regional cond for (const edge of g.getEdgesTo(posCond)) { - console.log(edge); if (edge.destination.field !== 'prompt') { // Clone the edge, but change the destination node to the regional conditioning node const clone = deepClone(edge); From 5743254a417c3a3bc23a7c95e0d1cbf105dcd3c5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 17:06:30 +1000 Subject: [PATCH 111/442] fix(ui): use arrays for edge methods --- .../graph/addGenerationTabControlLayers.ts | 36 ++++++++----------- .../nodes/util/graph/addGenerationTabLoRAs.ts | 6 ++-- .../util/graph/addGenerationTabSeamless.ts | 4 +-- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts index 1afa21ba9f..88a9c0859b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -118,13 +118,11 @@ export const addGenerationTabControlLayers = async ( // Connect the conditioning to the collector g.addEdge(regionalPosCond, 'conditioning', posCondCollect, 'item'); // Copy the connections to the "global" positive conditioning node to the regional cond - for (const edge of g.getEdgesTo(posCond)) { - if (edge.destination.field !== 'prompt') { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCond.id; - g.addEdgeFromObj(clone); - } + for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCond.id; + g.addEdgeFromObj(clone); } } @@ -149,13 +147,11 @@ export const addGenerationTabControlLayers = async ( // Connect the conditioning to the collector g.addEdge(regionalNegCond, 'conditioning', negCondCollect, 'item'); // Copy the connections to the "global" negative conditioning node to the regional cond - for (const edge of g.getEdgesTo(negCond)) { - if (edge.destination.field !== 'prompt') { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalNegCond.id; - g.addEdgeFromObj(clone); - } + for (const edge of g.getEdgesTo(negCond, ['clip', 'mask'])) { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalNegCond.id; + g.addEdgeFromObj(clone); } } @@ -188,13 +184,11 @@ export const addGenerationTabControlLayers = async ( // Connect the conditioning to the negative collector g.addEdge(regionalPosCondInverted, 'conditioning', negCondCollect, 'item'); // Copy the connections to the "global" positive conditioning node to our regional node - for (const edge of g.getEdgesTo(posCond)) { - if (edge.destination.field !== 'prompt') { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCondInverted.id; - g.addEdgeFromObj(clone); - } + for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCondInverted.id; + g.addEdgeFromObj(clone); } } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts index 5a7173f2d5..6374c72a93 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts @@ -42,9 +42,9 @@ export const addGenerationTabLoRAs = ( g.addEdge(seamless ?? modelLoader, 'unet', loraCollectionLoader, 'unet'); g.addEdge(clipSkip, 'clip', loraCollectionLoader, 'clip'); // Reroute UNet & CLIP connections through the LoRA collection loader - g.deleteEdgesTo(denoise, 'unet'); - g.deleteEdgesTo(posCond, 'clip'); - g.deleteEdgesTo(negCond, 'clip'); + g.deleteEdgesTo(denoise, ['unet']); + g.deleteEdgesTo(posCond, ['clip']); + g.deleteEdgesTo(negCond, ['clip']); g.addEdge(loraCollectionLoader, 'unet', denoise, 'unet'); g.addEdge(loraCollectionLoader, 'clip', posCond, 'clip'); g.addEdge(loraCollectionLoader, 'clip', negCond, 'clip'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts index ef0b38291f..d0ecc96482 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts @@ -41,8 +41,8 @@ export const addGenerationTabSeamless = ( }); // Seamless slots into the graph between the model loader and the denoise node - g.deleteEdgesFrom(modelLoader, 'unet'); - g.deleteEdgesFrom(modelLoader, 'vae'); + g.deleteEdgesFrom(modelLoader, ['unet']); + g.deleteEdgesFrom(modelLoader, ['vae']); g.addEdge(modelLoader, 'unet', seamless, 'unet'); g.addEdge(vaeLoader ?? modelLoader, 'vae', seamless, 'vae'); From 39aa70963bc3a0503ca022ff36fe4ccd15a80cbc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 17:06:51 +1000 Subject: [PATCH 112/442] docs(ui): update docstrings for addGenerationTabSeamless --- .../features/nodes/util/graph/addGenerationTabSeamless.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts index d0ecc96482..709ba1416c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts @@ -8,12 +8,13 @@ import { SEAMLESS } from './constants'; /** * Adds the seamless node to the graph and connects it to the model loader and denoise node. * Because the seamless node may insert a VAE loader node between the model loader and itself, - * this function returns the terminal model loader node in the graph. + * future nodes should be connected to the return value of this function. * @param state The current Redux state * @param g The graph to add the seamless node to * @param denoise The denoise node in the graph * @param modelLoader The model loader node in the graph - * @returns The terminal model loader node in the graph + * @param vaeLoader The VAE loader node in the graph, if it exists + * @returns The seamless node, if it was added to the graph */ export const addGenerationTabSeamless = ( state: RootState, From 04d12a1e9860ce9800d1743e13e9f1a068cb2c87 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 17:07:06 +1000 Subject: [PATCH 113/442] feat(ui): add HRF graph builder helper --- .../nodes/util/graph/addGenerationTabHRF.ts | 170 ++++++++++++++++++ .../util/graph/buildGenerationTabGraph2.ts | 10 +- 2 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts new file mode 100644 index 0000000000..1156ffa022 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts @@ -0,0 +1,170 @@ +import type { RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; +import { roundToMultiple } from 'common/util/roundDownToMultiple'; +import type { Graph } from 'features/nodes/util/graph/Graph'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import type { Invocation } from 'services/api/types'; + +import { + DENOISE_LATENTS_HRF, + ESRGAN_HRF, + IMAGE_TO_LATENTS_HRF, + LATENTS_TO_IMAGE_HRF_HR, + LATENTS_TO_IMAGE_HRF_LR, + NOISE_HRF, + RESIZE_HRF, +} from './constants'; + +/** + * Calculates the new resolution for high-resolution features (HRF) based on base model type. + * Adjusts the width and height to maintain the aspect ratio and constrains them by the model's dimension limits, + * rounding down to the nearest multiple of 8. + * + * @param {number} optimalDimension The optimal dimension for the base model. + * @param {number} width The current width to be adjusted for HRF. + * @param {number} height The current height to be adjusted for HRF. + * @return {{newWidth: number, newHeight: number}} The new width and height, adjusted and rounded as needed. + */ +function calculateHrfRes( + optimalDimension: number, + width: number, + height: number +): { newWidth: number; newHeight: number } { + const aspect = width / height; + + const minDimension = Math.floor(optimalDimension * 0.5); + const modelArea = optimalDimension * optimalDimension; // Assuming square images for model_area + + let initWidth; + let initHeight; + + if (aspect > 1.0) { + initHeight = Math.max(minDimension, Math.sqrt(modelArea / aspect)); + initWidth = initHeight * aspect; + } else { + initWidth = Math.max(minDimension, Math.sqrt(modelArea * aspect)); + initHeight = initWidth / aspect; + } + // Cap initial height and width to final height and width. + initWidth = Math.min(width, initWidth); + initHeight = Math.min(height, initHeight); + + const newWidth = roundToMultiple(Math.floor(initWidth), 8); + const newHeight = roundToMultiple(Math.floor(initHeight), 8); + + return { newWidth, newHeight }; +} + +/** + * Adds HRF to the graph. + * @param state The root redux state + * @param g The graph to add HRF to + * @param denoise The denoise node + * @param noise The noise node + * @param l2i The l2i node + * @param vaeSource The VAE source node (may be a model loader, VAE loader, or seamless node) + * @returns + */ +export const addGenerationTabHRF = ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + noise: Invocation<'noise'>, + l2i: Invocation<'l2i'>, + vaeSource: Invocation<'vae_loader'> | Invocation<'main_model_loader'> | Invocation<'seamless'> +): void => { + if (!state.hrf.hrfEnabled || state.config.disabledSDFeatures.includes('hrf')) { + return; + } + + const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; + const { width, height } = state.controlLayers.present.size; + const optimalDimension = selectOptimalDimension(state); + const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height); + + // Change height and width of original noise node to initial resolution. + if (noise) { + noise.width = hrfWidth; + noise.height = hrfHeight; + } + + // Define new nodes and their connections, roughly in order of operations. + const l2iHrfLR = g.addNode({ type: 'l2i', id: LATENTS_TO_IMAGE_HRF_LR, fp32: l2i.fp32 }); + g.addEdge(denoise, 'latents', l2iHrfLR, 'latents'); + g.addEdge(vaeSource, 'vae', l2iHrfLR, 'vae'); + + const resizeHrf = g.addNode({ + id: RESIZE_HRF, + type: 'img_resize', + width: width, + height: height, + }); + + if (hrfMethod === 'ESRGAN') { + let model_name: Invocation<'esrgan'>['model_name'] = 'RealESRGAN_x2plus.pth'; + if ((width * height) / (hrfWidth * hrfHeight) > 2) { + model_name = 'RealESRGAN_x4plus.pth'; + } + const esrganHrf = g.addNode({ id: ESRGAN_HRF, type: 'esrgan', model_name }); + g.addEdge(l2iHrfLR, 'image', esrganHrf, 'image'); + g.addEdge(esrganHrf, 'image', resizeHrf, 'image'); + } else { + g.addEdge(l2iHrfLR, 'image', resizeHrf, 'image'); + } + + const noiseHrf = g.addNode({ + type: 'noise', + id: NOISE_HRF, + seed: noise.seed, + use_cpu: noise.use_cpu, + }); + g.addEdge(resizeHrf, 'height', noiseHrf, 'height'); + g.addEdge(resizeHrf, 'width', noiseHrf, 'width'); + + const i2lHrf = g.addNode({ type: 'i2l', id: IMAGE_TO_LATENTS_HRF }); + g.addEdge(vaeSource, 'vae', i2lHrf, 'vae'); + g.addEdge(resizeHrf, 'image', i2lHrf, 'image'); + + const denoiseHrf = g.addNode({ + type: 'denoise_latents', + id: DENOISE_LATENTS_HRF, + cfg_scale: denoise.cfg_scale, + scheduler: denoise.scheduler, + steps: denoise.steps, + denoising_start: 1 - hrfStrength, + denoising_end: 1, + }); + g.addEdge(i2lHrf, 'latents', denoiseHrf, 'latents'); + g.addEdge(noiseHrf, 'noise', denoiseHrf, 'noise'); + + // Copy edges to the original denoise into the new denoise + g.getEdgesTo(denoise, ['control', 'ip_adapter', 'unet', 'positive_conditioning', 'negative_conditioning']).forEach( + (edge) => { + const clone = deepClone(edge); + clone.destination.node_id = denoiseHrf.id; + g.addEdgeFromObj(clone); + } + ); + + // The original l2i node is unnecessary now, remove it + g.deleteNode(l2i.id); + + const l2iHrfHR = g.addNode({ + type: 'l2i', + id: LATENTS_TO_IMAGE_HRF_HR, + fp32: l2i.fp32, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), + }); + g.addEdge(vaeSource, 'vae', l2iHrfHR, 'vae'); + g.addEdge(denoiseHrf, 'latents', l2iHrfHR, 'latents'); + + MetadataUtil.add(g, { + hrf_strength: hrfStrength, + hrf_enabled: hrfEnabled, + hrf_method: hrfMethod, + }); + MetadataUtil.setMetadataReceivingNode(g, l2iHrfHR); +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts index 2735103215..6cc3465e71 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts @@ -3,6 +3,7 @@ import type { RootState } from 'app/store/store'; import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; +import { addGenerationTabHRF } from 'features/nodes/util/graph/addGenerationTabHRF'; import { addGenerationTabLoRAs } from 'features/nodes/util/graph/addGenerationTabLoRAs'; import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; import type { GraphType } from 'features/nodes/util/graph/Graph'; @@ -11,7 +12,6 @@ import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import { isNonRefinerMainModelConfig } from 'services/api/types'; -import { addHrfToGraph } from './addHrfToGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { @@ -150,6 +150,7 @@ export const buildGenerationTabGraph2 = async (state: RootState): Promise isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); - if (state.hrf.hrfEnabled && !shouldUseHRF) { - addHrfToGraph(state, graph); + const isHRFAllowed = !addedLayers.some((l) => isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); + if (isHRFAllowed) { + addGenerationTabHRF(state, g, denoise, noise, l2i, vaeSource); } // NSFW & watermark - must be last thing added to graph From 8d39520232636b5af8502413f50a9fbdf76d0726 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 18:21:55 +1000 Subject: [PATCH 114/442] feat(ui): port NSFW and watermark nodes to graph builder --- .../nodes/util/graph/addGenerationTabHRF.ts | 10 +++--- .../util/graph/addGenerationTabNSFWChecker.ts | 31 +++++++++++++++++++ .../util/graph/addGenerationTabWatermarker.ts | 31 +++++++++++++++++++ .../util/graph/buildGenerationTabGraph2.ts | 20 ++++++------ 4 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabNSFWChecker.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabWatermarker.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts index 1156ffa022..7d1b20d018 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts @@ -65,7 +65,7 @@ function calculateHrfRes( * @param noise The noise node * @param l2i The l2i node * @param vaeSource The VAE source node (may be a model loader, VAE loader, or seamless node) - * @returns + * @returns The HRF image output node. */ export const addGenerationTabHRF = ( state: RootState, @@ -74,11 +74,7 @@ export const addGenerationTabHRF = ( noise: Invocation<'noise'>, l2i: Invocation<'l2i'>, vaeSource: Invocation<'vae_loader'> | Invocation<'main_model_loader'> | Invocation<'seamless'> -): void => { - if (!state.hrf.hrfEnabled || state.config.disabledSDFeatures.includes('hrf')) { - return; - } - +): Invocation<'l2i'> => { const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; const { width, height } = state.controlLayers.present.size; const optimalDimension = selectOptimalDimension(state); @@ -167,4 +163,6 @@ export const addGenerationTabHRF = ( hrf_method: hrfMethod, }); MetadataUtil.setMetadataReceivingNode(g, l2iHrfHR); + + return l2iHrfHR; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabNSFWChecker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabNSFWChecker.ts new file mode 100644 index 0000000000..7781dec685 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabNSFWChecker.ts @@ -0,0 +1,31 @@ +import type { Graph } from 'features/nodes/util/graph/Graph'; +import type { Invocation } from 'services/api/types'; + +import { NSFW_CHECKER } from './constants'; + +/** + * Adds the NSFW checker to the output image + * @param g The graph + * @param imageOutput The current image output node + * @returns The nsfw checker node + */ +export const addGenerationTabNSFWChecker = ( + g: Graph, + imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> +): Invocation<'img_nsfw'> => { + const nsfw = g.addNode({ + id: NSFW_CHECKER, + type: 'img_nsfw', + is_intermediate: imageOutput.is_intermediate, + board: imageOutput.board, + use_cache: false, + }); + + imageOutput.is_intermediate = true; + imageOutput.use_cache = true; + imageOutput.board = undefined; + + g.addEdge(imageOutput, 'image', nsfw, 'image'); + + return nsfw; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabWatermarker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabWatermarker.ts new file mode 100644 index 0000000000..584f24b67d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabWatermarker.ts @@ -0,0 +1,31 @@ +import type { Graph } from 'features/nodes/util/graph/Graph'; +import type { Invocation } from 'services/api/types'; + +import { WATERMARKER } from './constants'; + +/** + * Adds a watermark to the output image + * @param g The graph + * @param imageOutput The image output node + * @returns The watermark node + */ +export const addGenerationTabWatermarker = ( + g: Graph, + imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> +): Invocation<'img_watermark'> => { + const watermark = g.addNode({ + id: WATERMARKER, + type: 'img_watermark', + is_intermediate: imageOutput.is_intermediate, + board: imageOutput.board, + use_cache: false, + }); + + imageOutput.is_intermediate = true; + imageOutput.use_cache = true; + imageOutput.board = undefined; + + g.addEdge(imageOutput, 'image', watermark, 'image'); + + return watermark; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts index 6cc3465e71..ff12b7cf67 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts @@ -5,15 +5,16 @@ import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetch import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; import { addGenerationTabHRF } from 'features/nodes/util/graph/addGenerationTabHRF'; import { addGenerationTabLoRAs } from 'features/nodes/util/graph/addGenerationTabLoRAs'; +import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/addGenerationTabNSFWChecker'; import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; +import { addGenerationTabWatermarker } from 'features/nodes/util/graph/addGenerationTabWatermarker'; import type { GraphType } from 'features/nodes/util/graph/Graph'; import { Graph } from 'features/nodes/util/graph/Graph'; import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { CLIP_SKIP, CONTROL_LAYERS_GRAPH, @@ -116,6 +117,8 @@ export const buildGenerationTabGraph2 = async (state: RootState): Promise | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; + g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); g.addEdge(clipSkip, 'clip', posCond, 'clip'); @@ -145,7 +148,6 @@ export const buildGenerationTabGraph2 = async (state: RootState): Promise isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); - if (isHRFAllowed) { - addGenerationTabHRF(state, g, denoise, noise, l2i, vaeSource); + if (isHRFAllowed && state.hrf.hrfEnabled) { + imageOutput = addGenerationTabHRF(state, g, denoise, noise, l2i, vaeSource); } - // NSFW & watermark - must be last thing added to graph if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph); + imageOutput = addGenerationTabNSFWChecker(g, imageOutput); } if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph); + imageOutput = addGenerationTabWatermarker(g, imageOutput); } + MetadataUtil.setMetadataReceivingNode(g, imageOutput); return g.getGraph(); }; From 5a4b050e66e5dd89a4c2bc65e3598b3a3d8bf330 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 18:50:18 +1000 Subject: [PATCH 115/442] feat(ui): use asserts in graph builder --- .../features/nodes/util/graph/buildGenerationTabGraph2.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts index ff12b7cf67..f8d6b1a543 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts @@ -1,4 +1,3 @@ -import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; @@ -14,6 +13,7 @@ import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; import { CLIP_SKIP, @@ -30,7 +30,6 @@ import { } from './constants'; import { getModelMetadataField } from './metadata'; -const log = logger('nodes'); export const buildGenerationTabGraph2 = async (state: RootState): Promise => { const { model, @@ -47,10 +46,7 @@ export const buildGenerationTabGraph2 = async (state: RootState): Promise Date: Mon, 13 May 2024 20:58:51 +1000 Subject: [PATCH 116/442] feat(ui): use graph builder for generation tab sdxl --- .../listeners/enqueueRequestedLinear.ts | 4 +- .../graph/addGenerationTabControlLayers.ts | 53 ++++-- .../util/graph/addGenerationTabSDXLLoRAs.ts | 75 ++++++++ .../util/graph/addGenerationTabSDXLRefiner.ts | 104 ++++++++++ .../graph/buildGenerationTabSDXLGraph2.ts | 178 ++++++++++++++++++ 5 files changed, 397 insertions(+), 17 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph2.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index bbb77c9ac5..a2d9f253a1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -2,7 +2,7 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { buildGenerationTabGraph2 } from 'features/nodes/util/graph/buildGenerationTabGraph2'; -import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph'; +import { buildGenerationTabSDXLGraph2 } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph2'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { queueApi } from 'services/api/endpoints/queue'; @@ -19,7 +19,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let graph; if (model && model.base === 'sdxl') { - graph = await buildGenerationTabSDXLGraph(state); + graph = await buildGenerationTabSDXLGraph2(state); } else { graph = await buildGenerationTabGraph2(state); } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts index 88a9c0859b..3c7c0c9c66 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -118,11 +118,20 @@ export const addGenerationTabControlLayers = async ( // Connect the conditioning to the collector g.addEdge(regionalPosCond, 'conditioning', posCondCollect, 'item'); // Copy the connections to the "global" positive conditioning node to the regional cond - for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCond.id; - g.addEdgeFromObj(clone); + if (posCond.type === 'compel') { + for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCond.id; + g.addEdgeFromObj(clone); + } + } else { + for (const edge of g.getEdgesTo(posCond, ['clip', 'clip2', 'mask'])) { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCond.id; + g.addEdgeFromObj(clone); + } } } @@ -147,11 +156,18 @@ export const addGenerationTabControlLayers = async ( // Connect the conditioning to the collector g.addEdge(regionalNegCond, 'conditioning', negCondCollect, 'item'); // Copy the connections to the "global" negative conditioning node to the regional cond - for (const edge of g.getEdgesTo(negCond, ['clip', 'mask'])) { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalNegCond.id; - g.addEdgeFromObj(clone); + if (negCond.type === 'compel') { + for (const edge of g.getEdgesTo(negCond, ['clip', 'mask'])) { + const clone = deepClone(edge); + clone.destination.node_id = regionalNegCond.id; + g.addEdgeFromObj(clone); + } + } else { + for (const edge of g.getEdgesTo(negCond, ['clip', 'clip2', 'mask'])) { + const clone = deepClone(edge); + clone.destination.node_id = regionalNegCond.id; + g.addEdgeFromObj(clone); + } } } @@ -184,11 +200,18 @@ export const addGenerationTabControlLayers = async ( // Connect the conditioning to the negative collector g.addEdge(regionalPosCondInverted, 'conditioning', negCondCollect, 'item'); // Copy the connections to the "global" positive conditioning node to our regional node - for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCondInverted.id; - g.addEdgeFromObj(clone); + if (posCond.type === 'compel') { + for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCondInverted.id; + g.addEdgeFromObj(clone); + } + } else { + for (const edge of g.getEdgesTo(posCond, ['clip', 'clip2', 'mask'])) { + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCondInverted.id; + g.addEdgeFromObj(clone); + } } } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts new file mode 100644 index 0000000000..89f1f8f18e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts @@ -0,0 +1,75 @@ +import type { RootState } from 'app/store/store'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import { filter, size } from 'lodash-es'; +import type { Invocation, S } from 'services/api/types'; + +import { LORA_LOADER } from './constants'; + +export const addGenerationTabSDXLLoRAs = ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + modelLoader: Invocation<'sdxl_model_loader'>, + seamless: Invocation<'seamless'> | null, + posCond: Invocation<'sdxl_compel_prompt'>, + negCond: Invocation<'sdxl_compel_prompt'> +): void => { + const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); + const loraCount = size(enabledLoRAs); + + if (loraCount === 0) { + return; + } + + const loraMetadata: S['LoRAMetadataField'][] = []; + + // We will collect LoRAs into a single collection node, then pass them to the LoRA collection loader, which applies + // each LoRA to the UNet and CLIP. + const loraCollector = g.addNode({ + id: `${LORA_LOADER}_collect`, + type: 'collect', + }); + const loraCollectionLoader = g.addNode({ + id: LORA_LOADER, + type: 'sdxl_lora_collection_loader', + }); + + g.addEdge(loraCollector, 'collection', loraCollectionLoader, 'loras'); + // Use seamless as UNet input if it exists, otherwise use the model loader + g.addEdge(seamless ?? modelLoader, 'unet', loraCollectionLoader, 'unet'); + g.addEdge(modelLoader, 'clip', loraCollectionLoader, 'clip'); + g.addEdge(modelLoader, 'clip2', loraCollectionLoader, 'clip2'); + // Reroute UNet & CLIP connections through the LoRA collection loader + g.deleteEdgesTo(denoise, ['unet']); + g.deleteEdgesTo(posCond, ['clip', 'clip2']); + g.deleteEdgesTo(negCond, ['clip', 'clip2']); + g.addEdge(loraCollectionLoader, 'unet', denoise, 'unet'); + g.addEdge(loraCollectionLoader, 'clip', posCond, 'clip'); + g.addEdge(loraCollectionLoader, 'clip', negCond, 'clip'); + g.addEdge(loraCollectionLoader, 'clip2', posCond, 'clip2'); + g.addEdge(loraCollectionLoader, 'clip2', negCond, 'clip2'); + + for (const lora of enabledLoRAs) { + const { weight } = lora; + const { key } = lora.model; + const parsedModel = zModelIdentifierField.parse(lora.model); + + const loraSelector = g.addNode({ + type: 'lora_selector', + id: `${LORA_LOADER}_${key}`, + lora: parsedModel, + weight, + }); + + loraMetadata.push({ + model: parsedModel, + weight, + }); + + g.addEdge(loraSelector, 'lora', loraCollector, 'item'); + } + + MetadataUtil.add(g, { loras: loraMetadata }); +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts new file mode 100644 index 0000000000..7c207b75bb --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts @@ -0,0 +1,104 @@ +import type { RootState } from 'app/store/store'; +import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; +import type { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import type { Invocation } from 'services/api/types'; +import { isRefinerMainModelModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +import { + SDXL_REFINER_DENOISE_LATENTS, + SDXL_REFINER_MODEL_LOADER, + SDXL_REFINER_NEGATIVE_CONDITIONING, + SDXL_REFINER_POSITIVE_CONDITIONING, + SDXL_REFINER_SEAMLESS, +} from './constants'; +import { getModelMetadataField } from './metadata'; + +export const addGenerationTabSDXLRefiner = async ( + state: RootState, + g: Graph, + denoise: Invocation<'denoise_latents'>, + modelLoader: Invocation<'sdxl_model_loader'>, + seamless: Invocation<'seamless'> | null, + posCond: Invocation<'sdxl_compel_prompt'>, + negCond: Invocation<'sdxl_compel_prompt'>, + l2i: Invocation<'l2i'> +): Promise => { + const { + refinerModel, + refinerPositiveAestheticScore, + refinerNegativeAestheticScore, + refinerSteps, + refinerScheduler, + refinerCFGScale, + refinerStart, + } = state.sdxl; + + assert(refinerModel, 'No refiner model found in state'); + + const modelConfig = await fetchModelConfigWithTypeGuard(refinerModel.key, isRefinerMainModelModelConfig); + + // We need to re-route latents to the refiner + g.deleteEdgesFrom(denoise, ['latents']); + // Latents will now come from refiner - delete edges to the l2i VAE decode + g.deleteEdgesTo(l2i, ['latents']); + + const refinerModelLoader = g.addNode({ + type: 'sdxl_refiner_model_loader', + id: SDXL_REFINER_MODEL_LOADER, + model: refinerModel, + }); + const refinerPosCond = g.addNode({ + type: 'sdxl_refiner_compel_prompt', + id: SDXL_REFINER_POSITIVE_CONDITIONING, + style: posCond.style, + aesthetic_score: refinerPositiveAestheticScore, + }); + const refinerNegCond = g.addNode({ + type: 'sdxl_refiner_compel_prompt', + id: SDXL_REFINER_NEGATIVE_CONDITIONING, + style: negCond.style, + aesthetic_score: refinerNegativeAestheticScore, + }); + const refinerDenoise = g.addNode({ + type: 'denoise_latents', + id: SDXL_REFINER_DENOISE_LATENTS, + cfg_scale: refinerCFGScale, + steps: refinerSteps, + scheduler: refinerScheduler, + denoising_start: refinerStart, + denoising_end: 1, + }); + + if (seamless) { + const refinerSeamless = g.addNode({ + id: SDXL_REFINER_SEAMLESS, + type: 'seamless', + seamless_x: seamless.seamless_x, + seamless_y: seamless.seamless_y, + }); + g.addEdge(refinerModelLoader, 'unet', refinerSeamless, 'unet'); + g.addEdge(refinerModelLoader, 'vae', refinerSeamless, 'vae'); + g.addEdge(refinerSeamless, 'unet', refinerDenoise, 'unet'); + } else { + g.addEdge(refinerModelLoader, 'unet', refinerDenoise, 'unet'); + } + + g.addEdge(refinerModelLoader, 'clip2', refinerPosCond, 'clip2'); + g.addEdge(refinerModelLoader, 'clip2', refinerNegCond, 'clip2'); + g.addEdge(refinerPosCond, 'conditioning', refinerDenoise, 'positive_conditioning'); + g.addEdge(refinerNegCond, 'conditioning', refinerDenoise, 'negative_conditioning'); + g.addEdge(denoise, 'latents', refinerDenoise, 'latents'); + g.addEdge(refinerDenoise, 'latents', l2i, 'latents'); + + MetadataUtil.add(g, { + refiner_model: getModelMetadataField(modelConfig), + refiner_positive_aesthetic_score: refinerPositiveAestheticScore, + refiner_negative_aesthetic_score: refinerNegativeAestheticScore, + refiner_cfg_scale: refinerCFGScale, + refiner_scheduler: refinerScheduler, + refiner_start: refinerStart, + refiner_steps: refinerSteps, + }); +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph2.ts new file mode 100644 index 0000000000..05fe3d1565 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph2.ts @@ -0,0 +1,178 @@ +import type { RootState } from 'app/store/store'; +import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; +import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; +import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/addGenerationTabNSFWChecker'; +import { addGenerationTabSDXLLoRAs } from 'features/nodes/util/graph/addGenerationTabSDXLLoRAs'; +import { addGenerationTabSDXLRefiner } from 'features/nodes/util/graph/addGenerationTabSDXLRefiner'; +import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; +import { addGenerationTabWatermarker } from 'features/nodes/util/graph/addGenerationTabWatermarker'; +import { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import type { Invocation, NonNullableGraph } from 'services/api/types'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +import { + LATENTS_TO_IMAGE, + NEGATIVE_CONDITIONING, + NEGATIVE_CONDITIONING_COLLECT, + NOISE, + POSITIVE_CONDITIONING, + POSITIVE_CONDITIONING_COLLECT, + SDXL_CONTROL_LAYERS_GRAPH, + SDXL_DENOISE_LATENTS, + SDXL_MODEL_LOADER, + VAE_LOADER, +} from './constants'; +import { getBoardField, getSDXLStylePrompts } from './graphBuilderUtils'; +import { getModelMetadataField } from './metadata'; + +export const buildGenerationTabSDXLGraph2 = async (state: RootState): Promise => { + const { + model, + cfgScale: cfg_scale, + cfgRescaleMultiplier: cfg_rescale_multiplier, + scheduler, + seed, + steps, + shouldUseCpuNoise, + vaePrecision, + vae, + } = state.generation; + const { positivePrompt, negativePrompt } = state.controlLayers.present; + const { width, height } = state.controlLayers.present.size; + + const { refinerModel, refinerStart } = state.sdxl; + + assert(model, 'No model found in state'); + + const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state); + + const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH); + const modelLoader = g.addNode({ + type: 'sdxl_model_loader', + id: SDXL_MODEL_LOADER, + model, + }); + const posCond = g.addNode({ + type: 'sdxl_compel_prompt', + id: POSITIVE_CONDITIONING, + prompt: positivePrompt, + style: positiveStylePrompt, + }); + const posCondCollect = g.addNode({ + type: 'collect', + id: POSITIVE_CONDITIONING_COLLECT, + }); + const negCond = g.addNode({ + type: 'sdxl_compel_prompt', + id: NEGATIVE_CONDITIONING, + prompt: negativePrompt, + style: negativeStylePrompt, + }); + const negCondCollect = g.addNode({ + type: 'collect', + id: NEGATIVE_CONDITIONING_COLLECT, + }); + const noise = g.addNode({ type: 'noise', id: NOISE, seed, width, height, use_cpu: shouldUseCpuNoise }); + const denoise = g.addNode({ + type: 'denoise_latents', + id: SDXL_DENOISE_LATENTS, + cfg_scale, + cfg_rescale_multiplier, + scheduler, + steps, + denoising_start: 0, + denoising_end: refinerModel ? refinerStart : 1, + }); + const l2i = g.addNode({ + type: 'l2i', + id: LATENTS_TO_IMAGE, + fp32: vaePrecision === 'fp32', + board: getBoardField(state), + // This is the terminal node and must always save to gallery. + is_intermediate: false, + use_cache: false, + }); + const vaeLoader = + vae?.base === model.base + ? g.addNode({ + type: 'vae_loader', + id: VAE_LOADER, + vae_model: vae, + }) + : null; + + let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; + + g.addEdge(modelLoader, 'unet', denoise, 'unet'); + g.addEdge(modelLoader, 'clip', posCond, 'clip'); + g.addEdge(modelLoader, 'clip', negCond, 'clip'); + g.addEdge(modelLoader, 'clip2', posCond, 'clip2'); + g.addEdge(modelLoader, 'clip2', negCond, 'clip2'); + g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); + g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); + g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); + g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning'); + g.addEdge(noise, 'noise', denoise, 'noise'); + g.addEdge(denoise, 'latents', l2i, 'latents'); + + const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); + + MetadataUtil.add(g, { + generation_mode: 'txt2img', + cfg_scale, + cfg_rescale_multiplier, + height, + width, + positive_prompt: positivePrompt, + negative_prompt: negativePrompt, + model: getModelMetadataField(modelConfig), + seed, + steps, + rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda', + scheduler, + positive_style_prompt: positiveStylePrompt, + negative_style_prompt: negativeStylePrompt, + vae: vae ?? undefined, + }); + g.validate(); + + const seamless = addGenerationTabSeamless(state, g, denoise, modelLoader, vaeLoader); + g.validate(); + + addGenerationTabSDXLLoRAs(state, g, denoise, modelLoader, seamless, posCond, negCond); + g.validate(); + + // We might get the VAE from the main model, custom VAE, or seamless node. + const vaeSource = seamless ?? vaeLoader ?? modelLoader; + g.addEdge(vaeSource, 'vae', l2i, 'vae'); + + // Add Refiner if enabled + if (refinerModel) { + await addGenerationTabSDXLRefiner(state, g, denoise, modelLoader, seamless, posCond, negCond, l2i); + } + + await addGenerationTabControlLayers( + state, + g, + denoise, + posCond, + negCond, + posCondCollect, + negCondCollect, + noise, + vaeSource + ); + + if (state.system.shouldUseNSFWChecker) { + imageOutput = addGenerationTabNSFWChecker(g, imageOutput); + } + + if (state.system.shouldUseWatermarker) { + imageOutput = addGenerationTabWatermarker(g, imageOutput); + } + + MetadataUtil.setMetadataReceivingNode(g, imageOutput); + return g.getGraph(); +}; From 4897ce2a130e2a86c2e39baeca2364d0a5d82654 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 21:01:57 +1000 Subject: [PATCH 117/442] tidy(ui): remove unused files --- .../listeners/enqueueRequestedLinear.ts | 10 +- .../util/graph/addControlLayersToGraph.ts | 705 ------------------ .../graph/addGenerationTabInitialImage.ts | 79 -- .../nodes/util/graph/addGenerationTabVAE.ts | 37 - .../nodes/util/graph/addHrfToGraph.ts | 356 --------- .../util/graph/buildGenerationTabGraph.ts | 364 ++++----- .../util/graph/buildGenerationTabGraph2.ts | 187 ----- .../util/graph/buildGenerationTabSDXLGraph.ts | 354 ++++----- .../graph/buildGenerationTabSDXLGraph2.ts | 178 ----- .../src/features/nodes/util/graph/metadata.ts | 15 - 10 files changed, 274 insertions(+), 2011 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabInitialImage.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabVAE.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph2.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index a2d9f253a1..195bb5639d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,8 +1,8 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import { buildGenerationTabGraph2 } from 'features/nodes/util/graph/buildGenerationTabGraph2'; -import { buildGenerationTabSDXLGraph2 } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph2'; +import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph'; +import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { queueApi } from 'services/api/endpoints/queue'; @@ -18,10 +18,10 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let graph; - if (model && model.base === 'sdxl') { - graph = await buildGenerationTabSDXLGraph2(state); + if (model?.base === 'sdxl') { + graph = await buildGenerationTabSDXLGraph(state); } else { - graph = await buildGenerationTabGraph2(state); + graph = await buildGenerationTabGraph(state); } const batchConfig = prepareLinearUIBatch(state, graph, prepend); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts deleted file mode 100644 index e48b9fb376..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ /dev/null @@ -1,705 +0,0 @@ -import { getStore } from 'app/store/nanostores/store'; -import type { RootState } from 'app/store/store'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isRegionalGuidanceLayer, - rgLayerMaskImageUploaded, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import type { - ControlNetConfigV2, - ImageWithDims, - IPAdapterConfigV2, - ProcessorConfig, - T2IAdapterConfigV2, -} from 'features/controlLayers/util/controlAdapters'; -import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; -import type { ImageField } from 'features/nodes/types/common'; -import { - CONTROL_NET_COLLECT, - IMAGE_TO_LATENTS, - IP_ADAPTER_COLLECT, - NEGATIVE_CONDITIONING, - NEGATIVE_CONDITIONING_COLLECT, - NOISE, - POSITIVE_CONDITIONING, - POSITIVE_CONDITIONING_COLLECT, - PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, - PROMPT_REGION_MASK_TO_TENSOR_PREFIX, - PROMPT_REGION_NEGATIVE_COND_PREFIX, - PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, - PROMPT_REGION_POSITIVE_COND_PREFIX, - RESIZE, - T2I_ADAPTER_COLLECT, -} from 'features/nodes/util/graph/constants'; -import { upsertMetadata } from 'features/nodes/util/graph/metadata'; -import { size } from 'lodash-es'; -import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; -import type { - BaseModelType, - CollectInvocation, - ControlNetInvocation, - Edge, - ImageDTO, - Invocation, - IPAdapterInvocation, - NonNullableGraph, - T2IAdapterInvocation, -} from 'services/api/types'; -import { assert } from 'tsafe'; - -export const addControlLayersToGraph = async ( - state: RootState, - graph: NonNullableGraph, - denoiseNodeId: string -): Promise => { - const mainModel = state.generation.model; - assert(mainModel, 'Missing main model when building graph'); - const isSDXL = mainModel.base === 'sdxl'; - - // Filter out layers with incompatible base model, missing control image - const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, mainModel.base)); - - const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); - for (const ca of validControlAdapters) { - addGlobalControlAdapterToGraph(ca, graph, denoiseNodeId); - } - - const validIPAdapters = validLayers.filter(isIPAdapterLayer).map((l) => l.ipAdapter); - for (const ipAdapter of validIPAdapters) { - addGlobalIPAdapterToGraph(ipAdapter, graph, denoiseNodeId); - } - - const initialImageLayers = validLayers.filter(isInitialImageLayer); - assert(initialImageLayers.length <= 1, 'Only one initial image layer allowed'); - if (initialImageLayers[0]) { - addInitialImageLayerToGraph(state, graph, denoiseNodeId, initialImageLayers[0]); - } - // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing - // the existing conditioning nodes. - - // With regional prompts we have multiple conditioning nodes which much be routed into collectors. Set those up - const posCondCollectNode: CollectInvocation = { - id: POSITIVE_CONDITIONING_COLLECT, - type: 'collect', - }; - graph.nodes[POSITIVE_CONDITIONING_COLLECT] = posCondCollectNode; - const negCondCollectNode: CollectInvocation = { - id: NEGATIVE_CONDITIONING_COLLECT, - type: 'collect', - }; - graph.nodes[NEGATIVE_CONDITIONING_COLLECT] = negCondCollectNode; - - // Re-route the denoise node's OG conditioning inputs to the collect nodes - const newEdges: Edge[] = []; - for (const edge of graph.edges) { - if (edge.destination.node_id === denoiseNodeId && edge.destination.field === 'positive_conditioning') { - newEdges.push({ - source: edge.source, - destination: { - node_id: POSITIVE_CONDITIONING_COLLECT, - field: 'item', - }, - }); - } else if (edge.destination.node_id === denoiseNodeId && edge.destination.field === 'negative_conditioning') { - newEdges.push({ - source: edge.source, - destination: { - node_id: NEGATIVE_CONDITIONING_COLLECT, - field: 'item', - }, - }); - } else { - newEdges.push(edge); - } - } - graph.edges = newEdges; - - // Connect collectors to the denoise nodes - must happen _after_ rerouting else you get cycles - graph.edges.push({ - source: { - node_id: POSITIVE_CONDITIONING_COLLECT, - field: 'collection', - }, - destination: { - node_id: denoiseNodeId, - field: 'positive_conditioning', - }, - }); - graph.edges.push({ - source: { - node_id: NEGATIVE_CONDITIONING_COLLECT, - field: 'collection', - }, - destination: { - node_id: denoiseNodeId, - field: 'negative_conditioning', - }, - }); - - const validRGLayers = validLayers.filter(isRegionalGuidanceLayer); - const layerIds = validRGLayers.map((l) => l.id); - const blobs = await getRegionalPromptLayerBlobs(layerIds); - assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); - - for (const layer of validRGLayers) { - const blob = blobs[layer.id]; - assert(blob, `Blob for layer ${layer.id} not found`); - // Upload the mask image, or get the cached image if it exists - const { image_name } = await getMaskImage(layer, blob); - - // The main mask-to-tensor node - const maskToTensorNode: Invocation<'alpha_mask_to_tensor'> = { - id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${layer.id}`, - type: 'alpha_mask_to_tensor', - image: { - image_name, - }, - }; - graph.nodes[maskToTensorNode.id] = maskToTensorNode; - - if (layer.positivePrompt) { - // The main positive conditioning node - const regionalPositiveCondNode: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'> = isSDXL - ? { - type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - style: layer.positivePrompt, // TODO: Should we put the positive prompt in both fields? - } - : { - type: 'compel', - id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - }; - graph.nodes[regionalPositiveCondNode.id] = regionalPositiveCondNode; - - // Connect the mask to the conditioning - graph.edges.push({ - source: { node_id: maskToTensorNode.id, field: 'mask' }, - destination: { node_id: regionalPositiveCondNode.id, field: 'mask' }, - }); - - // Connect the conditioning to the collector - graph.edges.push({ - source: { node_id: regionalPositiveCondNode.id, field: 'conditioning' }, - destination: { node_id: posCondCollectNode.id, field: 'item' }, - }); - - // Copy the connections to the "global" positive conditioning node to the regional cond - for (const edge of graph.edges) { - if (edge.destination.node_id === POSITIVE_CONDITIONING && edge.destination.field !== 'prompt') { - graph.edges.push({ - source: edge.source, - destination: { node_id: regionalPositiveCondNode.id, field: edge.destination.field }, - }); - } - } - } - - if (layer.negativePrompt) { - // The main negative conditioning node - const regionalNegativeCondNode: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'> = isSDXL - ? { - type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.negativePrompt, - style: layer.negativePrompt, - } - : { - type: 'compel', - id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.negativePrompt, - }; - graph.nodes[regionalNegativeCondNode.id] = regionalNegativeCondNode; - - // Connect the mask to the conditioning - graph.edges.push({ - source: { node_id: maskToTensorNode.id, field: 'mask' }, - destination: { node_id: regionalNegativeCondNode.id, field: 'mask' }, - }); - - // Connect the conditioning to the collector - graph.edges.push({ - source: { node_id: regionalNegativeCondNode.id, field: 'conditioning' }, - destination: { node_id: negCondCollectNode.id, field: 'item' }, - }); - - // Copy the connections to the "global" negative conditioning node to the regional cond - for (const edge of graph.edges) { - if (edge.destination.node_id === NEGATIVE_CONDITIONING && edge.destination.field !== 'prompt') { - graph.edges.push({ - source: edge.source, - destination: { node_id: regionalNegativeCondNode.id, field: edge.destination.field }, - }); - } - } - } - - // If we are using the "invert" auto-negative setting, we need to add an additional negative conditioning node - if (layer.autoNegative === 'invert' && layer.positivePrompt) { - // We re-use the mask image, but invert it when converting to tensor - const invertTensorMaskNode: Invocation<'invert_tensor_mask'> = { - id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${layer.id}`, - type: 'invert_tensor_mask', - }; - graph.nodes[invertTensorMaskNode.id] = invertTensorMaskNode; - - // Connect the OG mask image to the inverted mask-to-tensor node - graph.edges.push({ - source: { - node_id: maskToTensorNode.id, - field: 'mask', - }, - destination: { - node_id: invertTensorMaskNode.id, - field: 'mask', - }, - }); - - // Create the conditioning node. It's going to be connected to the negative cond collector, but it uses the - // positive prompt - const regionalPositiveCondInvertedNode: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'> = isSDXL - ? { - type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - style: layer.positivePrompt, - } - : { - type: 'compel', - id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - }; - graph.nodes[regionalPositiveCondInvertedNode.id] = regionalPositiveCondInvertedNode; - // Connect the inverted mask to the conditioning - graph.edges.push({ - source: { node_id: invertTensorMaskNode.id, field: 'mask' }, - destination: { node_id: regionalPositiveCondInvertedNode.id, field: 'mask' }, - }); - // Connect the conditioning to the negative collector - graph.edges.push({ - source: { node_id: regionalPositiveCondInvertedNode.id, field: 'conditioning' }, - destination: { node_id: negCondCollectNode.id, field: 'item' }, - }); - // Copy the connections to the "global" positive conditioning node to our regional node - for (const edge of graph.edges) { - if (edge.destination.node_id === POSITIVE_CONDITIONING && edge.destination.field !== 'prompt') { - graph.edges.push({ - source: edge.source, - destination: { node_id: regionalPositiveCondInvertedNode.id, field: edge.destination.field }, - }); - } - } - } - - const validRegionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipa) => - isValidIPAdapter(ipa, mainModel.base) - ); - - for (const ipAdapter of validRegionalIPAdapters) { - addIPAdapterCollectorSafe(graph, denoiseNodeId); - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; - assert(model, 'IP Adapter model is required'); - assert(image, 'IP Adapter image is required'); - - const ipAdapterNode: IPAdapterInvocation = { - id: `ip_adapter_${id}`, - type: 'ip_adapter', - is_intermediate: true, - weight, - method, - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.name, - }, - }; - - graph.nodes[ipAdapterNode.id] = ipAdapterNode; - - // Connect the mask to the conditioning - graph.edges.push({ - source: { node_id: maskToTensorNode.id, field: 'mask' }, - destination: { node_id: ipAdapterNode.id, field: 'mask' }, - }); - - graph.edges.push({ - source: { node_id: ipAdapterNode.id, field: 'ip_adapter' }, - destination: { - node_id: IP_ADAPTER_COLLECT, - field: 'item', - }, - }); - } - } - - upsertMetadata(graph, { control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); - return validLayers; -}; - -const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { - if (layer.uploadedMaskImage) { - const imageDTO = await getImageDTO(layer.uploadedMaskImage.name); - if (imageDTO) { - return imageDTO; - } - } - const { dispatch } = getStore(); - // No cached mask, or the cached image no longer exists - we need to upload the mask image - const file = new File([blob], `${layer.id}_mask.png`, { type: 'image/png' }); - const req = dispatch( - imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) - ); - req.reset(); - - const imageDTO = await req.unwrap(); - dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO })); - return imageDTO; -}; - -const buildControlImage = ( - image: ImageWithDims | null, - processedImage: ImageWithDims | null, - processorConfig: ProcessorConfig | null -): ImageField => { - if (processedImage && processorConfig) { - // We've processed the image in the app - use it for the control image. - return { - image_name: processedImage.name, - }; - } else if (image) { - // No processor selected, and we have an image - the user provided a processed image, use it for the control image. - return { - image_name: image.name, - }; - } - assert(false, 'Attempted to add unprocessed control image'); -}; - -const addGlobalControlAdapterToGraph = ( - controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2, - graph: NonNullableGraph, - denoiseNodeId: string -) => { - if (controlAdapter.type === 'controlnet') { - addGlobalControlNetToGraph(controlAdapter, graph, denoiseNodeId); - } - if (controlAdapter.type === 't2i_adapter') { - addGlobalT2IAdapterToGraph(controlAdapter, graph, denoiseNodeId); - } -}; - -const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { - if (graph.nodes[CONTROL_NET_COLLECT]) { - // You see, we've already got one! - return; - } - // Add the ControlNet collector - const controlNetIterateNode: CollectInvocation = { - id: CONTROL_NET_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode; - graph.edges.push({ - source: { node_id: CONTROL_NET_COLLECT, field: 'collection' }, - destination: { - node_id: denoiseNodeId, - field: 'control', - }, - }); -}; - -const addGlobalControlNetToGraph = (controlNet: ControlNetConfigV2, graph: NonNullableGraph, denoiseNodeId: string) => { - const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; - assert(model, 'ControlNet model is required'); - const controlImage = buildControlImage(image, processedImage, processorConfig); - addControlNetCollectorSafe(graph, denoiseNodeId); - - const controlNetNode: ControlNetInvocation = { - id: `control_net_${id}`, - type: 'controlnet', - is_intermediate: true, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - control_mode: controlMode, - resize_mode: 'just_resize', - control_model: model, - control_weight: weight, - image: controlImage, - }; - - graph.nodes[controlNetNode.id] = controlNetNode; - - graph.edges.push({ - source: { node_id: controlNetNode.id, field: 'control' }, - destination: { - node_id: CONTROL_NET_COLLECT, - field: 'item', - }, - }); -}; - -const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { - if (graph.nodes[T2I_ADAPTER_COLLECT]) { - // You see, we've already got one! - return; - } - // Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect - const t2iAdapterCollectNode: CollectInvocation = { - id: T2I_ADAPTER_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[T2I_ADAPTER_COLLECT] = t2iAdapterCollectNode; - graph.edges.push({ - source: { node_id: T2I_ADAPTER_COLLECT, field: 'collection' }, - destination: { - node_id: denoiseNodeId, - field: 't2i_adapter', - }, - }); -}; - -const addGlobalT2IAdapterToGraph = (t2iAdapter: T2IAdapterConfigV2, graph: NonNullableGraph, denoiseNodeId: string) => { - const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; - assert(model, 'T2I Adapter model is required'); - const controlImage = buildControlImage(image, processedImage, processorConfig); - addT2IAdapterCollectorSafe(graph, denoiseNodeId); - - const t2iAdapterNode: T2IAdapterInvocation = { - id: `t2i_adapter_${id}`, - type: 't2i_adapter', - is_intermediate: true, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - resize_mode: 'just_resize', - t2i_adapter_model: model, - weight: weight, - image: controlImage, - }; - - graph.nodes[t2iAdapterNode.id] = t2iAdapterNode; - - graph.edges.push({ - source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' }, - destination: { - node_id: T2I_ADAPTER_COLLECT, - field: 'item', - }, - }); -}; - -const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { - if (graph.nodes[IP_ADAPTER_COLLECT]) { - // You see, we've already got one! - return; - } - - const ipAdapterCollectNode: CollectInvocation = { - id: IP_ADAPTER_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode; - graph.edges.push({ - source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' }, - destination: { - node_id: denoiseNodeId, - field: 'ip_adapter', - }, - }); -}; - -const addGlobalIPAdapterToGraph = (ipAdapter: IPAdapterConfigV2, graph: NonNullableGraph, denoiseNodeId: string) => { - addIPAdapterCollectorSafe(graph, denoiseNodeId); - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; - assert(image, 'IP Adapter image is required'); - assert(model, 'IP Adapter model is required'); - - const ipAdapterNode: IPAdapterInvocation = { - id: `ip_adapter_${id}`, - type: 'ip_adapter', - is_intermediate: true, - weight, - method, - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.name, - }, - }; - - graph.nodes[ipAdapterNode.id] = ipAdapterNode; - - graph.edges.push({ - source: { node_id: ipAdapterNode.id, field: 'ip_adapter' }, - destination: { - node_id: IP_ADAPTER_COLLECT, - field: 'item', - }, - }); -}; - -const addInitialImageLayerToGraph = ( - state: RootState, - graph: NonNullableGraph, - denoiseNodeId: string, - layer: InitialImageLayer -) => { - const { vaePrecision, model } = state.generation; - const { refinerModel, refinerStart } = state.sdxl; - const { width, height } = state.controlLayers.present.size; - assert(layer.isEnabled, 'Initial image layer is not enabled'); - assert(layer.image, 'Initial image layer has no image'); - - const isSDXL = model?.base === 'sdxl'; - const useRefinerStartEnd = isSDXL && Boolean(refinerModel); - - const denoiseNode = graph.nodes[denoiseNodeId]; - assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`); - - const { denoisingStrength } = layer; - denoiseNode.denoising_start = useRefinerStartEnd - ? Math.min(refinerStart, 1 - denoisingStrength) - : 1 - denoisingStrength; - denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1; - - const i2lNode: Invocation<'i2l'> = { - type: 'i2l', - id: IMAGE_TO_LATENTS, - is_intermediate: true, - use_cache: true, - fp32: vaePrecision === 'fp32', - }; - - graph.nodes[i2lNode.id] = i2lNode; - graph.edges.push({ - source: { - node_id: IMAGE_TO_LATENTS, - field: 'latents', - }, - destination: { - node_id: denoiseNode.id, - field: 'latents', - }, - }); - - if (layer.image.width !== width || layer.image.height !== height) { - // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` - - // Create a resize node, explicitly setting its image - const resizeNode: Invocation<'img_resize'> = { - id: RESIZE, - type: 'img_resize', - image: { - image_name: layer.image.name, - }, - is_intermediate: true, - width, - height, - }; - - graph.nodes[RESIZE] = resizeNode; - - // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` - graph.edges.push({ - source: { node_id: RESIZE, field: 'image' }, - destination: { - node_id: IMAGE_TO_LATENTS, - field: 'image', - }, - }); - - // The `RESIZE` node also passes its width and height to `NOISE` - graph.edges.push({ - source: { node_id: RESIZE, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - - graph.edges.push({ - source: { node_id: RESIZE, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } else { - // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly - i2lNode.image = { - image_name: layer.image.name, - }; - - // Pass the image's dimensions to the `NOISE` node - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } - - upsertMetadata(graph, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); -}; - -const isValidControlAdapter = (ca: ControlNetConfigV2 | T2IAdapterConfigV2, base: BaseModelType): boolean => { - // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === base; - const hasControlImage = Boolean(ca.image || (ca.processedImage && ca.processorConfig)); - return hasModel && modelMatchesBase && hasControlImage; -}; - -const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean => { - // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ipa.model); - const modelMatchesBase = ipa.model?.base === base; - const hasImage = Boolean(ipa.image); - return hasModel && modelMatchesBase && hasImage; -}; - -const isValidLayer = (layer: Layer, base: BaseModelType) => { - if (!layer.isEnabled) { - return false; - } - if (isControlAdapterLayer(layer)) { - return isValidControlAdapter(layer.controlAdapter, base); - } - if (isIPAdapterLayer(layer)) { - return isValidIPAdapter(layer.ipAdapter, base); - } - if (isInitialImageLayer(layer)) { - if (!layer.image) { - return false; - } - return true; - } - if (isRegionalGuidanceLayer(layer)) { - if (layer.maskObjects.length === 0) { - // Layer has no mask, meaning any guidance would be applied to an empty region. - return false; - } - const hasTextPrompt = Boolean(layer.positivePrompt) || Boolean(layer.negativePrompt); - const hasIPAdapter = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; - return hasTextPrompt || hasIPAdapter; - } - return false; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabInitialImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabInitialImage.ts deleted file mode 100644 index 3a6b124b30..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabInitialImage.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { isInitialImageLayer } from 'features/controlLayers/store/controlLayersSlice'; -import type { ImageField } from 'features/nodes/types/common'; -import type { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; -import type { Invocation } from 'services/api/types'; - -import { IMAGE_TO_LATENTS, RESIZE } from './constants'; - -/** - * Adds the initial image to the graph and connects it to the denoise and noise nodes. - * @param state The current Redux state - * @param g The graph to add the initial image to - * @param denoise The denoise node in the graph - * @param noise The noise node in the graph - * @returns Whether the initial image was added to the graph - */ -export const addGenerationTabInitialImage = ( - state: RootState, - g: Graph, - denoise: Invocation<'denoise_latents'>, - noise: Invocation<'noise'> -): Invocation<'i2l'> | null => { - // Remove Existing UNet Connections - const { img2imgStrength, vaePrecision, model } = state.generation; - const { refinerModel, refinerStart } = state.sdxl; - const { width, height } = state.controlLayers.present.size; - const initialImageLayer = state.controlLayers.present.layers.find(isInitialImageLayer); - const initialImage = initialImageLayer?.isEnabled ? initialImageLayer?.image : null; - - if (!initialImage) { - return null; - } - - const isSDXL = model?.base === 'sdxl'; - const useRefinerStartEnd = isSDXL && Boolean(refinerModel); - const image: ImageField = { - image_name: initialImage.imageName, - }; - - denoise.denoising_start = useRefinerStartEnd ? Math.min(refinerStart, 1 - img2imgStrength) : 1 - img2imgStrength; - denoise.denoising_end = useRefinerStartEnd ? refinerStart : 1; - - const i2l = g.addNode({ - type: 'i2l', - id: IMAGE_TO_LATENTS, - fp32: vaePrecision === 'fp32', - }); - g.addEdge(i2l, 'latents', denoise, 'latents'); - - if (initialImage.width !== width || initialImage.height !== height) { - // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` - const resize = g.addNode({ - id: RESIZE, - type: 'img_resize', - image, - width, - height, - }); - // The `RESIZE` node then passes its image, to `IMAGE_TO_LATENTS` - g.addEdge(resize, 'image', i2l, 'image'); - // The `RESIZE` node also passes its width and height to `NOISE` - g.addEdge(resize, 'width', noise, 'width'); - g.addEdge(resize, 'height', noise, 'height'); - } else { - // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly - i2l.image = image; - g.addEdge(i2l, 'width', noise, 'width'); - g.addEdge(i2l, 'height', noise, 'height'); - } - - MetadataUtil.add(g, { - generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img', - strength: img2imgStrength, - init_image: initialImage.imageName, - }); - - return i2l; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabVAE.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabVAE.ts deleted file mode 100644 index 037924d5cb..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabVAE.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { RootState } from 'app/store/store'; -import type { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; -import type { Invocation } from 'services/api/types'; - -import { VAE_LOADER } from './constants'; - -export const addGenerationTabVAE = ( - state: RootState, - g: Graph, - modelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'>, - l2i: Invocation<'l2i'>, - i2l: Invocation<'i2l'> | null, - seamless: Invocation<'seamless'> | null -): void => { - const { vae } = state.generation; - - // The seamless helper also adds the VAE loader... so we need to check if it's already there - const shouldAddVAELoader = !g.hasNode(VAE_LOADER) && vae; - const vaeLoader = shouldAddVAELoader - ? g.addNode({ - type: 'vae_loader', - id: VAE_LOADER, - vae_model: vae, - }) - : null; - - const vaeSource = seamless ? seamless : vaeLoader ? vaeLoader : modelLoader; - g.addEdge(vaeSource, 'vae', l2i, 'vae'); - if (i2l) { - g.addEdge(vaeSource, 'vae', i2l, 'vae'); - } - - if (vae) { - MetadataUtil.add(g, { vae }); - } -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts deleted file mode 100644 index d6709f7058..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { roundToMultiple } from 'common/util/roundDownToMultiple'; -import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import type { - DenoiseLatentsInvocation, - Edge, - ESRGANInvocation, - LatentsToImageInvocation, - NoiseInvocation, - NonNullableGraph, -} from 'services/api/types'; - -import { - DENOISE_LATENTS, - DENOISE_LATENTS_HRF, - ESRGAN_HRF, - IMAGE_TO_LATENTS_HRF, - LATENTS_TO_IMAGE, - LATENTS_TO_IMAGE_HRF_HR, - LATENTS_TO_IMAGE_HRF_LR, - MAIN_MODEL_LOADER, - NOISE, - NOISE_HRF, - RESIZE_HRF, - SEAMLESS, - VAE_LOADER, -} from './constants'; -import { setMetadataReceivingNode, upsertMetadata } from './metadata'; - -// Copy certain connections from previous DENOISE_LATENTS to new DENOISE_LATENTS_HRF. -function copyConnectionsToDenoiseLatentsHrf(graph: NonNullableGraph): void { - const destinationFields = [ - 'control', - 'ip_adapter', - 'metadata', - 'unet', - 'positive_conditioning', - 'negative_conditioning', - ]; - const newEdges: Edge[] = []; - - // Loop through the existing edges connected to DENOISE_LATENTS - graph.edges.forEach((edge: Edge) => { - if (edge.destination.node_id === DENOISE_LATENTS && destinationFields.includes(edge.destination.field)) { - // Add a similar connection to DENOISE_LATENTS_HRF - newEdges.push({ - source: { - node_id: edge.source.node_id, - field: edge.source.field, - }, - destination: { - node_id: DENOISE_LATENTS_HRF, - field: edge.destination.field, - }, - }); - } - }); - graph.edges = graph.edges.concat(newEdges); -} - -/** - * Calculates the new resolution for high-resolution features (HRF) based on base model type. - * Adjusts the width and height to maintain the aspect ratio and constrains them by the model's dimension limits, - * rounding down to the nearest multiple of 8. - * - * @param {number} optimalDimension The optimal dimension for the base model. - * @param {number} width The current width to be adjusted for HRF. - * @param {number} height The current height to be adjusted for HRF. - * @return {{newWidth: number, newHeight: number}} The new width and height, adjusted and rounded as needed. - */ -function calculateHrfRes( - optimalDimension: number, - width: number, - height: number -): { newWidth: number; newHeight: number } { - const aspect = width / height; - - const minDimension = Math.floor(optimalDimension * 0.5); - const modelArea = optimalDimension * optimalDimension; // Assuming square images for model_area - - let initWidth; - let initHeight; - - if (aspect > 1.0) { - initHeight = Math.max(minDimension, Math.sqrt(modelArea / aspect)); - initWidth = initHeight * aspect; - } else { - initWidth = Math.max(minDimension, Math.sqrt(modelArea * aspect)); - initHeight = initWidth / aspect; - } - // Cap initial height and width to final height and width. - initWidth = Math.min(width, initWidth); - initHeight = Math.min(height, initHeight); - - const newWidth = roundToMultiple(Math.floor(initWidth), 8); - const newHeight = roundToMultiple(Math.floor(initHeight), 8); - - return { newWidth, newHeight }; -} - -// Adds the high-res fix feature to the given graph. -export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void => { - // Double check hrf is enabled. - if (!state.hrf.hrfEnabled || state.config.disabledSDFeatures.includes('hrf')) { - return; - } - const log = logger('generation'); - - const { vae, seamlessXAxis, seamlessYAxis } = state.generation; - const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; - const { width, height } = state.controlLayers.present.size; - const isAutoVae = !vae; - const isSeamlessEnabled = seamlessXAxis || seamlessYAxis; - const optimalDimension = selectOptimalDimension(state); - const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height); - - // Pre-existing (original) graph nodes. - const originalDenoiseLatentsNode = graph.nodes[DENOISE_LATENTS] as DenoiseLatentsInvocation | undefined; - const originalNoiseNode = graph.nodes[NOISE] as NoiseInvocation | undefined; - const originalLatentsToImageNode = graph.nodes[LATENTS_TO_IMAGE] as LatentsToImageInvocation | undefined; - if (!originalDenoiseLatentsNode) { - log.error('originalDenoiseLatentsNode is undefined'); - return; - } - if (!originalNoiseNode) { - log.error('originalNoiseNode is undefined'); - return; - } - if (!originalLatentsToImageNode) { - log.error('originalLatentsToImageNode is undefined'); - return; - } - - // Change height and width of original noise node to initial resolution. - if (originalNoiseNode) { - originalNoiseNode.width = hrfWidth; - originalNoiseNode.height = hrfHeight; - } - - // Define new nodes and their connections, roughly in order of operations. - graph.nodes[LATENTS_TO_IMAGE_HRF_LR] = { - type: 'l2i', - id: LATENTS_TO_IMAGE_HRF_LR, - fp32: originalLatentsToImageNode?.fp32, - is_intermediate: true, - }; - graph.edges.push( - { - source: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE_HRF_LR, - field: 'latents', - }, - }, - { - source: { - node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: LATENTS_TO_IMAGE_HRF_LR, - field: 'vae', - }, - } - ); - - graph.nodes[RESIZE_HRF] = { - id: RESIZE_HRF, - type: 'img_resize', - is_intermediate: true, - width: width, - height: height, - }; - if (hrfMethod === 'ESRGAN') { - let model_name: ESRGANInvocation['model_name'] = 'RealESRGAN_x2plus.pth'; - if ((width * height) / (hrfWidth * hrfHeight) > 2) { - model_name = 'RealESRGAN_x4plus.pth'; - } - graph.nodes[ESRGAN_HRF] = { - id: ESRGAN_HRF, - type: 'esrgan', - model_name, - is_intermediate: true, - }; - graph.edges.push( - { - source: { - node_id: LATENTS_TO_IMAGE_HRF_LR, - field: 'image', - }, - destination: { - node_id: ESRGAN_HRF, - field: 'image', - }, - }, - { - source: { - node_id: ESRGAN_HRF, - field: 'image', - }, - destination: { - node_id: RESIZE_HRF, - field: 'image', - }, - } - ); - } else { - graph.edges.push({ - source: { - node_id: LATENTS_TO_IMAGE_HRF_LR, - field: 'image', - }, - destination: { - node_id: RESIZE_HRF, - field: 'image', - }, - }); - } - - graph.nodes[NOISE_HRF] = { - type: 'noise', - id: NOISE_HRF, - seed: originalNoiseNode?.seed, - use_cpu: originalNoiseNode?.use_cpu, - is_intermediate: true, - }; - graph.edges.push( - { - source: { - node_id: RESIZE_HRF, - field: 'height', - }, - destination: { - node_id: NOISE_HRF, - field: 'height', - }, - }, - { - source: { - node_id: RESIZE_HRF, - field: 'width', - }, - destination: { - node_id: NOISE_HRF, - field: 'width', - }, - } - ); - - graph.nodes[IMAGE_TO_LATENTS_HRF] = { - type: 'i2l', - id: IMAGE_TO_LATENTS_HRF, - is_intermediate: true, - }; - graph.edges.push( - { - source: { - node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: IMAGE_TO_LATENTS_HRF, - field: 'vae', - }, - }, - { - source: { - node_id: RESIZE_HRF, - field: 'image', - }, - destination: { - node_id: IMAGE_TO_LATENTS_HRF, - field: 'image', - }, - } - ); - - graph.nodes[DENOISE_LATENTS_HRF] = { - type: 'denoise_latents', - id: DENOISE_LATENTS_HRF, - is_intermediate: true, - cfg_scale: originalDenoiseLatentsNode?.cfg_scale, - scheduler: originalDenoiseLatentsNode?.scheduler, - steps: originalDenoiseLatentsNode?.steps, - denoising_start: 1 - hrfStrength, - denoising_end: 1, - }; - graph.edges.push( - { - source: { - node_id: IMAGE_TO_LATENTS_HRF, - field: 'latents', - }, - destination: { - node_id: DENOISE_LATENTS_HRF, - field: 'latents', - }, - }, - { - source: { - node_id: NOISE_HRF, - field: 'noise', - }, - destination: { - node_id: DENOISE_LATENTS_HRF, - field: 'noise', - }, - } - ); - copyConnectionsToDenoiseLatentsHrf(graph); - - // The original l2i node is unnecessary now, remove it - graph.edges = graph.edges.filter((edge) => edge.destination.node_id !== LATENTS_TO_IMAGE); - delete graph.nodes[LATENTS_TO_IMAGE]; - - graph.nodes[LATENTS_TO_IMAGE_HRF_HR] = { - type: 'l2i', - id: LATENTS_TO_IMAGE_HRF_HR, - fp32: originalLatentsToImageNode?.fp32, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - }; - graph.edges.push( - { - source: { - node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: LATENTS_TO_IMAGE_HRF_HR, - field: 'vae', - }, - }, - { - source: { - node_id: DENOISE_LATENTS_HRF, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE_HRF_HR, - field: 'latents', - }, - } - ); - upsertMetadata(graph, { - hrf_strength: hrfStrength, - hrf_enabled: hrfEnabled, - hrf_method: hrfMethod, - }); - setMetadataReceivingNode(graph, LATENTS_TO_IMAGE_HRF_HR); -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts index 2a6946708f..a50e722e43 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts @@ -1,17 +1,20 @@ -import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; -import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; +import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; +import { addGenerationTabHRF } from 'features/nodes/util/graph/addGenerationTabHRF'; +import { addGenerationTabLoRAs } from 'features/nodes/util/graph/addGenerationTabLoRAs'; +import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/addGenerationTabNSFWChecker'; +import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; +import { addGenerationTabWatermarker } from 'features/nodes/util/graph/addGenerationTabWatermarker'; +import type { GraphType } from 'features/nodes/util/graph/Graph'; +import { Graph } from 'features/nodes/util/graph/Graph'; +import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import type { Invocation } from 'services/api/types'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; -import { addHrfToGraph } from './addHrfToGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { CLIP_SKIP, CONTROL_LAYERS_GRAPH, @@ -19,249 +22,166 @@ import { LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, NEGATIVE_CONDITIONING, + NEGATIVE_CONDITIONING_COLLECT, NOISE, POSITIVE_CONDITIONING, - SEAMLESS, + POSITIVE_CONDITIONING_COLLECT, + VAE_LOADER, } from './constants'; -import { addCoreMetadataNode, getModelMetadataField } from './metadata'; +import { getModelMetadataField } from './metadata'; -export const buildGenerationTabGraph = async (state: RootState): Promise => { - const log = logger('nodes'); +export const buildGenerationTabGraph = async (state: RootState): Promise => { const { model, cfgScale: cfg_scale, cfgRescaleMultiplier: cfg_rescale_multiplier, scheduler, steps, - clipSkip, + clipSkip: skipped_layers, shouldUseCpuNoise, vaePrecision, - seamlessXAxis, - seamlessYAxis, seed, + vae, } = state.generation; const { positivePrompt, negativePrompt } = state.controlLayers.present; const { width, height } = state.controlLayers.present.size; - const use_cpu = shouldUseCpuNoise; + assert(model, 'No model found in state'); - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } + const g = new Graph(CONTROL_LAYERS_GRAPH); + const modelLoader = g.addNode({ + type: 'main_model_loader', + id: MAIN_MODEL_LOADER, + model, + }); + const clipSkip = g.addNode({ + type: 'clip_skip', + id: CLIP_SKIP, + skipped_layers, + }); + const posCond = g.addNode({ + type: 'compel', + id: POSITIVE_CONDITIONING, + prompt: positivePrompt, + }); + const posCondCollect = g.addNode({ + type: 'collect', + id: POSITIVE_CONDITIONING_COLLECT, + }); + const negCond = g.addNode({ + type: 'compel', + id: NEGATIVE_CONDITIONING, + prompt: negativePrompt, + }); + const negCondCollect = g.addNode({ + type: 'collect', + id: NEGATIVE_CONDITIONING_COLLECT, + }); + const noise = g.addNode({ + type: 'noise', + id: NOISE, + seed, + width, + height, + use_cpu: shouldUseCpuNoise, + }); + const denoise = g.addNode({ + type: 'denoise_latents', + id: DENOISE_LATENTS, + cfg_scale, + cfg_rescale_multiplier, + scheduler, + steps, + denoising_start: 0, + denoising_end: 1, + }); + const l2i = g.addNode({ + type: 'l2i', + id: LATENTS_TO_IMAGE, + fp32: vaePrecision === 'fp32', + board: getBoardField(state), + // This is the terminal node and must always save to gallery. + is_intermediate: false, + use_cache: false, + }); + const vaeLoader = + vae?.base === model.base + ? g.addNode({ + type: 'vae_loader', + id: VAE_LOADER, + vae_model: vae, + }) + : null; - const fp32 = vaePrecision === 'fp32'; - const is_intermediate = true; + let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; - let modelLoaderNodeId = MAIN_MODEL_LOADER; - - /** - * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the - * full graph here as a template. Then use the parameters from app state and set friendlier node - * ids. - * - * The only thing we need extra logic for is handling randomized seed, control net, and for img2img, - * the `fit` param. These are added to the graph at the end. - */ - - // copy-pasted graph from node editor, filled in with state values & friendly node ids - - const graph: NonNullableGraph = { - id: CONTROL_LAYERS_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'main_model_loader', - id: modelLoaderNodeId, - is_intermediate, - model, - }, - [CLIP_SKIP]: { - type: 'clip_skip', - id: CLIP_SKIP, - skipped_layers: clipSkip, - is_intermediate, - }, - [POSITIVE_CONDITIONING]: { - type: 'compel', - id: POSITIVE_CONDITIONING, - prompt: positivePrompt, - is_intermediate, - }, - [NEGATIVE_CONDITIONING]: { - type: 'compel', - id: NEGATIVE_CONDITIONING, - prompt: negativePrompt, - is_intermediate, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - seed, - width, - height, - use_cpu, - is_intermediate, - }, - [DENOISE_LATENTS]: { - type: 'denoise_latents', - id: DENOISE_LATENTS, - is_intermediate, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: 0, - denoising_end: 1, - }, - [LATENTS_TO_IMAGE]: { - type: 'l2i', - id: LATENTS_TO_IMAGE, - fp32, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - use_cache: false, - }, - }, - edges: [ - // Connect Model Loader to UNet and CLIP Skip - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: CLIP_SKIP, - field: 'clip', - }, - }, - // Connect CLIP Skip to Conditioning - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - // Connect everything to Denoise Latents - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'noise', - }, - }, - // Decode Denoised Latents To Image - { - source: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - ], - }; + g.addEdge(modelLoader, 'unet', denoise, 'unet'); + g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); + g.addEdge(clipSkip, 'clip', posCond, 'clip'); + g.addEdge(clipSkip, 'clip', negCond, 'clip'); + g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); + g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); + g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); + g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning'); + g.addEdge(noise, 'noise', denoise, 'noise'); + g.addEdge(denoise, 'latents', l2i, 'latents'); const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - addCoreMetadataNode( - graph, - { - generation_mode: 'txt2img', - cfg_scale, - cfg_rescale_multiplier, - height, - width, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: use_cpu ? 'cpu' : 'cuda', - scheduler, - clip_skip: clipSkip, - }, - LATENTS_TO_IMAGE + MetadataUtil.add(g, { + generation_mode: 'txt2img', + cfg_scale, + cfg_rescale_multiplier, + height, + width, + positive_prompt: positivePrompt, + negative_prompt: negativePrompt, + model: getModelMetadataField(modelConfig), + seed, + steps, + rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda', + scheduler, + clip_skip: skipped_layers, + vae: vae ?? undefined, + }); + g.validate(); + + const seamless = addGenerationTabSeamless(state, g, denoise, modelLoader, vaeLoader); + g.validate(); + + addGenerationTabLoRAs(state, g, denoise, modelLoader, seamless, clipSkip, posCond, negCond); + g.validate(); + + // We might get the VAE from the main model, custom VAE, or seamless node. + const vaeSource = seamless ?? vaeLoader ?? modelLoader; + g.addEdge(vaeSource, 'vae', l2i, 'vae'); + + const addedLayers = await addGenerationTabControlLayers( + state, + g, + denoise, + posCond, + negCond, + posCondCollect, + negCondCollect, + noise, + vaeSource ); + g.validate(); - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; + const isHRFAllowed = !addedLayers.some((l) => isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); + if (isHRFAllowed && state.hrf.hrfEnabled) { + imageOutput = addGenerationTabHRF(state, g, denoise, noise, l2i, vaeSource); } - // add LoRA support - await addLoRAsToGraph(state, graph, DENOISE_LATENTS, modelLoaderNodeId); - - const addedLayers = await addControlLayersToGraph(state, graph, DENOISE_LATENTS); - - // optionally add custom VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - const shouldUseHRF = !addedLayers.some((l) => isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); - // High resolution fix. - if (state.hrf.hrfEnabled && shouldUseHRF) { - addHrfToGraph(state, graph); - } - - // NSFW & watermark - must be last thing added to graph if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph); + imageOutput = addGenerationTabNSFWChecker(g, imageOutput); } if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph); + imageOutput = addGenerationTabWatermarker(g, imageOutput); } - return graph; + MetadataUtil.setMetadataReceivingNode(g, imageOutput); + return g.getGraph(); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts deleted file mode 100644 index f8d6b1a543..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph2.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; -import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; -import { addGenerationTabHRF } from 'features/nodes/util/graph/addGenerationTabHRF'; -import { addGenerationTabLoRAs } from 'features/nodes/util/graph/addGenerationTabLoRAs'; -import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/addGenerationTabNSFWChecker'; -import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; -import { addGenerationTabWatermarker } from 'features/nodes/util/graph/addGenerationTabWatermarker'; -import type { GraphType } from 'features/nodes/util/graph/Graph'; -import { Graph } from 'features/nodes/util/graph/Graph'; -import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; -import type { Invocation } from 'services/api/types'; -import { isNonRefinerMainModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; - -import { - CLIP_SKIP, - CONTROL_LAYERS_GRAPH, - DENOISE_LATENTS, - LATENTS_TO_IMAGE, - MAIN_MODEL_LOADER, - NEGATIVE_CONDITIONING, - NEGATIVE_CONDITIONING_COLLECT, - NOISE, - POSITIVE_CONDITIONING, - POSITIVE_CONDITIONING_COLLECT, - VAE_LOADER, -} from './constants'; -import { getModelMetadataField } from './metadata'; - -export const buildGenerationTabGraph2 = async (state: RootState): Promise => { - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - steps, - clipSkip: skipped_layers, - shouldUseCpuNoise, - vaePrecision, - seed, - vae, - } = state.generation; - const { positivePrompt, negativePrompt } = state.controlLayers.present; - const { width, height } = state.controlLayers.present.size; - - assert(model, 'No model found in state'); - - const g = new Graph(CONTROL_LAYERS_GRAPH); - const modelLoader = g.addNode({ - type: 'main_model_loader', - id: MAIN_MODEL_LOADER, - model, - }); - const clipSkip = g.addNode({ - type: 'clip_skip', - id: CLIP_SKIP, - skipped_layers, - }); - const posCond = g.addNode({ - type: 'compel', - id: POSITIVE_CONDITIONING, - prompt: positivePrompt, - }); - const posCondCollect = g.addNode({ - type: 'collect', - id: POSITIVE_CONDITIONING_COLLECT, - }); - const negCond = g.addNode({ - type: 'compel', - id: NEGATIVE_CONDITIONING, - prompt: negativePrompt, - }); - const negCondCollect = g.addNode({ - type: 'collect', - id: NEGATIVE_CONDITIONING_COLLECT, - }); - const noise = g.addNode({ - type: 'noise', - id: NOISE, - seed, - width, - height, - use_cpu: shouldUseCpuNoise, - }); - const denoise = g.addNode({ - type: 'denoise_latents', - id: DENOISE_LATENTS, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: 0, - denoising_end: 1, - }); - const l2i = g.addNode({ - type: 'l2i', - id: LATENTS_TO_IMAGE, - fp32: vaePrecision === 'fp32', - board: getBoardField(state), - // This is the terminal node and must always save to gallery. - is_intermediate: false, - use_cache: false, - }); - const vaeLoader = - vae?.base === model.base - ? g.addNode({ - type: 'vae_loader', - id: VAE_LOADER, - vae_model: vae, - }) - : null; - - let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; - - g.addEdge(modelLoader, 'unet', denoise, 'unet'); - g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); - g.addEdge(clipSkip, 'clip', posCond, 'clip'); - g.addEdge(clipSkip, 'clip', negCond, 'clip'); - g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); - g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); - g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); - g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning'); - g.addEdge(noise, 'noise', denoise, 'noise'); - g.addEdge(denoise, 'latents', l2i, 'latents'); - - const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - - MetadataUtil.add(g, { - generation_mode: 'txt2img', - cfg_scale, - cfg_rescale_multiplier, - height, - width, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda', - scheduler, - clip_skip: skipped_layers, - vae: vae ?? undefined, - }); - g.validate(); - - const seamless = addGenerationTabSeamless(state, g, denoise, modelLoader, vaeLoader); - g.validate(); - - addGenerationTabLoRAs(state, g, denoise, modelLoader, seamless, clipSkip, posCond, negCond); - g.validate(); - - // We might get the VAE from the main model, custom VAE, or seamless node. - const vaeSource = seamless ?? vaeLoader ?? modelLoader; - g.addEdge(vaeSource, 'vae', l2i, 'vae'); - - const addedLayers = await addGenerationTabControlLayers( - state, - g, - denoise, - posCond, - negCond, - posCondCollect, - negCondCollect, - noise, - vaeSource - ); - g.validate(); - - const isHRFAllowed = !addedLayers.some((l) => isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); - if (isHRFAllowed && state.hrf.hrfEnabled) { - imageOutput = addGenerationTabHRF(state, g, denoise, noise, l2i, vaeSource); - } - - if (state.system.shouldUseNSFWChecker) { - imageOutput = addGenerationTabNSFWChecker(g, imageOutput); - } - - if (state.system.shouldUseWatermarker) { - imageOutput = addGenerationTabWatermarker(g, imageOutput); - } - - MetadataUtil.setMetadataReceivingNode(g, imageOutput); - return g.getGraph(); -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts index fbf6b3848c..29caf3ac3a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts @@ -1,31 +1,33 @@ -import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; -import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; +import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; +import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/addGenerationTabNSFWChecker'; +import { addGenerationTabSDXLLoRAs } from 'features/nodes/util/graph/addGenerationTabSDXLLoRAs'; +import { addGenerationTabSDXLRefiner } from 'features/nodes/util/graph/addGenerationTabSDXLRefiner'; +import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; +import { addGenerationTabWatermarker } from 'features/nodes/util/graph/addGenerationTabWatermarker'; +import { Graph } from 'features/nodes/util/graph/Graph'; +import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; +import type { Invocation, NonNullableGraph } from 'services/api/types'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; -import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { LATENTS_TO_IMAGE, NEGATIVE_CONDITIONING, + NEGATIVE_CONDITIONING_COLLECT, NOISE, POSITIVE_CONDITIONING, + POSITIVE_CONDITIONING_COLLECT, SDXL_CONTROL_LAYERS_GRAPH, SDXL_DENOISE_LATENTS, SDXL_MODEL_LOADER, - SDXL_REFINER_SEAMLESS, - SEAMLESS, + VAE_LOADER, } from './constants'; -import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; -import { addCoreMetadataNode, getModelMetadataField } from './metadata'; +import { getBoardField, getSDXLStylePrompts } from './graphBuilderUtils'; +import { getModelMetadataField } from './metadata'; export const buildGenerationTabSDXLGraph = async (state: RootState): Promise => { - const log = logger('nodes'); const { model, cfgScale: cfg_scale, @@ -35,244 +37,142 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; - // copy-pasted graph from node editor, filled in with state values & friendly node ids - const graph: NonNullableGraph = { - id: SDXL_CONTROL_LAYERS_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'sdxl_model_loader', - id: modelLoaderNodeId, - model, - is_intermediate, - }, - [POSITIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: POSITIVE_CONDITIONING, - prompt: positivePrompt, - style: positiveStylePrompt, - is_intermediate, - }, - [NEGATIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: NEGATIVE_CONDITIONING, - prompt: negativePrompt, - style: negativeStylePrompt, - is_intermediate, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - seed, - width, - height, - use_cpu, - is_intermediate, - }, - [SDXL_DENOISE_LATENTS]: { - type: 'denoise_latents', - id: SDXL_DENOISE_LATENTS, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: 0, - denoising_end: refinerModel ? refinerStart : 1, - is_intermediate, - }, - [LATENTS_TO_IMAGE]: { - type: 'l2i', - id: LATENTS_TO_IMAGE, - fp32, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - use_cache: false, - }, - }, - edges: [ - // Connect Model Loader to UNet, VAE & CLIP - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip2', - }, - }, - // Connect everything to Denoise Latents - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'noise', - }, - }, - // Decode Denoised Latents To Image - { - source: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - ], - }; + g.addEdge(modelLoader, 'unet', denoise, 'unet'); + g.addEdge(modelLoader, 'clip', posCond, 'clip'); + g.addEdge(modelLoader, 'clip', negCond, 'clip'); + g.addEdge(modelLoader, 'clip2', posCond, 'clip2'); + g.addEdge(modelLoader, 'clip2', negCond, 'clip2'); + g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); + g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); + g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); + g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning'); + g.addEdge(noise, 'noise', denoise, 'noise'); + g.addEdge(denoise, 'latents', l2i, 'latents'); const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - addCoreMetadataNode( - graph, - { - generation_mode: 'txt2img', - cfg_scale, - cfg_rescale_multiplier, - height, - width, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: use_cpu ? 'cpu' : 'cuda', - scheduler, - positive_style_prompt: positiveStylePrompt, - negative_style_prompt: negativeStylePrompt, - }, - LATENTS_TO_IMAGE - ); + MetadataUtil.add(g, { + generation_mode: 'txt2img', + cfg_scale, + cfg_rescale_multiplier, + height, + width, + positive_prompt: positivePrompt, + negative_prompt: negativePrompt, + model: getModelMetadataField(modelConfig), + seed, + steps, + rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda', + scheduler, + positive_style_prompt: positiveStylePrompt, + negative_style_prompt: negativeStylePrompt, + vae: vae ?? undefined, + }); + g.validate(); - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } + const seamless = addGenerationTabSeamless(state, g, denoise, modelLoader, vaeLoader); + g.validate(); + + addGenerationTabSDXLLoRAs(state, g, denoise, modelLoader, seamless, posCond, negCond); + g.validate(); + + // We might get the VAE from the main model, custom VAE, or seamless node. + const vaeSource = seamless ?? vaeLoader ?? modelLoader; + g.addEdge(vaeSource, 'vae', l2i, 'vae'); // Add Refiner if enabled if (refinerModel) { - await addSDXLRefinerToGraph(state, graph, SDXL_DENOISE_LATENTS); - if (seamlessXAxis || seamlessYAxis) { - modelLoaderNodeId = SDXL_REFINER_SEAMLESS; - } + await addGenerationTabSDXLRefiner(state, g, denoise, modelLoader, seamless, posCond, negCond, l2i); } - // add LoRA support - await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); + await addGenerationTabControlLayers( + state, + g, + denoise, + posCond, + negCond, + posCondCollect, + negCondCollect, + noise, + vaeSource + ); - await addControlLayersToGraph(state, graph, SDXL_DENOISE_LATENTS); - - // optionally add custom VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // NSFW & watermark - must be last thing added to graph if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph); + imageOutput = addGenerationTabNSFWChecker(g, imageOutput); } if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph); + imageOutput = addGenerationTabWatermarker(g, imageOutput); } - return graph; + MetadataUtil.setMetadataReceivingNode(g, imageOutput); + return g.getGraph(); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph2.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph2.ts deleted file mode 100644 index 05fe3d1565..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph2.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; -import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/addGenerationTabNSFWChecker'; -import { addGenerationTabSDXLLoRAs } from 'features/nodes/util/graph/addGenerationTabSDXLLoRAs'; -import { addGenerationTabSDXLRefiner } from 'features/nodes/util/graph/addGenerationTabSDXLRefiner'; -import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; -import { addGenerationTabWatermarker } from 'features/nodes/util/graph/addGenerationTabWatermarker'; -import { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; -import type { Invocation, NonNullableGraph } from 'services/api/types'; -import { isNonRefinerMainModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; - -import { - LATENTS_TO_IMAGE, - NEGATIVE_CONDITIONING, - NEGATIVE_CONDITIONING_COLLECT, - NOISE, - POSITIVE_CONDITIONING, - POSITIVE_CONDITIONING_COLLECT, - SDXL_CONTROL_LAYERS_GRAPH, - SDXL_DENOISE_LATENTS, - SDXL_MODEL_LOADER, - VAE_LOADER, -} from './constants'; -import { getBoardField, getSDXLStylePrompts } from './graphBuilderUtils'; -import { getModelMetadataField } from './metadata'; - -export const buildGenerationTabSDXLGraph2 = async (state: RootState): Promise => { - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - seed, - steps, - shouldUseCpuNoise, - vaePrecision, - vae, - } = state.generation; - const { positivePrompt, negativePrompt } = state.controlLayers.present; - const { width, height } = state.controlLayers.present.size; - - const { refinerModel, refinerStart } = state.sdxl; - - assert(model, 'No model found in state'); - - const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state); - - const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH); - const modelLoader = g.addNode({ - type: 'sdxl_model_loader', - id: SDXL_MODEL_LOADER, - model, - }); - const posCond = g.addNode({ - type: 'sdxl_compel_prompt', - id: POSITIVE_CONDITIONING, - prompt: positivePrompt, - style: positiveStylePrompt, - }); - const posCondCollect = g.addNode({ - type: 'collect', - id: POSITIVE_CONDITIONING_COLLECT, - }); - const negCond = g.addNode({ - type: 'sdxl_compel_prompt', - id: NEGATIVE_CONDITIONING, - prompt: negativePrompt, - style: negativeStylePrompt, - }); - const negCondCollect = g.addNode({ - type: 'collect', - id: NEGATIVE_CONDITIONING_COLLECT, - }); - const noise = g.addNode({ type: 'noise', id: NOISE, seed, width, height, use_cpu: shouldUseCpuNoise }); - const denoise = g.addNode({ - type: 'denoise_latents', - id: SDXL_DENOISE_LATENTS, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: 0, - denoising_end: refinerModel ? refinerStart : 1, - }); - const l2i = g.addNode({ - type: 'l2i', - id: LATENTS_TO_IMAGE, - fp32: vaePrecision === 'fp32', - board: getBoardField(state), - // This is the terminal node and must always save to gallery. - is_intermediate: false, - use_cache: false, - }); - const vaeLoader = - vae?.base === model.base - ? g.addNode({ - type: 'vae_loader', - id: VAE_LOADER, - vae_model: vae, - }) - : null; - - let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; - - g.addEdge(modelLoader, 'unet', denoise, 'unet'); - g.addEdge(modelLoader, 'clip', posCond, 'clip'); - g.addEdge(modelLoader, 'clip', negCond, 'clip'); - g.addEdge(modelLoader, 'clip2', posCond, 'clip2'); - g.addEdge(modelLoader, 'clip2', negCond, 'clip2'); - g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); - g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); - g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); - g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning'); - g.addEdge(noise, 'noise', denoise, 'noise'); - g.addEdge(denoise, 'latents', l2i, 'latents'); - - const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - - MetadataUtil.add(g, { - generation_mode: 'txt2img', - cfg_scale, - cfg_rescale_multiplier, - height, - width, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda', - scheduler, - positive_style_prompt: positiveStylePrompt, - negative_style_prompt: negativeStylePrompt, - vae: vae ?? undefined, - }); - g.validate(); - - const seamless = addGenerationTabSeamless(state, g, denoise, modelLoader, vaeLoader); - g.validate(); - - addGenerationTabSDXLLoRAs(state, g, denoise, modelLoader, seamless, posCond, negCond); - g.validate(); - - // We might get the VAE from the main model, custom VAE, or seamless node. - const vaeSource = seamless ?? vaeLoader ?? modelLoader; - g.addEdge(vaeSource, 'vae', l2i, 'vae'); - - // Add Refiner if enabled - if (refinerModel) { - await addGenerationTabSDXLRefiner(state, g, denoise, modelLoader, seamless, posCond, negCond, l2i); - } - - await addGenerationTabControlLayers( - state, - g, - denoise, - posCond, - negCond, - posCondCollect, - negCondCollect, - noise, - vaeSource - ); - - if (state.system.shouldUseNSFWChecker) { - imageOutput = addGenerationTabNSFWChecker(g, imageOutput); - } - - if (state.system.shouldUseWatermarker) { - imageOutput = addGenerationTabWatermarker(g, imageOutput); - } - - MetadataUtil.setMetadataReceivingNode(g, imageOutput); - return g.getGraph(); -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts b/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts index 9e8b5a5a6b..366c8a936e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts @@ -58,21 +58,6 @@ export const getHasMetadata = (graph: NonNullableGraph): boolean => { return Boolean(metadataNode); }; -export const setMetadataReceivingNode = (graph: NonNullableGraph, nodeId: string) => { - graph.edges = graph.edges.filter((edge) => edge.source.node_id !== METADATA); - - graph.edges.push({ - source: { - node_id: METADATA, - field: 'metadata', - }, - destination: { - node_id: nodeId, - field: 'metadata', - }, - }); -}; - export const getModelMetadataField = ({ key, hash, name, base, type }: AnyModelConfig): ModelIdentifierField => ({ key, hash, From 5dd460c3ce38dfc471b5f96b40e7d29b96b248b1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 21:03:28 +1000 Subject: [PATCH 118/442] chore(ui): knip --- invokeai/frontend/web/src/services/api/types.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index e22f73ed9e..f9728f5e8a 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -121,7 +121,6 @@ export type ModelInstallStatus = S['InstallStatus']; // Graphs export type Graph = S['Graph']; export type NonNullableGraph = O.Required; -export type Edge = S['Edge']; export type GraphExecutionState = S['GraphExecutionState']; export type Batch = S['Batch']; export type SessionQueueItemDTO = S['SessionQueueItemDTO']; @@ -129,7 +128,7 @@ export type WorkflowRecordOrderBy = S['WorkflowRecordOrderBy']; export type SQLiteDirection = S['SQLiteDirection']; export type WorkflowRecordListItemDTO = S['WorkflowRecordListItemDTO']; -export type KeysOfUnion = T extends T ? keyof T : never; +type KeysOfUnion = T extends T ? keyof T : never; export type AnyInvocation = Exclude< Graph['nodes'][string], @@ -138,17 +137,17 @@ export type AnyInvocation = Exclude< export type AnyInvocationIncMetadata = S['Graph']['nodes'][string]; export type InvocationType = AnyInvocation['type']; -export type InvocationOutputMap = S['InvocationOutputMap']; -export type AnyInvocationOutput = InvocationOutputMap[InvocationType]; +type InvocationOutputMap = S['InvocationOutputMap']; +type AnyInvocationOutput = InvocationOutputMap[InvocationType]; export type Invocation = Extract; -export type InvocationOutput = InvocationOutputMap[T]; +// export type InvocationOutput = InvocationOutputMap[T]; -export type NonInputFields = 'id' | 'type' | 'is_intermediate' | 'use_cache' | 'board' | 'metadata'; +type NonInputFields = 'id' | 'type' | 'is_intermediate' | 'use_cache' | 'board' | 'metadata'; export type AnyInvocationInputField = Exclude>, NonInputFields>; export type InputFields = Extract; -export type NonOutputFields = 'type'; +type NonOutputFields = 'type'; export type AnyInvocationOutputField = Exclude>, NonOutputFields>; export type OutputFields = Extract< keyof InvocationOutputMap[T['type']], @@ -157,13 +156,11 @@ export type OutputFields = Extract< // General nodes export type CollectInvocation = Invocation<'collect'>; -export type ImageResizeInvocation = Invocation<'img_resize'>; export type InfillPatchMatchInvocation = Invocation<'infill_patchmatch'>; export type InfillTileInvocation = Invocation<'infill_tile'>; export type CreateGradientMaskInvocation = Invocation<'create_gradient_mask'>; export type CanvasPasteBackInvocation = Invocation<'canvas_paste_back'>; export type NoiseInvocation = Invocation<'noise'>; -export type DenoiseLatentsInvocation = Invocation<'denoise_latents'>; export type SDXLLoRALoaderInvocation = Invocation<'sdxl_lora_loader'>; export type ImageToLatentsInvocation = Invocation<'i2l'>; export type LatentsToImageInvocation = Invocation<'l2i'>; From 154b52ca4d1da46f26c08c8bf2022b3fea800066 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 13:12:43 +1000 Subject: [PATCH 119/442] docs(ui): update docstrings for Graph builder --- .../src/features/nodes/util/graph/Graph.ts | 64 ++++++++----------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts index 7cc976bfb0..8b3a2b6b21 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts @@ -39,7 +39,7 @@ export class Graph { /** * Add a node to the graph. If a node with the same id already exists, an `AssertionError` is raised. - * The optional `is_intermediate` and `use_cache` fields are set to `true` and `true` respectively if not set on the node. + * The optional `is_intermediate` and `use_cache` fields are both set to `true`, if not set on the node. * @param node The node to add. * @returns The added node. * @raises `AssertionError` if a node with the same id already exists. @@ -60,7 +60,7 @@ export class Graph { * Gets a node from the graph. * @param id The id of the node to get. * @returns The node. - * @raises `AssertionError` if the node does not exist or if a `type` is provided but the node is not of the expected type. + * @raises `AssertionError` if the node does not exist. */ getNode(id: string): AnyInvocation { const node = this._graph.nodes[id]; @@ -84,6 +84,7 @@ export class Graph { /** * Check if a node exists in the graph. * @param id The id of the node to check. + * @returns Whether the graph has a node with the given id. */ hasNode(id: string): boolean { try { @@ -96,9 +97,9 @@ export class Graph { /** * Get the immediate incomers of a node. - * @param nodeId The id of the node to get the incomers of. + * @param node The node to get the incomers of. * @returns The incoming nodes. - * @raises `AssertionError` if the node does not exist. + * @raises `AssertionError` if one of the target node's incoming edges has an invalid source node. */ getIncomers(node: AnyInvocation): AnyInvocation[] { return this.getEdgesTo(node).map((edge) => this.getNode(edge.source.node_id)); @@ -106,9 +107,9 @@ export class Graph { /** * Get the immediate outgoers of a node. - * @param nodeId The id of the node to get the outgoers of. + * @param node The node to get the outgoers of. * @returns The outgoing nodes. - * @raises `AssertionError` if the node does not exist. + * @raises `AssertionError` if one of the target node's outgoing edges has an invalid destination node. */ getOutgoers(node: AnyInvocation): AnyInvocation[] { return this.getEdgesFrom(node).map((edge) => this.getNode(edge.destination.node_id)); @@ -119,10 +120,9 @@ export class Graph { /** * Add an edge to the graph. If an edge with the same source and destination already exists, an `AssertionError` is raised. - * If providing node ids, provide the from and to node types as generics to get type hints for from and to field names. - * @param fromNode The source node or id of the source node. + * @param fromNode The source node. * @param fromField The field of the source node. - * @param toNode The source node or id of the destination node. + * @param toNode The destination node. * @param toField The field of the destination node. * @returns The added edge. * @raises `AssertionError` if an edge with the same source and destination already exists. @@ -145,11 +145,7 @@ export class Graph { /** * Add an edge to the graph. If an edge with the same source and destination already exists, an `AssertionError` is raised. - * If providing node ids, provide the from and to node types as generics to get type hints for from and to field names. - * @param fromNode The source node or id of the source node. - * @param fromField The field of the source node. - * @param toNode The source node or id of the destination node. - * @param toField The field of the destination node. + * @param edge The edge to add. * @returns The added edge. * @raises `AssertionError` if an edge with the same source and destination already exists. */ @@ -170,10 +166,9 @@ export class Graph { /** * Get an edge from the graph. If the edge does not exist, an `AssertionError` is raised. - * Provide the from and to node types as generics to get type hints for from and to field names. - * @param fromNodeId The id of the source node. + * @param fromNode The source node. * @param fromField The field of the source node. - * @param toNodeId The id of the destination node. + * @param toNode The destination node. * @param toField The field of the destination node. * @returns The edge. * @raises `AssertionError` if the edge does not exist. @@ -205,10 +200,9 @@ export class Graph { /** * Check if a graph has an edge. - * Provide the from and to node types as generics to get type hints for from and to field names. - * @param fromNodeId The id of the source node. + * @param fromNode The source node. * @param fromField The field of the source node. - * @param toNodeId The id of the destination node. + * @param toNode The destination node. * @param toField The field of the destination node. * @returns Whether the graph has the edge. */ @@ -228,10 +222,9 @@ export class Graph { } /** - * Get all edges from a node. If `fromField` is provided, only edges from that field are returned. - * Provide the from node type as a generic to get type hints for from field names. - * @param fromNodeId The id of the source node. - * @param fromFields The field of the source node (optional). + * Get all edges from a node. If `fromFields` is provided, only edges from those fields are returned. + * @param fromNode The source node. + * @param fromFields The fields of the source node (optional). * @returns The edges. */ getEdgesFrom(fromNode: T, fromFields?: OutputFields[]): Edge[] { @@ -244,10 +237,9 @@ export class Graph { } /** - * Get all edges to a node. If `toField` is provided, only edges to that field are returned. - * Provide the to node type as a generic to get type hints for to field names. - * @param toNodeId The id of the destination node. - * @param toFields The field of the destination node (optional). + * Get all edges to a node. If `toFields` is provided, only edges to those fields are returned. + * @param toNodeId The destination node. + * @param toFields The fields of the destination node (optional). * @returns The edges. */ getEdgesTo(toNode: T, toFields?: InputFields[]): Edge[] { @@ -259,7 +251,7 @@ export class Graph { } /** - * Delete _all_ matching edges from the graph. Uses _.isEqual for comparison. + * INTERNAL: Delete _all_ matching edges from the graph. Uses _.isEqual for comparison. * @param edge The edge to delete */ private _deleteEdge(edge: Edge): void { @@ -267,10 +259,9 @@ export class Graph { } /** - * Delete all edges to a node. If `toField` is provided, only edges to that field are deleted. - * Provide the to node type as a generic to get type hints for to field names. + * Delete all edges to a node. If `toFields` is provided, only edges to those fields are deleted. * @param toNode The destination node. - * @param toFields The field of the destination node (optional). + * @param toFields The fields of the destination node (optional). */ deleteEdgesTo(toNode: T, toFields?: InputFields[]): void { for (const edge of this.getEdgesTo(toNode, toFields)) { @@ -279,10 +270,9 @@ export class Graph { } /** - * Delete all edges from a node. If `fromField` is provided, only edges from that field are deleted. - * Provide the from node type as a generic to get type hints for from field names. - * @param toNodeId The id of the source node. - * @param fromFields The field of the source node (optional). + * Delete all edges from a node. If `fromFields` is provided, only edges from those fields are deleted. + * @param toNode The id of the source node. + * @param fromFields The fields of the source node (optional). */ deleteEdgesFrom(fromNode: T, fromFields?: OutputFields[]): void { for (const edge of this.getEdgesFrom(fromNode, fromFields)) { @@ -295,10 +285,10 @@ export class Graph { /** * Validate the graph. Checks that all edges have valid source and destination nodes. - * TODO(psyche): Add more validation checks - cycles, valid invocation types, etc. * @raises `AssertionError` if an edge has an invalid source or destination node. */ validate(): void { + // TODO(psyche): Add more validation checks - cycles, valid invocation types, etc. for (const edge of this._graph.edges) { this.getNode(edge.source.node_id); this.getNode(edge.destination.node_id); From ee647a05dc1675182c8dfb64a9a058acebbcc009 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 13:41:17 +1000 Subject: [PATCH 120/442] feat(ui): move metadata util to graph class No good reason to have it be separate. A bit cleaner this way. --- .../features/nodes/util/graph/Graph.test.ts | 104 +++++++++++++++++- .../src/features/nodes/util/graph/Graph.ts | 72 +++++++++++- 2 files changed, 173 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts index 148bbf1ded..921f5d265e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts @@ -1,5 +1,5 @@ import { Graph } from 'features/nodes/util/graph/Graph'; -import type { Invocation } from 'services/api/types'; +import type { AnyInvocation, Invocation } from 'services/api/types'; import { assert, AssertionError, is } from 'tsafe'; import { validate } from 'uuid'; import { describe, expect, it } from 'vitest'; @@ -414,4 +414,106 @@ describe('Graph', () => { expect(g.getEdgesTo(n3)).toEqual([e2]); }); }); + + describe('metadata utils', () => { + describe('_getMetadataNode', () => { + it("should get the metadata node, creating it if it doesn't exist", () => { + const g = new Graph(); + const metadata = g._getMetadataNode(); + expect(metadata.id).toBe('core_metadata'); + expect(metadata.type).toBe('core_metadata'); + g.upsertMetadata({ test: 'test' }); + const metadata2 = g._getMetadataNode(); + expect(metadata2).toHaveProperty('test'); + }); + }); + + describe('upsertMetadata', () => { + it('should add metadata to the metadata node', () => { + const g = new Graph(); + g.upsertMetadata({ test: 'test' }); + const metadata = g._getMetadataNode(); + expect(metadata).toHaveProperty('test'); + }); + it('should update metadata on the metadata node', () => { + const g = new Graph(); + g.upsertMetadata({ test: 'test' }); + g.upsertMetadata({ test: 'test2' }); + const metadata = g._getMetadataNode(); + expect(metadata.test).toBe('test2'); + }); + }); + + describe('removeMetadata', () => { + it('should remove metadata from the metadata node', () => { + const g = new Graph(); + g.upsertMetadata({ test: 'test', test2: 'test2' }); + g.removeMetadata(['test']); + const metadata = g._getMetadataNode(); + expect(metadata).not.toHaveProperty('test'); + }); + it('should remove multiple metadata from the metadata node', () => { + const g = new Graph(); + g.upsertMetadata({ test: 'test', test2: 'test2' }); + g.removeMetadata(['test', 'test2']); + const metadata = g._getMetadataNode(); + expect(metadata).not.toHaveProperty('test'); + expect(metadata).not.toHaveProperty('test2'); + }); + }); + + describe('setMetadataReceivingNode', () => { + it('should set the metadata receiving node', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'img_resize', + }); + g.upsertMetadata({ test: 'test' }); + g.setMetadataReceivingNode(n1); + const metadata = g._getMetadataNode(); + expect(g.getEdgesFrom(metadata as unknown as AnyInvocation).length).toBe(1); + expect(g.getEdgesTo(n1).length).toBe(1); + }); + }); + + describe('getModelMetadataField', () => { + it('should return a ModelIdentifierField', () => { + const field = Graph.getModelMetadataField({ + key: 'b00ee8df-523d-40d2-9578-597283b07cb2', + hash: 'random:9adf270422f525715297afa1649c4ff007a55f09937f57ca628278305624d194', + path: 'sdxl/main/stable-diffusion-xl-1.0-inpainting-0.1', + name: 'stable-diffusion-xl-1.0-inpainting-0.1', + base: 'sdxl', + description: 'sdxl main model stable-diffusion-xl-1.0-inpainting-0.1', + source: '/home/bat/invokeai-4.0.0/models/sdxl/main/stable-diffusion-xl-1.0-inpainting-0.1', + source_type: 'path', + source_api_response: null, + cover_image: null, + type: 'main', + trigger_phrases: null, + default_settings: { + vae: null, + vae_precision: null, + scheduler: null, + steps: null, + cfg_scale: null, + cfg_rescale_multiplier: null, + width: 1024, + height: 1024, + }, + variant: 'inpaint', + format: 'diffusers', + repo_variant: 'fp16', + }); + expect(field).toEqual({ + key: 'b00ee8df-523d-40d2-9578-597283b07cb2', + hash: 'random:9adf270422f525715297afa1649c4ff007a55f09937f57ca628278305624d194', + name: 'stable-diffusion-xl-1.0-inpainting-0.1', + base: 'sdxl', + type: 'main', + }); + }); + }); + }); }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts index 8b3a2b6b21..ae590b5044 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts @@ -1,8 +1,13 @@ -import { forEach, groupBy, isEqual, values } from 'lodash-es'; +import { type ModelIdentifierField, zModelIdentifierField } from 'features/nodes/types/common'; +import { METADATA } from 'features/nodes/util/graph/constants'; +import { forEach, groupBy, isEqual, unset, values } from 'lodash-es'; import type { AnyInvocation, + AnyInvocationIncMetadata, AnyInvocationInputField, AnyInvocationOutputField, + AnyModelConfig, + CoreMetadataInvocation, InputFields, Invocation, InvocationType, @@ -332,8 +337,71 @@ export class Graph { } //#endregion - //#region Util + //#region Metadata + /** + * INTERNAL: Get the metadata node. If it does not exist, it is created. + * @returns The metadata node. + */ + _getMetadataNode(): CoreMetadataInvocation { + try { + const node = this.getNode(METADATA) as AnyInvocationIncMetadata; + assert(node.type === 'core_metadata'); + return node; + } catch { + const node: CoreMetadataInvocation = { id: METADATA, type: 'core_metadata' }; + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing + return this.addNode(node); + } + } + + /** + * Add metadata to the graph. If the metadata node does not exist, it is created. If the specific metadata key exists, + * it is overwritten. + * @param metadata The metadata to add. + * @returns The metadata node. + */ + upsertMetadata(metadata: Partial): CoreMetadataInvocation { + const node = this._getMetadataNode(); + Object.assign(node, metadata); + return node; + } + + /** + * Remove metadata from the graph. + * @param keys The keys of the metadata to remove + * @returns The metadata node + */ + removeMetadata(keys: string[]): CoreMetadataInvocation { + const metadataNode = this._getMetadataNode(); + for (const k of keys) { + unset(metadataNode, k); + } + return metadataNode; + } + + /** + * Set the node that should receive metadata. All other edges from the metadata node are deleted. + * @param node The node to set as the receiving node + */ + setMetadataReceivingNode(node: AnyInvocation): void { + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing + this.deleteEdgesFrom(this._getMetadataNode()); + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing + this.addEdge(this._getMetadataNode(), 'metadata', node, 'metadata'); + } + + /** + * Given a model config, return the model metadata field. + * @param modelConfig The model config entity + * @returns + */ + static getModelMetadataField(modelConfig: AnyModelConfig): ModelIdentifierField { + return zModelIdentifierField.parse(modelConfig); + } + //#endregion + + //#region Util static getNodeNotFoundMsg(id: string): string { return `Node ${id} not found`; } From 48ccd63dbaf3bb5926400e64c74687d1c2f0a653 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 13:45:25 +1000 Subject: [PATCH 121/442] feat(ui): use integrated metadata helper --- .../nodes/util/graph/MetadataUtil.test.ts | 91 ------------------- .../features/nodes/util/graph/MetadataUtil.ts | 74 --------------- .../graph/addGenerationTabControlLayers.ts | 5 +- .../nodes/util/graph/addGenerationTabHRF.ts | 5 +- .../nodes/util/graph/addGenerationTabLoRAs.ts | 3 +- .../util/graph/addGenerationTabSDXLLoRAs.ts | 3 +- .../util/graph/addGenerationTabSDXLRefiner.ts | 3 +- .../util/graph/addGenerationTabSeamless.ts | 3 +- .../util/graph/buildGenerationTabGraph.ts | 5 +- .../util/graph/buildGenerationTabSDXLGraph.ts | 5 +- 10 files changed, 12 insertions(+), 185 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts deleted file mode 100644 index 69e3676641..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { isModelIdentifier } from 'features/nodes/types/common'; -import { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; -import { pick } from 'lodash-es'; -import type { AnyModelConfig } from 'services/api/types'; -import { AssertionError } from 'tsafe'; -import { describe, expect, it } from 'vitest'; - -describe('MetadataUtil', () => { - describe('getNode', () => { - it('should return the metadata node if one exists', () => { - const g = new Graph(); - // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing - const metadataNode = g.addNode({ id: MetadataUtil.metadataNodeId, type: 'core_metadata' }); - expect(MetadataUtil.getNode(g)).toEqual(metadataNode); - }); - it('should raise an error if the metadata node does not exist', () => { - const g = new Graph(); - expect(() => MetadataUtil.getNode(g)).toThrowError(AssertionError); - }); - }); - - describe('add', () => { - const g = new Graph(); - it("should add metadata, creating the node if it doesn't exist", () => { - MetadataUtil.add(g, { foo: 'bar' }); - const metadataNode = MetadataUtil.getNode(g); - expect(metadataNode['type']).toBe('core_metadata'); - expect(metadataNode['foo']).toBe('bar'); - }); - it('should update existing metadata keys', () => { - const updatedMetadataNode = MetadataUtil.add(g, { foo: 'bananas', baz: 'qux' }); - expect(updatedMetadataNode['foo']).toBe('bananas'); - expect(updatedMetadataNode['baz']).toBe('qux'); - }); - }); - - describe('remove', () => { - it('should remove a single key', () => { - const g = new Graph(); - MetadataUtil.add(g, { foo: 'bar', baz: 'qux' }); - const updatedMetadataNode = MetadataUtil.remove(g, 'foo'); - expect(updatedMetadataNode['foo']).toBeUndefined(); - expect(updatedMetadataNode['baz']).toBe('qux'); - }); - it('should remove multiple keys', () => { - const g = new Graph(); - MetadataUtil.add(g, { foo: 'bar', baz: 'qux' }); - const updatedMetadataNode = MetadataUtil.remove(g, ['foo', 'baz']); - expect(updatedMetadataNode['foo']).toBeUndefined(); - expect(updatedMetadataNode['baz']).toBeUndefined(); - }); - }); - - describe('setMetadataReceivingNode', () => { - const g = new Graph(); - it('should add an edge from from metadata to the receiving node', () => { - const n = g.addNode({ id: 'my-node', type: 'img_resize' }); - MetadataUtil.add(g, { foo: 'bar' }); - MetadataUtil.setMetadataReceivingNode(g, n); - // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing - expect(g.hasEdge(MetadataUtil.getNode(g), 'metadata', n, 'metadata')).toBe(true); - }); - it('should remove existing metadata edges', () => { - const n2 = g.addNode({ id: 'my-other-node', type: 'img_resize' }); - MetadataUtil.setMetadataReceivingNode(g, n2); - expect(g.getIncomers(n2).length).toBe(1); - // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing - expect(g.hasEdge(MetadataUtil.getNode(g), 'metadata', n2, 'metadata')).toBe(true); - }); - }); - - describe('getModelMetadataField', () => { - it('should return a ModelIdentifierField', () => { - const model: AnyModelConfig = { - key: 'model_key', - type: 'main', - hash: 'model_hash', - base: 'sd-1', - format: 'diffusers', - name: 'my model', - path: '/some/path', - source: 'www.models.com', - source_type: 'url', - }; - const metadataField = MetadataUtil.getModelMetadataField(model); - expect(isModelIdentifier(metadataField)).toBe(true); - expect(pick(model, ['key', 'hash', 'name', 'base', 'type'])).toEqual(metadataField); - }); - }); -}); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts b/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts deleted file mode 100644 index 38e57a5e65..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/MetadataUtil.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { ModelIdentifierField } from 'features/nodes/types/common'; -import { METADATA } from 'features/nodes/util/graph/constants'; -import { isString, unset } from 'lodash-es'; -import type { - AnyInvocation, - AnyInvocationIncMetadata, - AnyModelConfig, - CoreMetadataInvocation, - S, -} from 'services/api/types'; -import { assert } from 'tsafe'; - -import type { Graph } from './Graph'; - -const isCoreMetadata = (node: S['Graph']['nodes'][string]): node is CoreMetadataInvocation => - node.type === 'core_metadata'; - -export class MetadataUtil { - static metadataNodeId = METADATA; - - static getNode(g: Graph): CoreMetadataInvocation { - const node = g.getNode(this.metadataNodeId) as AnyInvocationIncMetadata; - assert(isCoreMetadata(node)); - return node; - } - - static add(g: Graph, metadata: Partial): CoreMetadataInvocation { - try { - const node = g.getNode(this.metadataNodeId) as AnyInvocationIncMetadata; - assert(isCoreMetadata(node)); - Object.assign(node, metadata); - return node; - } catch { - const metadataNode: CoreMetadataInvocation = { - id: this.metadataNodeId, - type: 'core_metadata', - ...metadata, - }; - // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing - return g.addNode(metadataNode); - } - } - - static remove(g: Graph, key: string): CoreMetadataInvocation; - static remove(g: Graph, keys: string[]): CoreMetadataInvocation; - static remove(g: Graph, keyOrKeys: string | string[]): CoreMetadataInvocation { - const metadataNode = this.getNode(g); - if (isString(keyOrKeys)) { - unset(metadataNode, keyOrKeys); - } else { - for (const key of keyOrKeys) { - unset(metadataNode, key); - } - } - return metadataNode; - } - - static setMetadataReceivingNode(g: Graph, node: AnyInvocation): void { - // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing - g.deleteEdgesFrom(this.getNode(g)); - // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing - g.addEdge(this.getNode(g), 'metadata', node, 'metadata'); - } - - static getModelMetadataField({ key, hash, name, base, type }: AnyModelConfig): ModelIdentifierField { - return { - key, - hash, - name, - base, - type, - }; - } -} diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts index 3c7c0c9c66..6d890a29e2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -31,7 +31,6 @@ import { T2I_ADAPTER_COLLECT, } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import { size } from 'lodash-es'; import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; @@ -245,7 +244,7 @@ export const addGenerationTabControlLayers = async ( } } - MetadataUtil.add(g, { control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); + g.upsertMetadata({ control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); return validLayers; }; @@ -490,7 +489,7 @@ const addInitialImageLayerToGraph = ( g.addEdge(i2l, 'height', noise, 'height'); } - MetadataUtil.add(g, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); + g.upsertMetadata({ generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); }; const isValidControlAdapter = (ca: ControlNetConfigV2 | T2IAdapterConfigV2, base: BaseModelType): boolean => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts index 7d1b20d018..b9adf6b8bb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts @@ -3,7 +3,6 @@ import { deepClone } from 'common/util/deepClone'; import { roundToMultiple } from 'common/util/roundDownToMultiple'; import type { Graph } from 'features/nodes/util/graph/Graph'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import type { Invocation } from 'services/api/types'; @@ -157,12 +156,12 @@ export const addGenerationTabHRF = ( g.addEdge(vaeSource, 'vae', l2iHrfHR, 'vae'); g.addEdge(denoiseHrf, 'latents', l2iHrfHR, 'latents'); - MetadataUtil.add(g, { + g.upsertMetadata({ hrf_strength: hrfStrength, hrf_enabled: hrfEnabled, hrf_method: hrfMethod, }); - MetadataUtil.setMetadataReceivingNode(g, l2iHrfHR); + g.setMetadataReceivingNode(l2iHrfHR); return l2iHrfHR; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts index 6374c72a93..ee812468b8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts @@ -1,7 +1,6 @@ import type { RootState } from 'app/store/store'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import { filter, size } from 'lodash-es'; import type { Invocation, S } from 'services/api/types'; @@ -69,5 +68,5 @@ export const addGenerationTabLoRAs = ( g.addEdge(loraSelector, 'lora', loraCollector, 'item'); } - MetadataUtil.add(g, { loras: loraMetadata }); + g.upsertMetadata({ loras: loraMetadata }); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts index 89f1f8f18e..bbd16e8f53 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts @@ -1,7 +1,6 @@ import type { RootState } from 'app/store/store'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import { filter, size } from 'lodash-es'; import type { Invocation, S } from 'services/api/types'; @@ -71,5 +70,5 @@ export const addGenerationTabSDXLLoRAs = ( g.addEdge(loraSelector, 'lora', loraCollector, 'item'); } - MetadataUtil.add(g, { loras: loraMetadata }); + g.upsertMetadata({ loras: loraMetadata }); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts index 7c207b75bb..0cbb637d03 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts @@ -1,7 +1,6 @@ import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import type { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import type { Invocation } from 'services/api/types'; import { isRefinerMainModelModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -92,7 +91,7 @@ export const addGenerationTabSDXLRefiner = async ( g.addEdge(denoise, 'latents', refinerDenoise, 'latents'); g.addEdge(refinerDenoise, 'latents', l2i, 'latents'); - MetadataUtil.add(g, { + g.upsertMetadata({ refiner_model: getModelMetadataField(modelConfig), refiner_positive_aesthetic_score: refinerPositiveAestheticScore, refiner_negative_aesthetic_score: refinerNegativeAestheticScore, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts index 709ba1416c..a3303e6c6f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts @@ -1,6 +1,5 @@ import type { RootState } from 'app/store/store'; import type { Graph } from 'features/nodes/util/graph/Graph'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import type { Invocation } from 'services/api/types'; import { SEAMLESS } from './constants'; @@ -36,7 +35,7 @@ export const addGenerationTabSeamless = ( seamless_y, }); - MetadataUtil.add(g, { + g.upsertMetadata({ seamless_x: seamless_x || undefined, seamless_y: seamless_y || undefined, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts index a50e722e43..c24353cc40 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts @@ -10,7 +10,6 @@ import { addGenerationTabWatermarker } from 'features/nodes/util/graph/addGenera import type { GraphType } from 'features/nodes/util/graph/Graph'; import { Graph } from 'features/nodes/util/graph/Graph'; import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; -import { MetadataUtil } from 'features/nodes/util/graph/MetadataUtil'; import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -128,7 +127,7 @@ export const buildGenerationTabGraph = async (state: RootState): Promise Date: Tue, 14 May 2024 13:46:08 +1000 Subject: [PATCH 122/442] tidy(ui): remove extraneous graph validate calls --- .../src/features/nodes/util/graph/buildGenerationTabGraph.ts | 4 ---- .../features/nodes/util/graph/buildGenerationTabSDXLGraph.ts | 3 --- 2 files changed, 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts index c24353cc40..3cbb33c315 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts @@ -143,13 +143,10 @@ export const buildGenerationTabGraph = async (state: RootState): Promise isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); if (isHRFAllowed && state.hrf.hrfEnabled) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts index bd51484995..3c5efaf73c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts @@ -135,13 +135,10 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise Date: Tue, 14 May 2024 14:04:56 +1000 Subject: [PATCH 123/442] tidy(ui): clean up graph builder helper functions --- .../src/features/nodes/util/graph/Graph.ts | 68 ++++++++----------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts index ae590b5044..3496d811d0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts @@ -34,7 +34,7 @@ export class Graph { constructor(id?: string) { this._graph = { - id: id ?? Graph.uuid(), + id: id ?? uuidv4(), nodes: {}, edges: [], }; @@ -50,7 +50,7 @@ export class Graph { * @raises `AssertionError` if a node with the same id already exists. */ addNode(node: Invocation): Invocation { - assert(this._graph.nodes[node.id] === undefined, Graph.getNodeAlreadyExistsMsg(node.id)); + assert(this._graph.nodes[node.id] === undefined, `Node with id ${node.id} already exists`); if (node.is_intermediate === undefined) { node.is_intermediate = true; } @@ -69,7 +69,7 @@ export class Graph { */ getNode(id: string): AnyInvocation { const node = this._graph.nodes[id]; - assert(node !== undefined, Graph.getNodeNotFoundMsg(id)); + assert(node !== undefined, `Node with id ${id} not found`); return node; } @@ -143,7 +143,7 @@ export class Graph { destination: { node_id: toNode.id, field: toField }, }; const edgeAlreadyExists = this._graph.edges.some((e) => isEqual(e, edge)); - assert(!edgeAlreadyExists, Graph.getEdgeAlreadyExistsMsg(fromNode.id, fromField, toNode.id, toField)); + assert(!edgeAlreadyExists, `Edge ${Graph.edgeToString(edge)} already exists`); this._graph.edges.push(edge); return edge; } @@ -156,15 +156,7 @@ export class Graph { */ addEdgeFromObj(edge: Edge): Edge { const edgeAlreadyExists = this._graph.edges.some((e) => isEqual(e, edge)); - assert( - !edgeAlreadyExists, - Graph.getEdgeAlreadyExistsMsg( - edge.source.node_id, - edge.source.field, - edge.destination.node_id, - edge.destination.field - ) - ); + assert(!edgeAlreadyExists, `Edge ${Graph.edgeToString(edge)} already exists`); this._graph.edges.push(edge); return edge; } @@ -191,7 +183,7 @@ export class Graph { e.destination.node_id === toNode.id && e.destination.field === toField ); - assert(edge !== undefined, Graph.getEdgeNotFoundMsg(fromNode.id, fromField, toNode.id, toField)); + assert(edge !== undefined, `Edge ${Graph.edgeToString(fromNode.id, fromField, toNode.id, toField)} not found`); return edge; } @@ -390,7 +382,9 @@ export class Graph { // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing this.addEdge(this._getMetadataNode(), 'metadata', node, 'metadata'); } + //#endregion + //#region Util /** * Given a model config, return the model metadata field. * @param modelConfig The model config entity @@ -399,33 +393,27 @@ export class Graph { static getModelMetadataField(modelConfig: AnyModelConfig): ModelIdentifierField { return zModelIdentifierField.parse(modelConfig); } - //#endregion - //#region Util - static getNodeNotFoundMsg(id: string): string { - return `Node ${id} not found`; + /** + * Given an edge object, return a string representation of the edge. + * @param edge The edge object + */ + static edgeToString(edge: Edge): string; + /** + * Given the source and destination nodes and fields, return a string representation of the edge. + * @param fromNodeId The source node id + * @param fromField The source field + * @param toNodeId The destination node id + * @param toField The destination field + */ + static edgeToString(fromNodeId: string, fromField: string, toNodeId: string, toField: string): string; + static edgeToString(fromNodeId: string | Edge, fromField?: string, toNodeId?: string, toField?: string): string { + if (typeof fromNodeId === 'object') { + const e = fromNodeId; + return `${e.source.node_id}.${e.source.field} -> ${e.destination.node_id}.${e.destination.field}`; + } + assert(fromField !== undefined && toNodeId !== undefined && toField !== undefined, 'Invalid edge arguments'); + return `${fromNodeId}.${fromField} -> ${toNodeId}.${toField}`; } - - static getNodeNotOfTypeMsg(node: AnyInvocation, expectedType: InvocationType): string { - return `Node ${node.id} is not of type ${expectedType}: ${node.type}`; - } - - static getNodeAlreadyExistsMsg(id: string): string { - return `Node ${id} already exists`; - } - - static getEdgeNotFoundMsg(fromNodeId: string, fromField: string, toNodeId: string, toField: string) { - return `Edge from ${fromNodeId}.${fromField} to ${toNodeId}.${toField} not found`; - } - - static getEdgeAlreadyExistsMsg(fromNodeId: string, fromField: string, toNodeId: string, toField: string) { - return `Edge from ${fromNodeId}.${fromField} to ${toNodeId}.${toField} already exists`; - } - - static edgeToString(edge: Edge): string { - return `${edge.source.node_id}.${edge.source.field} -> ${edge.destination.node_id}.${edge.destination.field}`; - } - - static uuid = uuidv4; //#endregion } From 9fb03d43ff2f622e3142743bfe5ed8da2c909df7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 14:12:19 +1000 Subject: [PATCH 124/442] tests(ui): get coverage to 100% for graph builder --- .../features/nodes/util/graph/Graph.test.ts | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts index 921f5d265e..94ad322e70 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts @@ -107,6 +107,29 @@ describe('Graph', () => { }); }); + describe('addEdgeFromObj', () => { + it('should add an edge to the graph with the provided values', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'add', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'sub', + }); + g.addEdgeFromObj({ + source: { node_id: n1.id, field: 'value' }, + destination: { node_id: n2.id, field: 'b' }, + }); + expect(g._graph.edges.length).toBe(1); + expect(g._graph.edges[0]).toEqual({ + source: { node_id: n1.id, field: 'value' }, + destination: { node_id: n2.id, field: 'b' }, + }); + }); + }); + describe('getNode', () => { const g = new Graph(); const node = g.addNode({ @@ -269,8 +292,9 @@ describe('Graph', () => { const g = new Graph(); expect(() => g.validate()).not.toThrow(); }); - it('should throw an error if the graph is invalid', () => { + it("should throw an error if the graph contains an edge without a source or destination node that doesn't exist", () => { const g = new Graph(); + // These nodes do not get added to the graph, only used to add the edge const add: Invocation<'add'> = { id: 'from-node', type: 'add', @@ -283,6 +307,42 @@ describe('Graph', () => { g.addEdge(add, 'value', sub, 'b'); expect(() => g.validate()).toThrowError(AssertionError); }); + it('should throw an error if a destination node is not a collect node and has multiple edges to it', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'add', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'add', + }); + const n3 = g.addNode({ + id: 'n3', + type: 'add', + }); + g.addEdge(n1, 'value', n3, 'a'); + g.addEdge(n2, 'value', n3, 'a'); + expect(() => g.validate()).toThrowError(AssertionError); + }); + it('should not throw an error if a destination node is a collect node and has multiple edges to it', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'add', + }); + const n2 = g.addNode({ + id: 'n2', + type: 'add', + }); + const n3 = g.addNode({ + id: 'n3', + type: 'collect', + }); + g.addEdge(n1, 'value', n3, 'item'); + g.addEdge(n2, 'value', n3, 'item'); + expect(() => g.validate()).not.toThrow(); + }); }); describe('traversal', () => { From b23989198687ffcb457a0c0c476e4228f4f42b7b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 14:21:52 +1000 Subject: [PATCH 125/442] tidy(ui): clean up base model handling in graph builder --- .../graph/addGenerationTabControlLayers.ts | 19 ++++++++----------- .../util/graph/buildGenerationTabGraph.ts | 2 ++ .../util/graph/buildGenerationTabSDXLGraph.ts | 2 ++ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts index 6d890a29e2..8b2e393767 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -39,6 +39,7 @@ import { assert } from 'tsafe'; export const addGenerationTabControlLayers = async ( state: RootState, g: Graph, + base: BaseModelType, denoise: Invocation<'denoise_latents'>, posCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, @@ -51,12 +52,9 @@ export const addGenerationTabControlLayers = async ( | Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> ): Promise => { - const mainModel = state.generation.model; - assert(mainModel, 'Missing main model when building graph'); - const isSDXL = mainModel.base === 'sdxl'; + const isSDXL = base === 'sdxl'; - // Filter out layers with incompatible base model, missing control image - const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, mainModel.base)); + const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, base)); const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); for (const ca of validControlAdapters) { @@ -71,7 +69,7 @@ export const addGenerationTabControlLayers = async ( const initialImageLayers = validLayers.filter(isInitialImageLayer); assert(initialImageLayers.length <= 1, 'Only one initial image layer allowed'); if (initialImageLayers[0]) { - addInitialImageLayerToGraph(state, g, denoise, noise, vaeSource, initialImageLayers[0]); + addInitialImageLayerToGraph(state, g, base, denoise, noise, vaeSource, initialImageLayers[0]); } // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing // the existing conditioning nodes. @@ -214,9 +212,7 @@ export const addGenerationTabControlLayers = async ( } } - const validRegionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipa) => - isValidIPAdapter(ipa, mainModel.base) - ); + const validRegionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); for (const ipAdapterConfig of validRegionalIPAdapters) { const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); @@ -428,6 +424,7 @@ const addGlobalIPAdapterToGraph = ( const addInitialImageLayerToGraph = ( state: RootState, g: Graph, + base: BaseModelType, denoise: Invocation<'denoise_latents'>, noise: Invocation<'noise'>, vaeSource: @@ -437,13 +434,13 @@ const addInitialImageLayerToGraph = ( | Invocation<'sdxl_model_loader'>, layer: InitialImageLayer ) => { - const { vaePrecision, model } = state.generation; + const { vaePrecision } = state.generation; const { refinerModel, refinerStart } = state.sdxl; const { width, height } = state.controlLayers.present.size; assert(layer.isEnabled, 'Initial image layer is not enabled'); assert(layer.image, 'Initial image layer has no image'); - const isSDXL = model?.base === 'sdxl'; + const isSDXL = base === 'sdxl'; const useRefinerStartEnd = isSDXL && Boolean(refinerModel); const { denoisingStrength } = layer; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts index 3cbb33c315..319bea3566 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts @@ -126,6 +126,7 @@ export const buildGenerationTabGraph = async (state: RootState): Promise Date: Tue, 14 May 2024 14:26:40 +1000 Subject: [PATCH 126/442] tidy(ui): organise CL graph builder --- .../graph/addGenerationTabControlLayers.ts | 101 +++++++++++------- 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts index 8b2e393767..9198b76ed3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts @@ -36,6 +36,20 @@ import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; import { assert } from 'tsafe'; +/** + * Adds the control layers to the graph + * @param state The app root state + * @param g The graph to add the layers to + * @param base The base model type + * @param denoise The main denoise node + * @param posCond The positive conditioning node + * @param negCond The negative conditioning node + * @param posCondCollect The positive conditioning collector + * @param negCondCollect The negative conditioning collector + * @param noise The noise node + * @param vaeSource The VAE source (either seamless, vae_loader, main_model_loader, or sdxl_model_loader) + * @returns A promise that resolves to the layers that were added to the graph + */ export const addGenerationTabControlLayers = async ( state: RootState, g: Graph, @@ -244,45 +258,7 @@ export const addGenerationTabControlLayers = async ( return validLayers; }; -const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { - if (layer.uploadedMaskImage) { - const imageDTO = await getImageDTO(layer.uploadedMaskImage.name); - if (imageDTO) { - return imageDTO; - } - } - const { dispatch } = getStore(); - // No cached mask, or the cached image no longer exists - we need to upload the mask image - const file = new File([blob], `${layer.id}_mask.png`, { type: 'image/png' }); - const req = dispatch( - imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) - ); - req.reset(); - - const imageDTO = await req.unwrap(); - dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO })); - return imageDTO; -}; - -const buildControlImage = ( - image: ImageWithDims | null, - processedImage: ImageWithDims | null, - processorConfig: ProcessorConfig | null -): ImageField => { - if (processedImage && processorConfig) { - // We've processed the image in the app - use it for the control image. - return { - image_name: processedImage.name, - }; - } else if (image) { - // No processor selected, and we have an image - the user provided a processed image, use it for the control image. - return { - image_name: image.name, - }; - } - assert(false, 'Attempted to add unprocessed control image'); -}; - +//#region Control Adapters const addGlobalControlAdapterToGraph = ( controlAdapterConfig: ControlNetConfigV2 | T2IAdapterConfigV2, g: Graph, @@ -379,6 +355,7 @@ const addGlobalT2IAdapterToGraph = ( g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); }; +//#region IP Adapter const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { try { // You see, we've already got one! @@ -420,7 +397,9 @@ const addGlobalIPAdapterToGraph = ( }); g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); }; +//#endregion +//#region Initial Image const addInitialImageLayerToGraph = ( state: RootState, g: Graph, @@ -488,7 +467,9 @@ const addInitialImageLayerToGraph = ( g.upsertMetadata({ generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); }; +//#endregion +//#region Layer validators const isValidControlAdapter = (ca: ControlNetConfigV2 | T2IAdapterConfigV2, base: BaseModelType): boolean => { // Must be have a model that matches the current base and must have a control image const hasModel = Boolean(ca.model); @@ -534,3 +515,45 @@ const isValidLayer = (layer: Layer, base: BaseModelType) => { } return false; }; +//#endregion + +//#region Helpers +const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { + if (layer.uploadedMaskImage) { + const imageDTO = await getImageDTO(layer.uploadedMaskImage.name); + if (imageDTO) { + return imageDTO; + } + } + const { dispatch } = getStore(); + // No cached mask, or the cached image no longer exists - we need to upload the mask image + const file = new File([blob], `${layer.id}_mask.png`, { type: 'image/png' }); + const req = dispatch( + imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) + ); + req.reset(); + + const imageDTO = await req.unwrap(); + dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO })); + return imageDTO; +}; + +const buildControlImage = ( + image: ImageWithDims | null, + processedImage: ImageWithDims | null, + processorConfig: ProcessorConfig | null +): ImageField => { + if (processedImage && processorConfig) { + // We've processed the image in the app - use it for the control image. + return { + image_name: processedImage.name, + }; + } else if (image) { + // No processor selected, and we have an image - the user provided a processed image, use it for the control image. + return { + image_name: image.name, + }; + } + assert(false, 'Attempted to add unprocessed control image'); +}; +//#endregion From 77024bfca7703a13bde2259a2dd0fc5bc283cb11 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 14:30:36 +1000 Subject: [PATCH 127/442] fix(ui): fix sdxl generation mode metadata --- .../features/nodes/util/graph/buildGenerationTabSDXLGraph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts index 2a1a266666..fba97f5899 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts @@ -120,7 +120,7 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise Date: Tue, 14 May 2024 19:54:20 +1000 Subject: [PATCH 128/442] tidy(ui): use Invocation<> type in control layers types --- .../controlLayers/util/controlAdapters.ts | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 41f1cf438f..7c9ead1899 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -3,24 +3,11 @@ import { zModelIdentifierField } from 'features/nodes/types/common'; import { merge, omit } from 'lodash-es'; import type { BaseModelType, - CannyImageProcessorInvocation, - ColorMapImageProcessorInvocation, - ContentShuffleImageProcessorInvocation, ControlNetModelConfig, - DepthAnythingImageProcessorInvocation, - DWOpenposeImageProcessorInvocation, Graph, - HedImageProcessorInvocation, ImageDTO, - LineartAnimeImageProcessorInvocation, - LineartImageProcessorInvocation, - MediapipeFaceProcessorInvocation, - MidasDepthImageProcessorInvocation, - MlsdImageProcessorInvocation, - NormalbaeImageProcessorInvocation, - PidiImageProcessorInvocation, + Invocation, T2IAdapterModelConfig, - ZoeDepthImageProcessorInvocation, } from 'services/api/types'; import { z } from 'zod'; @@ -33,7 +20,7 @@ const zCannyProcessorConfig = z.object({ high_threshold: z.number().int().gte(0).lte(255), }); export type _CannyProcessorConfig = Required< - Pick + Pick, 'id' | 'type' | 'low_threshold' | 'high_threshold'> >; export type CannyProcessorConfig = z.infer; @@ -43,7 +30,7 @@ const zColorMapProcessorConfig = z.object({ color_map_tile_size: z.number().int().gte(1), }); export type _ColorMapProcessorConfig = Required< - Pick + Pick, 'id' | 'type' | 'color_map_tile_size'> >; export type ColorMapProcessorConfig = z.infer; @@ -55,7 +42,7 @@ const zContentShuffleProcessorConfig = z.object({ f: z.number().int().gte(0), }); export type _ContentShuffleProcessorConfig = Required< - Pick + Pick, 'id' | 'type' | 'w' | 'h' | 'f'> >; export type ContentShuffleProcessorConfig = z.infer; @@ -69,7 +56,7 @@ const zDepthAnythingProcessorConfig = z.object({ model_size: zDepthAnythingModelSize, }); export type _DepthAnythingProcessorConfig = Required< - Pick + Pick, 'id' | 'type' | 'model_size'> >; export type DepthAnythingProcessorConfig = z.infer; @@ -78,14 +65,14 @@ const zHedProcessorConfig = z.object({ type: z.literal('hed_image_processor'), scribble: z.boolean(), }); -export type _HedProcessorConfig = Required>; +export type _HedProcessorConfig = Required, 'id' | 'type' | 'scribble'>>; export type HedProcessorConfig = z.infer; const zLineartAnimeProcessorConfig = z.object({ id: zId, type: z.literal('lineart_anime_image_processor'), }); -export type _LineartAnimeProcessorConfig = Required>; +export type _LineartAnimeProcessorConfig = Required, 'id' | 'type'>>; export type LineartAnimeProcessorConfig = z.infer; const zLineartProcessorConfig = z.object({ @@ -93,7 +80,7 @@ const zLineartProcessorConfig = z.object({ type: z.literal('lineart_image_processor'), coarse: z.boolean(), }); -export type _LineartProcessorConfig = Required>; +export type _LineartProcessorConfig = Required, 'id' | 'type' | 'coarse'>>; export type LineartProcessorConfig = z.infer; const zMediapipeFaceProcessorConfig = z.object({ @@ -103,7 +90,7 @@ const zMediapipeFaceProcessorConfig = z.object({ min_confidence: z.number().gte(0).lte(1), }); export type _MediapipeFaceProcessorConfig = Required< - Pick + Pick, 'id' | 'type' | 'max_faces' | 'min_confidence'> >; export type MediapipeFaceProcessorConfig = z.infer; @@ -114,7 +101,7 @@ const zMidasDepthProcessorConfig = z.object({ bg_th: z.number().gte(0), }); export type _MidasDepthProcessorConfig = Required< - Pick + Pick, 'id' | 'type' | 'a_mult' | 'bg_th'> >; export type MidasDepthProcessorConfig = z.infer; @@ -124,14 +111,16 @@ const zMlsdProcessorConfig = z.object({ thr_v: z.number().gte(0), thr_d: z.number().gte(0), }); -export type _MlsdProcessorConfig = Required>; +export type _MlsdProcessorConfig = Required< + Pick, 'id' | 'type' | 'thr_v' | 'thr_d'> +>; export type MlsdProcessorConfig = z.infer; const zNormalbaeProcessorConfig = z.object({ id: zId, type: z.literal('normalbae_image_processor'), }); -export type _NormalbaeProcessorConfig = Required>; +export type _NormalbaeProcessorConfig = Required, 'id' | 'type'>>; export type NormalbaeProcessorConfig = z.infer; const zDWOpenposeProcessorConfig = z.object({ @@ -142,7 +131,7 @@ const zDWOpenposeProcessorConfig = z.object({ draw_hands: z.boolean(), }); export type _DWOpenposeProcessorConfig = Required< - Pick + Pick, 'id' | 'type' | 'draw_body' | 'draw_face' | 'draw_hands'> >; export type DWOpenposeProcessorConfig = z.infer; @@ -152,14 +141,16 @@ const zPidiProcessorConfig = z.object({ safe: z.boolean(), scribble: z.boolean(), }); -export type _PidiProcessorConfig = Required>; +export type _PidiProcessorConfig = Required< + Pick, 'id' | 'type' | 'safe' | 'scribble'> +>; export type PidiProcessorConfig = z.infer; const zZoeDepthProcessorConfig = z.object({ id: zId, type: z.literal('zoe_depth_image_processor'), }); -export type _ZoeDepthProcessorConfig = Required>; +export type _ZoeDepthProcessorConfig = Required, 'id' | 'type'>>; export type ZoeDepthProcessorConfig = z.infer; const zProcessorConfig = z.discriminatedUnion('type', [ From c8f30b1392917f90648add08827df6ffe3ead2ea Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 20:01:55 +1000 Subject: [PATCH 129/442] tidy(ui): move testing-only types to test file --- .../util/controlAdapters.test.ts | 44 +++++++++++++------ .../controlLayers/util/controlAdapters.ts | 41 +---------------- 2 files changed, 31 insertions(+), 54 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts index fa617b0541..22f54d622c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts @@ -4,20 +4,6 @@ import { assert } from 'tsafe'; import { describe, test } from 'vitest'; import type { - _CannyProcessorConfig, - _ColorMapProcessorConfig, - _ContentShuffleProcessorConfig, - _DepthAnythingProcessorConfig, - _DWOpenposeProcessorConfig, - _HedProcessorConfig, - _LineartAnimeProcessorConfig, - _LineartProcessorConfig, - _MediapipeFaceProcessorConfig, - _MidasDepthProcessorConfig, - _MlsdProcessorConfig, - _NormalbaeProcessorConfig, - _PidiProcessorConfig, - _ZoeDepthProcessorConfig, CannyProcessorConfig, CLIPVisionModelV2, ColorMapProcessorConfig, @@ -75,3 +61,33 @@ describe('Control Adapter Types', () => { assert>(); }); }); + +// Types derived from OpenAPI +type _CannyProcessorConfig = Required< + Pick, 'id' | 'type' | 'low_threshold' | 'high_threshold'> +>; +type _ColorMapProcessorConfig = Required< + Pick, 'id' | 'type' | 'color_map_tile_size'> +>; +type _ContentShuffleProcessorConfig = Required< + Pick, 'id' | 'type' | 'w' | 'h' | 'f'> +>; +type _DepthAnythingProcessorConfig = Required< + Pick, 'id' | 'type' | 'model_size'> +>; +type _HedProcessorConfig = Required, 'id' | 'type' | 'scribble'>>; +type _LineartAnimeProcessorConfig = Required, 'id' | 'type'>>; +type _LineartProcessorConfig = Required, 'id' | 'type' | 'coarse'>>; +type _MediapipeFaceProcessorConfig = Required< + Pick, 'id' | 'type' | 'max_faces' | 'min_confidence'> +>; +type _MidasDepthProcessorConfig = Required< + Pick, 'id' | 'type' | 'a_mult' | 'bg_th'> +>; +type _MlsdProcessorConfig = Required, 'id' | 'type' | 'thr_v' | 'thr_d'>>; +type _NormalbaeProcessorConfig = Required, 'id' | 'type'>>; +type _DWOpenposeProcessorConfig = Required< + Pick, 'id' | 'type' | 'draw_body' | 'draw_face' | 'draw_hands'> +>; +type _PidiProcessorConfig = Required, 'id' | 'type' | 'safe' | 'scribble'>>; +type _ZoeDepthProcessorConfig = Required, 'id' | 'type'>>; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 7c9ead1899..708e089008 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -1,14 +1,7 @@ import { deepClone } from 'common/util/deepClone'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { merge, omit } from 'lodash-es'; -import type { - BaseModelType, - ControlNetModelConfig, - Graph, - ImageDTO, - Invocation, - T2IAdapterModelConfig, -} from 'services/api/types'; +import type { BaseModelType, ControlNetModelConfig, Graph, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; import { z } from 'zod'; const zId = z.string().min(1); @@ -19,9 +12,6 @@ const zCannyProcessorConfig = z.object({ low_threshold: z.number().int().gte(0).lte(255), high_threshold: z.number().int().gte(0).lte(255), }); -export type _CannyProcessorConfig = Required< - Pick, 'id' | 'type' | 'low_threshold' | 'high_threshold'> ->; export type CannyProcessorConfig = z.infer; const zColorMapProcessorConfig = z.object({ @@ -29,9 +19,6 @@ const zColorMapProcessorConfig = z.object({ type: z.literal('color_map_image_processor'), color_map_tile_size: z.number().int().gte(1), }); -export type _ColorMapProcessorConfig = Required< - Pick, 'id' | 'type' | 'color_map_tile_size'> ->; export type ColorMapProcessorConfig = z.infer; const zContentShuffleProcessorConfig = z.object({ @@ -41,9 +28,6 @@ const zContentShuffleProcessorConfig = z.object({ h: z.number().int().gte(0), f: z.number().int().gte(0), }); -export type _ContentShuffleProcessorConfig = Required< - Pick, 'id' | 'type' | 'w' | 'h' | 'f'> ->; export type ContentShuffleProcessorConfig = z.infer; const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']); @@ -55,9 +39,6 @@ const zDepthAnythingProcessorConfig = z.object({ type: z.literal('depth_anything_image_processor'), model_size: zDepthAnythingModelSize, }); -export type _DepthAnythingProcessorConfig = Required< - Pick, 'id' | 'type' | 'model_size'> ->; export type DepthAnythingProcessorConfig = z.infer; const zHedProcessorConfig = z.object({ @@ -65,14 +46,12 @@ const zHedProcessorConfig = z.object({ type: z.literal('hed_image_processor'), scribble: z.boolean(), }); -export type _HedProcessorConfig = Required, 'id' | 'type' | 'scribble'>>; export type HedProcessorConfig = z.infer; const zLineartAnimeProcessorConfig = z.object({ id: zId, type: z.literal('lineart_anime_image_processor'), }); -export type _LineartAnimeProcessorConfig = Required, 'id' | 'type'>>; export type LineartAnimeProcessorConfig = z.infer; const zLineartProcessorConfig = z.object({ @@ -80,7 +59,6 @@ const zLineartProcessorConfig = z.object({ type: z.literal('lineart_image_processor'), coarse: z.boolean(), }); -export type _LineartProcessorConfig = Required, 'id' | 'type' | 'coarse'>>; export type LineartProcessorConfig = z.infer; const zMediapipeFaceProcessorConfig = z.object({ @@ -89,9 +67,6 @@ const zMediapipeFaceProcessorConfig = z.object({ max_faces: z.number().int().gte(1), min_confidence: z.number().gte(0).lte(1), }); -export type _MediapipeFaceProcessorConfig = Required< - Pick, 'id' | 'type' | 'max_faces' | 'min_confidence'> ->; export type MediapipeFaceProcessorConfig = z.infer; const zMidasDepthProcessorConfig = z.object({ @@ -100,9 +75,6 @@ const zMidasDepthProcessorConfig = z.object({ a_mult: z.number().gte(0), bg_th: z.number().gte(0), }); -export type _MidasDepthProcessorConfig = Required< - Pick, 'id' | 'type' | 'a_mult' | 'bg_th'> ->; export type MidasDepthProcessorConfig = z.infer; const zMlsdProcessorConfig = z.object({ @@ -111,16 +83,12 @@ const zMlsdProcessorConfig = z.object({ thr_v: z.number().gte(0), thr_d: z.number().gte(0), }); -export type _MlsdProcessorConfig = Required< - Pick, 'id' | 'type' | 'thr_v' | 'thr_d'> ->; export type MlsdProcessorConfig = z.infer; const zNormalbaeProcessorConfig = z.object({ id: zId, type: z.literal('normalbae_image_processor'), }); -export type _NormalbaeProcessorConfig = Required, 'id' | 'type'>>; export type NormalbaeProcessorConfig = z.infer; const zDWOpenposeProcessorConfig = z.object({ @@ -130,9 +98,6 @@ const zDWOpenposeProcessorConfig = z.object({ draw_face: z.boolean(), draw_hands: z.boolean(), }); -export type _DWOpenposeProcessorConfig = Required< - Pick, 'id' | 'type' | 'draw_body' | 'draw_face' | 'draw_hands'> ->; export type DWOpenposeProcessorConfig = z.infer; const zPidiProcessorConfig = z.object({ @@ -141,16 +106,12 @@ const zPidiProcessorConfig = z.object({ safe: z.boolean(), scribble: z.boolean(), }); -export type _PidiProcessorConfig = Required< - Pick, 'id' | 'type' | 'safe' | 'scribble'> ->; export type PidiProcessorConfig = z.infer; const zZoeDepthProcessorConfig = z.object({ id: zId, type: z.literal('zoe_depth_image_processor'), }); -export type _ZoeDepthProcessorConfig = Required, 'id' | 'type'>>; export type ZoeDepthProcessorConfig = z.infer; const zProcessorConfig = z.discriminatedUnion('type', [ From cadea55521165abc921c87d8d53a969b3ee46a64 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 20:12:32 +1000 Subject: [PATCH 130/442] tidy(ui): organise graph builder files --- .../listeners/enqueueRequestedCanvas.ts | 2 +- .../listeners/enqueueRequestedLinear.ts | 4 +- .../util/graph/buildAdHocUpscaleGraph.ts | 2 +- .../util/graph/buildLinearBatchConfig.ts | 2 +- .../addControlNetToLinearGraph.ts | 5 +-- .../{ => canvas}/addIPAdapterToLinearGraph.ts | 5 +-- .../graph/{ => canvas}/addLoRAsToGraph.ts | 11 +++-- .../{ => canvas}/addNSFWCheckerToGraph.ts | 5 +-- .../graph/{ => canvas}/addSDXLLoRAstoGraph.ts | 9 ++--- .../{ => canvas}/addSDXLRefinerToGraph.ts | 11 +++-- .../{ => canvas}/addSeamlessToLinearGraph.ts | 7 ++-- .../addT2IAdapterToLinearGraph.ts | 5 +-- .../util/graph/{ => canvas}/addVAEToGraph.ts | 7 ++-- .../{ => canvas}/addWatermarkerToGraph.ts | 5 +-- .../graph/{ => canvas}/buildCanvasGraph.ts | 0 .../buildCanvasImageToImageGraph.ts | 30 +++++++------- .../{ => canvas}/buildCanvasInpaintGraph.ts | 38 +++++++++--------- .../{ => canvas}/buildCanvasOutpaintGraph.ts | 38 +++++++++--------- .../buildCanvasSDXLImageToImageGraph.ts | 32 +++++++-------- .../buildCanvasSDXLInpaintGraph.ts | 38 +++++++++--------- .../buildCanvasSDXLOutpaintGraph.ts | 40 +++++++++---------- .../buildCanvasSDXLTextToImageGraph.ts | 28 ++++++------- .../buildCanvasTextToImageGraph.ts | 26 ++++++------ .../nodes/util/graph/{ => canvas}/metadata.ts | 3 +- .../util/graph/{ => generation}/Graph.test.ts | 2 +- .../util/graph/{ => generation}/Graph.ts | 0 .../addGenerationTabControlLayers.ts | 2 +- .../{ => generation}/addGenerationTabHRF.ts | 13 +++--- .../{ => generation}/addGenerationTabLoRAs.ts | 5 +-- .../addGenerationTabNSFWChecker.ts | 5 +-- .../addGenerationTabSDXLLoRAs.ts | 5 +-- .../addGenerationTabSDXLRefiner.ts | 13 +++--- .../addGenerationTabSeamless.ts | 5 +-- .../addGenerationTabWatermarker.ts | 5 +-- .../buildGenerationTabGraph.ts | 30 +++++++------- .../buildGenerationTabSDXLGraph.ts | 28 ++++++------- 36 files changed, 225 insertions(+), 241 deletions(-) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addControlNetToLinearGraph.ts (96%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addIPAdapterToLinearGraph.ts (95%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addLoRAsToGraph.ts (94%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addNSFWCheckerToGraph.ts (84%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addSDXLLoRAstoGraph.ts (97%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addSDXLRefinerToGraph.ts (96%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addSeamlessToLinearGraph.ts (94%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addT2IAdapterToLinearGraph.ts (96%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addVAEToGraph.ts (97%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/addWatermarkerToGraph.ts (89%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/buildCanvasGraph.ts (100%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/buildCanvasImageToImageGraph.ts (98%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/buildCanvasInpaintGraph.ts (98%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/buildCanvasOutpaintGraph.ts (99%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/buildCanvasSDXLImageToImageGraph.ts (98%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/buildCanvasSDXLInpaintGraph.ts (99%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/buildCanvasSDXLOutpaintGraph.ts (99%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/buildCanvasSDXLTextToImageGraph.ts (97%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/buildCanvasTextToImageGraph.ts (98%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => canvas}/metadata.ts (96%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/Graph.test.ts (99%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/Graph.ts (100%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/addGenerationTabControlLayers.ts (99%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/addGenerationTabHRF.ts (96%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/addGenerationTabLoRAs.ts (94%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/addGenerationTabNSFWChecker.ts (84%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/addGenerationTabSDXLLoRAs.ts (94%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/addGenerationTabSDXLRefiner.ts (94%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/addGenerationTabSeamless.ts (92%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/addGenerationTabWatermarker.ts (84%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/buildGenerationTabGraph.ts (90%) rename invokeai/frontend/web/src/features/nodes/util/graph/{ => generation}/buildGenerationTabSDXLGraph.ts (90%) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts index cdcc99ade2..a7491ab01b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts @@ -8,8 +8,8 @@ import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode'; import { canvasGraphBuilt } from 'features/nodes/store/actions'; -import { buildCanvasGraph } from 'features/nodes/util/graph/buildCanvasGraph'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; +import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph'; import { imagesApi } from 'services/api/endpoints/images'; import { queueApi } from 'services/api/endpoints/queue'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 195bb5639d..6ca7ee7ffa 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,9 +1,9 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph'; -import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; +import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph'; +import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph'; import { queueApi } from 'services/api/endpoints/queue'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts index 6c90dafd25..74631c99cf 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts @@ -2,8 +2,8 @@ import type { RootState } from 'app/store/store'; import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ESRGANInvocation, Graph, NonNullableGraph } from 'services/api/types'; +import { addCoreMetadataNode, upsertMetadata } from './canvas/metadata'; import { ESRGAN } from './constants'; -import { addCoreMetadataNode, upsertMetadata } from './metadata'; type Arg = { image_name: string; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index 7bda644dfe..28eb083844 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -5,8 +5,8 @@ import { range } from 'lodash-es'; import type { components } from 'services/api/schema'; import type { Batch, BatchConfig, NonNullableGraph } from 'services/api/types'; +import { getHasMetadata, removeMetadata } from './canvas/metadata'; import { CANVAS_COHERENCE_NOISE, METADATA, NOISE, POSITIVE_CONDITIONING } from './constants'; -import { getHasMetadata, removeMetadata } from './metadata'; export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => { const { iterations, model, shouldRandomizeSeed, seed } = state.generation; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts similarity index 96% rename from invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts index 531c88335b..c9ffc35b9c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts @@ -2,6 +2,8 @@ import type { RootState } from 'app/store/store'; import { selectValidControlNets } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { ControlAdapterProcessorType, ControlNetConfig } from 'features/controlAdapters/store/types'; import type { ImageField } from 'features/nodes/types/common'; +import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; +import { CONTROL_NET_COLLECT } from 'features/nodes/util/graph/constants'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import type { CollectInvocation, @@ -12,9 +14,6 @@ import type { } from 'services/api/types'; import { assert } from 'tsafe'; -import { CONTROL_NET_COLLECT } from './constants'; -import { upsertMetadata } from './metadata'; - export const addControlNetToLinearGraph = async ( state: RootState, graph: NonNullableGraph, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts similarity index 95% rename from invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts index 2cf93100eb..607e7f72a4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts @@ -2,6 +2,8 @@ import type { RootState } from 'app/store/store'; import { selectValidIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { IPAdapterConfig } from 'features/controlAdapters/store/types'; import type { ImageField } from 'features/nodes/types/common'; +import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; +import { IP_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import type { CollectInvocation, @@ -12,9 +14,6 @@ import type { } from 'services/api/types'; import { assert } from 'tsafe'; -import { IP_ADAPTER_COLLECT } from './constants'; -import { upsertMetadata } from './metadata'; - export const addIPAdapterToLinearGraph = async ( state: RootState, graph: NonNullableGraph, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts similarity index 94% rename from invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts index 28981a1a8a..998f275a1e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts @@ -1,11 +1,16 @@ import type { RootState } from 'app/store/store'; import { zModelIdentifierField } from 'features/nodes/types/common'; +import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; +import { + CLIP_SKIP, + LORA_LOADER, + MAIN_MODEL_LOADER, + NEGATIVE_CONDITIONING, + POSITIVE_CONDITIONING, +} from 'features/nodes/util/graph/constants'; import { filter, size } from 'lodash-es'; import type { CoreMetadataInvocation, LoRALoaderInvocation, NonNullableGraph } from 'services/api/types'; -import { CLIP_SKIP, LORA_LOADER, MAIN_MODEL_LOADER, NEGATIVE_CONDITIONING, POSITIVE_CONDITIONING } from './constants'; -import { upsertMetadata } from './metadata'; - export const addLoRAsToGraph = async ( state: RootState, graph: NonNullableGraph, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addNSFWCheckerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts similarity index 84% rename from invokeai/frontend/web/src/features/nodes/util/graph/addNSFWCheckerToGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts index 35fc324689..6b73aa9f63 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addNSFWCheckerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts @@ -1,9 +1,8 @@ import type { RootState } from 'app/store/store'; +import { LATENTS_TO_IMAGE, NSFW_CHECKER } from 'features/nodes/util/graph/constants'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ImageNSFWBlurInvocation, LatentsToImageInvocation, NonNullableGraph } from 'services/api/types'; -import { LATENTS_TO_IMAGE, NSFW_CHECKER } from './constants'; -import { getBoardField, getIsIntermediate } from './graphBuilderUtils'; - export const addNSFWCheckerToGraph = ( state: RootState, graph: NonNullableGraph, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts similarity index 97% rename from invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts index 1a803102b1..9b8665e6db 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts @@ -1,8 +1,6 @@ import type { RootState } from 'app/store/store'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import { filter, size } from 'lodash-es'; -import type { CoreMetadataInvocation, NonNullableGraph, SDXLLoRALoaderInvocation } from 'services/api/types'; - +import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; import { LORA_LOADER, NEGATIVE_CONDITIONING, @@ -10,8 +8,9 @@ import { SDXL_MODEL_LOADER, SDXL_REFINER_INPAINT_CREATE_MASK, SEAMLESS, -} from './constants'; -import { upsertMetadata } from './metadata'; +} from 'features/nodes/util/graph/constants'; +import { filter, size } from 'lodash-es'; +import type { CoreMetadataInvocation, NonNullableGraph, SDXLLoRALoaderInvocation } from 'services/api/types'; export const addSDXLLoRAsToGraph = async ( state: RootState, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLRefinerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts similarity index 96% rename from invokeai/frontend/web/src/features/nodes/util/graph/addSDXLRefinerToGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts index 9df1d63630..df4d6f1364 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLRefinerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts @@ -1,8 +1,6 @@ import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import type { NonNullableGraph, SeamlessModeInvocation } from 'services/api/types'; -import { isRefinerMainModelModelConfig } from 'services/api/types'; - +import { getModelMetadataField, upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; import { CANVAS_OUTPUT, INPAINT_CREATE_MASK, @@ -17,9 +15,10 @@ import { SDXL_REFINER_NEGATIVE_CONDITIONING, SDXL_REFINER_POSITIVE_CONDITIONING, SDXL_REFINER_SEAMLESS, -} from './constants'; -import { getSDXLStylePrompts } from './graphBuilderUtils'; -import { getModelMetadataField, upsertMetadata } from './metadata'; +} from 'features/nodes/util/graph/constants'; +import { getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; +import type { NonNullableGraph, SeamlessModeInvocation } from 'services/api/types'; +import { isRefinerMainModelModelConfig } from 'services/api/types'; export const addSDXLRefinerToGraph = async ( state: RootState, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts similarity index 94% rename from invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts index d6fcd411a4..98bf62d32f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts @@ -1,6 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { NonNullableGraph, SeamlessModeInvocation } from 'services/api/types'; - +import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; import { DENOISE_LATENTS, SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, @@ -11,8 +10,8 @@ import { SDXL_DENOISE_LATENTS, SEAMLESS, VAE_LOADER, -} from './constants'; -import { upsertMetadata } from './metadata'; +} from 'features/nodes/util/graph/constants'; +import type { NonNullableGraph, SeamlessModeInvocation } from 'services/api/types'; export const addSeamlessToLinearGraph = ( state: RootState, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts similarity index 96% rename from invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts index ee21bbff1b..f60cb8366d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts @@ -2,6 +2,8 @@ import type { RootState } from 'app/store/store'; import { selectValidT2IAdapters } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { ControlAdapterProcessorType, T2IAdapterConfig } from 'features/controlAdapters/store/types'; import type { ImageField } from 'features/nodes/types/common'; +import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; +import { T2I_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import type { CollectInvocation, @@ -12,9 +14,6 @@ import type { } from 'services/api/types'; import { assert } from 'tsafe'; -import { T2I_ADAPTER_COLLECT } from './constants'; -import { upsertMetadata } from './metadata'; - export const addT2IAdaptersToLinearGraph = async ( state: RootState, graph: NonNullableGraph, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts similarity index 97% rename from invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts index f464723381..dfa3818d07 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts @@ -1,6 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { NonNullableGraph } from 'services/api/types'; - +import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; import { CANVAS_IMAGE_TO_IMAGE_GRAPH, CANVAS_INPAINT_GRAPH, @@ -21,8 +20,8 @@ import { SDXL_REFINER_SEAMLESS, SEAMLESS, VAE_LOADER, -} from './constants'; -import { upsertMetadata } from './metadata'; +} from 'features/nodes/util/graph/constants'; +import type { NonNullableGraph } from 'services/api/types'; export const addVAEToGraph = async ( state: RootState, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addWatermarkerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts similarity index 89% rename from invokeai/frontend/web/src/features/nodes/util/graph/addWatermarkerToGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts index 61beb11df4..1759646c54 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addWatermarkerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts @@ -1,4 +1,6 @@ import type { RootState } from 'app/store/store'; +import { LATENTS_TO_IMAGE, NSFW_CHECKER, WATERMARKER } from 'features/nodes/util/graph/constants'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ImageNSFWBlurInvocation, ImageWatermarkInvocation, @@ -6,9 +8,6 @@ import type { NonNullableGraph, } from 'services/api/types'; -import { LATENTS_TO_IMAGE, NSFW_CHECKER, WATERMARKER } from './constants'; -import { getBoardField, getIsIntermediate } from './graphBuilderUtils'; - export const addWatermarkerToGraph = ( state: RootState, graph: NonNullableGraph, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts similarity index 100% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts similarity index 98% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts index f2c9957edc..b165699d59 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts @@ -1,6 +1,21 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; +import { addCoreMetadataNode, getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; +import { + CANVAS_IMAGE_TO_IMAGE_GRAPH, + CANVAS_OUTPUT, + CLIP_SKIP, + DENOISE_LATENTS, + IMAGE_TO_LATENTS, + IMG2IMG_RESIZE, + LATENTS_TO_IMAGE, + MAIN_MODEL_LOADER, + NEGATIVE_CONDITIONING, + NOISE, + POSITIVE_CONDITIONING, + SEAMLESS, +} from 'features/nodes/util/graph/constants'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import { type ImageDTO, @@ -17,21 +32,6 @@ import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; -import { - CANVAS_IMAGE_TO_IMAGE_GRAPH, - CANVAS_OUTPUT, - CLIP_SKIP, - DENOISE_LATENTS, - IMAGE_TO_LATENTS, - IMG2IMG_RESIZE, - LATENTS_TO_IMAGE, - MAIN_MODEL_LOADER, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SEAMLESS, -} from './constants'; -import { addCoreMetadataNode, getModelMetadataField } from './metadata'; /** * Builds the Canvas tab's Image to Image graph. diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts similarity index 98% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts index ab73953008..082572e43a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts @@ -1,22 +1,5 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; -import type { - CanvasPasteBackInvocation, - CreateGradientMaskInvocation, - ImageDTO, - ImageToLatentsInvocation, - NoiseInvocation, - NonNullableGraph, -} from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { CANVAS_INPAINT_GRAPH, CANVAS_OUTPUT, @@ -34,8 +17,25 @@ import { NOISE, POSITIVE_CONDITIONING, SEAMLESS, -} from './constants'; -import { getBoardField, getIsIntermediate } from './graphBuilderUtils'; +} from 'features/nodes/util/graph/constants'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; +import type { + CanvasPasteBackInvocation, + CreateGradientMaskInvocation, + ImageDTO, + ImageToLatentsInvocation, + NoiseInvocation, + NonNullableGraph, +} from 'services/api/types'; + +import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; +import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; +import { addLoRAsToGraph } from './addLoRAsToGraph'; +import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; +import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; +import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; +import { addVAEToGraph } from './addVAEToGraph'; +import { addWatermarkerToGraph } from './addWatermarkerToGraph'; /** * Builds the Canvas tab's Inpaint graph. diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts similarity index 99% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts index 6114cb5e5c..598db7db48 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts @@ -1,22 +1,5 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; -import type { - ImageDTO, - ImageToLatentsInvocation, - InfillPatchMatchInvocation, - InfillTileInvocation, - NoiseInvocation, - NonNullableGraph, -} from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { CANVAS_OUTPAINT_GRAPH, CANVAS_OUTPUT, @@ -38,8 +21,25 @@ import { NOISE, POSITIVE_CONDITIONING, SEAMLESS, -} from './constants'; -import { getBoardField, getIsIntermediate } from './graphBuilderUtils'; +} from 'features/nodes/util/graph/constants'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; +import type { + ImageDTO, + ImageToLatentsInvocation, + InfillPatchMatchInvocation, + InfillTileInvocation, + NoiseInvocation, + NonNullableGraph, +} from 'services/api/types'; + +import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; +import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; +import { addLoRAsToGraph } from './addLoRAsToGraph'; +import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; +import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; +import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; +import { addVAEToGraph } from './addVAEToGraph'; +import { addWatermarkerToGraph } from './addWatermarkerToGraph'; /** * Builds the Canvas tab's Outpaint graph. diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts similarity index 98% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts index ee918d1470..4db1532f76 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts @@ -1,6 +1,22 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; +import { addCoreMetadataNode, getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; +import { + CANVAS_OUTPUT, + IMAGE_TO_LATENTS, + IMG2IMG_RESIZE, + LATENTS_TO_IMAGE, + NEGATIVE_CONDITIONING, + NOISE, + POSITIVE_CONDITIONING, + SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, + SDXL_DENOISE_LATENTS, + SDXL_MODEL_LOADER, + SDXL_REFINER_SEAMLESS, + SEAMLESS, +} from 'features/nodes/util/graph/constants'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; import { type ImageDTO, type ImageToLatentsInvocation, @@ -17,22 +33,6 @@ import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; -import { - CANVAS_OUTPUT, - IMAGE_TO_LATENTS, - IMG2IMG_RESIZE, - LATENTS_TO_IMAGE, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, - SDXL_DENOISE_LATENTS, - SDXL_MODEL_LOADER, - SDXL_REFINER_SEAMLESS, - SEAMLESS, -} from './constants'; -import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; -import { addCoreMetadataNode, getModelMetadataField } from './metadata'; /** * Builds the Canvas tab's Image to Image graph. diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts similarity index 99% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts index 68b948a44a..f3ade076e7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts @@ -1,5 +1,24 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { + CANVAS_OUTPUT, + INPAINT_CREATE_MASK, + INPAINT_IMAGE, + INPAINT_IMAGE_RESIZE_DOWN, + INPAINT_IMAGE_RESIZE_UP, + LATENTS_TO_IMAGE, + MASK_RESIZE_DOWN, + MASK_RESIZE_UP, + NEGATIVE_CONDITIONING, + NOISE, + POSITIVE_CONDITIONING, + SDXL_CANVAS_INPAINT_GRAPH, + SDXL_DENOISE_LATENTS, + SDXL_MODEL_LOADER, + SDXL_REFINER_SEAMLESS, + SEAMLESS, +} from 'features/nodes/util/graph/constants'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; import type { CanvasPasteBackInvocation, CreateGradientMaskInvocation, @@ -18,25 +37,6 @@ import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; -import { - CANVAS_OUTPUT, - INPAINT_CREATE_MASK, - INPAINT_IMAGE, - INPAINT_IMAGE_RESIZE_DOWN, - INPAINT_IMAGE_RESIZE_UP, - LATENTS_TO_IMAGE, - MASK_RESIZE_DOWN, - MASK_RESIZE_UP, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SDXL_CANVAS_INPAINT_GRAPH, - SDXL_DENOISE_LATENTS, - SDXL_MODEL_LOADER, - SDXL_REFINER_SEAMLESS, - SEAMLESS, -} from './constants'; -import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; /** * Builds the Canvas tab's Inpaint graph. diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts similarity index 99% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts index eca498bf79..ce8e87bcb5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts @@ -1,23 +1,5 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; -import type { - ImageDTO, - ImageToLatentsInvocation, - InfillPatchMatchInvocation, - InfillTileInvocation, - NoiseInvocation, - NonNullableGraph, -} from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; -import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { CANVAS_OUTPUT, INPAINT_CREATE_MASK, @@ -39,8 +21,26 @@ import { SDXL_MODEL_LOADER, SDXL_REFINER_SEAMLESS, SEAMLESS, -} from './constants'; -import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; +} from 'features/nodes/util/graph/constants'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; +import type { + ImageDTO, + ImageToLatentsInvocation, + InfillPatchMatchInvocation, + InfillTileInvocation, + NoiseInvocation, + NonNullableGraph, +} from 'services/api/types'; + +import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; +import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; +import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; +import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; +import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; +import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; +import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; +import { addVAEToGraph } from './addVAEToGraph'; +import { addWatermarkerToGraph } from './addWatermarkerToGraph'; /** * Builds the Canvas tab's Outpaint graph. diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts similarity index 97% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts index f6ac645580..b2a8aa6ada 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts @@ -1,17 +1,7 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; -import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; +import { addCoreMetadataNode, getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; import { CANVAS_OUTPUT, LATENTS_TO_IMAGE, @@ -23,9 +13,19 @@ import { SDXL_MODEL_LOADER, SDXL_REFINER_SEAMLESS, SEAMLESS, -} from './constants'; -import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; -import { addCoreMetadataNode, getModelMetadataField } from './metadata'; +} from 'features/nodes/util/graph/constants'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; +import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; + +import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; +import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; +import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; +import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; +import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; +import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; +import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; +import { addVAEToGraph } from './addVAEToGraph'; +import { addWatermarkerToGraph } from './addWatermarkerToGraph'; /** * Builds the Canvas tab's Text to Image graph. diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts similarity index 98% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts index 0749308fb8..8ce5134480 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts @@ -1,17 +1,7 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; +import { addCoreMetadataNode, getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; import { CANVAS_OUTPUT, CANVAS_TEXT_TO_IMAGE_GRAPH, @@ -23,8 +13,18 @@ import { NOISE, POSITIVE_CONDITIONING, SEAMLESS, -} from './constants'; -import { addCoreMetadataNode, getModelMetadataField } from './metadata'; +} from 'features/nodes/util/graph/constants'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; +import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; + +import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; +import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; +import { addLoRAsToGraph } from './addLoRAsToGraph'; +import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; +import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; +import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; +import { addVAEToGraph } from './addVAEToGraph'; +import { addWatermarkerToGraph } from './addWatermarkerToGraph'; /** * Builds the Canvas tab's Text to Image graph. diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts similarity index 96% rename from invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts index 366c8a936e..b3870c9636 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts @@ -1,9 +1,8 @@ import type { JSONObject } from 'common/types'; import type { ModelIdentifierField } from 'features/nodes/types/common'; +import { METADATA } from 'features/nodes/util/graph/constants'; import type { AnyModelConfig, CoreMetadataInvocation, NonNullableGraph } from 'services/api/types'; -import { METADATA } from './constants'; - export const addCoreMetadataNode = ( graph: NonNullableGraph, metadata: Partial, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts similarity index 99% rename from invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts index 94ad322e70..a8be96e484 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts @@ -1,4 +1,4 @@ -import { Graph } from 'features/nodes/util/graph/Graph'; +import { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { AnyInvocation, Invocation } from 'services/api/types'; import { assert, AssertionError, is } from 'tsafe'; import { validate } from 'uuid'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts similarity index 100% rename from invokeai/frontend/web/src/features/nodes/util/graph/Graph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabControlLayers.ts similarity index 99% rename from invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabControlLayers.ts index 9198b76ed3..303800720c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabControlLayers.ts @@ -30,7 +30,7 @@ import { RESIZE, T2I_ADAPTER_COLLECT, } from 'features/nodes/util/graph/constants'; -import type { Graph } from 'features/nodes/util/graph/Graph'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { size } from 'lodash-es'; import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabHRF.ts similarity index 96% rename from invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabHRF.ts index b9adf6b8bb..99e3f4f28d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabHRF.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabHRF.ts @@ -1,11 +1,6 @@ import type { RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { roundToMultiple } from 'common/util/roundDownToMultiple'; -import type { Graph } from 'features/nodes/util/graph/Graph'; -import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import type { Invocation } from 'services/api/types'; - import { DENOISE_LATENTS_HRF, ESRGAN_HRF, @@ -14,7 +9,11 @@ import { LATENTS_TO_IMAGE_HRF_LR, NOISE_HRF, RESIZE_HRF, -} from './constants'; +} from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import type { Invocation } from 'services/api/types'; /** * Calculates the new resolution for high-resolution features (HRF) based on base model type. @@ -150,7 +149,7 @@ export const addGenerationTabHRF = ( type: 'l2i', id: LATENTS_TO_IMAGE_HRF_HR, fp32: l2i.fp32, - is_intermediate: getIsIntermediate(state), + is_intermediate: false, board: getBoardField(state), }); g.addEdge(vaeSource, 'vae', l2iHrfHR, 'vae'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabLoRAs.ts similarity index 94% rename from invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabLoRAs.ts index ee812468b8..b4623c82eb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabLoRAs.ts @@ -1,11 +1,10 @@ import type { RootState } from 'app/store/store'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { Graph } from 'features/nodes/util/graph/Graph'; +import { LORA_LOADER } from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { filter, size } from 'lodash-es'; import type { Invocation, S } from 'services/api/types'; -import { LORA_LOADER } from './constants'; - export const addGenerationTabLoRAs = ( state: RootState, g: Graph, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabNSFWChecker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabNSFWChecker.ts similarity index 84% rename from invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabNSFWChecker.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabNSFWChecker.ts index 7781dec685..8ddc501cee 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabNSFWChecker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabNSFWChecker.ts @@ -1,8 +1,7 @@ -import type { Graph } from 'features/nodes/util/graph/Graph'; +import { NSFW_CHECKER } from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation } from 'services/api/types'; -import { NSFW_CHECKER } from './constants'; - /** * Adds the NSFW checker to the output image * @param g The graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLLoRAs.ts similarity index 94% rename from invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLLoRAs.ts index bbd16e8f53..40b99cbf8f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLLoRAs.ts @@ -1,11 +1,10 @@ import type { RootState } from 'app/store/store'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { Graph } from 'features/nodes/util/graph/Graph'; +import { LORA_LOADER } from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { filter, size } from 'lodash-es'; import type { Invocation, S } from 'services/api/types'; -import { LORA_LOADER } from './constants'; - export const addGenerationTabSDXLLoRAs = ( state: RootState, g: Graph, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLRefiner.ts similarity index 94% rename from invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLRefiner.ts index 0cbb637d03..e2b85628be 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSDXLRefiner.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLRefiner.ts @@ -1,18 +1,17 @@ import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import type { Graph } from 'features/nodes/util/graph/Graph'; -import type { Invocation } from 'services/api/types'; -import { isRefinerMainModelModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; - +import { getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; import { SDXL_REFINER_DENOISE_LATENTS, SDXL_REFINER_MODEL_LOADER, SDXL_REFINER_NEGATIVE_CONDITIONING, SDXL_REFINER_POSITIVE_CONDITIONING, SDXL_REFINER_SEAMLESS, -} from './constants'; -import { getModelMetadataField } from './metadata'; +} from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import type { Invocation } from 'services/api/types'; +import { isRefinerMainModelModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; export const addGenerationTabSDXLRefiner = async ( state: RootState, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSeamless.ts similarity index 92% rename from invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSeamless.ts index a3303e6c6f..958a163f56 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabSeamless.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSeamless.ts @@ -1,9 +1,8 @@ import type { RootState } from 'app/store/store'; -import type { Graph } from 'features/nodes/util/graph/Graph'; +import { SEAMLESS } from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation } from 'services/api/types'; -import { SEAMLESS } from './constants'; - /** * Adds the seamless node to the graph and connects it to the model loader and denoise node. * Because the seamless node may insert a VAE loader node between the model loader and itself, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabWatermarker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabWatermarker.ts similarity index 84% rename from invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabWatermarker.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabWatermarker.ts index 584f24b67d..ba4d194992 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addGenerationTabWatermarker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabWatermarker.ts @@ -1,8 +1,7 @@ -import type { Graph } from 'features/nodes/util/graph/Graph'; +import { WATERMARKER } from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation } from 'services/api/types'; -import { WATERMARKER } from './constants'; - /** * Adds a watermark to the output image * @param g The graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts similarity index 90% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts index 319bea3566..7c15fcaaac 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts @@ -1,19 +1,6 @@ import type { RootState } from 'app/store/store'; import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addGenerationTabControlLayers } from 'features/nodes/util/graph/addGenerationTabControlLayers'; -import { addGenerationTabHRF } from 'features/nodes/util/graph/addGenerationTabHRF'; -import { addGenerationTabLoRAs } from 'features/nodes/util/graph/addGenerationTabLoRAs'; -import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/addGenerationTabNSFWChecker'; -import { addGenerationTabSeamless } from 'features/nodes/util/graph/addGenerationTabSeamless'; -import { addGenerationTabWatermarker } from 'features/nodes/util/graph/addGenerationTabWatermarker'; -import type { GraphType } from 'features/nodes/util/graph/Graph'; -import { Graph } from 'features/nodes/util/graph/Graph'; -import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { Invocation } from 'services/api/types'; -import { isNonRefinerMainModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; - import { CLIP_SKIP, CONTROL_LAYERS_GRAPH, @@ -26,8 +13,19 @@ import { POSITIVE_CONDITIONING, POSITIVE_CONDITIONING_COLLECT, VAE_LOADER, -} from './constants'; -import { getModelMetadataField } from './metadata'; +} from 'features/nodes/util/graph/constants'; +import { addGenerationTabControlLayers } from 'features/nodes/util/graph/generation/addGenerationTabControlLayers'; +import { addGenerationTabHRF } from 'features/nodes/util/graph/generation/addGenerationTabHRF'; +import { addGenerationTabLoRAs } from 'features/nodes/util/graph/generation/addGenerationTabLoRAs'; +import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/generation/addGenerationTabNSFWChecker'; +import { addGenerationTabSeamless } from 'features/nodes/util/graph/generation/addGenerationTabSeamless'; +import { addGenerationTabWatermarker } from 'features/nodes/util/graph/generation/addGenerationTabWatermarker'; +import type { GraphType } from 'features/nodes/util/graph/generation/Graph'; +import { Graph } from 'features/nodes/util/graph/generation/Graph'; +import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; +import type { Invocation } from 'services/api/types'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; export const buildGenerationTabGraph = async (state: RootState): Promise => { const { @@ -136,7 +134,7 @@ export const buildGenerationTabGraph = async (state: RootState): Promise => { const { @@ -127,7 +125,7 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise Date: Tue, 14 May 2024 20:18:32 +1000 Subject: [PATCH 131/442] tidy(ui): organise graph builder files --- ...abControlLayers.ts => addControlLayers.ts} | 2 +- .../{addGenerationTabHRF.ts => addHRF.ts} | 2 +- .../{addGenerationTabLoRAs.ts => addLoRAs.ts} | 2 +- ...ionTabNSFWChecker.ts => addNSFWChecker.ts} | 2 +- ...erationTabSDXLLoRAs.ts => addSDXLLoRAs.ts} | 2 +- ...ionTabSDXLRefiner.ts => addSDXLRefiner.ts} | 2 +- ...enerationTabSeamless.ts => addSeamless.ts} | 2 +- ...ionTabWatermarker.ts => addWatermarker.ts} | 2 +- .../generation/buildGenerationTabGraph.ts | 24 +++++++++---------- .../generation/buildGenerationTabSDXLGraph.ts | 24 +++++++++---------- 10 files changed, 32 insertions(+), 32 deletions(-) rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{addGenerationTabControlLayers.ts => addControlLayers.ts} (99%) rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{addGenerationTabHRF.ts => addHRF.ts} (99%) rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{addGenerationTabLoRAs.ts => addLoRAs.ts} (98%) rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{addGenerationTabNSFWChecker.ts => addNSFWChecker.ts} (94%) rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{addGenerationTabSDXLLoRAs.ts => addSDXLLoRAs.ts} (98%) rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{addGenerationTabSDXLRefiner.ts => addSDXLRefiner.ts} (98%) rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{addGenerationTabSeamless.ts => addSeamless.ts} (97%) rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{addGenerationTabWatermarker.ts => addWatermarker.ts} (94%) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts similarity index 99% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabControlLayers.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts index 303800720c..b5e5f8f246 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts @@ -50,7 +50,7 @@ import { assert } from 'tsafe'; * @param vaeSource The VAE source (either seamless, vae_loader, main_model_loader, or sdxl_model_loader) * @returns A promise that resolves to the layers that were added to the graph */ -export const addGenerationTabControlLayers = async ( +export const addControlLayers = async ( state: RootState, g: Graph, base: BaseModelType, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts similarity index 99% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabHRF.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts index 99e3f4f28d..68286e337c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabHRF.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts @@ -65,7 +65,7 @@ function calculateHrfRes( * @param vaeSource The VAE source node (may be a model loader, VAE loader, or seamless node) * @returns The HRF image output node. */ -export const addGenerationTabHRF = ( +export const addHRF = ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts similarity index 98% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabLoRAs.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts index b4623c82eb..3623343367 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts @@ -5,7 +5,7 @@ import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { filter, size } from 'lodash-es'; import type { Invocation, S } from 'services/api/types'; -export const addGenerationTabLoRAs = ( +export const addLoRAs = ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabNSFWChecker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts similarity index 94% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabNSFWChecker.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts index 8ddc501cee..504cad14aa 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabNSFWChecker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts @@ -8,7 +8,7 @@ import type { Invocation } from 'services/api/types'; * @param imageOutput The current image output node * @returns The nsfw checker node */ -export const addGenerationTabNSFWChecker = ( +export const addNSFWChecker = ( g: Graph, imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> ): Invocation<'img_nsfw'> => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts similarity index 98% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLLoRAs.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts index 40b99cbf8f..f38e8de570 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts @@ -5,7 +5,7 @@ import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { filter, size } from 'lodash-es'; import type { Invocation, S } from 'services/api/types'; -export const addGenerationTabSDXLLoRAs = ( +export const addSDXLLoRas = ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts similarity index 98% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLRefiner.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts index e2b85628be..806a138b62 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSDXLRefiner.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts @@ -13,7 +13,7 @@ import type { Invocation } from 'services/api/types'; import { isRefinerMainModelModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; -export const addGenerationTabSDXLRefiner = async ( +export const addSDXLRefiner = async ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSeamless.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts similarity index 97% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSeamless.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts index 958a163f56..25a3e7e3ac 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabSeamless.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts @@ -14,7 +14,7 @@ import type { Invocation } from 'services/api/types'; * @param vaeLoader The VAE loader node in the graph, if it exists * @returns The seamless node, if it was added to the graph */ -export const addGenerationTabSeamless = ( +export const addSeamless = ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabWatermarker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts similarity index 94% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabWatermarker.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts index ba4d194992..50986ac3ff 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addGenerationTabWatermarker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts @@ -8,7 +8,7 @@ import type { Invocation } from 'services/api/types'; * @param imageOutput The image output node * @returns The watermark node */ -export const addGenerationTabWatermarker = ( +export const addWatermarker = ( g: Graph, imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> ): Invocation<'img_watermark'> => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts index 7c15fcaaac..d6afbace72 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts @@ -14,12 +14,12 @@ import { POSITIVE_CONDITIONING_COLLECT, VAE_LOADER, } from 'features/nodes/util/graph/constants'; -import { addGenerationTabControlLayers } from 'features/nodes/util/graph/generation/addGenerationTabControlLayers'; -import { addGenerationTabHRF } from 'features/nodes/util/graph/generation/addGenerationTabHRF'; -import { addGenerationTabLoRAs } from 'features/nodes/util/graph/generation/addGenerationTabLoRAs'; -import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/generation/addGenerationTabNSFWChecker'; -import { addGenerationTabSeamless } from 'features/nodes/util/graph/generation/addGenerationTabSeamless'; -import { addGenerationTabWatermarker } from 'features/nodes/util/graph/generation/addGenerationTabWatermarker'; +import { addControlLayers } from 'features/nodes/util/graph/generation/addControlLayers'; +import { addHRF } from 'features/nodes/util/graph/generation/addHRF'; +import { addLoRAs } from 'features/nodes/util/graph/generation/addLoRAs'; +import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; +import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; +import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; import type { GraphType } from 'features/nodes/util/graph/generation/Graph'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; @@ -143,15 +143,15 @@ export const buildGenerationTabGraph = async (state: RootState): Promise isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); if (isHRFAllowed && state.hrf.hrfEnabled) { - imageOutput = addGenerationTabHRF(state, g, denoise, noise, l2i, vaeSource); + imageOutput = addHRF(state, g, denoise, noise, l2i, vaeSource); } if (state.system.shouldUseNSFWChecker) { - imageOutput = addGenerationTabNSFWChecker(g, imageOutput); + imageOutput = addNSFWChecker(g, imageOutput); } if (state.system.shouldUseWatermarker) { - imageOutput = addGenerationTabWatermarker(g, imageOutput); + imageOutput = addWatermarker(g, imageOutput); } g.setMetadataReceivingNode(imageOutput); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts index 8210964820..06c2ded18a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts @@ -12,12 +12,12 @@ import { SDXL_MODEL_LOADER, VAE_LOADER, } from 'features/nodes/util/graph/constants'; -import { addGenerationTabControlLayers } from 'features/nodes/util/graph/generation/addGenerationTabControlLayers'; -import { addGenerationTabNSFWChecker } from 'features/nodes/util/graph/generation/addGenerationTabNSFWChecker'; -import { addGenerationTabSDXLLoRAs } from 'features/nodes/util/graph/generation/addGenerationTabSDXLLoRAs'; -import { addGenerationTabSDXLRefiner } from 'features/nodes/util/graph/generation/addGenerationTabSDXLRefiner'; -import { addGenerationTabSeamless } from 'features/nodes/util/graph/generation/addGenerationTabSeamless'; -import { addGenerationTabWatermarker } from 'features/nodes/util/graph/generation/addGenerationTabWatermarker'; +import { addControlLayers } from 'features/nodes/util/graph/generation/addControlLayers'; +import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; +import { addSDXLLoRas } from 'features/nodes/util/graph/generation/addSDXLLoRAs'; +import { addSDXLRefiner } from 'features/nodes/util/graph/generation/addSDXLRefiner'; +import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; +import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getBoardField, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; import type { Invocation, NonNullableGraph } from 'services/api/types'; @@ -135,9 +135,9 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise Date: Tue, 14 May 2024 20:23:51 +1000 Subject: [PATCH 132/442] tidy(ui): use Invocation<> helper type in OG control adapters --- .../features/controlAdapters/store/types.ts | 73 ++++++++----------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts index 7e2f18af5c..b76a729263 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts @@ -5,22 +5,7 @@ import type { ParameterT2IAdapterModel, } from 'features/parameters/types/parameterSchemas'; import type { components } from 'services/api/schema'; -import type { - CannyImageProcessorInvocation, - ColorMapImageProcessorInvocation, - ContentShuffleImageProcessorInvocation, - DepthAnythingImageProcessorInvocation, - DWOpenposeImageProcessorInvocation, - HedImageProcessorInvocation, - LineartAnimeImageProcessorInvocation, - LineartImageProcessorInvocation, - MediapipeFaceProcessorInvocation, - MidasDepthImageProcessorInvocation, - MlsdImageProcessorInvocation, - NormalbaeImageProcessorInvocation, - PidiImageProcessorInvocation, - ZoeDepthImageProcessorInvocation, -} from 'services/api/types'; +import type { Invocation } from 'services/api/types'; import type { O } from 'ts-toolbelt'; import { z } from 'zod'; @@ -28,20 +13,20 @@ import { z } from 'zod'; * Any ControlNet processor node */ export type ControlAdapterProcessorNode = - | CannyImageProcessorInvocation - | ColorMapImageProcessorInvocation - | ContentShuffleImageProcessorInvocation - | DepthAnythingImageProcessorInvocation - | HedImageProcessorInvocation - | LineartAnimeImageProcessorInvocation - | LineartImageProcessorInvocation - | MediapipeFaceProcessorInvocation - | MidasDepthImageProcessorInvocation - | MlsdImageProcessorInvocation - | NormalbaeImageProcessorInvocation - | DWOpenposeImageProcessorInvocation - | PidiImageProcessorInvocation - | ZoeDepthImageProcessorInvocation; + | Invocation<'canny_image_processor'> + | Invocation<'color_map_image_processor'> + | Invocation<'content_shuffle_image_processor'> + | Invocation<'depth_anything_image_processor'> + | Invocation<'hed_image_processor'> + | Invocation<'lineart_anime_image_processor'> + | Invocation<'lineart_image_processor'> + | Invocation<'mediapipe_face_processor'> + | Invocation<'midas_depth_image_processor'> + | Invocation<'mlsd_image_processor'> + | Invocation<'normalbae_image_processor'> + | Invocation<'dw_openpose_image_processor'> + | Invocation<'pidi_image_processor'> + | Invocation<'zoe_depth_image_processor'>; /** * Any ControlNet processor type @@ -71,7 +56,7 @@ export const isControlAdapterProcessorType = (v: unknown): v is ControlAdapterPr * The Canny processor node, with parameters flagged as required */ export type RequiredCannyImageProcessorInvocation = O.Required< - CannyImageProcessorInvocation, + Invocation<'canny_image_processor'>, 'type' | 'low_threshold' | 'high_threshold' | 'image_resolution' | 'detect_resolution' >; @@ -79,7 +64,7 @@ export type RequiredCannyImageProcessorInvocation = O.Required< * The Color Map processor node, with parameters flagged as required */ export type RequiredColorMapImageProcessorInvocation = O.Required< - ColorMapImageProcessorInvocation, + Invocation<'color_map_image_processor'>, 'type' | 'color_map_tile_size' >; @@ -87,7 +72,7 @@ export type RequiredColorMapImageProcessorInvocation = O.Required< * The ContentShuffle processor node, with parameters flagged as required */ export type RequiredContentShuffleImageProcessorInvocation = O.Required< - ContentShuffleImageProcessorInvocation, + Invocation<'content_shuffle_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'w' | 'h' | 'f' >; @@ -95,7 +80,7 @@ export type RequiredContentShuffleImageProcessorInvocation = O.Required< * The DepthAnything processor node, with parameters flagged as required */ export type RequiredDepthAnythingImageProcessorInvocation = O.Required< - DepthAnythingImageProcessorInvocation, + Invocation<'depth_anything_image_processor'>, 'type' | 'model_size' | 'resolution' | 'offload' >; @@ -108,7 +93,7 @@ export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSiz * The HED processor node, with parameters flagged as required */ export type RequiredHedImageProcessorInvocation = O.Required< - HedImageProcessorInvocation, + Invocation<'hed_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'scribble' >; @@ -116,7 +101,7 @@ export type RequiredHedImageProcessorInvocation = O.Required< * The Lineart Anime processor node, with parameters flagged as required */ export type RequiredLineartAnimeImageProcessorInvocation = O.Required< - LineartAnimeImageProcessorInvocation, + Invocation<'lineart_anime_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' >; @@ -124,7 +109,7 @@ export type RequiredLineartAnimeImageProcessorInvocation = O.Required< * The Lineart processor node, with parameters flagged as required */ export type RequiredLineartImageProcessorInvocation = O.Required< - LineartImageProcessorInvocation, + Invocation<'lineart_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'coarse' >; @@ -132,7 +117,7 @@ export type RequiredLineartImageProcessorInvocation = O.Required< * The MediapipeFace processor node, with parameters flagged as required */ export type RequiredMediapipeFaceProcessorInvocation = O.Required< - MediapipeFaceProcessorInvocation, + Invocation<'mediapipe_face_processor'>, 'type' | 'max_faces' | 'min_confidence' | 'image_resolution' | 'detect_resolution' >; @@ -140,7 +125,7 @@ export type RequiredMediapipeFaceProcessorInvocation = O.Required< * The MidasDepth processor node, with parameters flagged as required */ export type RequiredMidasDepthImageProcessorInvocation = O.Required< - MidasDepthImageProcessorInvocation, + Invocation<'midas_depth_image_processor'>, 'type' | 'a_mult' | 'bg_th' | 'image_resolution' | 'detect_resolution' >; @@ -148,7 +133,7 @@ export type RequiredMidasDepthImageProcessorInvocation = O.Required< * The MLSD processor node, with parameters flagged as required */ export type RequiredMlsdImageProcessorInvocation = O.Required< - MlsdImageProcessorInvocation, + Invocation<'mlsd_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'thr_v' | 'thr_d' >; @@ -156,7 +141,7 @@ export type RequiredMlsdImageProcessorInvocation = O.Required< * The NormalBae processor node, with parameters flagged as required */ export type RequiredNormalbaeImageProcessorInvocation = O.Required< - NormalbaeImageProcessorInvocation, + Invocation<'normalbae_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' >; @@ -164,7 +149,7 @@ export type RequiredNormalbaeImageProcessorInvocation = O.Required< * The DW Openpose processor node, with parameters flagged as required */ export type RequiredDWOpenposeImageProcessorInvocation = O.Required< - DWOpenposeImageProcessorInvocation, + Invocation<'dw_openpose_image_processor'>, 'type' | 'image_resolution' | 'draw_body' | 'draw_face' | 'draw_hands' >; @@ -172,14 +157,14 @@ export type RequiredDWOpenposeImageProcessorInvocation = O.Required< * The Pidi processor node, with parameters flagged as required */ export type RequiredPidiImageProcessorInvocation = O.Required< - PidiImageProcessorInvocation, + Invocation<'pidi_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'safe' | 'scribble' >; /** * The ZoeDepth processor node, with parameters flagged as required */ -export type RequiredZoeDepthImageProcessorInvocation = O.Required; +export type RequiredZoeDepthImageProcessorInvocation = O.Required, 'type'>; /** * Any ControlNet Processor node, with its parameters flagged as required From 0ff02907355eda3b1fcbc6f45289c5c507b85007 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 14 May 2024 20:36:01 +1000 Subject: [PATCH 133/442] tidy(ui): use Invocation<> helper type in canvas graph builders, elsewhere --- .../util/graph/buildAdHocUpscaleGraph.ts | 4 +-- .../canvas/addControlNetToLinearGraph.ts | 14 +++----- .../graph/canvas/addIPAdapterToLinearGraph.ts | 14 +++----- .../util/graph/canvas/addLoRAsToGraph.ts | 6 ++-- .../graph/canvas/addNSFWCheckerToGraph.ts | 8 ++--- .../util/graph/canvas/addSDXLLoRAstoGraph.ts | 6 ++-- .../graph/canvas/addSDXLRefinerToGraph.ts | 4 +-- .../graph/canvas/addSeamlessToLinearGraph.ts | 4 +-- .../canvas/addT2IAdapterToLinearGraph.ts | 14 +++----- .../graph/canvas/addWatermarkerToGraph.ts | 13 +++---- .../canvas/buildCanvasImageToImageGraph.ts | 10 ++---- .../graph/canvas/buildCanvasInpaintGraph.ts | 23 +++++------- .../graph/canvas/buildCanvasOutpaintGraph.ts | 21 ++++------- .../buildCanvasSDXLImageToImageGraph.ts | 10 ++---- .../canvas/buildCanvasSDXLInpaintGraph.ts | 23 +++++------- .../canvas/buildCanvasSDXLOutpaintGraph.ts | 21 ++++------- .../nodes/util/graph/canvas/metadata.ts | 14 ++++---- .../nodes/util/graph/generation/Graph.ts | 10 +++--- .../frontend/web/src/services/api/types.ts | 36 ------------------- 19 files changed, 80 insertions(+), 175 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts index 74631c99cf..60343c5e89 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts @@ -1,6 +1,6 @@ import type { RootState } from 'app/store/store'; import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ESRGANInvocation, Graph, NonNullableGraph } from 'services/api/types'; +import type { Graph, Invocation, NonNullableGraph } from 'services/api/types'; import { addCoreMetadataNode, upsertMetadata } from './canvas/metadata'; import { ESRGAN } from './constants'; @@ -13,7 +13,7 @@ type Arg = { export const buildAdHocUpscaleGraph = ({ image_name, state }: Arg): Graph => { const { esrganModelName } = state.postprocessing; - const realesrganNode: ESRGANInvocation = { + const realesrganNode: Invocation<'esrgan'> = { id: ESRGAN, type: 'esrgan', image: { image_name }, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts index c9ffc35b9c..2feba262c2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts @@ -5,13 +5,7 @@ import type { ImageField } from 'features/nodes/types/common'; import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; import { CONTROL_NET_COLLECT } from 'features/nodes/util/graph/constants'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { - CollectInvocation, - ControlNetInvocation, - CoreMetadataInvocation, - NonNullableGraph, - S, -} from 'services/api/types'; +import type { Invocation, NonNullableGraph, S } from 'services/api/types'; import { assert } from 'tsafe'; export const addControlNetToLinearGraph = async ( @@ -19,7 +13,7 @@ export const addControlNetToLinearGraph = async ( graph: NonNullableGraph, baseNodeId: string ): Promise => { - const controlNetMetadata: CoreMetadataInvocation['controlnets'] = []; + const controlNetMetadata: S['CoreMetadataInvocation']['controlnets'] = []; const controlNets = selectValidControlNets(state.controlAdapters).filter( ({ model, processedControlImage, processorType, controlImage, isEnabled }) => { const hasModel = Boolean(model); @@ -36,7 +30,7 @@ export const addControlNetToLinearGraph = async ( if (controlNets.length) { // Even though denoise_latents' control input is collection or scalar, keep it simple and always use a collect - const controlNetIterateNode: CollectInvocation = { + const controlNetIterateNode: Invocation<'collect'> = { id: CONTROL_NET_COLLECT, type: 'collect', is_intermediate: true, @@ -67,7 +61,7 @@ export const addControlNetToLinearGraph = async ( weight, } = controlNet; - const controlNetNode: ControlNetInvocation = { + const controlNetNode: Invocation<'controlnet'> = { id: `control_net_${id}`, type: 'controlnet', is_intermediate: true, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts index 607e7f72a4..e9d9bd4663 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts @@ -5,13 +5,7 @@ import type { ImageField } from 'features/nodes/types/common'; import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; import { IP_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { - CollectInvocation, - CoreMetadataInvocation, - IPAdapterInvocation, - NonNullableGraph, - S, -} from 'services/api/types'; +import type { Invocation, NonNullableGraph, S } from 'services/api/types'; import { assert } from 'tsafe'; export const addIPAdapterToLinearGraph = async ( @@ -32,7 +26,7 @@ export const addIPAdapterToLinearGraph = async ( if (ipAdapters.length) { // Even though denoise_latents' ip adapter input is collection or scalar, keep it simple and always use a collect - const ipAdapterCollectNode: CollectInvocation = { + const ipAdapterCollectNode: Invocation<'collect'> = { id: IP_ADAPTER_COLLECT, type: 'collect', is_intermediate: true, @@ -46,7 +40,7 @@ export const addIPAdapterToLinearGraph = async ( }, }); - const ipAdapterMetdata: CoreMetadataInvocation['ipAdapters'] = []; + const ipAdapterMetdata: S['CoreMetadataInvocation']['ipAdapters'] = []; for (const ipAdapter of ipAdapters) { if (!ipAdapter.model) { @@ -56,7 +50,7 @@ export const addIPAdapterToLinearGraph = async ( assert(controlImage, 'IP Adapter image is required'); - const ipAdapterNode: IPAdapterInvocation = { + const ipAdapterNode: Invocation<'ip_adapter'> = { id: `ip_adapter_${id}`, type: 'ip_adapter', is_intermediate: true, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts index 998f275a1e..6c4ac9fc69 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts @@ -9,7 +9,7 @@ import { POSITIVE_CONDITIONING, } from 'features/nodes/util/graph/constants'; import { filter, size } from 'lodash-es'; -import type { CoreMetadataInvocation, LoRALoaderInvocation, NonNullableGraph } from 'services/api/types'; +import type { Invocation, NonNullableGraph, S } from 'services/api/types'; export const addLoRAsToGraph = async ( state: RootState, @@ -43,7 +43,7 @@ export const addLoRAsToGraph = async ( // we need to remember the last lora so we can chain from it let lastLoraNodeId = ''; let currentLoraIndex = 0; - const loraMetadata: CoreMetadataInvocation['loras'] = []; + const loraMetadata: S['CoreMetadataInvocation']['loras'] = []; enabledLoRAs.forEach(async (lora) => { const { weight } = lora; @@ -51,7 +51,7 @@ export const addLoRAsToGraph = async ( const currentLoraNodeId = `${LORA_LOADER}_${key}`; const parsedModel = zModelIdentifierField.parse(lora.model); - const loraLoaderNode: LoRALoaderInvocation = { + const loraLoaderNode: Invocation<'lora_loader'> = { type: 'lora_loader', id: currentLoraNodeId, is_intermediate: true, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts index 6b73aa9f63..56bc3cc2ad 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts @@ -1,14 +1,14 @@ import type { RootState } from 'app/store/store'; import { LATENTS_TO_IMAGE, NSFW_CHECKER } from 'features/nodes/util/graph/constants'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ImageNSFWBlurInvocation, LatentsToImageInvocation, NonNullableGraph } from 'services/api/types'; +import type { Invocation, NonNullableGraph } from 'services/api/types'; export const addNSFWCheckerToGraph = ( state: RootState, graph: NonNullableGraph, nodeIdToAddTo = LATENTS_TO_IMAGE ): void => { - const nodeToAddTo = graph.nodes[nodeIdToAddTo] as LatentsToImageInvocation | undefined; + const nodeToAddTo = graph.nodes[nodeIdToAddTo] as Invocation<'l2i'> | undefined; if (!nodeToAddTo) { // something has gone terribly awry @@ -18,14 +18,14 @@ export const addNSFWCheckerToGraph = ( nodeToAddTo.is_intermediate = true; nodeToAddTo.use_cache = true; - const nsfwCheckerNode: ImageNSFWBlurInvocation = { + const nsfwCheckerNode: Invocation<'img_nsfw'> = { id: NSFW_CHECKER, type: 'img_nsfw', is_intermediate: getIsIntermediate(state), board: getBoardField(state), }; - graph.nodes[NSFW_CHECKER] = nsfwCheckerNode as ImageNSFWBlurInvocation; + graph.nodes[NSFW_CHECKER] = nsfwCheckerNode; graph.edges.push({ source: { node_id: nodeIdToAddTo, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts index 9b8665e6db..695b7a73ed 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts @@ -10,7 +10,7 @@ import { SEAMLESS, } from 'features/nodes/util/graph/constants'; import { filter, size } from 'lodash-es'; -import type { CoreMetadataInvocation, NonNullableGraph, SDXLLoRALoaderInvocation } from 'services/api/types'; +import type { Invocation, NonNullableGraph, S } from 'services/api/types'; export const addSDXLLoRAsToGraph = async ( state: RootState, @@ -34,7 +34,7 @@ export const addSDXLLoRAsToGraph = async ( return; } - const loraMetadata: CoreMetadataInvocation['loras'] = []; + const loraMetadata: S['CoreMetadataInvocation']['loras'] = []; // Handle Seamless Plugs const unetLoaderId = modelLoaderNodeId; @@ -60,7 +60,7 @@ export const addSDXLLoRAsToGraph = async ( const currentLoraNodeId = `${LORA_LOADER}_${lora.model.key}`; const parsedModel = zModelIdentifierField.parse(lora.model); - const loraLoaderNode: SDXLLoRALoaderInvocation = { + const loraLoaderNode: Invocation<'sdxl_lora_loader'> = { type: 'sdxl_lora_loader', id: currentLoraNodeId, is_intermediate: true, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts index df4d6f1364..6fc406ca74 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts @@ -17,7 +17,7 @@ import { SDXL_REFINER_SEAMLESS, } from 'features/nodes/util/graph/constants'; import { getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { NonNullableGraph, SeamlessModeInvocation } from 'services/api/types'; +import type { NonNullableGraph } from 'services/api/types'; import { isRefinerMainModelModelConfig } from 'services/api/types'; export const addSDXLRefinerToGraph = async ( @@ -100,7 +100,7 @@ export const addSDXLRefinerToGraph = async ( type: 'seamless', seamless_x: seamlessXAxis, seamless_y: seamlessYAxis, - } as SeamlessModeInvocation; + }; graph.edges.push( { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts index 98bf62d32f..357b3357e2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts @@ -11,7 +11,7 @@ import { SEAMLESS, VAE_LOADER, } from 'features/nodes/util/graph/constants'; -import type { NonNullableGraph, SeamlessModeInvocation } from 'services/api/types'; +import type { NonNullableGraph } from 'services/api/types'; export const addSeamlessToLinearGraph = ( state: RootState, @@ -27,7 +27,7 @@ export const addSeamlessToLinearGraph = ( type: 'seamless', seamless_x: seamlessXAxis, seamless_y: seamlessYAxis, - } as SeamlessModeInvocation; + }; if (!isAutoVae) { graph.nodes[VAE_LOADER] = { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts index f60cb8366d..7c51d9488f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts @@ -5,13 +5,7 @@ import type { ImageField } from 'features/nodes/types/common'; import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; import { T2I_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { - CollectInvocation, - CoreMetadataInvocation, - NonNullableGraph, - S, - T2IAdapterInvocation, -} from 'services/api/types'; +import type { Invocation, NonNullableGraph, S } from 'services/api/types'; import { assert } from 'tsafe'; export const addT2IAdaptersToLinearGraph = async ( @@ -35,7 +29,7 @@ export const addT2IAdaptersToLinearGraph = async ( if (t2iAdapters.length) { // Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect - const t2iAdapterCollectNode: CollectInvocation = { + const t2iAdapterCollectNode: Invocation<'collect'> = { id: T2I_ADAPTER_COLLECT, type: 'collect', is_intermediate: true, @@ -49,7 +43,7 @@ export const addT2IAdaptersToLinearGraph = async ( }, }); - const t2iAdapterMetadata: CoreMetadataInvocation['t2iAdapters'] = []; + const t2iAdapterMetadata: S['CoreMetadataInvocation']['t2iAdapters'] = []; for (const t2iAdapter of t2iAdapters) { if (!t2iAdapter.model) { @@ -67,7 +61,7 @@ export const addT2IAdaptersToLinearGraph = async ( weight, } = t2iAdapter; - const t2iAdapterNode: T2IAdapterInvocation = { + const t2iAdapterNode: Invocation<'t2i_adapter'> = { id: `t2i_adapter_${id}`, type: 't2i_adapter', is_intermediate: true, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts index 1759646c54..f87517b5d1 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts @@ -1,28 +1,23 @@ import type { RootState } from 'app/store/store'; import { LATENTS_TO_IMAGE, NSFW_CHECKER, WATERMARKER } from 'features/nodes/util/graph/constants'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { - ImageNSFWBlurInvocation, - ImageWatermarkInvocation, - LatentsToImageInvocation, - NonNullableGraph, -} from 'services/api/types'; +import type { Invocation, NonNullableGraph } from 'services/api/types'; export const addWatermarkerToGraph = ( state: RootState, graph: NonNullableGraph, nodeIdToAddTo = LATENTS_TO_IMAGE ): void => { - const nodeToAddTo = graph.nodes[nodeIdToAddTo] as LatentsToImageInvocation | undefined; + const nodeToAddTo = graph.nodes[nodeIdToAddTo] as Invocation<'l2i'> | undefined; - const nsfwCheckerNode = graph.nodes[NSFW_CHECKER] as ImageNSFWBlurInvocation | undefined; + const nsfwCheckerNode = graph.nodes[NSFW_CHECKER] as Invocation<'img_nsfw'> | undefined; if (!nodeToAddTo) { // something has gone terribly awry return; } - const watermarkerNode: ImageWatermarkInvocation = { + const watermarkerNode: Invocation<'img_watermark'> = { id: WATERMARKER, type: 'img_watermark', is_intermediate: getIsIntermediate(state), diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts index b165699d59..8f5fe9f2b8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts @@ -17,12 +17,8 @@ import { SEAMLESS, } from 'features/nodes/util/graph/constants'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import { - type ImageDTO, - type ImageToLatentsInvocation, - isNonRefinerMainModelConfig, - type NonNullableGraph, -} from 'services/api/types'; +import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; @@ -300,7 +296,7 @@ export const buildCanvasImageToImageGraph = async ( use_cache: false, }; - (graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image = initialImage; + (graph.nodes[IMAGE_TO_LATENTS] as Invocation<'i2l'>).image = initialImage; graph.edges.push({ source: { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts index 082572e43a..c995c38a3c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts @@ -19,14 +19,7 @@ import { SEAMLESS, } from 'features/nodes/util/graph/constants'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { - CanvasPasteBackInvocation, - CreateGradientMaskInvocation, - ImageDTO, - ImageToLatentsInvocation, - NoiseInvocation, - NonNullableGraph, -} from 'services/api/types'; +import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; @@ -316,8 +309,8 @@ export const buildCanvasInpaintGraph = async ( height: height, }; - (graph.nodes[NOISE] as NoiseInvocation).width = scaledWidth; - (graph.nodes[NOISE] as NoiseInvocation).height = scaledHeight; + (graph.nodes[NOISE] as Invocation<'noise'>).width = scaledWidth; + (graph.nodes[NOISE] as Invocation<'noise'>).height = scaledHeight; // Connect Nodes graph.edges.push( @@ -397,22 +390,22 @@ export const buildCanvasInpaintGraph = async ( ); } else { // Add Images To Nodes - (graph.nodes[NOISE] as NoiseInvocation).width = width; - (graph.nodes[NOISE] as NoiseInvocation).height = height; + (graph.nodes[NOISE] as Invocation<'noise'>).width = width; + (graph.nodes[NOISE] as Invocation<'noise'>).height = height; graph.nodes[INPAINT_IMAGE] = { - ...(graph.nodes[INPAINT_IMAGE] as ImageToLatentsInvocation), + ...(graph.nodes[INPAINT_IMAGE] as Invocation<'i2l'>), image: canvasInitImage, }; graph.nodes[INPAINT_CREATE_MASK] = { - ...(graph.nodes[INPAINT_CREATE_MASK] as CreateGradientMaskInvocation), + ...(graph.nodes[INPAINT_CREATE_MASK] as Invocation<'create_gradient_mask'>), mask: canvasMaskImage, }; // Paste Back graph.nodes[CANVAS_OUTPUT] = { - ...(graph.nodes[CANVAS_OUTPUT] as CanvasPasteBackInvocation), + ...(graph.nodes[CANVAS_OUTPUT] as Invocation<'canvas_paste_back'>), mask: canvasMaskImage, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts index 598db7db48..e4a9b11b96 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts @@ -23,14 +23,7 @@ import { SEAMLESS, } from 'features/nodes/util/graph/constants'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { - ImageDTO, - ImageToLatentsInvocation, - InfillPatchMatchInvocation, - InfillTileInvocation, - NoiseInvocation, - NonNullableGraph, -} from 'services/api/types'; +import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; @@ -437,8 +430,8 @@ export const buildCanvasOutpaintGraph = async ( height: height, }; - (graph.nodes[NOISE] as NoiseInvocation).width = scaledWidth; - (graph.nodes[NOISE] as NoiseInvocation).height = scaledHeight; + (graph.nodes[NOISE] as Invocation<'noise'>).width = scaledWidth; + (graph.nodes[NOISE] as Invocation<'noise'>).height = scaledHeight; // Connect Nodes graph.edges.push( @@ -540,15 +533,15 @@ export const buildCanvasOutpaintGraph = async ( } else { // Add Images To Nodes graph.nodes[INPAINT_INFILL] = { - ...(graph.nodes[INPAINT_INFILL] as InfillTileInvocation | InfillPatchMatchInvocation), + ...(graph.nodes[INPAINT_INFILL] as Invocation<'infill_tile'> | Invocation<'infill_patchmatch'>), image: canvasInitImage, }; - (graph.nodes[NOISE] as NoiseInvocation).width = width; - (graph.nodes[NOISE] as NoiseInvocation).height = height; + (graph.nodes[NOISE] as Invocation<'noise'>).width = width; + (graph.nodes[NOISE] as Invocation<'noise'>).height = height; graph.nodes[INPAINT_IMAGE] = { - ...(graph.nodes[INPAINT_IMAGE] as ImageToLatentsInvocation), + ...(graph.nodes[INPAINT_IMAGE] as Invocation<'i2l'>), image: canvasInitImage, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts index 4db1532f76..186dfa53b3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts @@ -17,12 +17,8 @@ import { SEAMLESS, } from 'features/nodes/util/graph/constants'; import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; -import { - type ImageDTO, - type ImageToLatentsInvocation, - isNonRefinerMainModelConfig, - type NonNullableGraph, -} from 'services/api/types'; +import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; @@ -301,7 +297,7 @@ export const buildCanvasSDXLImageToImageGraph = async ( use_cache: false, }; - (graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image = initialImage; + (graph.nodes[IMAGE_TO_LATENTS] as Invocation<'i2l'>).image = initialImage; graph.edges.push({ source: { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts index f3ade076e7..277b713079 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts @@ -19,14 +19,7 @@ import { SEAMLESS, } from 'features/nodes/util/graph/constants'; import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { - CanvasPasteBackInvocation, - CreateGradientMaskInvocation, - ImageDTO, - ImageToLatentsInvocation, - NoiseInvocation, - NonNullableGraph, -} from 'services/api/types'; +import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; @@ -327,8 +320,8 @@ export const buildCanvasSDXLInpaintGraph = async ( height: height, }; - (graph.nodes[NOISE] as NoiseInvocation).width = scaledWidth; - (graph.nodes[NOISE] as NoiseInvocation).height = scaledHeight; + (graph.nodes[NOISE] as Invocation<'noise'>).width = scaledWidth; + (graph.nodes[NOISE] as Invocation<'noise'>).height = scaledHeight; // Connect Nodes graph.edges.push( @@ -408,22 +401,22 @@ export const buildCanvasSDXLInpaintGraph = async ( ); } else { // Add Images To Nodes - (graph.nodes[NOISE] as NoiseInvocation).width = width; - (graph.nodes[NOISE] as NoiseInvocation).height = height; + (graph.nodes[NOISE] as Invocation<'noise'>).width = width; + (graph.nodes[NOISE] as Invocation<'noise'>).height = height; graph.nodes[INPAINT_IMAGE] = { - ...(graph.nodes[INPAINT_IMAGE] as ImageToLatentsInvocation), + ...(graph.nodes[INPAINT_IMAGE] as Invocation<'i2l'>), image: canvasInitImage, }; graph.nodes[INPAINT_CREATE_MASK] = { - ...(graph.nodes[INPAINT_CREATE_MASK] as CreateGradientMaskInvocation), + ...(graph.nodes[INPAINT_CREATE_MASK] as Invocation<'create_gradient_mask'>), mask: canvasMaskImage, }; // Paste Back graph.nodes[CANVAS_OUTPUT] = { - ...(graph.nodes[CANVAS_OUTPUT] as CanvasPasteBackInvocation), + ...(graph.nodes[CANVAS_OUTPUT] as Invocation<'canvas_paste_back'>), mask: canvasMaskImage, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts index ce8e87bcb5..b09d7d8b90 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts @@ -23,14 +23,7 @@ import { SEAMLESS, } from 'features/nodes/util/graph/constants'; import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { - ImageDTO, - ImageToLatentsInvocation, - InfillPatchMatchInvocation, - InfillTileInvocation, - NoiseInvocation, - NonNullableGraph, -} from 'services/api/types'; +import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; @@ -446,8 +439,8 @@ export const buildCanvasSDXLOutpaintGraph = async ( height: height, }; - (graph.nodes[NOISE] as NoiseInvocation).width = scaledWidth; - (graph.nodes[NOISE] as NoiseInvocation).height = scaledHeight; + (graph.nodes[NOISE] as Invocation<'noise'>).width = scaledWidth; + (graph.nodes[NOISE] as Invocation<'noise'>).height = scaledHeight; // Connect Nodes graph.edges.push( @@ -549,15 +542,15 @@ export const buildCanvasSDXLOutpaintGraph = async ( } else { // Add Images To Nodes graph.nodes[INPAINT_INFILL] = { - ...(graph.nodes[INPAINT_INFILL] as InfillTileInvocation | InfillPatchMatchInvocation), + ...(graph.nodes[INPAINT_INFILL] as Invocation<'infill_tile'> | Invocation<'infill_patchmatch'>), image: canvasInitImage, }; - (graph.nodes[NOISE] as NoiseInvocation).width = width; - (graph.nodes[NOISE] as NoiseInvocation).height = height; + (graph.nodes[NOISE] as Invocation<'noise'>).width = width; + (graph.nodes[NOISE] as Invocation<'noise'>).height = height; graph.nodes[INPAINT_IMAGE] = { - ...(graph.nodes[INPAINT_IMAGE] as ImageToLatentsInvocation), + ...(graph.nodes[INPAINT_IMAGE] as Invocation<'i2l'>), image: canvasInitImage, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts index b3870c9636..97f77f58d9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts @@ -1,11 +1,11 @@ import type { JSONObject } from 'common/types'; import type { ModelIdentifierField } from 'features/nodes/types/common'; import { METADATA } from 'features/nodes/util/graph/constants'; -import type { AnyModelConfig, CoreMetadataInvocation, NonNullableGraph } from 'services/api/types'; +import type { AnyModelConfig, NonNullableGraph, S } from 'services/api/types'; export const addCoreMetadataNode = ( graph: NonNullableGraph, - metadata: Partial, + metadata: Partial, nodeId: string ): void => { graph.nodes[METADATA] = { @@ -30,9 +30,9 @@ export const addCoreMetadataNode = ( export const upsertMetadata = ( graph: NonNullableGraph, - metadata: Partial | JSONObject + metadata: Partial | JSONObject ): void => { - const metadataNode = graph.nodes[METADATA] as CoreMetadataInvocation | undefined; + const metadataNode = graph.nodes[METADATA] as S['CoreMetadataInvocation'] | undefined; if (!metadataNode) { return; @@ -41,8 +41,8 @@ export const upsertMetadata = ( Object.assign(metadataNode, metadata); }; -export const removeMetadata = (graph: NonNullableGraph, key: keyof CoreMetadataInvocation): void => { - const metadataNode = graph.nodes[METADATA] as CoreMetadataInvocation | undefined; +export const removeMetadata = (graph: NonNullableGraph, key: keyof S['CoreMetadataInvocation']): void => { + const metadataNode = graph.nodes[METADATA] as S['CoreMetadataInvocation'] | undefined; if (!metadataNode) { return; @@ -52,7 +52,7 @@ export const removeMetadata = (graph: NonNullableGraph, key: keyof CoreMetadataI }; export const getHasMetadata = (graph: NonNullableGraph): boolean => { - const metadataNode = graph.nodes[METADATA] as CoreMetadataInvocation | undefined; + const metadataNode = graph.nodes[METADATA] as S['CoreMetadataInvocation'] | undefined; return Boolean(metadataNode); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts index 3496d811d0..008f86918a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts @@ -7,11 +7,11 @@ import type { AnyInvocationInputField, AnyInvocationOutputField, AnyModelConfig, - CoreMetadataInvocation, InputFields, Invocation, InvocationType, OutputFields, + S, } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -335,13 +335,13 @@ export class Graph { * INTERNAL: Get the metadata node. If it does not exist, it is created. * @returns The metadata node. */ - _getMetadataNode(): CoreMetadataInvocation { + _getMetadataNode(): S['CoreMetadataInvocation'] { try { const node = this.getNode(METADATA) as AnyInvocationIncMetadata; assert(node.type === 'core_metadata'); return node; } catch { - const node: CoreMetadataInvocation = { id: METADATA, type: 'core_metadata' }; + const node: S['CoreMetadataInvocation'] = { id: METADATA, type: 'core_metadata' }; // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing return this.addNode(node); } @@ -353,7 +353,7 @@ export class Graph { * @param metadata The metadata to add. * @returns The metadata node. */ - upsertMetadata(metadata: Partial): CoreMetadataInvocation { + upsertMetadata(metadata: Partial): S['CoreMetadataInvocation'] { const node = this._getMetadataNode(); Object.assign(node, metadata); return node; @@ -364,7 +364,7 @@ export class Graph { * @param keys The keys of the metadata to remove * @returns The metadata node */ - removeMetadata(keys: string[]): CoreMetadataInvocation { + removeMetadata(keys: string[]): S['CoreMetadataInvocation'] { const metadataNode = this._getMetadataNode(); for (const k of keys) { unset(metadataNode, k); diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index f9728f5e8a..6e2a70264f 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -154,42 +154,6 @@ export type OutputFields = Extract< AnyInvocationOutputField >; -// General nodes -export type CollectInvocation = Invocation<'collect'>; -export type InfillPatchMatchInvocation = Invocation<'infill_patchmatch'>; -export type InfillTileInvocation = Invocation<'infill_tile'>; -export type CreateGradientMaskInvocation = Invocation<'create_gradient_mask'>; -export type CanvasPasteBackInvocation = Invocation<'canvas_paste_back'>; -export type NoiseInvocation = Invocation<'noise'>; -export type SDXLLoRALoaderInvocation = Invocation<'sdxl_lora_loader'>; -export type ImageToLatentsInvocation = Invocation<'i2l'>; -export type LatentsToImageInvocation = Invocation<'l2i'>; -export type LoRALoaderInvocation = Invocation<'lora_loader'>; -export type ESRGANInvocation = Invocation<'esrgan'>; -export type ImageNSFWBlurInvocation = Invocation<'img_nsfw'>; -export type ImageWatermarkInvocation = Invocation<'img_watermark'>; -export type SeamlessModeInvocation = Invocation<'seamless'>; -export type CoreMetadataInvocation = Extract; - -// ControlNet Nodes -export type ControlNetInvocation = Invocation<'controlnet'>; -export type T2IAdapterInvocation = Invocation<'t2i_adapter'>; -export type IPAdapterInvocation = Invocation<'ip_adapter'>; -export type CannyImageProcessorInvocation = Invocation<'canny_image_processor'>; -export type ColorMapImageProcessorInvocation = Invocation<'color_map_image_processor'>; -export type ContentShuffleImageProcessorInvocation = Invocation<'content_shuffle_image_processor'>; -export type DepthAnythingImageProcessorInvocation = Invocation<'depth_anything_image_processor'>; -export type HedImageProcessorInvocation = Invocation<'hed_image_processor'>; -export type LineartAnimeImageProcessorInvocation = Invocation<'lineart_anime_image_processor'>; -export type LineartImageProcessorInvocation = Invocation<'lineart_image_processor'>; -export type MediapipeFaceProcessorInvocation = Invocation<'mediapipe_face_processor'>; -export type MidasDepthImageProcessorInvocation = Invocation<'midas_depth_image_processor'>; -export type MlsdImageProcessorInvocation = Invocation<'mlsd_image_processor'>; -export type NormalbaeImageProcessorInvocation = Invocation<'normalbae_image_processor'>; -export type DWOpenposeImageProcessorInvocation = Invocation<'dw_openpose_image_processor'>; -export type PidiImageProcessorInvocation = Invocation<'pidi_image_processor'>; -export type ZoeDepthImageProcessorInvocation = Invocation<'zoe_depth_image_processor'>; - // Node Outputs export type ImageOutput = S['ImageOutput']; From af477fa295135b1debc4f93c9bbaeb861028daa3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 15 May 2024 13:44:30 +1000 Subject: [PATCH 134/442] tidy(ui): remove unused modelLoader from refiner helper --- .../src/features/nodes/util/graph/generation/addSDXLRefiner.ts | 2 -- .../nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts index 806a138b62..caab153b60 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts @@ -17,7 +17,6 @@ export const addSDXLRefiner = async ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'>, - modelLoader: Invocation<'sdxl_model_loader'>, seamless: Invocation<'seamless'> | null, posCond: Invocation<'sdxl_compel_prompt'>, negCond: Invocation<'sdxl_compel_prompt'>, @@ -77,7 +76,6 @@ export const addSDXLRefiner = async ( seamless_y: seamless.seamless_y, }); g.addEdge(refinerModelLoader, 'unet', refinerSeamless, 'unet'); - g.addEdge(refinerModelLoader, 'vae', refinerSeamless, 'vae'); g.addEdge(refinerSeamless, 'unet', refinerDenoise, 'unet'); } else { g.addEdge(refinerModelLoader, 'unet', refinerDenoise, 'unet'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts index 06c2ded18a..416e81a632 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts @@ -145,7 +145,7 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise Date: Wed, 15 May 2024 13:44:46 +1000 Subject: [PATCH 135/442] docs(ui): add comments to nsfw & watermarker helpers --- .../src/features/nodes/util/graph/generation/addNSFWChecker.ts | 1 + .../src/features/nodes/util/graph/generation/addWatermarker.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts index 504cad14aa..7850413195 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts @@ -20,6 +20,7 @@ export const addNSFWChecker = ( use_cache: false, }); + // The NSFW checker node is the new image output - make the previous one intermediate imageOutput.is_intermediate = true; imageOutput.use_cache = true; imageOutput.board = undefined; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts index 50986ac3ff..2a7af866f8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts @@ -20,6 +20,7 @@ export const addWatermarker = ( use_cache: false, }); + // The watermarker node is the new image output - make the previous one intermediate imageOutput.is_intermediate = true; imageOutput.use_cache = true; imageOutput.board = undefined; From 3b1743b7c2140332786291c1d22fcdf912114912 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 07:42:24 +1000 Subject: [PATCH 136/442] docs: fix install reqs link --- docs/installation/010_INSTALL_AUTOMATED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/010_INSTALL_AUTOMATED.md b/docs/installation/010_INSTALL_AUTOMATED.md index 9eb8620321..3c6c90afdc 100644 --- a/docs/installation/010_INSTALL_AUTOMATED.md +++ b/docs/installation/010_INSTALL_AUTOMATED.md @@ -98,7 +98,7 @@ Updating is exactly the same as installing - download the latest installer, choo If you have installation issues, please review the [FAQ]. You can also [create an issue] or ask for help on [discord]. -[installation requirements]: INSTALLATION.md#installation-requirements +[installation requirements]: INSTALL_REQUIREMENTS.md [FAQ]: ../help/FAQ.md [install some models]: 050_INSTALLING_MODELS.md [configuration docs]: ../features/CONFIGURATION.md From 40b4fa723819b984678f1cd868108c2506e79010 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 15:55:22 +1000 Subject: [PATCH 137/442] feat(ui): SDXL clip skip Uses the same CLIP Skip value for both CLIP1 and CLIP2. Adjusted SDXL CLIP Skip min/max/markers to be within the valid range (0 to 11). Closes #4583 --- .../util/graph/generation/addSDXLLoRAs.ts | 6 +++-- .../generation/buildGenerationTabSDXLGraph.ts | 25 +++++++++++++++---- .../components/Advanced/ParamClipSkip.tsx | 4 --- .../features/parameters/types/constants.ts | 4 +-- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts index f38e8de570..cef2ad2f47 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts @@ -11,6 +11,8 @@ export const addSDXLLoRas = ( denoise: Invocation<'denoise_latents'>, modelLoader: Invocation<'sdxl_model_loader'>, seamless: Invocation<'seamless'> | null, + clipSkip: Invocation<'clip_skip'>, + clipSkip2: Invocation<'clip_skip'>, posCond: Invocation<'sdxl_compel_prompt'>, negCond: Invocation<'sdxl_compel_prompt'> ): void => { @@ -37,8 +39,8 @@ export const addSDXLLoRas = ( g.addEdge(loraCollector, 'collection', loraCollectionLoader, 'loras'); // Use seamless as UNet input if it exists, otherwise use the model loader g.addEdge(seamless ?? modelLoader, 'unet', loraCollectionLoader, 'unet'); - g.addEdge(modelLoader, 'clip', loraCollectionLoader, 'clip'); - g.addEdge(modelLoader, 'clip2', loraCollectionLoader, 'clip2'); + g.addEdge(clipSkip, 'clip', loraCollectionLoader, 'clip'); + g.addEdge(clipSkip2, 'clip', loraCollectionLoader, 'clip2'); // Reroute UNet & CLIP connections through the LoRA collection loader g.deleteEdgesTo(denoise, ['unet']); g.deleteEdgesTo(posCond, ['clip', 'clip2']); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts index 416e81a632..4e8d716a11 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts @@ -1,6 +1,7 @@ import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { + CLIP_SKIP, LATENTS_TO_IMAGE, NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING_COLLECT, @@ -29,6 +30,7 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); - g.addEdge(modelLoader, 'clip', posCond, 'clip'); - g.addEdge(modelLoader, 'clip', negCond, 'clip'); - g.addEdge(modelLoader, 'clip2', posCond, 'clip2'); - g.addEdge(modelLoader, 'clip2', negCond, 'clip2'); + g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); + g.addEdge(modelLoader, 'clip2', clipSkip2, 'clip'); + g.addEdge(clipSkip, 'clip', posCond, 'clip'); + g.addEdge(clipSkip, 'clip', negCond, 'clip'); + g.addEdge(clipSkip2, 'clip', posCond, 'clip2'); + g.addEdge(clipSkip2, 'clip', negCond, 'clip2'); g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); @@ -132,12 +146,13 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise { return CLIP_SKIP_MAP[model.base].markers; }, [model]); - if (model?.base === 'sdxl') { - return null; - } - return ( diff --git a/invokeai/frontend/web/src/features/parameters/types/constants.ts b/invokeai/frontend/web/src/features/parameters/types/constants.ts index 6d7b4f9248..05d16a7eda 100644 --- a/invokeai/frontend/web/src/features/parameters/types/constants.ts +++ b/invokeai/frontend/web/src/features/parameters/types/constants.ts @@ -39,8 +39,8 @@ export const CLIP_SKIP_MAP = { markers: [0, 1, 2, 3, 5, 10, 15, 20, 24], }, sdxl: { - maxClip: 24, - markers: [0, 1, 2, 3, 5, 10, 15, 20, 24], + maxClip: 11, + markers: [0, 1, 2, 5, 11], }, 'sdxl-refiner': { maxClip: 24, From 31d8b502765fdc1950a99260bed2f7519b6cf748 Mon Sep 17 00:00:00 2001 From: H0onnn Date: Fri, 17 May 2024 08:59:07 +0900 Subject: [PATCH 138/442] [Refactor] Update min and max values for LoRACard weight input --- .../frontend/web/src/features/lora/components/LoRACard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index ddcdd58e75..f7261b4608 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -75,8 +75,8 @@ export const LoRACard = memo((props: LoRACardProps) => { Date: Wed, 15 May 2024 16:37:13 +1000 Subject: [PATCH 139/442] feat(ui): make nodesSlice undoable --- .../listeners/boardAndImagesDeleted.ts | 2 +- .../listeners/enqueueRequestedNodes.ts | 4 +-- .../listeners/imageDeleted.ts | 2 +- .../listeners/updateAllNodesRequested.ts | 2 +- .../listeners/workflowLoadRequested.ts | 2 +- invokeai/frontend/web/src/app/store/store.ts | 4 +-- .../flow/AddNodePopover/AddNodePopover.tsx | 6 ++-- .../features/nodes/components/flow/Flow.tsx | 28 +++++++++++++++---- .../flow/edges/InvocationDefaultEdge.tsx | 2 +- .../flow/nodes/common/NodeWrapper.tsx | 2 +- .../BottomLeftPanel/NodeOpacitySlider.tsx | 2 +- .../BottomLeftPanel/ViewportControls.tsx | 4 +-- .../flow/panels/MinimapPanel/MinimapPanel.tsx | 2 +- .../src/features/nodes/hooks/useBuildNode.ts | 2 +- .../nodes/hooks/useIsValidConnection.ts | 4 +-- .../src/features/nodes/store/nodesSlice.ts | 21 ++++++++++++-- .../nodes/util/workflow/graphToWorkflow.ts | 2 +- .../nodes/util/workflow/migrations.ts | 2 +- 18 files changed, 64 insertions(+), 29 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index a0b07b9419..244e0cdf8a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -21,7 +21,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS const { canvas, nodes, controlAdapters, controlLayers } = getState(); deleted_images.forEach((image_name) => { - const imageUsage = getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name); + const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name); if (imageUsage.isCanvasImage && !wasCanvasReset) { dispatch(resetCanvas()); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 12741c52f5..c4087aacde 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -11,9 +11,9 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = enqueueRequested.match(action) && action.payload.tabName === 'workflows', effect: async (action, { getState, dispatch }) => { const state = getState(); - const { nodes, edges } = state.nodes; + const { nodes, edges } = state.nodes.present; const workflow = state.workflow; - const graph = buildNodesGraph(state.nodes); + const graph = buildNodesGraph(state.nodes.present); const builtWorkflow = buildWorkflowWithValidation({ nodes, edges, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index 501f71db70..8c24badc76 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -29,7 +29,7 @@ import type { ImageDTO } from 'services/api/types'; import { imagesSelectors } from 'services/api/util'; const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.nodes.nodes.forEach((node) => { + state.nodes.present.nodes.forEach((node) => { if (!isInvocationNode(node)) { return; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts index 5ee9de3c11..ebd4d00901 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts @@ -14,7 +14,7 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi actionCreator: updateAllNodesRequested, effect: (action, { dispatch, getState }) => { const log = logger('nodes'); - const { nodes, templates } = getState().nodes; + const { nodes, templates } = getState().nodes.present; let unableToUpdateCount = 0; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts index 0227597fe9..5a2c270b2a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts @@ -17,7 +17,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList effect: (action, { dispatch, getState }) => { const log = logger('nodes'); const { workflow, asCopy } = action.payload; - const nodeTemplates = getState().nodes.templates; + const nodeTemplates = getState().nodes.present.templates; try { const { workflow: validatedWorkflow, warnings } = validateWorkflow(workflow, nodeTemplates); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 9661f57f99..876f079529 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -21,7 +21,7 @@ import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/galle import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice'; import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice'; import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice'; -import { nodesPersistConfig, nodesSlice } from 'features/nodes/store/nodesSlice'; +import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice'; import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice'; import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice'; import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice'; @@ -50,7 +50,7 @@ const allReducers = { [canvasSlice.name]: canvasSlice.reducer, [gallerySlice.name]: gallerySlice.reducer, [generationSlice.name]: generationSlice.reducer, - [nodesSlice.name]: nodesSlice.reducer, + [nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig), [postprocessingSlice.name]: postprocessingSlice.reducer, [systemSlice.name]: systemSlice.reducer, [configSlice.name]: configSlice.reducer, diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 061209cafc..6cfc95e311 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -55,8 +55,8 @@ const AddNodePopover = () => { const selectRef = useRef | null>(null); const inputRef = useRef(null); - const fieldFilter = useAppSelector((s) => s.nodes.connectionStartFieldType); - const handleFilter = useAppSelector((s) => s.nodes.connectionStartParams?.handleType); + const fieldFilter = useAppSelector((s) => s.nodes.present.connectionStartFieldType); + const handleFilter = useAppSelector((s) => s.nodes.present.connectionStartParams?.handleType); const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { // If we have a connection in progress, we need to filter the node choices @@ -105,7 +105,7 @@ const AddNodePopover = () => { }); const { options } = useAppSelector(selector); - const isOpen = useAppSelector((s) => s.nodes.isAddNodePopoverOpen); + const isOpen = useAppSelector((s) => s.nodes.present.isAddNodePopoverOpen); const addNode = useCallback( (nodeType: string) => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 4b9249e94f..bc3700e6be 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -14,11 +14,13 @@ import { edgesDeleted, nodesChanged, nodesDeleted, + redo, selectedAll, selectedEdgesChanged, selectedNodesChanged, selectionCopied, selectionPasted, + undo, viewportChanged, } from 'features/nodes/store/nodesSlice'; import { $flow } from 'features/nodes/store/reactFlowInstance'; @@ -70,11 +72,11 @@ const snapGrid: [number, number] = [25, 25]; export const Flow = memo(() => { const dispatch = useAppDispatch(); - const nodes = useAppSelector((s) => s.nodes.nodes); - const edges = useAppSelector((s) => s.nodes.edges); - const viewport = useAppSelector((s) => s.nodes.viewport); - const shouldSnapToGrid = useAppSelector((s) => s.nodes.shouldSnapToGrid); - const selectionMode = useAppSelector((s) => s.nodes.selectionMode); + const nodes = useAppSelector((s) => s.nodes.present.nodes); + const edges = useAppSelector((s) => s.nodes.present.edges); + const viewport = useAppSelector((s) => s.nodes.present.viewport); + const shouldSnapToGrid = useAppSelector((s) => s.nodes.present.shouldSnapToGrid); + const selectionMode = useAppSelector((s) => s.nodes.present.selectionMode); const flowWrapper = useRef(null); const cursorPosition = useRef(null); const isValidConnection = useIsValidConnection(); @@ -251,6 +253,22 @@ export const Flow = memo(() => { dispatch(selectionPasted({ cursorPosition: cursorPosition.current })); }); + useHotkeys( + ['meta+z', 'ctrl+z'], + () => { + dispatch(undo()); + }, + [dispatch] + ); + + useHotkeys( + ['meta+shift+z', 'ctrl+shift+z'], + () => { + dispatch(redo()); + }, + [dispatch] + ); + return ( s.nodes.shouldShowEdgeLabels); + const shouldShowEdgeLabels = useAppSelector((s) => s.nodes.present.shouldShowEdgeLabels); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx index 3811514ad4..9b018c8c34 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx @@ -39,7 +39,7 @@ const NodeWrapper = (props: NodeWrapperProps) => { const dispatch = useAppDispatch(); - const opacity = useAppSelector((s) => s.nodes.nodeOpacity); + const opacity = useAppSelector((s) => s.nodes.present.nodeOpacity); const { onCloseGlobal } = useGlobalMenuClose(); const handleClick = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx index b24b2058bd..008ae12361 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; const NodeOpacitySlider = () => { const dispatch = useAppDispatch(); - const nodeOpacity = useAppSelector((s) => s.nodes.nodeOpacity); + const nodeOpacity = useAppSelector((s) => s.nodes.present.nodeOpacity); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx index b2251480d7..c354efb348 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx @@ -19,9 +19,9 @@ const ViewportControls = () => { const { zoomIn, zoomOut, fitView } = useReactFlow(); const dispatch = useAppDispatch(); // const shouldShowFieldTypeLegend = useAppSelector( - // (s) => s.nodes.shouldShowFieldTypeLegend + // (s) => s.nodes.present.shouldShowFieldTypeLegend // ); - const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.shouldShowMinimapPanel); + const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.present.shouldShowMinimapPanel); const handleClickedZoomIn = useCallback(() => { zoomIn(); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx index b34ae11c85..72b7091fd2 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx @@ -16,7 +16,7 @@ const minimapStyles: SystemStyleObject = { }; const MinimapPanel = () => { - const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.shouldShowMinimapPanel); + const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.present.shouldShowMinimapPanel); return ( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts index cce2265d83..b166b71788 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts @@ -8,7 +8,7 @@ import { useCallback } from 'react'; import { useReactFlow } from 'reactflow'; export const useBuildNode = () => { - const nodeTemplates = useAppSelector((s) => s.nodes.templates); + const nodeTemplates = useAppSelector((s) => s.nodes.present.templates); const flow = useReactFlow(); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index ded05c7b9b..cf5ede364c 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -13,7 +13,7 @@ import type { Connection, Node } from 'reactflow'; export const useIsValidConnection = () => { const store = useAppStore(); - const shouldValidateGraph = useAppSelector((s) => s.nodes.shouldValidateGraph); + const shouldValidateGraph = useAppSelector((s) => s.nodes.present.shouldValidateGraph); const isValidConnection = useCallback( ({ source, sourceHandle, target, targetHandle }: Connection): boolean => { // Connection must have valid targets @@ -27,7 +27,7 @@ export const useIsValidConnection = () => { } const state = store.getState(); - const { nodes, edges, templates } = state.nodes; + const { nodes, edges, templates } = state.nodes.present; // Find the source and target nodes const sourceNode = nodes.find((node) => node.id === source) as Node; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 0f0417cf71..63c9262d8a 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -1,4 +1,4 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; +import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; @@ -66,6 +66,7 @@ import { getOutgoers, SelectionMode, } from 'reactflow'; +import type { UndoableOptions } from 'redux-undo'; import { socketGeneratorProgress, socketInvocationComplete, @@ -705,6 +706,8 @@ export const nodesSlice = createSlice({ nodeTemplatesBuilt: (state, action: PayloadAction>) => { state.templates = action.payload; }, + undo: (state) => state, + redo: (state) => state, }, extraReducers: (builder) => { builder.addCase(workflowLoaded, (state, action) => { @@ -836,6 +839,8 @@ export const { edgeAdded, nodeTemplatesBuilt, shouldShowEdgeLabelsChanged, + undo, + redo, } = nodesSlice.actions; // This is used for tracking `state.workflow.isTouched` @@ -874,7 +879,7 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( edgeAdded ); -export const selectNodesSlice = (state: RootState) => state.nodes; +export const selectNodesSlice = (state: RootState) => state.nodes.present; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateNodesState = (state: any): any => { @@ -900,3 +905,15 @@ export const nodesPersistConfig: PersistConfig = { 'addNewNodePosition', ], }; + +export const nodesUndoableConfig: UndoableOptions = { + limit: 64, + undoType: nodesSlice.actions.undo.type, + redoType: nodesSlice.actions.redo.type, + groupBy: (action, state, history) => { + return null; + }, + filter: (action, _state, _history) => { + return true; + }, +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts index eec9c6cf4b..361e3134ae 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts @@ -18,7 +18,7 @@ import { v4 as uuidv4 } from 'uuid'; * @returns The workflow. */ export const graphToWorkflow = (graph: NonNullableGraph, autoLayout = true): WorkflowV3 => { - const invocationTemplates = getStore().getState().nodes.templates; + const invocationTemplates = getStore().getState().nodes.present.templates; if (!invocationTemplates) { throw new Error(t('app.storeNotInitialized')); diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts index 56fb04d61d..3f666e8771 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts @@ -33,7 +33,7 @@ const zWorkflowMetaVersion = z.object({ * - Workflow schema version bumped to 2.0.0 */ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => { - const invocationTemplates = $store.get()?.getState().nodes.templates; + const invocationTemplates = $store.get()?.getState().nodes.present.templates; if (!invocationTemplates) { throw new Error(t('app.storeNotInitialized')); From 9c0d44b412c8712a705977d03b73d6d1de4ada44 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 15 May 2024 17:20:51 +1000 Subject: [PATCH 140/442] feat(ui): split workflow editor settings to separate slice We need the undoable slice to be only undoable state - settings are not undoable. --- invokeai/frontend/web/src/app/store/store.ts | 3 + .../src/common/hooks/useIsReadyToEnqueue.ts | 6 +- .../features/nodes/components/flow/Flow.tsx | 4 +- .../connectionLines/CustomConnectionLine.tsx | 11 ++- .../flow/edges/InvocationDefaultEdge.tsx | 2 +- .../flow/edges/util/makeEdgeSelector.ts | 8 +- .../flow/nodes/common/NodeWrapper.tsx | 2 +- .../BottomLeftPanel/NodeOpacitySlider.tsx | 4 +- .../BottomLeftPanel/ViewportControls.tsx | 7 +- .../flow/panels/MinimapPanel/MinimapPanel.tsx | 2 +- .../TopRightPanel/WorkflowEditorSettings.tsx | 8 +- .../nodes/hooks/useIsValidConnection.ts | 2 +- .../src/features/nodes/store/nodesSlice.ts | 50 +---------- .../web/src/features/nodes/store/types.ts | 10 +-- .../nodes/store/workflowSettingsSlice.ts | 87 +++++++++++++++++++ 15 files changed, 122 insertions(+), 84 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 876f079529..062cdc1cbf 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -22,6 +22,7 @@ import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice'; import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice'; import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice'; +import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice'; import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice'; import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice'; @@ -66,6 +67,7 @@ const allReducers = { [workflowSlice.name]: workflowSlice.reducer, [hrfSlice.name]: hrfSlice.reducer, [controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig), + [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, [api.reducerPath]: api.reducer, }; @@ -111,6 +113,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, [hrfPersistConfig.name]: hrfPersistConfig, [controlLayersPersistConfig.name]: controlLayersPersistConfig, + [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 3c863d0c93..972cb063cf 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -10,6 +10,7 @@ import type { Layer } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { selectSystemSlice } from 'features/system/store/systemSlice'; @@ -31,11 +32,12 @@ const selector = createMemoizedSelector( selectGenerationSlice, selectSystemSlice, selectNodesSlice, + selectWorkflowSettingsSlice, selectDynamicPromptsSlice, selectControlLayersSlice, activeTabNameSelector, ], - (controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => { + (controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, controlLayers, activeTabName) => { const { model } = generation; const { size } = controlLayers.present; const { positivePrompt } = controlLayers.present; @@ -50,7 +52,7 @@ const selector = createMemoizedSelector( } if (activeTabName === 'workflows') { - if (nodes.shouldValidateGraph) { + if (workflowSettings.shouldValidateGraph) { if (!nodes.nodes.length) { reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') }); } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index bc3700e6be..7176ba3574 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -75,8 +75,8 @@ export const Flow = memo(() => { const nodes = useAppSelector((s) => s.nodes.present.nodes); const edges = useAppSelector((s) => s.nodes.present.edges); const viewport = useAppSelector((s) => s.nodes.present.viewport); - const shouldSnapToGrid = useAppSelector((s) => s.nodes.present.shouldSnapToGrid); - const selectionMode = useAppSelector((s) => s.nodes.present.selectionMode); + const shouldSnapToGrid = useAppSelector((s) => s.workflowSettings.shouldSnapToGrid); + const selectionMode = useAppSelector((s) => s.workflowSettings.selectionMode); const flowWrapper = useRef(null); const cursorPosition = useRef(null); const isValidConnection = useIsValidConnection(); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx index 61efcea06a..ad0ed5957c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx @@ -3,17 +3,20 @@ import { useAppSelector } from 'app/store/storeHooks'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import type { CSSProperties } from 'react'; import { memo } from 'react'; import type { ConnectionLineComponentProps } from 'reactflow'; import { getBezierPath } from 'reactflow'; -const selectStroke = createSelector(selectNodesSlice, (nodes) => - nodes.shouldColorEdges ? getFieldColor(nodes.connectionStartFieldType) : colorTokenToCssVar('base.500') +const selectStroke = createSelector([selectNodesSlice, selectWorkflowSettingsSlice], (nodes, workflowSettings) => + workflowSettings.shouldColorEdges ? getFieldColor(nodes.connectionStartFieldType) : colorTokenToCssVar('base.500') ); -const selectClassName = createSelector(selectNodesSlice, (nodes) => - nodes.shouldAnimateEdges ? 'react-flow__custom_connection-path animated' : 'react-flow__custom_connection-path' +const selectClassName = createSelector(selectWorkflowSettingsSlice, (workflowSettings) => + workflowSettings.shouldAnimateEdges + ? 'react-flow__custom_connection-path animated' + : 'react-flow__custom_connection-path' ); const pathStyles: CSSProperties = { opacity: 0.8 }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx index 9fc07ae708..0966bca88e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx @@ -27,7 +27,7 @@ const InvocationDefaultEdge = ({ ); const { isSelected, shouldAnimate, stroke, label } = useAppSelector(selector); - const shouldShowEdgeLabels = useAppSelector((s) => s.nodes.present.shouldShowEdgeLabels); + const shouldShowEdgeLabels = useAppSelector((s) => s.workflowSettings.shouldShowEdgeLabels); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts index a485bf64c1..f251b4d20c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts @@ -2,6 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { selectFieldOutputTemplate, selectNodeTemplate } from 'features/nodes/store/selectors'; +import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getFieldColor } from './getEdgeColor'; @@ -22,7 +23,8 @@ export const makeEdgeSelector = ( ) => createMemoizedSelector( selectNodesSlice, - (nodes): { isSelected: boolean; shouldAnimate: boolean; stroke: string; label: string } => { + selectWorkflowSettingsSlice, + (nodes, workflowSettings): { isSelected: boolean; shouldAnimate: boolean; stroke: string; label: string } => { const sourceNode = nodes.nodes.find((node) => node.id === source); const targetNode = nodes.nodes.find((node) => node.id === target); @@ -36,7 +38,7 @@ export const makeEdgeSelector = ( const outputFieldTemplate = selectFieldOutputTemplate(nodes, sourceNode.id, sourceHandleId); const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined; - const stroke = sourceType && nodes.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); + const stroke = sourceType && workflowSettings.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); const sourceNodeTemplate = selectNodeTemplate(nodes, sourceNode.id); const targetNodeTemplate = selectNodeTemplate(nodes, targetNode.id); @@ -45,7 +47,7 @@ export const makeEdgeSelector = ( return { isSelected, - shouldAnimate: nodes.shouldAnimateEdges && isSelected, + shouldAnimate: workflowSettings.shouldAnimateEdges && isSelected, stroke, label, }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx index 9b018c8c34..51649f4f82 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx @@ -39,7 +39,7 @@ const NodeWrapper = (props: NodeWrapperProps) => { const dispatch = useAppDispatch(); - const opacity = useAppSelector((s) => s.nodes.present.nodeOpacity); + const opacity = useAppSelector((s) => s.workflowSettings.nodeOpacity); const { onCloseGlobal } = useGlobalMenuClose(); const handleClick = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx index 008ae12361..7a46782f1b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx @@ -1,12 +1,12 @@ import { CompositeSlider, Flex } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { nodeOpacityChanged } from 'features/nodes/store/nodesSlice'; +import { nodeOpacityChanged } from 'features/nodes/store/workflowSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const NodeOpacitySlider = () => { const dispatch = useAppDispatch(); - const nodeOpacity = useAppSelector((s) => s.nodes.present.nodeOpacity); + const nodeOpacity = useAppSelector((s) => s.workflowSettings.nodeOpacity); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx index c354efb348..f2624de58e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx @@ -1,9 +1,6 @@ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - // shouldShowFieldTypeLegendChanged, - shouldShowMinimapPanelChanged, -} from 'features/nodes/store/nodesSlice'; +import { shouldShowMinimapPanelChanged } from 'features/nodes/store/workflowSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -21,7 +18,7 @@ const ViewportControls = () => { // const shouldShowFieldTypeLegend = useAppSelector( // (s) => s.nodes.present.shouldShowFieldTypeLegend // ); - const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.present.shouldShowMinimapPanel); + const shouldShowMinimapPanel = useAppSelector((s) => s.workflowSettings.shouldShowMinimapPanel); const handleClickedZoomIn = useCallback(() => { zoomIn(); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx index 72b7091fd2..92668c3fa8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx @@ -16,7 +16,7 @@ const minimapStyles: SystemStyleObject = { }; const MinimapPanel = () => { - const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.present.shouldShowMinimapPanel); + const shouldShowMinimapPanel = useAppSelector((s) => s.workflowSettings.shouldShowMinimapPanel); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx index b366737e59..37fac8ee7b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx @@ -21,13 +21,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton'; import { selectionModeChanged, - selectNodesSlice, + selectWorkflowSettingsSlice, shouldAnimateEdgesChanged, shouldColorEdgesChanged, shouldShowEdgeLabelsChanged, shouldSnapToGridChanged, shouldValidateGraphChanged, -} from 'features/nodes/store/nodesSlice'; +} from 'features/nodes/store/workflowSettingsSlice'; import type { ChangeEvent, ReactNode } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -35,7 +35,7 @@ import { SelectionMode } from 'reactflow'; const formLabelProps: FormLabelProps = { flexGrow: 1 }; -const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { +const selector = createMemoizedSelector(selectWorkflowSettingsSlice, (workflowSettings) => { const { shouldAnimateEdges, shouldValidateGraph, @@ -43,7 +43,7 @@ const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { shouldColorEdges, shouldShowEdgeLabels, selectionMode, - } = nodes; + } = workflowSettings; return { shouldAnimateEdges, shouldValidateGraph, diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index cf5ede364c..7ab28f58c2 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -13,7 +13,7 @@ import type { Connection, Node } from 'reactflow'; export const useIsValidConnection = () => { const store = useAppStore(); - const shouldValidateGraph = useAppSelector((s) => s.nodes.present.shouldValidateGraph); + const shouldValidateGraph = useAppSelector((s) => s.workflowSettings.shouldValidateGraph); const isValidConnection = useCallback( ({ source, sourceHandle, target, targetHandle }: Connection): boolean => { // Connection must have valid targets diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 63c9262d8a..73cd664dd0 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -57,15 +57,7 @@ import type { Viewport, XYPosition, } from 'reactflow'; -import { - addEdge, - applyEdgeChanges, - applyNodeChanges, - getConnectedEdges, - getIncomers, - getOutgoers, - SelectionMode, -} from 'reactflow'; +import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow'; import type { UndoableOptions } from 'redux-undo'; import { socketGeneratorProgress, @@ -99,21 +91,13 @@ const initialNodesState: NodesState = { connectionMade: false, modifyingEdge: false, addNewNodePosition: null, - shouldShowMinimapPanel: true, - shouldValidateGraph: true, - shouldAnimateEdges: true, - shouldSnapToGrid: false, - shouldColorEdges: true, - shouldShowEdgeLabels: false, isAddNodePopoverOpen: false, - nodeOpacity: 1, selectedNodes: [], selectedEdges: [], nodeExecutionStates: {}, viewport: { x: 0, y: 0, zoom: 1 }, nodesToCopy: [], edgesToCopy: [], - selectionMode: SelectionMode.Partial, }; type FieldValueAction = PayloadAction<{ @@ -538,31 +522,10 @@ export const nodesSlice = createSlice({ } node.data.notes = value; }, - shouldShowMinimapPanelChanged: (state, action: PayloadAction) => { - state.shouldShowMinimapPanel = action.payload; - }, nodeEditorReset: (state) => { state.nodes = []; state.edges = []; }, - shouldValidateGraphChanged: (state, action: PayloadAction) => { - state.shouldValidateGraph = action.payload; - }, - shouldAnimateEdgesChanged: (state, action: PayloadAction) => { - state.shouldAnimateEdges = action.payload; - }, - shouldShowEdgeLabelsChanged: (state, action: PayloadAction) => { - state.shouldShowEdgeLabels = action.payload; - }, - shouldSnapToGridChanged: (state, action: PayloadAction) => { - state.shouldSnapToGrid = action.payload; - }, - shouldColorEdgesChanged: (state, action: PayloadAction) => { - state.shouldColorEdges = action.payload; - }, - nodeOpacityChanged: (state, action: PayloadAction) => { - state.nodeOpacity = action.payload; - }, viewportChanged: (state, action: PayloadAction) => { state.viewport = action.payload; }, @@ -700,9 +663,6 @@ export const nodesSlice = createSlice({ state.connectionStartParams = null; state.connectionStartFieldType = null; }, - selectionModeChanged: (state, action: PayloadAction) => { - state.selectionMode = action.payload ? SelectionMode.Full : SelectionMode.Partial; - }, nodeTemplatesBuilt: (state, action: PayloadAction>) => { state.templates = action.payload; }, @@ -819,7 +779,6 @@ export const { nodeIsOpenChanged, nodeLabelChanged, nodeNotesChanged, - nodeOpacityChanged, nodesChanged, nodesDeleted, nodeUseCacheChanged, @@ -828,17 +787,10 @@ export const { selectedEdgesChanged, selectedNodesChanged, selectionCopied, - selectionModeChanged, selectionPasted, - shouldAnimateEdgesChanged, - shouldColorEdgesChanged, - shouldShowMinimapPanelChanged, - shouldSnapToGridChanged, - shouldValidateGraphChanged, viewportChanged, edgeAdded, nodeTemplatesBuilt, - shouldShowEdgeLabelsChanged, undo, redo, } = nodesSlice.actions; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index 89b4855193..fdf3e638e3 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -6,7 +6,7 @@ import type { NodeExecutionState, } from 'features/nodes/types/invocation'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; -import type { OnConnectStartParams, SelectionMode, Viewport, XYPosition } from 'reactflow'; +import type { OnConnectStartParams, Viewport, XYPosition } from 'reactflow'; export type NodesState = { _version: 1; @@ -17,13 +17,6 @@ export type NodesState = { connectionStartFieldType: FieldType | null; connectionMade: boolean; modifyingEdge: boolean; - shouldShowMinimapPanel: boolean; - shouldValidateGraph: boolean; - shouldAnimateEdges: boolean; - nodeOpacity: number; - shouldSnapToGrid: boolean; - shouldColorEdges: boolean; - shouldShowEdgeLabels: boolean; selectedNodes: string[]; selectedEdges: string[]; nodeExecutionStates: Record; @@ -32,7 +25,6 @@ export type NodesState = { edgesToCopy: InvocationNodeEdge[]; isAddNodePopoverOpen: boolean; addNewNodePosition: XYPosition | null; - selectionMode: SelectionMode; }; export type WorkflowMode = 'edit' | 'view'; diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts new file mode 100644 index 0000000000..7487fd488b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts @@ -0,0 +1,87 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import { SelectionMode } from 'reactflow'; + +export type WorkflowSettingsState = { + _version: 1; + shouldShowMinimapPanel: boolean; + shouldValidateGraph: boolean; + shouldAnimateEdges: boolean; + nodeOpacity: number; + shouldSnapToGrid: boolean; + shouldColorEdges: boolean; + shouldShowEdgeLabels: boolean; + selectionMode: SelectionMode; +}; + +const initialState: WorkflowSettingsState = { + _version: 1, + shouldShowMinimapPanel: true, + shouldValidateGraph: true, + shouldAnimateEdges: true, + shouldSnapToGrid: false, + shouldColorEdges: true, + shouldShowEdgeLabels: false, + nodeOpacity: 1, + selectionMode: SelectionMode.Partial, +}; + +export const workflowSettingsSlice = createSlice({ + name: 'workflowSettings', + initialState, + reducers: { + shouldShowMinimapPanelChanged: (state, action: PayloadAction) => { + state.shouldShowMinimapPanel = action.payload; + }, + shouldValidateGraphChanged: (state, action: PayloadAction) => { + state.shouldValidateGraph = action.payload; + }, + shouldAnimateEdgesChanged: (state, action: PayloadAction) => { + state.shouldAnimateEdges = action.payload; + }, + shouldShowEdgeLabelsChanged: (state, action: PayloadAction) => { + state.shouldShowEdgeLabels = action.payload; + }, + shouldSnapToGridChanged: (state, action: PayloadAction) => { + state.shouldSnapToGrid = action.payload; + }, + shouldColorEdgesChanged: (state, action: PayloadAction) => { + state.shouldColorEdges = action.payload; + }, + nodeOpacityChanged: (state, action: PayloadAction) => { + state.nodeOpacity = action.payload; + }, + selectionModeChanged: (state, action: PayloadAction) => { + state.selectionMode = action.payload ? SelectionMode.Full : SelectionMode.Partial; + }, + }, +}); + +export const { + shouldAnimateEdgesChanged, + shouldColorEdgesChanged, + shouldShowMinimapPanelChanged, + shouldShowEdgeLabelsChanged, + shouldSnapToGridChanged, + shouldValidateGraphChanged, + nodeOpacityChanged, + selectionModeChanged, +} = workflowSettingsSlice.actions; + +export const selectWorkflowSettingsSlice = (state: RootState) => state.workflowSettings; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrateWorkflowSettingsState = (state: any): any => { + if (!('_version' in state)) { + state._version = 1; + } + return state; +}; + +export const workflowSettingsPersistConfig: PersistConfig = { + name: workflowSettingsSlice.name, + initialState, + migrate: migrateWorkflowSettingsState, + persistDenylist: [], +}; From d4df31230024b40547bb455e95046dac9c08b914 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 14:20:22 +1000 Subject: [PATCH 141/442] feat(ui): move nodes copy/paste out of slice --- .../features/nodes/components/flow/Flow.tsx | 25 ++- .../src/features/nodes/hooks/useCopyPaste.ts | 63 +++++++ .../src/features/nodes/store/nodesSlice.ts | 154 ++++++------------ .../web/src/features/nodes/store/types.ts | 2 - .../store/util/findUnoccupiedPosition.ts | 4 +- 5 files changed, 124 insertions(+), 124 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 7176ba3574..80bdf6577b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -1,9 +1,11 @@ import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { + $cursorPos, connectionEnded, connectionMade, connectionStarted, @@ -18,8 +20,6 @@ import { selectedAll, selectedEdgesChanged, selectedNodesChanged, - selectionCopied, - selectionPasted, undo, viewportChanged, } from 'features/nodes/store/nodesSlice'; @@ -41,7 +41,6 @@ import type { OnSelectionChangeFunc, ProOptions, ReactFlowProps, - XYPosition, } from 'reactflow'; import { Background, ReactFlow } from 'reactflow'; @@ -78,7 +77,6 @@ export const Flow = memo(() => { const shouldSnapToGrid = useAppSelector((s) => s.workflowSettings.shouldSnapToGrid); const selectionMode = useAppSelector((s) => s.workflowSettings.selectionMode); const flowWrapper = useRef(null); - const cursorPosition = useRef(null); const isValidConnection = useIsValidConnection(); useWorkflowWatcher(); const [borderRadius] = useToken('radii', ['base']); @@ -119,12 +117,13 @@ export const Flow = memo(() => { ); const onConnectEnd: OnConnectEnd = useCallback(() => { - if (!cursorPosition.current) { + const cursorPosition = $cursorPos.get(); + if (!cursorPosition) { return; } dispatch( connectionEnded({ - cursorPosition: cursorPosition.current, + cursorPosition, mouseOverNodeId: $mouseOverNode.get(), }) ); @@ -171,11 +170,12 @@ export const Flow = memo(() => { const onMouseMove = useCallback((event: MouseEvent) => { if (flowWrapper.current?.getBoundingClientRect()) { - cursorPosition.current = + $cursorPos.set( $flow.get()?.screenToFlowPosition({ x: event.clientX, y: event.clientY, - }) ?? null; + }) ?? null + ); } }, []); @@ -235,9 +235,11 @@ export const Flow = memo(() => { // #endregion + const { copySelection, pasteSelection } = useCopyPaste(); + useHotkeys(['Ctrl+c', 'Meta+c'], (e) => { e.preventDefault(); - dispatch(selectionCopied()); + copySelection(); }); useHotkeys(['Ctrl+a', 'Meta+a'], (e) => { @@ -246,11 +248,8 @@ export const Flow = memo(() => { }); useHotkeys(['Ctrl+v', 'Meta+v'], (e) => { - if (!cursorPosition.current) { - return; - } e.preventDefault(); - dispatch(selectionPasted({ cursorPosition: cursorPosition.current })); + pasteSelection(); }); useHotkeys( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts new file mode 100644 index 0000000000..727c0932f7 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts @@ -0,0 +1,63 @@ +import { getStore } from 'app/store/nanostores/store'; +import { deepClone } from 'common/util/deepClone'; +import { $copiedEdges,$copiedNodes,$cursorPos, selectionPasted, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; +import { v4 as uuidv4 } from 'uuid'; + +const copySelection = () => { + // Use the imperative API here so we don't have to pass the whole slice around + const { getState } = getStore(); + const { nodes, edges } = selectNodesSlice(getState()); + const selectedNodes = nodes.filter((node) => node.selected); + const selectedEdges = edges.filter((edge) => edge.selected); + $copiedNodes.set(selectedNodes); + $copiedEdges.set(selectedEdges); +}; + +const pasteSelection = () => { + const { getState, dispatch } = getStore(); + const currentNodes = selectNodesSlice(getState()).nodes; + const cursorPos = $cursorPos.get(); + + const copiedNodes = deepClone($copiedNodes.get()); + const copiedEdges = deepClone($copiedEdges.get()); + + // Calculate an offset to reposition nodes to surround the cursor position, maintaining relative positioning + const xCoords = copiedNodes.map((node) => node.position.x); + const yCoords = copiedNodes.map((node) => node.position.y); + const minX = Math.min(...xCoords); + const minY = Math.min(...yCoords); + const offsetX = cursorPos ? cursorPos.x - minX : 50; + const offsetY = cursorPos ? cursorPos.y - minY : 50; + + copiedNodes.forEach((node) => { + const { x, y } = findUnoccupiedPosition(currentNodes, node.position.x + offsetX, node.position.y + offsetY); + node.position.x = x; + node.position.y = y; + // Pasted nodes are selected + node.selected = true; + // Also give em a fresh id + const id = uuidv4(); + // Update the edges to point to the new node id + for (const edge of copiedEdges) { + if (edge.source === node.id) { + edge.source = id; + edge.id = edge.id.replace(node.data.id, id); + } + if (edge.target === node.id) { + edge.target = id; + edge.id = edge.id.replace(node.data.id, id); + } + } + node.id = id; + node.data.id = id; + }); + + dispatch(selectionPasted({ nodes: copiedNodes, edges: copiedEdges })); +}; + +const api = { copySelection, pasteSelection }; + +export const useCopyPaste = () => { + return api; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 73cd664dd0..21092bb7df 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -43,9 +43,15 @@ import { zT2IAdapterModelFieldValue, zVAEModelFieldValue, } from 'features/nodes/types/field'; -import type { AnyNode, InvocationTemplate, NodeExecutionState } from 'features/nodes/types/invocation'; +import type { + AnyNode, + InvocationNodeEdge, + InvocationTemplate, + NodeExecutionState, +} from 'features/nodes/types/invocation'; import { isInvocationNode, isNotesNode, zNodeStatus } from 'features/nodes/types/invocation'; import { forEach } from 'lodash-es'; +import { atom } from 'nanostores'; import type { Connection, Edge, @@ -66,7 +72,6 @@ import { socketInvocationStarted, socketQueueItemStatusChanged, } from 'services/events/actions'; -import { v4 as uuidv4 } from 'uuid'; import type { z } from 'zod'; import type { NodesState } from './types'; @@ -96,8 +101,6 @@ const initialNodesState: NodesState = { selectedEdges: [], nodeExecutionStates: {}, viewport: { x: 0, y: 0, zoom: 1 }, - nodesToCopy: [], - edgesToCopy: [], }; type FieldValueAction = PayloadAction<{ @@ -539,116 +542,52 @@ export const nodesSlice = createSlice({ state.edges ); }, - selectionCopied: (state) => { - const nodesToCopy: AnyNode[] = []; - const edgesToCopy: Edge[] = []; + selectionPasted: (state, action: PayloadAction<{ nodes: AnyNode[]; edges: InvocationNodeEdge[] }>) => { + const { nodes, edges } = action.payload; - for (const node of state.nodes) { - if (node.selected) { - nodesToCopy.push(deepClone(node)); - } - } + const nodeChanges: NodeChange[] = []; - for (const edge of state.edges) { - if (edge.selected) { - edgesToCopy.push(deepClone(edge)); - } - } - - state.nodesToCopy = nodesToCopy; - state.edgesToCopy = edgesToCopy; - - if (state.nodesToCopy.length > 0) { - const averagePosition = { x: 0, y: 0 }; - state.nodesToCopy.forEach((e) => { - const xOffset = 0.15 * (e.width ?? 0); - const yOffset = 0.5 * (e.height ?? 0); - averagePosition.x += e.position.x + xOffset; - averagePosition.y += e.position.y + yOffset; + // Deselect existing nodes + state.nodes.forEach((n) => { + nodeChanges.push({ + id: n.data.id, + type: 'select', + selected: false, }); - - averagePosition.x /= state.nodesToCopy.length; - averagePosition.y /= state.nodesToCopy.length; - - state.nodesToCopy.forEach((e) => { - e.position.x -= averagePosition.x; - e.position.y -= averagePosition.y; + }); + // Add new nodes + nodes.forEach((n) => { + nodeChanges.push({ + item: n, + type: 'add', }); - } - }, - selectionPasted: (state, action: PayloadAction<{ cursorPosition?: XYPosition }>) => { - const { cursorPosition } = action.payload; - const newNodes: AnyNode[] = []; - - for (const node of state.nodesToCopy) { - newNodes.push(deepClone(node)); - } - - const oldNodeIds = newNodes.map((n) => n.data.id); - - const newEdges: Edge[] = []; - - for (const edge of state.edgesToCopy) { - if (oldNodeIds.includes(edge.source) && oldNodeIds.includes(edge.target)) { - newEdges.push(deepClone(edge)); - } - } - - newEdges.forEach((e) => (e.selected = true)); - - newNodes.forEach((node) => { - const newNodeId = uuidv4(); - newEdges.forEach((edge) => { - if (edge.source === node.data.id) { - edge.source = newNodeId; - edge.id = edge.id.replace(node.data.id, newNodeId); - } - if (edge.target === node.data.id) { - edge.target = newNodeId; - edge.id = edge.id.replace(node.data.id, newNodeId); - } - }); - node.selected = true; - node.id = newNodeId; - node.data.id = newNodeId; - - const position = findUnoccupiedPosition( - state.nodes, - node.position.x + (cursorPosition?.x ?? 0), - node.position.y + (cursorPosition?.y ?? 0) - ); - - node.position = position; }); - const nodeAdditions: NodeChange[] = newNodes.map((n) => ({ - item: n, - type: 'add', - })); - const nodeSelectionChanges: NodeChange[] = state.nodes.map((n) => ({ - id: n.data.id, - type: 'select', - selected: false, - })); + const edgeChanges: EdgeChange[] = []; + // Deselect existing edges + state.edges.forEach((e) => { + edgeChanges.push({ + id: e.id, + type: 'select', + selected: false, + }); + }); + // Add new edges + edges.forEach((e) => { + edgeChanges.push({ + item: e, + type: 'add', + }); + }); - const edgeAdditions: EdgeChange[] = newEdges.map((e) => ({ - item: e, - type: 'add', - })); - const edgeSelectionChanges: EdgeChange[] = state.edges.map((e) => ({ - id: e.id, - type: 'select', - selected: false, - })); + state.nodes = applyNodeChanges(nodeChanges, state.nodes); + state.edges = applyEdgeChanges(edgeChanges, state.edges); - state.nodes = applyNodeChanges(nodeAdditions.concat(nodeSelectionChanges), state.nodes); - - state.edges = applyEdgeChanges(edgeAdditions.concat(edgeSelectionChanges), state.edges); - - newNodes.forEach((node) => { + // Add node execution states for new nodes + nodes.forEach((node) => { state.nodeExecutionStates[node.id] = { nodeId: node.id, - ...initialNodeExecutionState, + ...deepClone(initialNodeExecutionState), }; }); }, @@ -786,7 +725,6 @@ export const { selectedAll, selectedEdgesChanged, selectedNodesChanged, - selectionCopied, selectionPasted, viewportChanged, edgeAdded, @@ -831,6 +769,10 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( edgeAdded ); +export const $cursorPos = atom(null); +export const $copiedNodes = atom([]); +export const $copiedEdges = atom([]); + export const selectNodesSlice = (state: RootState) => state.nodes.present; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -850,8 +792,6 @@ export const nodesPersistConfig: PersistConfig = { 'connectionStartFieldType', 'selectedNodes', 'selectedEdges', - 'nodesToCopy', - 'edgesToCopy', 'connectionMade', 'modifyingEdge', 'addNewNodePosition', diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index fdf3e638e3..f9c859fcc5 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -21,8 +21,6 @@ export type NodesState = { selectedEdges: string[]; nodeExecutionStates: Record; viewport: Viewport; - nodesToCopy: AnyNode[]; - edgesToCopy: InvocationNodeEdge[]; isAddNodePopoverOpen: boolean; addNewNodePosition: XYPosition | null; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts b/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts index 114633e875..bd110a50a1 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts @@ -4,8 +4,8 @@ export const findUnoccupiedPosition = (nodes: Node[], x: number, y: number) => { let newX = x; let newY = y; while (nodes.find((n) => n.position.x === newX && n.position.y === newY)) { - newX = newX + 50; - newY = newY + 50; + newX = Math.floor(newX + 50); + newY = Math.floor(newY + 50); } return { x: newX, y: newY }; }; From f6a44681a86b534a4db765956416fc88b5c64e32 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 15:17:23 +1000 Subject: [PATCH 142/442] feat(ui): move invocation templates out of redux (wip) --- .../middleware/devtools/actionSanitizer.ts | 8 - .../listeners/getOpenAPISchema.ts | 6 +- .../listeners/updateAllNodesRequested.ts | 5 +- .../listeners/workflowLoadRequested.ts | 5 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 352 +++++++++--------- .../flow/AddNodePopover/AddNodePopover.tsx | 23 +- .../Invocation/InvocationNodeWrapper.tsx | 14 +- .../inspector/InspectorDetailsTab.tsx | 43 ++- .../inspector/InspectorOutputsTab.tsx | 45 +-- .../inspector/InspectorTemplateTab.tsx | 31 +- .../src/features/nodes/hooks/useBuildNode.ts | 10 +- .../nodes/hooks/useGetNodesNeedUpdate.ts | 32 +- .../nodes/hooks/useIsValidConnection.ts | 7 +- .../src/features/nodes/store/nodesSlice.ts | 5 +- .../web/src/features/nodes/store/selectors.ts | 10 +- .../web/src/features/nodes/store/types.ts | 2 - .../nodes/util/workflow/graphToWorkflow.ts | 13 +- .../nodes/util/workflow/migrations.ts | 10 +- 18 files changed, 303 insertions(+), 318 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts index 508109caf5..f0ea175aec 100644 --- a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -1,7 +1,6 @@ import type { UnknownAction } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import { isAnyGraphBuilt } from 'features/nodes/store/actions'; -import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice'; import { appInfoApi } from 'services/api/endpoints/appInfo'; import type { Graph } from 'services/api/types'; import { socketGeneratorProgress } from 'services/events/actions'; @@ -25,13 +24,6 @@ export const actionSanitizer = (action: A): A => { }; } - if (nodeTemplatesBuilt.match(action)) { - return { - ...action, - payload: '', - }; - } - if (socketGeneratorProgress.match(action)) { const sanitized = deepClone(action); if (sanitized.payload.data.progress_image) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts index acb2bdb698..923b2c0197 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts @@ -1,7 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { parseify } from 'common/util/serialize'; -import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { parseSchema } from 'features/nodes/util/schema/parseSchema'; import { size } from 'lodash-es'; import { appInfoApi } from 'services/api/endpoints/appInfo'; @@ -9,7 +9,7 @@ import { appInfoApi } from 'services/api/endpoints/appInfo'; export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening) => { startAppListening({ matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled, - effect: (action, { dispatch, getState }) => { + effect: (action, { getState }) => { const log = logger('system'); const schemaJSON = action.payload; @@ -20,7 +20,7 @@ export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening log.debug({ nodeTemplates: parseify(nodeTemplates) }, `Built ${size(nodeTemplates)} node templates`); - dispatch(nodeTemplatesBuilt(nodeTemplates)); + $templates.set(nodeTemplates); }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts index ebd4d00901..9c2ab4278d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts @@ -1,7 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { updateAllNodesRequested } from 'features/nodes/store/actions'; -import { nodeReplaced } from 'features/nodes/store/nodesSlice'; +import { $templates, nodeReplaced } from 'features/nodes/store/nodesSlice'; import { NodeUpdateError } from 'features/nodes/types/error'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate'; @@ -14,7 +14,8 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi actionCreator: updateAllNodesRequested, effect: (action, { dispatch, getState }) => { const log = logger('nodes'); - const { nodes, templates } = getState().nodes.present; + const { nodes } = getState().nodes.present; + const templates = $templates.get(); let unableToUpdateCount = 0; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts index 5a2c270b2a..4052c75bf3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts @@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { parseify } from 'common/util/serialize'; import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { $flow } from 'features/nodes/store/reactFlowInstance'; import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error'; import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow'; @@ -14,10 +15,10 @@ import { fromZodError } from 'zod-validation-error'; export const addWorkflowLoadRequestedListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: workflowLoadRequested, - effect: (action, { dispatch, getState }) => { + effect: (action, { dispatch }) => { const log = logger('nodes'); const { workflow, asCopy } = action.payload; - const nodeTemplates = getState().nodes.present.templates; + const nodeTemplates = $templates.get(); try { const { workflow: validatedWorkflow, warnings } = validateWorkflow(workflow, nodeTemplates); diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 972cb063cf..868509a59b 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -1,3 +1,4 @@ +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { @@ -9,14 +10,16 @@ import { selectControlLayersSlice } from 'features/controlLayers/store/controlLa import type { Layer } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; +import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { selectSystemSlice } from 'features/system/store/systemSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import i18n from 'i18next'; import { forEach, upperFirst } from 'lodash-es'; +import { useMemo } from 'react'; import { getConnectedEdges } from 'reactflow'; const LAYER_TYPE_TO_TKEY: Record = { @@ -26,200 +29,205 @@ const LAYER_TYPE_TO_TKEY: Record = { regional_guidance_layer: 'controlLayers.regionalGuidance', }; -const selector = createMemoizedSelector( - [ - selectControlAdaptersSlice, - selectGenerationSlice, - selectSystemSlice, - selectNodesSlice, - selectWorkflowSettingsSlice, - selectDynamicPromptsSlice, - selectControlLayersSlice, - activeTabNameSelector, - ], - (controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, controlLayers, activeTabName) => { - const { model } = generation; - const { size } = controlLayers.present; - const { positivePrompt } = controlLayers.present; +const createSelector = (templates: Record) => + createMemoizedSelector( + [ + selectControlAdaptersSlice, + selectGenerationSlice, + selectSystemSlice, + selectNodesSlice, + selectWorkflowSettingsSlice, + selectDynamicPromptsSlice, + selectControlLayersSlice, + activeTabNameSelector, + ], + (controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, controlLayers, activeTabName) => { + const { model } = generation; + const { size } = controlLayers.present; + const { positivePrompt } = controlLayers.present; - const { isConnected } = system; + const { isConnected } = system; - const reasons: { prefix?: string; content: string }[] = []; + const reasons: { prefix?: string; content: string }[] = []; - // Cannot generate if not connected - if (!isConnected) { - reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') }); - } + // Cannot generate if not connected + if (!isConnected) { + reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') }); + } - if (activeTabName === 'workflows') { - if (workflowSettings.shouldValidateGraph) { - if (!nodes.nodes.length) { - reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') }); + if (activeTabName === 'workflows') { + if (workflowSettings.shouldValidateGraph) { + if (!nodes.nodes.length) { + reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') }); + } + + nodes.nodes.forEach((node) => { + if (!isInvocationNode(node)) { + return; + } + + const nodeTemplate = templates[node.data.type]; + + if (!nodeTemplate) { + // Node type not found + reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') }); + return; + } + + const connectedEdges = getConnectedEdges([node], nodes.edges); + + forEach(node.data.inputs, (field) => { + const fieldTemplate = nodeTemplate.inputs[field.name]; + const hasConnection = connectedEdges.some( + (edge) => edge.target === node.id && edge.targetHandle === field.name + ); + + if (!fieldTemplate) { + reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') }); + return; + } + + if (fieldTemplate.required && field.value === undefined && !hasConnection) { + reasons.push({ + content: i18n.t('parameters.invoke.missingInputForField', { + nodeLabel: node.data.label || nodeTemplate.title, + fieldLabel: field.label || fieldTemplate.title, + }), + }); + return; + } + }); + }); + } + } else { + if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) { + reasons.push({ content: i18n.t('parameters.invoke.noPrompts') }); } - nodes.nodes.forEach((node) => { - if (!isInvocationNode(node)) { - return; - } + if (!model) { + reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); + } - const nodeTemplate = nodes.templates[node.data.type]; - - if (!nodeTemplate) { - // Node type not found - reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') }); - return; - } - - const connectedEdges = getConnectedEdges([node], nodes.edges); - - forEach(node.data.inputs, (field) => { - const fieldTemplate = nodeTemplate.inputs[field.name]; - const hasConnection = connectedEdges.some( - (edge) => edge.target === node.id && edge.targetHandle === field.name - ); - - if (!fieldTemplate) { - reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') }); - return; - } - - if (fieldTemplate.required && field.value === undefined && !hasConnection) { - reasons.push({ - content: i18n.t('parameters.invoke.missingInputForField', { - nodeLabel: node.data.label || nodeTemplate.title, - fieldLabel: field.label || fieldTemplate.title, - }), - }); - return; - } - }); - }); - } - } else { - if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) { - reasons.push({ content: i18n.t('parameters.invoke.noPrompts') }); - } - - if (!model) { - reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); - } - - if (activeTabName === 'generation') { - // Handling for generation tab - controlLayers.present.layers - .filter((l) => l.isEnabled) - .forEach((l, i) => { - const layerLiteral = i18n.t('controlLayers.layers_one'); - const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); - const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; - const problems: string[] = []; - if (l.type === 'control_adapter_layer') { - // Must have model - if (!l.controlAdapter.model) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); - } - // Model base must match - if (l.controlAdapter.model?.base !== model?.base) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); - } - // Must have a control image OR, if it has a processor, it must have a processed image - if (!l.controlAdapter.image) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); - } else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); - } - // T2I Adapters require images have dimensions that are multiples of 64 - if (l.controlAdapter.type === 't2i_adapter' && (size.width % 64 !== 0 || size.height % 64 !== 0)) { - problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions')); - } - } - - if (l.type === 'ip_adapter_layer') { - // Must have model - if (!l.ipAdapter.model) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); - } - // Model base must match - if (l.ipAdapter.model?.base !== model?.base) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); - } - // Must have an image - if (!l.ipAdapter.image) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); - } - } - - if (l.type === 'initial_image_layer') { - // Must have an image - if (!l.image) { - problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected')); - } - } - - if (l.type === 'regional_guidance_layer') { - // Must have a region - if (l.maskObjects.length === 0) { - problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); - } - // Must have at least 1 prompt or IP Adapter - if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) { - problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters')); - } - l.ipAdapters.forEach((ipAdapter) => { + if (activeTabName === 'generation') { + // Handling for generation tab + controlLayers.present.layers + .filter((l) => l.isEnabled) + .forEach((l, i) => { + const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerNumber = i + 1; + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); + const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + const problems: string[] = []; + if (l.type === 'control_adapter_layer') { // Must have model - if (!ipAdapter.model) { + if (!l.controlAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); + } + // Model base must match + if (l.controlAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); + } + // Must have a control image OR, if it has a processor, it must have a processed image + if (!l.controlAdapter.image) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); + } else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); + } + // T2I Adapters require images have dimensions that are multiples of 64 + if (l.controlAdapter.type === 't2i_adapter' && (size.width % 64 !== 0 || size.height % 64 !== 0)) { + problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions')); + } + } + + if (l.type === 'ip_adapter_layer') { + // Must have model + if (!l.ipAdapter.model) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); } // Model base must match - if (ipAdapter.model?.base !== model?.base) { + if (l.ipAdapter.model?.base !== model?.base) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); } // Must have an image - if (!ipAdapter.image) { + if (!l.ipAdapter.image) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); } - }); - } + } - if (problems.length) { - const content = upperFirst(problems.join(', ')); - reasons.push({ prefix, content }); - } - }); - } else { - // Handling for all other tabs - selectControlAdapterAll(controlAdapters) - .filter((ca) => ca.isEnabled) - .forEach((ca, i) => { - if (!ca.isEnabled) { - return; - } + if (l.type === 'initial_image_layer') { + // Must have an image + if (!l.image) { + problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected')); + } + } - if (!ca.model) { - reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) }); - } else if (ca.model.base !== model?.base) { - // This should never happen, just a sanity check - reasons.push({ - content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }), - }); - } + if (l.type === 'regional_guidance_layer') { + // Must have a region + if (l.maskObjects.length === 0) { + problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); + } + // Must have at least 1 prompt or IP Adapter + if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) { + problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters')); + } + l.ipAdapters.forEach((ipAdapter) => { + // Must have model + if (!ipAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); + } + // Model base must match + if (ipAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); + } + // Must have an image + if (!ipAdapter.image) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); + } + }); + } - if ( - !ca.controlImage || - (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') - ) { - reasons.push({ content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }) }); - } - }); + if (problems.length) { + const content = upperFirst(problems.join(', ')); + reasons.push({ prefix, content }); + } + }); + } else { + // Handling for all other tabs + selectControlAdapterAll(controlAdapters) + .filter((ca) => ca.isEnabled) + .forEach((ca, i) => { + if (!ca.isEnabled) { + return; + } + + if (!ca.model) { + reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) }); + } else if (ca.model.base !== model?.base) { + // This should never happen, just a sanity check + reasons.push({ + content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }), + }); + } + + if ( + !ca.controlImage || + (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') + ) { + reasons.push({ + content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }), + }); + } + }); + } } - } - return { isReady: !reasons.length, reasons }; - } -); + return { isReady: !reasons.length, reasons }; + } + ); export const useIsReadyToEnqueue = () => { + const templates = useStore($templates); + const selector = useMemo(() => createSelector(templates), [templates]); const value = useAppSelector(selector); return value; }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 6cfc95e311..6d33905f4c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -2,21 +2,16 @@ import 'reactflow/dist/style.css'; import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppToaster } from 'app/components/Toaster'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import type { SelectInstance } from 'chakra-react-select'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; -import { - addNodePopoverClosed, - addNodePopoverOpened, - nodeAdded, - selectNodesSlice, -} from 'features/nodes/store/nodesSlice'; +import { $templates, addNodePopoverClosed, addNodePopoverOpened, nodeAdded } from 'features/nodes/store/nodesSlice'; import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; import { filter, map, memoize, some } from 'lodash-es'; import type { KeyboardEventHandler } from 'react'; -import { memo, useCallback, useRef } from 'react'; +import { memo, useCallback, useMemo, useRef } from 'react'; import { flushSync } from 'react-dom'; import { useHotkeys } from 'react-hotkeys-hook'; import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types'; @@ -54,14 +49,15 @@ const AddNodePopover = () => { const { t } = useTranslation(); const selectRef = useRef | null>(null); const inputRef = useRef(null); + const templates = useStore($templates); const fieldFilter = useAppSelector((s) => s.nodes.present.connectionStartFieldType); const handleFilter = useAppSelector((s) => s.nodes.present.connectionStartParams?.handleType); - const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { + const options = useMemo(() => { // If we have a connection in progress, we need to filter the node choices const filteredNodeTemplates = fieldFilter - ? filter(nodes.templates, (template) => { + ? filter(templates, (template) => { const handles = handleFilter === 'source' ? template.inputs : template.outputs; return some(handles, (handle) => { @@ -71,7 +67,7 @@ const AddNodePopover = () => { return validateSourceAndTargetTypes(sourceType, targetType); }); }) - : map(nodes.templates); + : map(templates); const options: ComboboxOption[] = map(filteredNodeTemplates, (template) => { return { @@ -101,10 +97,9 @@ const AddNodePopover = () => { options.sort((a, b) => a.label.localeCompare(b.label)); - return { options }; - }); + return options; + }, [fieldFilter, handleFilter, t, templates]); - const { options } = useAppSelector(selector); const isOpen = useAppSelector((s) => s.nodes.present.isAddNodePopoverOpen); const addNode = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx index 0fe81c0882..cebf9cf3c5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx @@ -1,7 +1,6 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; import InvocationNode from 'features/nodes/components/flow/nodes/Invocation/InvocationNode'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates } from 'features/nodes/store/nodesSlice'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { memo, useMemo } from 'react'; import type { NodeProps } from 'reactflow'; @@ -11,13 +10,8 @@ import InvocationNodeUnknownFallback from './InvocationNodeUnknownFallback'; const InvocationNodeWrapper = (props: NodeProps) => { const { data, selected } = props; const { id: nodeId, type, isOpen, label } = data; - - const hasTemplateSelector = useMemo( - () => createSelector(selectNodesSlice, (nodes) => Boolean(nodes.templates[type])), - [type] - ); - - const hasTemplate = useAppSelector(hasTemplateSelector); + const templates = useStore($templates); + const hasTemplate = useMemo(() => Boolean(templates[type]), [templates, type]); if (!hasTemplate) { return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx index d72d2f5aa8..354a0ed179 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx @@ -1,36 +1,39 @@ import { Box, Flex, FormControl, FormLabel, HStack, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea'; import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import EditableNodeTitle from './details/EditableNodeTitle'; -const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); - - const lastSelectedNodeTemplate = lastSelectedNode ? nodes.templates[lastSelectedNode.data.type] : undefined; - - if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) { - return; - } - - return { - nodeId: lastSelectedNode.data.id, - nodeVersion: lastSelectedNode.data.version, - templateTitle: lastSelectedNodeTemplate.title, - }; -}); - const InspectorDetailsTab = () => { + const templates = useStore($templates); + const selector = useMemo( + () => + createMemoizedSelector(selectNodesSlice, (nodes) => { + const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; + const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); + const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; + + if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) { + return; + } + + return { + nodeId: lastSelectedNode.data.id, + nodeVersion: lastSelectedNode.data.version, + templateTitle: lastSelectedNodeTemplate.title, + }; + }), + [templates] + ); const data = useAppSelector(selector); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx index 978eeddd24..381a510b8b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx @@ -1,38 +1,41 @@ import { Box, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import type { ImageOutput } from 'services/api/types'; import type { AnyResult } from 'services/events/types'; import ImageOutputPreview from './outputs/ImageOutputPreview'; -const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); - - const lastSelectedNodeTemplate = lastSelectedNode ? nodes.templates[lastSelectedNode.data.type] : undefined; - - const nes = nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__']; - - if (!isInvocationNode(lastSelectedNode) || !nes || !lastSelectedNodeTemplate) { - return; - } - - return { - outputs: nes.outputs, - outputType: lastSelectedNodeTemplate.outputType, - }; -}); - const InspectorOutputsTab = () => { + const templates = useStore($templates); + const selector = useMemo( + () => + createMemoizedSelector(selectNodesSlice, (nodes) => { + const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; + const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); + const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; + + const nes = nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__']; + + if (!isInvocationNode(lastSelectedNode) || !nes || !lastSelectedNodeTemplate) { + return; + } + + return { + outputs: nes.outputs, + outputType: lastSelectedNodeTemplate.outputType, + }; + }), + [templates] + ); const data = useAppSelector(selector); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx index ea6e8ed704..fbe86ba32c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx @@ -1,25 +1,26 @@ +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { memo } from 'react'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); - - const lastSelectedNodeTemplate = lastSelectedNode ? nodes.templates[lastSelectedNode.data.type] : undefined; - - return { - template: lastSelectedNodeTemplate, - }; -}); - const NodeTemplateInspector = () => { - const { template } = useAppSelector(selector); + const templates = useStore($templates); + const selector = useMemo( + () => + createMemoizedSelector(selectNodesSlice, (nodes) => { + const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; + const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); + const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; + + return lastSelectedNodeTemplate; + }), + [templates] + ); + const template = useAppSelector(selector); const { t } = useTranslation(); if (!template) { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts index b166b71788..4e96c219f8 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts @@ -1,4 +1,5 @@ -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { NODE_WIDTH } from 'features/nodes/types/constants'; import type { AnyNode, InvocationTemplate } from 'features/nodes/types/invocation'; import { buildCurrentImageNode } from 'features/nodes/util/node/buildCurrentImageNode'; @@ -8,8 +9,7 @@ import { useCallback } from 'react'; import { useReactFlow } from 'reactflow'; export const useBuildNode = () => { - const nodeTemplates = useAppSelector((s) => s.nodes.present.templates); - + const templates = useStore($templates); const flow = useReactFlow(); return useCallback( @@ -41,10 +41,10 @@ export const useBuildNode = () => { // TODO: Keep track of invocation types so we do not need to cast this // We know it is safe because the caller of this function gets the `type` arg from the list of invocation templates. - const template = nodeTemplates[type] as InvocationTemplate; + const template = templates[type] as InvocationTemplate; return buildInvocationNode(position, template); }, - [nodeTemplates, flow] + [templates, flow] ); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts index 71344197d5..4adbb19c5c 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts @@ -1,20 +1,26 @@ +import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; - -const selector = createSelector(selectNodesSlice, (nodes) => - nodes.nodes.filter(isInvocationNode).some((node) => { - const template = nodes.templates[node.data.type]; - if (!template) { - return false; - } - return getNeedsUpdate(node, template); - }) -); +import { useMemo } from 'react'; export const useGetNodesNeedUpdate = () => { - const getNeedsUpdate = useAppSelector(selector); - return getNeedsUpdate; + const templates = useStore($templates); + const selector = useMemo( + () => + createSelector(selectNodesSlice, (nodes) => + nodes.nodes.filter(isInvocationNode).some((node) => { + const template = templates[node.data.type]; + if (!template) { + return false; + } + return getNeedsUpdate(node, template); + }) + ), + [templates] + ); + const needsUpdate = useAppSelector(selector); + return needsUpdate; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index 7ab28f58c2..041faab149 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -1,5 +1,7 @@ // TODO: enable this at some point +import { useStore } from '@nanostores/react'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { getIsGraphAcyclic } from 'features/nodes/store/util/getIsGraphAcyclic'; import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; @@ -13,6 +15,7 @@ import type { Connection, Node } from 'reactflow'; export const useIsValidConnection = () => { const store = useAppStore(); + const templates = useStore($templates); const shouldValidateGraph = useAppSelector((s) => s.workflowSettings.shouldValidateGraph); const isValidConnection = useCallback( ({ source, sourceHandle, target, targetHandle }: Connection): boolean => { @@ -27,7 +30,7 @@ export const useIsValidConnection = () => { } const state = store.getState(); - const { nodes, edges, templates } = state.nodes.present; + const { nodes, edges } = state.nodes.present; // Find the source and target nodes const sourceNode = nodes.find((node) => node.id === source) as Node; @@ -76,7 +79,7 @@ export const useIsValidConnection = () => { // Graphs much be acyclic (no loops!) return getIsGraphAcyclic(source, target, nodes, edges); }, - [shouldValidateGraph, store] + [shouldValidateGraph, templates, store] ); return isValidConnection; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 21092bb7df..3d18a01493 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -602,9 +602,6 @@ export const nodesSlice = createSlice({ state.connectionStartParams = null; state.connectionStartFieldType = null; }, - nodeTemplatesBuilt: (state, action: PayloadAction>) => { - state.templates = action.payload; - }, undo: (state) => state, redo: (state) => state, }, @@ -728,7 +725,6 @@ export const { selectionPasted, viewportChanged, edgeAdded, - nodeTemplatesBuilt, undo, redo, } = nodesSlice.actions; @@ -770,6 +766,7 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( ); export const $cursorPos = atom(null); +export const $templates = atom>({}); export const $copiedNodes = atom([]); export const $copiedEdges = atom([]); diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts index 90675d6270..d473005395 100644 --- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts +++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts @@ -1,6 +1,6 @@ import type { NodesState } from 'features/nodes/store/types'; import type { FieldInputInstance, FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; -import type { InvocationNode, InvocationNodeData, InvocationTemplate } from 'features/nodes/types/invocation'; +import type { InvocationNode, InvocationNodeData } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; export const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode | null => { @@ -15,14 +15,6 @@ export const selectNodeData = (nodesSlice: NodesState, nodeId: string): Invocati return selectInvocationNode(nodesSlice, nodeId)?.data ?? null; }; -export const selectNodeTemplate = (nodesSlice: NodesState, nodeId: string): InvocationTemplate | null => { - const node = selectInvocationNode(nodesSlice, nodeId); - if (!node) { - return null; - } - return nodesSlice.templates[node.data.type] ?? null; -}; - export const selectFieldInputInstance = ( nodesSlice: NodesState, nodeId: string, diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index f9c859fcc5..28b87128d0 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -2,7 +2,6 @@ import type { FieldIdentifier, FieldType, StatefulFieldValue } from 'features/no import type { AnyNode, InvocationNodeEdge, - InvocationTemplate, NodeExecutionState, } from 'features/nodes/types/invocation'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; @@ -12,7 +11,6 @@ export type NodesState = { _version: 1; nodes: AnyNode[]; edges: InvocationNodeEdge[]; - templates: Record; connectionStartParams: OnConnectStartParams | null; connectionStartFieldType: FieldType | null; connectionMade: boolean; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts index 361e3134ae..af66d3cc6b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts @@ -1,11 +1,10 @@ import * as dagre from '@dagrejs/dagre'; import { logger } from 'app/logging/logger'; -import { getStore } from 'app/store/nanostores/store'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { NODE_WIDTH } from 'features/nodes/types/constants'; import type { FieldInputInstance } from 'features/nodes/types/field'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; import { buildFieldInputInstance } from 'features/nodes/util/schema/buildFieldInputInstance'; -import { t } from 'i18next'; import { forEach } from 'lodash-es'; import type { NonNullableGraph } from 'services/api/types'; import { v4 as uuidv4 } from 'uuid'; @@ -18,11 +17,7 @@ import { v4 as uuidv4 } from 'uuid'; * @returns The workflow. */ export const graphToWorkflow = (graph: NonNullableGraph, autoLayout = true): WorkflowV3 => { - const invocationTemplates = getStore().getState().nodes.present.templates; - - if (!invocationTemplates) { - throw new Error(t('app.storeNotInitialized')); - } + const templates = $templates.get(); // Initialize the workflow const workflow: WorkflowV3 = { @@ -44,11 +39,11 @@ export const graphToWorkflow = (graph: NonNullableGraph, autoLayout = true): Wor // Convert nodes forEach(graph.nodes, (node) => { - const template = invocationTemplates[node.type]; + const template = templates[node.type]; // Skip missing node templates - this is a best-effort if (!template) { - logger('nodes').warn(`Node type ${node.type} not found in invocationTemplates`); + logger('nodes').warn(`Node type ${node.type} not found in templates`); return; } diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts index 3f666e8771..32369b88c9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts @@ -1,5 +1,5 @@ -import { $store } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error'; import type { FieldType } from 'features/nodes/types/field'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; @@ -33,11 +33,7 @@ const zWorkflowMetaVersion = z.object({ * - Workflow schema version bumped to 2.0.0 */ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => { - const invocationTemplates = $store.get()?.getState().nodes.present.templates; - - if (!invocationTemplates) { - throw new Error(t('app.storeNotInitialized')); - } + const templates = $templates.get(); workflowToMigrate.nodes.forEach((node) => { if (node.type === 'invocation') { @@ -57,7 +53,7 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => { (output.type as unknown as FieldType) = newFieldType; }); // Add node pack - const invocationTemplate = invocationTemplates[node.data.type]; + const invocationTemplate = templates[node.data.type]; const nodePack = invocationTemplate ? invocationTemplate.nodePack : t('common.unknown'); (node.data as unknown as InvocationNodeData).nodePack = nodePack; From 1d884fb79405daa71e24ee1170be7a9e247d1dd0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 17:00:08 +1000 Subject: [PATCH 143/442] feat(ui): move invocation templates out of redux Templates are stored in nanostores. All hooks, selectors, etc are reworked to reference the nanostore. --- .../listeners/updateAllNodesRequested.ts | 2 +- .../flow/edges/InvocationCollapsedEdge.tsx | 7 +++- .../flow/edges/InvocationDefaultEdge.tsx | 7 +++- .../flow/edges/util/makeEdgeSelector.ts | 14 ++++--- .../Invocation/InvocationNodeWrapper.tsx | 6 +++ .../hooks/useAnyOrDirectInputFieldNames.ts | 32 +++++---------- .../hooks/useConnectionInputFieldNames.ts | 36 ++++++----------- .../nodes/hooks/useFieldInputTemplate.ts | 17 ++------ .../nodes/hooks/useFieldOutputTemplate.ts | 17 ++------ .../features/nodes/hooks/useFieldTemplate.ts | 39 ++++++++++++------- .../nodes/hooks/useFieldTemplateTitle.ts | 20 ++-------- .../features/nodes/hooks/useFieldType.ts.ts | 22 ++--------- .../nodes/hooks/useGetNodesNeedUpdate.ts | 2 +- .../features/nodes/hooks/useHasImageOutput.ts | 28 ++++++------- .../nodes/hooks/useNodeClassification.ts | 20 +++------- .../src/features/nodes/hooks/useNodeData.ts | 2 +- .../nodes/hooks/useNodeNeedsUpdate.ts | 24 +++--------- .../features/nodes/hooks/useNodeTemplate.ts | 27 +++++++------ .../nodes/hooks/useNodeTemplateTitle.ts | 16 ++------ .../nodes/hooks/useOutputFieldNames.ts | 24 ++---------- .../web/src/features/nodes/store/selectors.ts | 37 ++++++------------ .../features/nodes/util/node/nodeUpdate.ts | 10 ++--- .../nodes/util/workflow/validateWorkflow.ts | 2 +- 23 files changed, 146 insertions(+), 265 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts index 9c2ab4278d..63d960b406 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts @@ -25,7 +25,7 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi unableToUpdateCount++; return; } - if (!getNeedsUpdate(node, template)) { + if (!getNeedsUpdate(node.data, template)) { // No need to increment the count here, since we're not actually updating return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx index eae7970804..2e2fb31154 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx @@ -1,6 +1,8 @@ import { Badge, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { memo, useMemo } from 'react'; import type { EdgeProps } from 'reactflow'; import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow'; @@ -22,9 +24,10 @@ const InvocationCollapsedEdge = ({ sourceHandleId, targetHandleId, }: EdgeProps<{ count: number }>) => { + const templates = useStore($templates); const selector = useMemo( - () => makeEdgeSelector(source, sourceHandleId, target, targetHandleId, selected), - [selected, source, sourceHandleId, target, targetHandleId] + () => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId, selected), + [templates, selected, source, sourceHandleId, target, targetHandleId] ); const { isSelected, shouldAnimate } = useAppSelector(selector); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx index 0966bca88e..2e4340975b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx @@ -1,5 +1,7 @@ import { Flex, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; +import { $templates } from 'features/nodes/store/nodesSlice'; import type { CSSProperties } from 'react'; import { memo, useMemo } from 'react'; import type { EdgeProps } from 'reactflow'; @@ -21,9 +23,10 @@ const InvocationDefaultEdge = ({ sourceHandleId, targetHandleId, }: EdgeProps) => { + const templates = useStore($templates); const selector = useMemo( - () => makeEdgeSelector(source, sourceHandleId, target, targetHandleId, selected), - [source, sourceHandleId, target, targetHandleId, selected] + () => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId, selected), + [templates, source, sourceHandleId, target, targetHandleId, selected] ); const { isSelected, shouldAnimate, stroke, label } = useAppSelector(selector); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts index f251b4d20c..b2ed01e8d8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts @@ -1,8 +1,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectFieldOutputTemplate, selectNodeTemplate } from 'features/nodes/store/selectors'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; +import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getFieldColor } from './getEdgeColor'; @@ -15,6 +15,7 @@ const defaultReturnValue = { }; export const makeEdgeSelector = ( + templates: Record, source: string, sourceHandleId: string | null | undefined, target: string, @@ -35,13 +36,14 @@ export const makeEdgeSelector = ( return defaultReturnValue; } - const outputFieldTemplate = selectFieldOutputTemplate(nodes, sourceNode.id, sourceHandleId); + const sourceNodeTemplate = templates[sourceNode.data.type]; + const targetNodeTemplate = templates[targetNode.data.type]; + + const outputFieldTemplate = sourceNodeTemplate?.outputs[sourceHandleId]; const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined; - const stroke = sourceType && workflowSettings.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); - - const sourceNodeTemplate = selectNodeTemplate(nodes, sourceNode.id); - const targetNodeTemplate = selectNodeTemplate(nodes, targetNode.id); + const stroke = + sourceType && workflowSettings.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); const label = `${sourceNodeTemplate?.title || sourceNode.data?.label} -> ${targetNodeTemplate?.title || targetNode.data?.label}`; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx index cebf9cf3c5..1d12b6a454 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx @@ -1,4 +1,5 @@ import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; import InvocationNode from 'features/nodes/components/flow/nodes/Invocation/InvocationNode'; import { $templates } from 'features/nodes/store/nodesSlice'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; @@ -12,6 +13,11 @@ const InvocationNodeWrapper = (props: NodeProps) => { const { id: nodeId, type, isOpen, label } = data; const templates = useStore($templates); const hasTemplate = useMemo(() => Boolean(templates[type]), [templates, type]); + const nodeExists = useAppSelector((s) => Boolean(s.nodes.present.nodes.find((n) => n.id === nodeId))); + + if (!nodeExists) { + return null; + } if (!hasTemplate) { return ( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts index 4d0e54b239..f5931db87e 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts @@ -1,31 +1,19 @@ -import { EMPTY_ARRAY } from 'app/store/constants'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplate } from 'features/nodes/store/selectors'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; import { useMemo } from 'react'; export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - const template = selectNodeTemplate(nodes, nodeId); - if (!template) { - return EMPTY_ARRAY; - } - const fields = map(template.inputs).filter( - (field) => - (['any', 'direct'].includes(field.input) || field.type.isCollectionOrScalar) && - keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) - ); - return getSortedFilteredFieldNames(fields); - }), - [nodeId] - ); + const template = useNodeTemplate(nodeId); + const fieldNames = useMemo(() => { + const fields = map(template.inputs).filter( + (field) => + (['any', 'direct'].includes(field.input) || field.type.isCollectionOrScalar) && + keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) + ); + return getSortedFilteredFieldNames(fields); + }, [template]); - const fieldNames = useAppSelector(selector); return fieldNames; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts index d332bf46e3..84413fc9c8 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts @@ -1,34 +1,20 @@ -import { EMPTY_ARRAY } from 'app/store/constants'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplate } from 'features/nodes/store/selectors'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; import { useMemo } from 'react'; export const useConnectionInputFieldNames = (nodeId: string): string[] => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - const template = selectNodeTemplate(nodes, nodeId); - if (!template) { - return EMPTY_ARRAY; - } + const template = useNodeTemplate(nodeId); + const fieldNames = useMemo(() => { + // get the visible fields + const fields = map(template.inputs).filter( + (field) => + (field.input === 'connection' && !field.type.isCollectionOrScalar) || + !keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) + ); - // get the visible fields - const fields = map(template.inputs).filter( - (field) => - (field.input === 'connection' && !field.type.isCollectionOrScalar) || - !keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) - ); - - return getSortedFilteredFieldNames(fields); - }), - [nodeId] - ); - - const fieldNames = useAppSelector(selector); + return getSortedFilteredFieldNames(fields); + }, [template]); return fieldNames; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts index e8289d7e07..4b70847ad1 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts @@ -1,20 +1,9 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectFieldInputTemplate } from 'features/nodes/store/selectors'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import type { FieldInputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate | null => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - return selectFieldInputTemplate(nodes, nodeId, fieldName); - }), - [fieldName, nodeId] - ); - - const fieldTemplate = useAppSelector(selector); - + const template = useNodeTemplate(nodeId); + const fieldTemplate = useMemo(() => template.inputs[fieldName] ?? null, [fieldName, template.inputs]); return fieldTemplate; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts index cb154071e9..585ef3fe1c 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts @@ -1,20 +1,9 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectFieldOutputTemplate } from 'features/nodes/store/selectors'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import type { FieldOutputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; export const useFieldOutputTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate | null => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - return selectFieldOutputTemplate(nodes, nodeId, fieldName); - }), - [fieldName, nodeId] - ); - - const fieldTemplate = useAppSelector(selector); - + const template = useNodeTemplate(nodeId); + const fieldTemplate = useMemo(() => template.outputs[fieldName] ?? null, [fieldName, template.outputs]); return fieldTemplate; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts index 7be4ecfd4d..a7e1911720 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts @@ -1,27 +1,36 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectInvocationNodeType } from 'features/nodes/store/selectors'; import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; +import { assert } from 'tsafe'; export const useFieldTemplate = ( nodeId: string, fieldName: string, kind: 'inputs' | 'outputs' -): FieldInputTemplate | FieldOutputTemplate | null => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - if (kind === 'inputs') { - return selectFieldInputTemplate(nodes, nodeId, fieldName); - } - return selectFieldOutputTemplate(nodes, nodeId, fieldName); - }), - [fieldName, kind, nodeId] +): FieldInputTemplate | FieldOutputTemplate => { + const templates = useStore($templates); + const selectNodeType = useMemo( + () => createSelector(selectNodesSlice, (nodes) => selectInvocationNodeType(nodes, nodeId)), + [nodeId] ); - - const fieldTemplate = useAppSelector(selector); + const nodeType = useAppSelector(selectNodeType); + const fieldTemplate = useMemo(() => { + const template = templates[nodeType]; + assert(template, `Template for node type ${nodeType} not found`); + if (kind === 'inputs') { + const fieldTemplate = template.inputs[fieldName]; + assert(fieldTemplate, `Field template for field ${fieldName} not found`); + return fieldTemplate; + } else { + const fieldTemplate = template.outputs[fieldName]; + assert(fieldTemplate, `Field template for field ${fieldName} not found`); + return fieldTemplate; + } + }, [fieldName, kind, nodeType, templates]); return fieldTemplate; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts index e41e019572..5484044e9a 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts @@ -1,22 +1,8 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors'; +import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate'; import { useMemo } from 'react'; export const useFieldTemplateTitle = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): string | null => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => { - if (kind === 'inputs') { - return selectFieldInputTemplate(nodes, nodeId, fieldName)?.title ?? null; - } - return selectFieldOutputTemplate(nodes, nodeId, fieldName)?.title ?? null; - }), - [fieldName, kind, nodeId] - ); - - const fieldTemplateTitle = useAppSelector(selector); - + const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); + const fieldTemplateTitle = useMemo(() => fieldTemplate.title, [fieldTemplate]); return fieldTemplateTitle; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts index a71a4d044e..90c08a94aa 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts @@ -1,23 +1,9 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors'; +import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate'; import type { FieldType } from 'features/nodes/types/field'; import { useMemo } from 'react'; -export const useFieldType = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): FieldType | null => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - if (kind === 'inputs') { - return selectFieldInputTemplate(nodes, nodeId, fieldName)?.type ?? null; - } - return selectFieldOutputTemplate(nodes, nodeId, fieldName)?.type ?? null; - }), - [fieldName, kind, nodeId] - ); - - const fieldType = useAppSelector(selector); - +export const useFieldType = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): FieldType => { + const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); + const fieldType = useMemo(() => fieldTemplate.type, [fieldTemplate]); return fieldType; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts index 4adbb19c5c..adf724bdcd 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts @@ -16,7 +16,7 @@ export const useGetNodesNeedUpdate = () => { if (!template) { return false; } - return getNeedsUpdate(node, template); + return getNeedsUpdate(node.data, template); }) ), [templates] diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts b/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts index 3ac3cabb22..1078b18cc6 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts @@ -1,26 +1,20 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplate } from 'features/nodes/store/selectors'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import { some } from 'lodash-es'; import { useMemo } from 'react'; export const useHasImageOutput = (nodeId: string): boolean => { - const selector = useMemo( + const template = useNodeTemplate(nodeId); + const hasImageOutput = useMemo( () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - const template = selectNodeTemplate(nodes, nodeId); - return some( - template?.outputs, - (output) => - output.type.name === 'ImageField' && - // the image primitive node (node type "image") does not actually save the image, do not show the image-saving checkboxes - template?.type !== 'image' - ); - }), - [nodeId] + some( + template?.outputs, + (output) => + output.type.name === 'ImageField' && + // the image primitive node (node type "image") does not actually save the image, do not show the image-saving checkboxes + template?.type !== 'image' + ), + [template] ); - const hasImageOutput = useAppSelector(selector); return hasImageOutput; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts index bab8ff3f19..75431c949f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts @@ -1,19 +1,9 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplate } from 'features/nodes/store/selectors'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import type { Classification } from 'features/nodes/types/common'; import { useMemo } from 'react'; -export const useNodeClassification = (nodeId: string): Classification | null => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => { - return selectNodeTemplate(nodes, nodeId)?.classification ?? null; - }), - [nodeId] - ); - - const title = useAppSelector(selector); - return title; +export const useNodeClassification = (nodeId: string): Classification => { + const template = useNodeTemplate(nodeId); + const classification = useMemo(() => template.classification, [template]); + return classification; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts index fa21008ff8..738cf80aba 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -5,7 +5,7 @@ import { selectNodeData } from 'features/nodes/store/selectors'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; -export const useNodeData = (nodeId: string): InvocationNodeData | null => { +export const useNodeData = (nodeId: string): InvocationNodeData => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts index aa0294f70f..519bb4728d 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts @@ -1,25 +1,11 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectInvocationNode, selectNodeTemplate } from 'features/nodes/store/selectors'; +import { useNodeData } from 'features/nodes/hooks/useNodeData'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; import { useMemo } from 'react'; export const useNodeNeedsUpdate = (nodeId: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - const node = selectInvocationNode(nodes, nodeId); - const template = selectNodeTemplate(nodes, nodeId); - if (!node || !template) { - return false; - } - return getNeedsUpdate(node, template); - }), - [nodeId] - ); - - const needsUpdate = useAppSelector(selector); - + const data = useNodeData(nodeId); + const template = useNodeTemplate(nodeId); + const needsUpdate = useMemo(() => getNeedsUpdate(data, template), [data, template]); return needsUpdate; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts index 866c9275fb..8b076ade1f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts @@ -1,20 +1,23 @@ +import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplate } from 'features/nodes/store/selectors'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectInvocationNodeType } from 'features/nodes/store/selectors'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; +import { assert } from 'tsafe'; -export const useNodeTemplate = (nodeId: string): InvocationTemplate | null => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => { - return selectNodeTemplate(nodes, nodeId); - }), +export const useNodeTemplate = (nodeId: string): InvocationTemplate => { + const templates = useStore($templates); + const selectNodeType = useMemo( + () => createSelector(selectNodesSlice, (nodes) => selectInvocationNodeType(nodes, nodeId)), [nodeId] ); - - const nodeTemplate = useAppSelector(selector); - - return nodeTemplate; + const nodeType = useAppSelector(selectNodeType); + const template = useMemo(() => { + const t = templates[nodeType]; + assert(t, `Template for node type ${nodeType} not found`); + return t; + }, [nodeType, templates]); + return template; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts index 120b8c758b..a63e0433aa 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts @@ -1,18 +1,8 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplate } from 'features/nodes/store/selectors'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import { useMemo } from 'react'; export const useNodeTemplateTitle = (nodeId: string): string | null => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => { - return selectNodeTemplate(nodes, nodeId)?.title ?? null; - }), - [nodeId] - ); - - const title = useAppSelector(selector); + const template = useNodeTemplate(nodeId); + const title = useMemo(() => template.title, [template.title]); return title; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts index 54c092370b..b19d20ab80 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts @@ -1,26 +1,10 @@ -import { EMPTY_ARRAY } from 'app/store/constants'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplate } from 'features/nodes/store/selectors'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { map } from 'lodash-es'; import { useMemo } from 'react'; -export const useOutputFieldNames = (nodeId: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - const template = selectNodeTemplate(nodes, nodeId); - if (!template) { - return EMPTY_ARRAY; - } - - return getSortedFilteredFieldNames(map(template.outputs)); - }), - [nodeId] - ); - - const fieldNames = useAppSelector(selector); +export const useOutputFieldNames = (nodeId: string): string[] => { + const template = useNodeTemplate(nodeId); + const fieldNames = useMemo(() => getSortedFilteredFieldNames(map(template.outputs)), [template.outputs]); return fieldNames; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts index d473005395..6d1e5e38ec 100644 --- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts +++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts @@ -1,18 +1,23 @@ import type { NodesState } from 'features/nodes/store/types'; -import type { FieldInputInstance, FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; +import type { FieldInputInstance } from 'features/nodes/types/field'; import type { InvocationNode, InvocationNodeData } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; +import { assert } from 'tsafe'; -export const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode | null => { +export const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode => { const node = nodesSlice.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return null; - } + assert(isInvocationNode(node), `Node ${nodeId} is not an invocation node`); return node; }; -export const selectNodeData = (nodesSlice: NodesState, nodeId: string): InvocationNodeData | null => { - return selectInvocationNode(nodesSlice, nodeId)?.data ?? null; +export const selectInvocationNodeType = (nodesSlice: NodesState, nodeId: string): string => { + const node = selectInvocationNode(nodesSlice, nodeId); + return node.data.type; +}; + +export const selectNodeData = (nodesSlice: NodesState, nodeId: string): InvocationNodeData => { + const node = selectInvocationNode(nodesSlice, nodeId); + return node.data; }; export const selectFieldInputInstance = ( @@ -23,21 +28,3 @@ export const selectFieldInputInstance = ( const data = selectNodeData(nodesSlice, nodeId); return data?.inputs[fieldName] ?? null; }; - -export const selectFieldInputTemplate = ( - nodesSlice: NodesState, - nodeId: string, - fieldName: string -): FieldInputTemplate | null => { - const template = selectNodeTemplate(nodesSlice, nodeId); - return template?.inputs[fieldName] ?? null; -}; - -export const selectFieldOutputTemplate = ( - nodesSlice: NodesState, - nodeId: string, - fieldName: string -): FieldOutputTemplate | null => { - const template = selectNodeTemplate(nodesSlice, nodeId); - return template?.outputs[fieldName] ?? null; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts b/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts index 7fa5e1552d..1851698374 100644 --- a/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts @@ -1,17 +1,17 @@ import { deepClone } from 'common/util/deepClone'; import { satisfies } from 'compare-versions'; import { NodeUpdateError } from 'features/nodes/types/error'; -import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation'; +import type { InvocationNode, InvocationNodeData, InvocationTemplate } from 'features/nodes/types/invocation'; import { zParsedSemver } from 'features/nodes/types/semver'; import { defaultsDeep, keys, pick } from 'lodash-es'; import { buildInvocationNode } from './buildInvocationNode'; -export const getNeedsUpdate = (node: InvocationNode, template: InvocationTemplate): boolean => { - if (node.data.type !== template.type) { +export const getNeedsUpdate = (data: InvocationNodeData, template: InvocationTemplate): boolean => { + if (data.type !== template.type) { return true; } - return node.data.version !== template.version; + return data.version !== template.version; }; /** @@ -20,7 +20,7 @@ export const getNeedsUpdate = (node: InvocationNode, template: InvocationTemplat * @param template The invocation template to check against. */ const getMayUpdateNode = (node: InvocationNode, template: InvocationTemplate): boolean => { - const needsUpdate = getNeedsUpdate(node, template); + const needsUpdate = getNeedsUpdate(node.data, template); if (!needsUpdate || node.data.type !== template.type) { return false; } diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts index b402f2f8af..4015202f8f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts @@ -68,7 +68,7 @@ export const validateWorkflow = ( return; } - if (getNeedsUpdate(node, template)) { + if (getNeedsUpdate(node.data, template)) { // This node needs to be updated, based on comparison of its version to the template version const message = t('nodes.mismatchedVersion', { node: node.id, From 708c68413dc3100205bc2c32d06950555e8f7833 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 17:06:07 +1000 Subject: [PATCH 144/442] tidy(ui): add type for templates --- .../web/src/common/hooks/useIsReadyToEnqueue.ts | 4 ++-- .../components/flow/edges/util/makeEdgeSelector.ts | 4 ++-- .../web/src/features/nodes/store/nodesSlice.ts | 11 +++-------- .../frontend/web/src/features/nodes/store/types.ts | 3 +++ .../nodes/store/util/findConnectionToValidHandle.ts | 5 +++-- .../web/src/features/nodes/util/schema/parseSchema.ts | 5 +++-- .../features/nodes/util/workflow/validateWorkflow.ts | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 868509a59b..ac4fed4c63 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -11,8 +11,8 @@ import type { Layer } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import type { Templates } from 'features/nodes/store/types'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; -import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { selectSystemSlice } from 'features/system/store/systemSlice'; @@ -29,7 +29,7 @@ const LAYER_TYPE_TO_TKEY: Record = { regional_guidance_layer: 'controlLayers.regionalGuidance', }; -const createSelector = (templates: Record) => +const createSelector = (templates: Templates) => createMemoizedSelector( [ selectControlAdaptersSlice, diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts index b2ed01e8d8..87ef8eb629 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts @@ -1,8 +1,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import type { Templates } from 'features/nodes/store/types'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; -import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getFieldColor } from './getEdgeColor'; @@ -15,7 +15,7 @@ const defaultReturnValue = { }; export const makeEdgeSelector = ( - templates: Record, + templates: Templates, source: string, sourceHandleId: string | null | undefined, target: string, diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 3d18a01493..064aa0ceb8 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -43,12 +43,7 @@ import { zT2IAdapterModelFieldValue, zVAEModelFieldValue, } from 'features/nodes/types/field'; -import type { - AnyNode, - InvocationNodeEdge, - InvocationTemplate, - NodeExecutionState, -} from 'features/nodes/types/invocation'; +import type { AnyNode, InvocationNodeEdge, NodeExecutionState } from 'features/nodes/types/invocation'; import { isInvocationNode, isNotesNode, zNodeStatus } from 'features/nodes/types/invocation'; import { forEach } from 'lodash-es'; import { atom } from 'nanostores'; @@ -74,7 +69,7 @@ import { } from 'services/events/actions'; import type { z } from 'zod'; -import type { NodesState } from './types'; +import type { NodesState, Templates } from './types'; import { findConnectionToValidHandle } from './util/findConnectionToValidHandle'; import { findUnoccupiedPosition } from './util/findUnoccupiedPosition'; @@ -766,7 +761,7 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( ); export const $cursorPos = atom(null); -export const $templates = atom>({}); +export const $templates = atom({}); export const $copiedNodes = atom([]); export const $copiedEdges = atom([]); diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index 28b87128d0..f84147ac68 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -2,11 +2,14 @@ import type { FieldIdentifier, FieldType, StatefulFieldValue } from 'features/no import type { AnyNode, InvocationNodeEdge, + InvocationTemplate, NodeExecutionState, } from 'features/nodes/types/invocation'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; import type { OnConnectStartParams, Viewport, XYPosition } from 'reactflow'; +export type Templates = Record; + export type NodesState = { _version: 1; nodes: AnyNode[]; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts index ef899c5f41..1f21aea6d1 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts @@ -1,5 +1,6 @@ +import type { Templates } from 'features/nodes/store/types'; import type { FieldInputTemplate, FieldOutputTemplate, FieldType } from 'features/nodes/types/field'; -import type { AnyNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; +import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import type { Connection, Edge, HandleType, Node } from 'reactflow'; @@ -43,7 +44,7 @@ export const findConnectionToValidHandle = ( node: AnyNode, nodes: AnyNode[], edges: InvocationNodeEdge[], - templates: Record, + templates: Templates, handleCurrentNodeId: string, handleCurrentName: string, handleCurrentType: HandleType, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts index 990c517ad3..3178209f93 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import { parseify } from 'common/util/serialize'; +import type { Templates } from 'features/nodes/store/types'; import { FieldParseError } from 'features/nodes/types/error'; import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; @@ -58,14 +59,14 @@ export const parseSchema = ( openAPI: OpenAPIV3_1.Document, nodesAllowlistExtra: string[] | undefined = undefined, nodesDenylistExtra: string[] | undefined = undefined -): Record => { +): Templates => { const filteredSchemas = Object.values(openAPI.components?.schemas ?? {}) .filter(isInvocationSchemaObject) .filter(isNotInDenylist) .filter((schema) => (nodesAllowlistExtra ? nodesAllowlistExtra.includes(schema.properties.type.default) : true)) .filter((schema) => (nodesDenylistExtra ? !nodesDenylistExtra.includes(schema.properties.type.default) : true)); - const invocations = filteredSchemas.reduce>((invocationsAccumulator, schema) => { + const invocations = filteredSchemas.reduce((invocationsAccumulator, schema) => { const type = schema.properties.type.default; const title = schema.title.replace('Invocation', ''); const tags = schema.tags ?? []; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts index 4015202f8f..c57a7213b8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts @@ -1,6 +1,6 @@ import type { JSONObject } from 'common/types'; import { parseify } from 'common/util/serialize'; -import type { InvocationTemplate } from 'features/nodes/types/invocation'; +import type { Templates } from 'features/nodes/store/types'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; import { isWorkflowInvocationNode } from 'features/nodes/types/workflow'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; @@ -33,7 +33,7 @@ type ValidateWorkflowResult = { */ export const validateWorkflow = ( workflow: unknown, - invocationTemplates: Record + invocationTemplates: Templates ): ValidateWorkflowResult => { // Parse the raw workflow data & migrate it to the latest version const _workflow = parseAndMigrateWorkflow(workflow); From 2c1fa306393c8eb8c74b04ee6117c2f38b3b3077 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 18:56:11 +1000 Subject: [PATCH 145/442] feat(ui): recreate edge autoconnect logic --- .../features/nodes/components/flow/Flow.tsx | 61 +++++------ .../src/features/nodes/hooks/useConnection.ts | 68 ++++++++++++ .../src/features/nodes/store/nodesSlice.ts | 10 +- .../web/src/features/nodes/store/types.ts | 15 ++- .../store/util/findConnectionToValidHandle.ts | 101 +++++++++++++++++- 5 files changed, 215 insertions(+), 40 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 80bdf6577b..16634d2ada 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -1,14 +1,14 @@ import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useConnection } from 'features/nodes/hooks/useConnection'; import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; -import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { $cursorPos, - connectionEnded, + $pendingConnection, connectionMade, - connectionStarted, edgeAdded, edgeChangeStarted, edgeDeleted, @@ -28,9 +28,6 @@ import type { CSSProperties, MouseEvent } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import type { - OnConnect, - OnConnectEnd, - OnConnectStart, OnEdgesChange, OnEdgesDelete, OnEdgeUpdateFunc, @@ -76,6 +73,7 @@ export const Flow = memo(() => { const viewport = useAppSelector((s) => s.nodes.present.viewport); const shouldSnapToGrid = useAppSelector((s) => s.workflowSettings.shouldSnapToGrid); const selectionMode = useAppSelector((s) => s.workflowSettings.selectionMode); + const { onConnectStart, onConnect, onConnectEnd } = useConnection(); const flowWrapper = useRef(null); const isValidConnection = useIsValidConnection(); useWorkflowWatcher(); @@ -102,32 +100,35 @@ export const Flow = memo(() => { [dispatch] ); - const onConnectStart: OnConnectStart = useCallback( - (event, params) => { - dispatch(connectionStarted(params)); - }, - [dispatch] - ); + // const onConnectStart: OnConnectStart = useCallback( + // (event, params) => { + // dispatch(connectionStarted(params)); + // }, + // [dispatch] + // ); - const onConnect: OnConnect = useCallback( - (connection) => { - dispatch(connectionMade(connection)); - }, - [dispatch] - ); + // const onConnect: OnConnect = useCallback( + // (connection) => { + // dispatch(connectionMade(connection)); + // }, + // [dispatch] + // ); - const onConnectEnd: OnConnectEnd = useCallback(() => { - const cursorPosition = $cursorPos.get(); - if (!cursorPosition) { - return; - } - dispatch( - connectionEnded({ - cursorPosition, - mouseOverNodeId: $mouseOverNode.get(), - }) - ); - }, [dispatch]); + // const onConnectEnd: OnConnectEnd = useCallback(() => { + // const cursorPosition = $cursorPos.get(); + // if (!cursorPosition) { + // return; + // } + // dispatch( + // connectionEnded({ + // cursorPosition, + // mouseOverNodeId: $mouseOverNode.get(), + // }) + // ); + // }, [dispatch]); + + const pendingConnection = useStore($pendingConnection); + console.log(pendingConnection) const onEdgesDelete: OnEdgesDelete = useCallback( (edges) => { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts new file mode 100644 index 0000000000..fb6212cd26 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -0,0 +1,68 @@ +import { useStore } from '@nanostores/react'; +import { useAppStore } from 'app/store/storeHooks'; +import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; +import { $pendingConnection, $templates, connectionMade } from 'features/nodes/store/nodesSlice'; +import { getFirstValidConnection } from 'features/nodes/store/util/findConnectionToValidHandle'; +import { isInvocationNode } from 'features/nodes/types/invocation'; +import { useCallback, useMemo } from 'react'; +import type { OnConnect, OnConnectEnd, OnConnectStart } from 'reactflow'; +import { assert } from 'tsafe'; + +export const useConnection = () => { + const store = useAppStore(); + const templates = useStore($templates); + + const onConnectStart = useCallback( + (event, params) => { + const nodes = store.getState().nodes.present.nodes; + const { nodeId, handleId, handleType } = params; + assert(nodeId && handleId && handleType, `Invalid connection start params: ${JSON.stringify(params)}`); + const node = nodes.find((n) => n.id === nodeId); + assert(isInvocationNode(node), `Invalid node during connection: ${JSON.stringify(node)}`); + const template = templates[node.data.type]; + assert(template, `Template not found for node type: ${node.data.type}`); + const fieldTemplate = handleType === 'source' ? template.outputs[handleId] : template.inputs[handleId]; + assert(fieldTemplate, `Field template not found for field: ${node.data.type}.${handleId}`); + $pendingConnection.set({ + node, + template, + fieldTemplate, + }); + }, + [store, templates] + ); + const onConnect = useCallback( + (connection) => { + const { dispatch } = store; + dispatch(connectionMade(connection)); + $pendingConnection.set(null); + }, + [store] + ); + const onConnectEnd = useCallback(() => { + const { dispatch } = store; + const pendingConnection = $pendingConnection.get(); + if (!pendingConnection) { + return; + } + const mouseOverNodeId = $mouseOverNode.get(); + const { nodes, edges } = store.getState().nodes.present; + if (mouseOverNodeId) { + const candidateNode = nodes.filter(isInvocationNode).find((n) => n.id === mouseOverNodeId); + if (!candidateNode) { + // The mouse is over a non-invocation node - bail + return; + } + const candidateTemplate = templates[candidateNode.data.type]; + assert(candidateTemplate, `Template not found for node type: ${candidateNode.data.type}`); + const connection = getFirstValidConnection(nodes, edges, pendingConnection, candidateNode, candidateTemplate); + if (connection) { + dispatch(connectionMade(connection)); + } + } + $pendingConnection.set(null); + }, [store, templates]); + + const api = useMemo(() => ({ onConnectStart, onConnect, onConnectEnd }), [onConnectStart, onConnect, onConnectEnd]); + return api; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 064aa0ceb8..0418515f5d 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -69,7 +69,7 @@ import { } from 'services/events/actions'; import type { z } from 'zod'; -import type { NodesState, Templates } from './types'; +import type { NodesState, PendingConnection, Templates } from './types'; import { findConnectionToValidHandle } from './util/findConnectionToValidHandle'; import { findUnoccupiedPosition } from './util/findUnoccupiedPosition'; @@ -215,13 +215,7 @@ export const nodesSlice = createSlice({ state.connectionStartFieldType = field?.type ?? null; }, connectionMade: (state, action: PayloadAction) => { - const fieldType = state.connectionStartFieldType; - if (!fieldType) { - return; - } state.edges = addEdge({ ...action.payload, type: 'default' }, state.edges); - - state.connectionMade = true; }, connectionEnded: ( state, @@ -764,6 +758,8 @@ export const $cursorPos = atom(null); export const $templates = atom({}); export const $copiedNodes = atom([]); export const $copiedEdges = atom([]); +export const $pendingConnection = atom(null); +export const $isModifyingEdge = atom(false); export const selectNodesSlice = (state: RootState) => state.nodes.present; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index f84147ac68..b6c7ef95b3 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -1,6 +1,13 @@ -import type { FieldIdentifier, FieldType, StatefulFieldValue } from 'features/nodes/types/field'; +import type { + FieldIdentifier, + FieldInputTemplate, + FieldOutputTemplate, + FieldType, + StatefulFieldValue, +} from 'features/nodes/types/field'; import type { AnyNode, + InvocationNode, InvocationNodeEdge, InvocationTemplate, NodeExecutionState, @@ -10,6 +17,12 @@ import type { OnConnectStartParams, Viewport, XYPosition } from 'reactflow'; export type Templates = Record; +export type PendingConnection = { + node: InvocationNode; + template: InvocationTemplate; + fieldTemplate: FieldInputTemplate | FieldOutputTemplate; +}; + export type NodesState = { _version: 1; nodes: AnyNode[]; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts index 1f21aea6d1..dcd805d32e 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts @@ -1,8 +1,10 @@ -import type { Templates } from 'features/nodes/store/types'; +import type { PendingConnection, Templates } from 'features/nodes/store/types'; import type { FieldInputTemplate, FieldOutputTemplate, FieldType } from 'features/nodes/types/field'; -import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; +import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; +import { differenceWith, map } from 'lodash-es'; import type { Connection, Edge, HandleType, Node } from 'reactflow'; +import { assert } from 'tsafe'; import { getIsGraphAcyclic } from './getIsGraphAcyclic'; import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; @@ -111,3 +113,98 @@ export const findConnectionToValidHandle = ( } return null; }; + +export const getFirstValidConnection = ( + nodes: AnyNode[], + edges: InvocationNodeEdge[], + pendingConnection: PendingConnection, + candidateNode: InvocationNode, + candidateTemplate: InvocationTemplate +): Connection | null => { + if (pendingConnection.node.id === candidateNode.id) { + // Cannot connect to self + return null; + } + + const pendingFieldKind = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; + + if (pendingFieldKind === 'source') { + // Connecting from a source to a target + if (!getIsGraphAcyclic(pendingConnection.node.id, candidateNode.id, nodes, edges)) { + return null; + } + if (candidateNode.data.type === 'collect') { + // Special handling for collect node - the `item` field takes any number of connections + return { + source: pendingConnection.node.id, + sourceHandle: pendingConnection.fieldTemplate.name, + target: candidateNode.id, + targetHandle: 'item', + }; + } + // Only one connection per target field is allowed - look for an unconnected target field + const candidateFields = map(candidateTemplate.inputs); + const candidateConnectedFields = edges + .filter((edge) => edge.target === candidateNode.id) + .map((edge) => { + // Edges must always have a targetHandle, safe to assert here + assert(edge.targetHandle); + return edge.targetHandle; + }); + const candidateUnconnectedFields = differenceWith( + candidateFields, + candidateConnectedFields, + (field, connectedFieldName) => field.name === connectedFieldName + ); + const candidateField = candidateUnconnectedFields.find((field) => + validateSourceAndTargetTypes(pendingConnection.fieldTemplate.type, field.type) + ); + if (candidateField) { + return { + source: pendingConnection.node.id, + sourceHandle: pendingConnection.fieldTemplate.name, + target: candidateNode.id, + targetHandle: candidateField.name, + }; + } + } else { + // Connecting from a target to a source + // Ensure we there is not already an edge to the target + if ( + edges.some( + (e) => e.target === pendingConnection.node.id && e.targetHandle === pendingConnection.fieldTemplate.name + ) + ) { + return null; + } + + if (!getIsGraphAcyclic(candidateNode.id, pendingConnection.node.id, nodes, edges)) { + return null; + } + + if (candidateNode.data.type === 'collect') { + // Special handling for collect node - connect to the `collection` field + return { + source: candidateNode.id, + sourceHandle: 'collection', + target: pendingConnection.node.id, + targetHandle: pendingConnection.fieldTemplate.name, + }; + } + // Sources/outputs can have any number of edges, we can take the first matching output field + const candidateFields = map(candidateTemplate.outputs); + const candidateField = candidateFields.find((field) => + validateSourceAndTargetTypes(field.type, pendingConnection.fieldTemplate.type) + ); + if (candidateField) { + return { + source: candidateNode.id, + sourceHandle: candidateField.name, + target: pendingConnection.node.id, + targetHandle: pendingConnection.fieldTemplate.name, + }; + } + } + + return null; +}; From 4d68cd8dbb1518b6f932910e89076a0f9b651779 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 19:17:56 +1000 Subject: [PATCH 146/442] feat(ui): recreate edge auto-add-node logic --- .../flow/AddNodePopover/AddNodePopover.tsx | 136 ++++++++++-------- .../features/nodes/components/flow/Flow.tsx | 32 ----- .../src/features/nodes/hooks/useConnection.ts | 7 +- .../src/features/nodes/store/nodesSlice.ts | 8 ++ 4 files changed, 87 insertions(+), 96 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 6d33905f4c..d9602a9679 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -4,11 +4,22 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppToaster } from 'app/components/Toaster'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppStore } from 'app/store/storeHooks'; import type { SelectInstance } from 'chakra-react-select'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; -import { $templates, addNodePopoverClosed, addNodePopoverOpened, nodeAdded } from 'features/nodes/store/nodesSlice'; +import { + $isAddNodePopoverOpen, + $pendingConnection, + $templates, + closeAddNodePopover, + connectionMade, + nodeAdded, + openAddNodePopover, +} from 'features/nodes/store/nodesSlice'; +import { getFirstValidConnection } from 'features/nodes/store/util/findConnectionToValidHandle'; import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; +import type { AnyNode } from 'features/nodes/types/invocation'; +import { isInvocationNode } from 'features/nodes/types/invocation'; import { filter, map, memoize, some } from 'lodash-es'; import type { KeyboardEventHandler } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; @@ -17,6 +28,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types'; import { useTranslation } from 'react-i18next'; import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; +import { assert } from 'tsafe'; const createRegex = memoize( (inputValue: string) => @@ -50,26 +62,29 @@ const AddNodePopover = () => { const selectRef = useRef | null>(null); const inputRef = useRef(null); const templates = useStore($templates); + const pendingConnection = useStore($pendingConnection); + const isOpen = useStore($isAddNodePopoverOpen); + const store = useAppStore(); - const fieldFilter = useAppSelector((s) => s.nodes.present.connectionStartFieldType); - const handleFilter = useAppSelector((s) => s.nodes.present.connectionStartParams?.handleType); + const filteredTemplates = useMemo(() => { + // If we have a connection in progress, we need to filter the node choices + if (!pendingConnection) { + return map(templates); + } + + return filter(templates, (template) => { + const pendingFieldKind = pendingConnection.fieldTemplate.fieldKind; + const fields = pendingFieldKind === 'input' ? template.outputs : template.inputs; + return some(fields, (field) => { + const sourceType = pendingFieldKind === 'input' ? field.type : pendingConnection.fieldTemplate.type; + const targetType = pendingFieldKind === 'output' ? field.type : pendingConnection.fieldTemplate.type; + return validateSourceAndTargetTypes(sourceType, targetType); + }); + }); + }, [templates, pendingConnection]); const options = useMemo(() => { - // If we have a connection in progress, we need to filter the node choices - const filteredNodeTemplates = fieldFilter - ? filter(templates, (template) => { - const handles = handleFilter === 'source' ? template.inputs : template.outputs; - - return some(handles, (handle) => { - const sourceType = handleFilter === 'source' ? fieldFilter : handle.type; - const targetType = handleFilter === 'target' ? fieldFilter : handle.type; - - return validateSourceAndTargetTypes(sourceType, targetType); - }); - }) - : map(templates); - - const options: ComboboxOption[] = map(filteredNodeTemplates, (template) => { + const _options: ComboboxOption[] = map(filteredTemplates, (template) => { return { label: template.title, value: template.type, @@ -79,15 +94,15 @@ const AddNodePopover = () => { }); //We only want these nodes if we're not filtered - if (fieldFilter === null) { - options.push({ + if (!pendingConnection) { + _options.push({ label: t('nodes.currentImage'), value: 'current_image', description: t('nodes.currentImageDescription'), tags: ['progress'], }); - options.push({ + _options.push({ label: t('nodes.notes'), value: 'notes', description: t('nodes.notesDescription'), @@ -95,15 +110,13 @@ const AddNodePopover = () => { }); } - options.sort((a, b) => a.label.localeCompare(b.label)); + _options.sort((a, b) => a.label.localeCompare(b.label)); - return options; - }, [fieldFilter, handleFilter, t, templates]); - - const isOpen = useAppSelector((s) => s.nodes.present.isAddNodePopoverOpen); + return _options; + }, [filteredTemplates, pendingConnection, t]); const addNode = useCallback( - (nodeType: string) => { + (nodeType: string): AnyNode | null => { const invocation = buildInvocation(nodeType); if (!invocation) { const errorMessage = t('nodes.unknownNode', { @@ -113,10 +126,11 @@ const AddNodePopover = () => { status: 'error', title: errorMessage, }); - return; + return null; } dispatch(nodeAdded(invocation)); + return invocation; }, [dispatch, buildInvocation, toaster, t] ); @@ -126,52 +140,50 @@ const AddNodePopover = () => { if (!v) { return; } - addNode(v.value); - dispatch(addNodePopoverClosed()); + const node = addNode(v.value); + + // Auto-connect an edge if we just added a node and have a pending connection + if (pendingConnection && isInvocationNode(node)) { + const template = templates[node.data.type]; + assert(template, 'Template not found'); + const { nodes, edges } = store.getState().nodes.present; + const connection = getFirstValidConnection(nodes, edges, pendingConnection, node, template); + if (connection) { + dispatch(connectionMade(connection)); + } + } + + closeAddNodePopover(); }, - [addNode, dispatch] + [addNode, dispatch, pendingConnection, store, templates] ); - const onClose = useCallback(() => { - dispatch(addNodePopoverClosed()); - }, [dispatch]); - - const onOpen = useCallback(() => { - dispatch(addNodePopoverOpened()); - }, [dispatch]); - - const handleHotkeyOpen: HotkeyCallback = useCallback( - (e) => { - e.preventDefault(); - onOpen(); - flushSync(() => { - selectRef.current?.inputRef?.focus(); - }); - }, - [onOpen] - ); + const handleHotkeyOpen: HotkeyCallback = useCallback((e) => { + e.preventDefault(); + openAddNodePopover(); + flushSync(() => { + selectRef.current?.inputRef?.focus(); + }); + }, []); const handleHotkeyClose: HotkeyCallback = useCallback(() => { - onClose(); - }, [onClose]); + closeAddNodePopover(); + }, []); useHotkeys(['shift+a', 'space'], handleHotkeyOpen); useHotkeys(['escape'], handleHotkeyClose); - const onKeyDown: KeyboardEventHandler = useCallback( - (e) => { - if (e.key === 'Escape') { - onClose(); - } - }, - [onClose] - ); + const onKeyDown: KeyboardEventHandler = useCallback((e) => { + if (e.key === 'Escape') { + closeAddNodePopover(); + } + }, []); const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]); return ( { noOptionsMessage={noOptionsMessage} filterOption={filterOption} onChange={onChange} - onMenuClose={onClose} + onMenuClose={closeAddNodePopover} onKeyDown={onKeyDown} inputRef={inputRef} closeMenuOnSelect={false} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 16634d2ada..fa04d60bca 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -1,5 +1,4 @@ import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useConnection } from 'features/nodes/hooks/useConnection'; import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste'; @@ -7,7 +6,6 @@ import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection' import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { $cursorPos, - $pendingConnection, connectionMade, edgeAdded, edgeChangeStarted, @@ -100,36 +98,6 @@ export const Flow = memo(() => { [dispatch] ); - // const onConnectStart: OnConnectStart = useCallback( - // (event, params) => { - // dispatch(connectionStarted(params)); - // }, - // [dispatch] - // ); - - // const onConnect: OnConnect = useCallback( - // (connection) => { - // dispatch(connectionMade(connection)); - // }, - // [dispatch] - // ); - - // const onConnectEnd: OnConnectEnd = useCallback(() => { - // const cursorPosition = $cursorPos.get(); - // if (!cursorPosition) { - // return; - // } - // dispatch( - // connectionEnded({ - // cursorPosition, - // mouseOverNodeId: $mouseOverNode.get(), - // }) - // ); - // }, [dispatch]); - - const pendingConnection = useStore($pendingConnection); - console.log(pendingConnection) - const onEdgesDelete: OnEdgesDelete = useCallback( (edges) => { dispatch(edgesDeleted(edges)); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index fb6212cd26..468a0bd645 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -1,7 +1,7 @@ import { useStore } from '@nanostores/react'; import { useAppStore } from 'app/store/storeHooks'; import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; -import { $pendingConnection, $templates, connectionMade } from 'features/nodes/store/nodesSlice'; +import { $isAddNodePopoverOpen, $pendingConnection, $templates, connectionMade } from 'features/nodes/store/nodesSlice'; import { getFirstValidConnection } from 'features/nodes/store/util/findConnectionToValidHandle'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { useCallback, useMemo } from 'react'; @@ -59,8 +59,11 @@ export const useConnection = () => { if (connection) { dispatch(connectionMade(connection)); } + $pendingConnection.set(null); + } else { + // The mouse is not over a node - we should open the add node popover + $isAddNodePopoverOpen.set(true); } - $pendingConnection.set(null); }, [store, templates]); const api = useMemo(() => ({ onConnectStart, onConnect, onConnectEnd }), [onConnectStart, onConnect, onConnectEnd]); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 0418515f5d..4ba4e2c0fe 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -760,6 +760,14 @@ export const $copiedNodes = atom([]); export const $copiedEdges = atom([]); export const $pendingConnection = atom(null); export const $isModifyingEdge = atom(false); +export const $isAddNodePopoverOpen = atom(false); +export const closeAddNodePopover = () => { + $isAddNodePopoverOpen.set(false); + $pendingConnection.set(null); +}; +export const openAddNodePopover = () => { + $isAddNodePopoverOpen.set(true); +}; export const selectNodesSlice = (state: RootState) => state.nodes.present; From b0c7c7cb475023ee5e116543702a3b0989b31241 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 19:36:21 +1000 Subject: [PATCH 147/442] feat(ui): remove remaining extraneous state from nodes slice --- .../flow/AddNodePopover/AddNodePopover.tsx | 11 +- .../features/nodes/components/flow/Flow.tsx | 2 - .../connectionLines/CustomConnectionLine.tsx | 36 +++-- .../flow/panels/TopPanel/AddNodeButton.tsx | 11 +- .../nodes/hooks/useConnectionState.ts | 41 +++--- .../src/features/nodes/store/nodesSlice.ts | 137 +----------------- .../web/src/features/nodes/store/types.ts | 9 +- .../util/makeIsConnectionValidSelector.ts | 17 ++- 8 files changed, 66 insertions(+), 198 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index d9602a9679..c53bed31f5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -8,6 +8,7 @@ import { useAppDispatch, useAppStore } from 'app/store/storeHooks'; import type { SelectInstance } from 'chakra-react-select'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; import { + $cursorPos, $isAddNodePopoverOpen, $pendingConnection, $templates, @@ -117,8 +118,8 @@ const AddNodePopover = () => { const addNode = useCallback( (nodeType: string): AnyNode | null => { - const invocation = buildInvocation(nodeType); - if (!invocation) { + const node = buildInvocation(nodeType); + if (!node) { const errorMessage = t('nodes.unknownNode', { nodeType: nodeType, }); @@ -128,9 +129,9 @@ const AddNodePopover = () => { }); return null; } - - dispatch(nodeAdded(invocation)); - return invocation; + const cursorPos = $cursorPos.get(); + dispatch(nodeAdded({ node, cursorPos })); + return node; }, [dispatch, buildInvocation, toaster, t] ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index fa04d60bca..44c7e1ce7b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -8,7 +8,6 @@ import { $cursorPos, connectionMade, edgeAdded, - edgeChangeStarted, edgeDeleted, edgesChanged, edgesDeleted, @@ -170,7 +169,6 @@ export const Flow = memo(() => { edgeUpdateMouseEvent.current = e; // always delete the edge when starting an updated dispatch(edgeDeleted(edge.id)); - dispatch(edgeChangeStarted()); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx index ad0ed5957c..09c88c713b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx @@ -1,29 +1,33 @@ -import { createSelector } from '@reduxjs/toolkit'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; +import { $pendingConnection } from 'features/nodes/store/nodesSlice'; import type { CSSProperties } from 'react'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import type { ConnectionLineComponentProps } from 'reactflow'; import { getBezierPath } from 'reactflow'; -const selectStroke = createSelector([selectNodesSlice, selectWorkflowSettingsSlice], (nodes, workflowSettings) => - workflowSettings.shouldColorEdges ? getFieldColor(nodes.connectionStartFieldType) : colorTokenToCssVar('base.500') -); - -const selectClassName = createSelector(selectWorkflowSettingsSlice, (workflowSettings) => - workflowSettings.shouldAnimateEdges - ? 'react-flow__custom_connection-path animated' - : 'react-flow__custom_connection-path' -); - const pathStyles: CSSProperties = { opacity: 0.8 }; const CustomConnectionLine = ({ fromX, fromY, fromPosition, toX, toY, toPosition }: ConnectionLineComponentProps) => { - const stroke = useAppSelector(selectStroke); - const className = useAppSelector(selectClassName); + const pendingConnection = useStore($pendingConnection); + const shouldColorEdges = useAppSelector((state) => state.workflowSettings.shouldColorEdges); + const shouldAnimateEdges = useAppSelector((state) => state.workflowSettings.shouldAnimateEdges); + const stroke = useMemo(() => { + if (shouldColorEdges && pendingConnection) { + return getFieldColor(pendingConnection.fieldTemplate.type); + } else { + return colorTokenToCssVar('base.500'); + } + }, [pendingConnection, shouldColorEdges]); + const className = useMemo(() => { + if (shouldAnimateEdges) { + return 'react-flow__custom_connection-path animated'; + } else { + return 'react-flow__custom_connection-path'; + } + }, [shouldAnimateEdges]); const pathParams = { sourceX: fromX, diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx index 63a5e7eccb..c7eb9bdbb0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx @@ -1,23 +1,18 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice'; -import { memo, useCallback } from 'react'; +import { openAddNodePopover } from 'features/nodes/store/nodesSlice'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; const AddNodeButton = () => { - const dispatch = useAppDispatch(); const { t } = useTranslation(); - const handleOpenAddNodePopover = useCallback(() => { - dispatch(addNodePopoverOpened()); - }, [dispatch]); return ( } - onClick={handleOpenAddNodePopover} + onClick={openAddNodePopover} pointerEvents="auto" /> ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index f0a512277a..32f6adcddb 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -1,16 +1,12 @@ +import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $pendingConnection, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector'; import { useMemo } from 'react'; import { useFieldType } from './useFieldType.ts'; -const selectIsConnectionInProgress = createSelector( - selectNodesSlice, - (nodes) => nodes.connectionStartFieldType !== null && nodes.connectionStartParams !== null -); - type UseConnectionStateProps = { nodeId: string; fieldName: string; @@ -18,6 +14,7 @@ type UseConnectionStateProps = { }; export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => { + const pendingConnection = useStore($pendingConnection); const fieldType = useFieldType(nodeId, fieldName, kind); const selectIsConnected = useMemo( @@ -36,25 +33,29 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta ); const selectConnectionError = useMemo( - () => makeConnectionErrorSelector(nodeId, fieldName, kind === 'inputs' ? 'target' : 'source', fieldType), - [nodeId, fieldName, kind, fieldType] - ); - - const selectIsConnectionStartField = useMemo( () => - createSelector(selectNodesSlice, (nodes) => - Boolean( - nodes.connectionStartParams?.nodeId === nodeId && - nodes.connectionStartParams?.handleId === fieldName && - nodes.connectionStartParams?.handleType === { inputs: 'target', outputs: 'source' }[kind] - ) + makeConnectionErrorSelector( + pendingConnection, + nodeId, + fieldName, + kind === 'inputs' ? 'target' : 'source', + fieldType ), - [fieldName, kind, nodeId] + [pendingConnection, nodeId, fieldName, kind, fieldType] ); const isConnected = useAppSelector(selectIsConnected); - const isConnectionInProgress = useAppSelector(selectIsConnectionInProgress); - const isConnectionStartField = useAppSelector(selectIsConnectionStartField); + const isConnectionInProgress = useMemo(() => Boolean(pendingConnection), [pendingConnection]); + const isConnectionStartField = useMemo(() => { + if (!pendingConnection) { + return false; + } + return ( + pendingConnection.node.id === nodeId && + pendingConnection.fieldTemplate.name === fieldName && + pendingConnection.fieldTemplate.fieldKind === { inputs: 'input', outputs: 'output' }[kind] + ); + }, [fieldName, kind, nodeId, pendingConnection]); const connectionError = useAppSelector(selectConnectionError); const shouldDim = useMemo( diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 4ba4e2c0fe..69530902a4 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -47,17 +47,7 @@ import type { AnyNode, InvocationNodeEdge, NodeExecutionState } from 'features/n import { isInvocationNode, isNotesNode, zNodeStatus } from 'features/nodes/types/invocation'; import { forEach } from 'lodash-es'; import { atom } from 'nanostores'; -import type { - Connection, - Edge, - EdgeChange, - EdgeRemoveChange, - Node, - NodeChange, - OnConnectStartParams, - Viewport, - XYPosition, -} from 'reactflow'; +import type { Connection, Edge, EdgeChange, EdgeRemoveChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow'; import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow'; import type { UndoableOptions } from 'redux-undo'; import { @@ -70,7 +60,6 @@ import { import type { z } from 'zod'; import type { NodesState, PendingConnection, Templates } from './types'; -import { findConnectionToValidHandle } from './util/findConnectionToValidHandle'; import { findUnoccupiedPosition } from './util/findUnoccupiedPosition'; const initialNodeExecutionState: Omit = { @@ -85,13 +74,6 @@ const initialNodesState: NodesState = { _version: 1, nodes: [], edges: [], - templates: {}, - connectionStartParams: null, - connectionStartFieldType: null, - connectionMade: false, - modifyingEdge: false, - addNewNodePosition: null, - isAddNodePopoverOpen: false, selectedNodes: [], selectedEdges: [], nodeExecutionStates: {}, @@ -137,12 +119,12 @@ export const nodesSlice = createSlice({ } state.nodes[nodeIndex] = action.payload.node; }, - nodeAdded: (state, action: PayloadAction) => { - const node = action.payload; + nodeAdded: (state, action: PayloadAction<{ node: AnyNode; cursorPos: XYPosition | null }>) => { + const { node, cursorPos } = action.payload; const position = findUnoccupiedPosition( state.nodes, - state.addNewNodePosition?.x ?? node.position.x, - state.addNewNodePosition?.y ?? node.position.y + cursorPos?.x ?? node.position.x, + cursorPos?.y ?? node.position.y ); node.position = position; node.selected = true; @@ -167,31 +149,6 @@ export const nodesSlice = createSlice({ nodeId: node.id, ...initialNodeExecutionState, }; - - if (state.connectionStartParams) { - const { nodeId, handleId, handleType } = state.connectionStartParams; - if (nodeId && handleId && handleType && state.connectionStartFieldType) { - const newConnection = findConnectionToValidHandle( - node, - state.nodes, - state.edges, - state.templates, - nodeId, - handleId, - handleType, - state.connectionStartFieldType - ); - if (newConnection) { - state.edges = addEdge({ ...newConnection, type: 'default' }, state.edges); - } - } - } - - state.connectionStartParams = null; - state.connectionStartFieldType = null; - }, - edgeChangeStarted: (state) => { - state.modifyingEdge = true; }, edgesChanged: (state, action: PayloadAction) => { state.edges = applyEdgeChanges(action.payload, state.edges); @@ -199,66 +156,9 @@ export const nodesSlice = createSlice({ edgeAdded: (state, action: PayloadAction) => { state.edges = addEdge(action.payload, state.edges); }, - connectionStarted: (state, action: PayloadAction) => { - state.connectionStartParams = action.payload; - state.connectionMade = state.modifyingEdge; - const { nodeId, handleId, handleType } = action.payload; - if (!nodeId || !handleId) { - return; - } - const node = state.nodes.find((n) => n.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - const template = state.templates[node.data.type]; - const field = handleType === 'source' ? template?.outputs[handleId] : template?.inputs[handleId]; - state.connectionStartFieldType = field?.type ?? null; - }, connectionMade: (state, action: PayloadAction) => { state.edges = addEdge({ ...action.payload, type: 'default' }, state.edges); }, - connectionEnded: ( - state, - action: PayloadAction<{ - cursorPosition: XYPosition; - mouseOverNodeId: string | null; - }> - ) => { - const { cursorPosition, mouseOverNodeId } = action.payload; - if (!state.connectionMade) { - if (mouseOverNodeId) { - const nodeIndex = state.nodes.findIndex((n) => n.id === mouseOverNodeId); - const mouseOverNode = state.nodes?.[nodeIndex]; - if (mouseOverNode && state.connectionStartParams) { - const { nodeId, handleId, handleType } = state.connectionStartParams; - if (nodeId && handleId && handleType && state.connectionStartFieldType) { - const newConnection = findConnectionToValidHandle( - mouseOverNode, - state.nodes, - state.edges, - state.templates, - nodeId, - handleId, - handleType, - state.connectionStartFieldType - ); - if (newConnection) { - state.edges = addEdge({ ...newConnection, type: 'default' }, state.edges); - } - } - } - state.connectionStartParams = null; - state.connectionStartFieldType = null; - } else { - state.addNewNodePosition = cursorPosition; - state.isAddNodePopoverOpen = true; - } - } else { - state.connectionStartParams = null; - state.connectionStartFieldType = null; - } - state.modifyingEdge = false; - }, fieldLabelChanged: ( state, action: PayloadAction<{ @@ -580,17 +480,6 @@ export const nodesSlice = createSlice({ }; }); }, - addNodePopoverOpened: (state) => { - state.addNewNodePosition = null; //Create the node in viewport center by default - state.isAddNodePopoverOpen = true; - }, - addNodePopoverClosed: (state) => { - state.isAddNodePopoverOpen = false; - - //Make sure these get reset if we close the popover and haven't selected a node - state.connectionStartParams = null; - state.connectionStartFieldType = null; - }, undo: (state) => state, redo: (state) => state, }, @@ -670,13 +559,8 @@ export const nodesSlice = createSlice({ }); export const { - addNodePopoverClosed, - addNodePopoverOpened, - connectionEnded, connectionMade, - connectionStarted, edgeDeleted, - edgeChangeStarted, edgesChanged, edgesDeleted, fieldValueReset, @@ -720,7 +604,6 @@ export const { // This is used for tracking `state.workflow.isTouched` export const isAnyNodeOrEdgeMutation = isAnyOf( - connectionEnded, connectionMade, edgeDeleted, edgesChanged, @@ -783,15 +666,7 @@ export const nodesPersistConfig: PersistConfig = { name: nodesSlice.name, initialState: initialNodesState, migrate: migrateNodesState, - persistDenylist: [ - 'connectionStartParams', - 'connectionStartFieldType', - 'selectedNodes', - 'selectedEdges', - 'connectionMade', - 'modifyingEdge', - 'addNewNodePosition', - ], + persistDenylist: ['selectedNodes', 'selectedEdges'], }; export const nodesUndoableConfig: UndoableOptions = { diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index b6c7ef95b3..e2249b177e 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -2,7 +2,6 @@ import type { FieldIdentifier, FieldInputTemplate, FieldOutputTemplate, - FieldType, StatefulFieldValue, } from 'features/nodes/types/field'; import type { @@ -13,7 +12,7 @@ import type { NodeExecutionState, } from 'features/nodes/types/invocation'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; -import type { OnConnectStartParams, Viewport, XYPosition } from 'reactflow'; +import type { Viewport } from 'reactflow'; export type Templates = Record; @@ -27,16 +26,10 @@ export type NodesState = { _version: 1; nodes: AnyNode[]; edges: InvocationNodeEdge[]; - connectionStartParams: OnConnectStartParams | null; - connectionStartFieldType: FieldType | null; - connectionMade: boolean; - modifyingEdge: boolean; selectedNodes: string[]; selectedEdges: string[]; nodeExecutionStates: Record; viewport: Viewport; - isAddNodePopoverOpen: boolean; - addNewNodePosition: XYPosition | null; }; export type WorkflowMode = 'edit' | 'view'; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts index d6ea0d9c86..212a0b804a 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts @@ -1,5 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import type { PendingConnection } from 'features/nodes/store/types'; import type { FieldType } from 'features/nodes/types/field'; import i18n from 'i18next'; import type { HandleType } from 'reactflow'; @@ -13,27 +14,27 @@ import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; */ export const makeConnectionErrorSelector = ( + pendingConnection: PendingConnection | null, nodeId: string, fieldName: string, handleType: HandleType, fieldType?: FieldType | null ) => { return createSelector(selectNodesSlice, (nodesSlice) => { + const { nodes, edges } = nodesSlice; + if (!fieldType) { return i18n.t('nodes.noFieldType'); } - const { connectionStartFieldType, connectionStartParams, nodes, edges } = nodesSlice; - - if (!connectionStartParams || !connectionStartFieldType) { + if (!pendingConnection) { return i18n.t('nodes.noConnectionInProgress'); } - const { - handleType: connectionHandleType, - nodeId: connectionNodeId, - handleId: connectionFieldName, - } = connectionStartParams; + const connectionNodeId = pendingConnection.node.id; + const connectionFieldName = pendingConnection.fieldTemplate.name; + const connectionHandleType = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; + const connectionStartFieldType = pendingConnection.fieldTemplate.type; if (!connectionHandleType || !connectionNodeId || !connectionFieldName) { return i18n.t('nodes.noConnectionData'); From 6cf5b402c6a96ba1ac106eb3cec9f25825e74794 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 21:10:43 +1000 Subject: [PATCH 148/442] feat(ui): remove extraneous selectedEdges and selectedNodes state --- .../features/nodes/components/flow/Flow.tsx | 12 -- .../sidePanel/inspector/InspectorDataTab.tsx | 16 +-- .../inspector/InspectorDetailsTab.tsx | 4 +- .../inspector/InspectorOutputsTab.tsx | 6 +- .../inspector/InspectorTemplateTab.tsx | 4 +- .../src/features/nodes/store/nodesSlice.ts | 126 ++++++++++-------- .../web/src/features/nodes/store/selectors.ts | 8 ++ .../web/src/features/nodes/store/types.ts | 2 - 8 files changed, 92 insertions(+), 86 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 44c7e1ce7b..dd10eb8cba 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -15,8 +15,6 @@ import { nodesDeleted, redo, selectedAll, - selectedEdgesChanged, - selectedNodesChanged, undo, viewportChanged, } from 'features/nodes/store/nodesSlice'; @@ -32,7 +30,6 @@ import type { OnMoveEnd, OnNodesChange, OnNodesDelete, - OnSelectionChangeFunc, ProOptions, ReactFlowProps, } from 'reactflow'; @@ -111,14 +108,6 @@ export const Flow = memo(() => { [dispatch] ); - const handleSelectionChange: OnSelectionChangeFunc = useCallback( - ({ nodes, edges }) => { - dispatch(selectedNodesChanged(nodes ? nodes.map((n) => n.id) : [])); - dispatch(selectedEdgesChanged(edges ? edges.map((e) => e.id) : [])); - }, - [dispatch] - ); - const handleMoveEnd: OnMoveEnd = useCallback( (e, viewport) => { dispatch(viewportChanged(viewport)); @@ -258,7 +247,6 @@ export const Flow = memo(() => { onConnectEnd={onConnectEnd} onMoveEnd={handleMoveEnd} connectionLineComponent={CustomConnectionLine} - onSelectionChange={handleSelectionChange} isValidConnection={isValidConnection} minZoom={0.1} snapToGrid={shouldSnapToGrid} diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx index 8f1a3249ee..af0ea710d6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx @@ -3,27 +3,21 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode } from 'features/nodes/store/selectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); - - return { - data: lastSelectedNode?.data, - }; -}); +const selector = createMemoizedSelector(selectNodesSlice, (nodes) => selectLastSelectedNode(nodes)); const InspectorDataTab = () => { const { t } = useTranslation(); - const { data } = useAppSelector(selector); + const lastSelectedNode = useAppSelector(selector); - if (!data) { + if (!lastSelectedNode) { return ; } - return ; + return ; }; export default memo(InspectorDataTab); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx index 354a0ed179..f38fa819dd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx @@ -7,6 +7,7 @@ import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableCon import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea'; import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,8 +19,7 @@ const InspectorDetailsTab = () => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); + const lastSelectedNode = selectLastSelectedNode(nodes); const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) { diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx index 381a510b8b..17a1dd33f1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx @@ -6,6 +6,7 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -19,11 +20,10 @@ const InspectorOutputsTab = () => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); + const lastSelectedNode = selectLastSelectedNode(nodes); const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; - const nes = nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__']; + const nes = nodes.nodeExecutionStates[lastSelectedNode?.id ?? '__UNKNOWN_NODE__']; if (!isInvocationNode(lastSelectedNode) || !nes || !lastSelectedNodeTemplate) { return; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx index fbe86ba32c..d95b215dd6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx @@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode } from 'features/nodes/store/selectors'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,8 +13,7 @@ const NodeTemplateInspector = () => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); + const lastSelectedNode = selectLastSelectedNode(nodes); const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; return lastSelectedNodeTemplate; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 69530902a4..47bf3a5ab1 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -74,8 +74,6 @@ const initialNodesState: NodesState = { _version: 1, nodes: [], edges: [], - selectedNodes: [], - selectedEdges: [], nodeExecutionStates: {}, viewport: { x: 0, y: 0, zoom: 1 }, }; @@ -351,12 +349,6 @@ export const nodesSlice = createSlice({ state.nodes ); }, - selectedNodesChanged: (state, action: PayloadAction) => { - state.selectedNodes = action.payload; - }, - selectedEdgesChanged: (state, action: PayloadAction) => { - state.selectedEdges = action.payload; - }, fieldValueReset: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zStatefulFieldValue); }, @@ -593,8 +585,6 @@ export const { nodeUseCacheChanged, notesNodeValueChanged, selectedAll, - selectedEdgesChanged, - selectedNodesChanged, selectionPasted, viewportChanged, edgeAdded, @@ -602,6 +592,78 @@ export const { redo, } = nodesSlice.actions; +export const $cursorPos = atom(null); +export const $templates = atom({}); +export const $copiedNodes = atom([]); +export const $copiedEdges = atom([]); +export const $pendingConnection = atom(null); +export const $isModifyingEdge = atom(false); +export const $isAddNodePopoverOpen = atom(false); +export const closeAddNodePopover = () => { + $isAddNodePopoverOpen.set(false); + $pendingConnection.set(null); +}; +export const openAddNodePopover = () => { + $isAddNodePopoverOpen.set(true); +}; + +export const selectNodesSlice = (state: RootState) => state.nodes.present; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrateNodesState = (state: any): any => { + if (!('_version' in state)) { + state._version = 1; + } + return state; +}; + +export const nodesPersistConfig: PersistConfig = { + name: nodesSlice.name, + initialState: initialNodesState, + migrate: migrateNodesState, + persistDenylist: [], +}; + +const selectionMatcher = isAnyOf(selectedAll, selectionPasted, nodeExclusivelySelected); + +const isSelectionAction = (action: UnknownAction) => { + if (selectionMatcher(action)) { + return true; + } + if (nodesChanged.match(action)) { + if (action.payload.every((change) => change.type === 'select')) { + return true; + } + } + return false; +}; + +const individualGroupByMatcher = isAnyOf(nodesChanged, viewportChanged); + +export const nodesUndoableConfig: UndoableOptions = { + limit: 64, + undoType: nodesSlice.actions.undo.type, + redoType: nodesSlice.actions.redo.type, + groupBy: (action, state, history) => { + if (isSelectionAction(action)) { + // Changes to selection should never be recorded on their own + return history.group; + } + if (individualGroupByMatcher(action)) { + return action.type; + } + return null; + }, + filter: (action, _state, _history) => { + if (nodesChanged.match(action)) { + if (action.payload.every((change) => change.type === 'dimensions')) { + return false; + } + } + return true; + }, +}; + // This is used for tracking `state.workflow.isTouched` export const isAnyNodeOrEdgeMutation = isAnyOf( connectionMade, @@ -636,47 +698,3 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( selectionPasted, edgeAdded ); - -export const $cursorPos = atom(null); -export const $templates = atom({}); -export const $copiedNodes = atom([]); -export const $copiedEdges = atom([]); -export const $pendingConnection = atom(null); -export const $isModifyingEdge = atom(false); -export const $isAddNodePopoverOpen = atom(false); -export const closeAddNodePopover = () => { - $isAddNodePopoverOpen.set(false); - $pendingConnection.set(null); -}; -export const openAddNodePopover = () => { - $isAddNodePopoverOpen.set(true); -}; - -export const selectNodesSlice = (state: RootState) => state.nodes.present; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateNodesState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - } - return state; -}; - -export const nodesPersistConfig: PersistConfig = { - name: nodesSlice.name, - initialState: initialNodesState, - migrate: migrateNodesState, - persistDenylist: ['selectedNodes', 'selectedEdges'], -}; - -export const nodesUndoableConfig: UndoableOptions = { - limit: 64, - undoType: nodesSlice.actions.undo.type, - redoType: nodesSlice.actions.redo.type, - groupBy: (action, state, history) => { - return null; - }, - filter: (action, _state, _history) => { - return true; - }, -}; diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts index 6d1e5e38ec..be8cfafa8b 100644 --- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts +++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts @@ -28,3 +28,11 @@ export const selectFieldInputInstance = ( const data = selectNodeData(nodesSlice, nodeId); return data?.inputs[fieldName] ?? null; }; + +export const selectLastSelectedNode = (nodesSlice: NodesState) => { + const selectedNodes = nodesSlice.nodes.filter((n) => n.selected); + if (selectedNodes.length === 1) { + return selectedNodes[0]; + } + return null; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index e2249b177e..47e9ecaf85 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -26,8 +26,6 @@ export type NodesState = { _version: 1; nodes: AnyNode[]; edges: InvocationNodeEdge[]; - selectedNodes: string[]; - selectedEdges: string[]; nodeExecutionStates: Record; viewport: Viewport; }; From 7f78fe7a36fac8c97f3b23c52a26f3bb09cc0d13 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 21:21:39 +1000 Subject: [PATCH 149/442] feat(ui): move viewport state to nanostores --- .../features/dnd/hooks/useScaledCenteredModifer.ts | 14 ++++++-------- .../src/features/nodes/components/flow/Flow.tsx | 14 ++++++-------- .../web/src/features/nodes/store/nodesSlice.ts | 8 ++------ .../frontend/web/src/features/nodes/store/types.ts | 2 -- 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts b/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts index f3f0c50f03..ce93611ff4 100644 --- a/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts +++ b/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts @@ -1,23 +1,21 @@ import type { Modifier } from '@dnd-kit/core'; import { getEventCoordinates } from '@dnd-kit/utilities'; -import { createSelector } from '@reduxjs/toolkit'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $viewport } from 'features/nodes/store/nodesSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useCallback } from 'react'; -const selectZoom = createSelector([selectNodesSlice, activeTabNameSelector], (nodes, activeTabName) => - activeTabName === 'workflows' ? nodes.viewport.zoom : 1 -); - /** * Applies scaling to the drag transform (if on node editor tab) and centers it on cursor. */ export const useScaledModifer = () => { - const zoom = useAppSelector(selectZoom); + const activeTabName = useAppSelector(activeTabNameSelector); + const workflowsViewport = useStore($viewport); const modifier: Modifier = useCallback( ({ activatorEvent, draggingNodeRect, transform }) => { if (draggingNodeRect && activatorEvent) { + const zoom = activeTabName === 'workflows' ? workflowsViewport.zoom : 1; const activatorCoordinates = getEventCoordinates(activatorEvent); if (!activatorCoordinates) { @@ -42,7 +40,7 @@ export const useScaledModifer = () => { return transform; }, - [zoom] + [activeTabName, workflowsViewport.zoom] ); return modifier; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index dd10eb8cba..177abd57f5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -1,4 +1,5 @@ import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useConnection } from 'features/nodes/hooks/useConnection'; import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste'; @@ -6,6 +7,7 @@ import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection' import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { $cursorPos, + $viewport, connectionMade, edgeAdded, edgeDeleted, @@ -16,7 +18,6 @@ import { redo, selectedAll, undo, - viewportChanged, } from 'features/nodes/store/nodesSlice'; import { $flow } from 'features/nodes/store/reactFlowInstance'; import type { CSSProperties, MouseEvent } from 'react'; @@ -64,7 +65,7 @@ export const Flow = memo(() => { const dispatch = useAppDispatch(); const nodes = useAppSelector((s) => s.nodes.present.nodes); const edges = useAppSelector((s) => s.nodes.present.edges); - const viewport = useAppSelector((s) => s.nodes.present.viewport); + const viewport = useStore($viewport); const shouldSnapToGrid = useAppSelector((s) => s.workflowSettings.shouldSnapToGrid); const selectionMode = useAppSelector((s) => s.workflowSettings.selectionMode); const { onConnectStart, onConnect, onConnectEnd } = useConnection(); @@ -108,12 +109,9 @@ export const Flow = memo(() => { [dispatch] ); - const handleMoveEnd: OnMoveEnd = useCallback( - (e, viewport) => { - dispatch(viewportChanged(viewport)); - }, - [dispatch] - ); + const handleMoveEnd: OnMoveEnd = useCallback((e, viewport) => { + $viewport.set(viewport); + }, []); const { onCloseGlobal } = useGlobalMenuClose(); const handlePaneClick = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 47bf3a5ab1..cc3c9db3e8 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -75,7 +75,6 @@ const initialNodesState: NodesState = { nodes: [], edges: [], nodeExecutionStates: {}, - viewport: { x: 0, y: 0, zoom: 1 }, }; type FieldValueAction = PayloadAction<{ @@ -410,9 +409,6 @@ export const nodesSlice = createSlice({ state.nodes = []; state.edges = []; }, - viewportChanged: (state, action: PayloadAction) => { - state.viewport = action.payload; - }, selectedAll: (state) => { state.nodes = applyNodeChanges( state.nodes.map((n) => ({ id: n.id, type: 'select', selected: true })), @@ -586,7 +582,6 @@ export const { notesNodeValueChanged, selectedAll, selectionPasted, - viewportChanged, edgeAdded, undo, redo, @@ -598,6 +593,7 @@ export const $copiedNodes = atom([]); export const $copiedEdges = atom([]); export const $pendingConnection = atom(null); export const $isModifyingEdge = atom(false); +export const $viewport = atom({ x: 0, y: 0, zoom: 1 }); export const $isAddNodePopoverOpen = atom(false); export const closeAddNodePopover = () => { $isAddNodePopoverOpen.set(false); @@ -638,7 +634,7 @@ const isSelectionAction = (action: UnknownAction) => { return false; }; -const individualGroupByMatcher = isAnyOf(nodesChanged, viewportChanged); +const individualGroupByMatcher = isAnyOf(nodesChanged); export const nodesUndoableConfig: UndoableOptions = { limit: 64, diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index 47e9ecaf85..090a967626 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -12,7 +12,6 @@ import type { NodeExecutionState, } from 'features/nodes/types/invocation'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; -import type { Viewport } from 'reactflow'; export type Templates = Record; @@ -27,7 +26,6 @@ export type NodesState = { nodes: AnyNode[]; edges: InvocationNodeEdge[]; nodeExecutionStates: Record; - viewport: Viewport; }; export type WorkflowMode = 'edit' | 'view'; From dbfaa07e030c1c5cb9124b0cd6ecfc0ec25a384d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 21:22:46 +1000 Subject: [PATCH 150/442] feat(ui): add checks for undo/redo actions --- .../features/nodes/components/flow/Flow.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 177abd57f5..b47c5c38f7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -66,6 +66,8 @@ export const Flow = memo(() => { const nodes = useAppSelector((s) => s.nodes.present.nodes); const edges = useAppSelector((s) => s.nodes.present.edges); const viewport = useStore($viewport); + const mayUndo = useAppSelector((s) => s.nodes.past.length > 0); + const mayRedo = useAppSelector((s) => s.nodes.future.length > 0); const shouldSnapToGrid = useAppSelector((s) => s.workflowSettings.shouldSnapToGrid); const selectionMode = useAppSelector((s) => s.workflowSettings.selectionMode); const { onConnectStart, onConnect, onConnectEnd } = useConnection(); @@ -192,13 +194,13 @@ export const Flow = memo(() => { const { copySelection, pasteSelection } = useCopyPaste(); useHotkeys(['Ctrl+c', 'Meta+c'], (e) => { - e.preventDefault(); - copySelection(); + e.preventDefault(); + copySelection(); }); useHotkeys(['Ctrl+a', 'Meta+a'], (e) => { - e.preventDefault(); - dispatch(selectedAll()); + e.preventDefault(); + dispatch(selectedAll()); }); useHotkeys(['Ctrl+v', 'Meta+v'], (e) => { @@ -222,6 +224,20 @@ export const Flow = memo(() => { [dispatch] ); + const onUndoHotkey = useCallback(() => { + if (mayUndo) { + dispatch(undo()); + } + }, [dispatch, mayUndo]); + useHotkeys(['meta+z', 'ctrl+z'], onUndoHotkey); + + const onRedoHotkey = useCallback(() => { + if (mayRedo) { + dispatch(redo()); + } + }, [dispatch, mayRedo]); + useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey); + return ( Date: Thu, 16 May 2024 21:23:18 +1000 Subject: [PATCH 151/442] fix(ui): fix dependency tracking for copy/paste hotkeys --- .../features/nodes/components/flow/Flow.tsx | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index b47c5c38f7..34c180ec53 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -193,36 +193,32 @@ export const Flow = memo(() => { const { copySelection, pasteSelection } = useCopyPaste(); - useHotkeys(['Ctrl+c', 'Meta+c'], (e) => { + const onCopyHotkey = useCallback( + (e: KeyboardEvent) => { e.preventDefault(); copySelection(); - }); + }, + [copySelection] + ); + useHotkeys(['Ctrl+c', 'Meta+c'], onCopyHotkey); - useHotkeys(['Ctrl+a', 'Meta+a'], (e) => { + const onSelectAllHotkey = useCallback( + (e: KeyboardEvent) => { e.preventDefault(); dispatch(selectedAll()); - }); - - useHotkeys(['Ctrl+v', 'Meta+v'], (e) => { - e.preventDefault(); - pasteSelection(); - }); - - useHotkeys( - ['meta+z', 'ctrl+z'], - () => { - dispatch(undo()); }, [dispatch] ); + useHotkeys(['Ctrl+a', 'Meta+a'], onSelectAllHotkey); - useHotkeys( - ['meta+shift+z', 'ctrl+shift+z'], - () => { - dispatch(redo()); + const onPasteHotkey = useCallback( + (e: KeyboardEvent) => { + e.preventDefault(); + pasteSelection(); }, - [dispatch] + [pasteSelection] ); + useHotkeys(['Ctrl+v', 'Meta+v'], onPasteHotkey); const onUndoHotkey = useCallback(() => { if (mayUndo) { From e3a143eaed67e4eef3babaef95724c955cff2bfc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 21:56:00 +1000 Subject: [PATCH 152/442] fix(ui): fix jank w/ stale connections --- .../src/features/nodes/components/flow/Flow.tsx | 15 ++++++++++++++- .../flow/nodes/Invocation/fields/InputField.tsx | 10 +++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 34c180ec53..c1542caabc 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -7,6 +7,8 @@ import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection' import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { $cursorPos, + $isAddNodePopoverOpen, + $pendingConnection, $viewport, connectionMade, edgeAdded, @@ -33,8 +35,9 @@ import type { OnNodesDelete, ProOptions, ReactFlowProps, + ReactFlowState, } from 'reactflow'; -import { Background, ReactFlow } from 'reactflow'; +import { Background, ReactFlow, useStore as useReactFlowStore } from 'reactflow'; import CustomConnectionLine from './connectionLines/CustomConnectionLine'; import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge'; @@ -61,6 +64,8 @@ const proOptions: ProOptions = { hideAttribution: true }; const snapGrid: [number, number] = [25, 25]; +const selectCancelConnection = (state: ReactFlowState) => state.cancelConnection; + export const Flow = memo(() => { const dispatch = useAppDispatch(); const nodes = useAppSelector((s) => s.nodes.present.nodes); @@ -73,6 +78,7 @@ export const Flow = memo(() => { const { onConnectStart, onConnect, onConnectEnd } = useConnection(); const flowWrapper = useRef(null); const isValidConnection = useIsValidConnection(); + const cancelConnection = useReactFlowStore(selectCancelConnection); useWorkflowWatcher(); const [borderRadius] = useToken('radii', ['base']); @@ -234,6 +240,13 @@ export const Flow = memo(() => { }, [dispatch, mayRedo]); useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey); + const onEscapeHotkey = useCallback(() => { + $pendingConnection.set(null); + $isAddNodePopoverOpen.set(false); + cancelConnection(); + }, [cancelConnection]); + useHotkeys('esc', onEscapeHotkey); + return ( { return ( - + Date: Thu, 16 May 2024 22:13:02 +1000 Subject: [PATCH 153/442] fix(ui): janky editable field title - Do not allow whitespace-only field titles - Make only preview text trigger editable - Tooltip over the preview, not the whole "row" --- .../Invocation/fields/EditableFieldTitle.tsx | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx index e02b1a1474..04bcd81db8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx @@ -37,7 +37,8 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => { const [localTitle, setLocalTitle] = useState(label || fieldTemplateTitle || t('nodes.unknownField')); const handleSubmit = useCallback( - async (newTitle: string) => { + async (newTitleRaw: string) => { + const newTitle = newTitleRaw.trim(); if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) { return; } @@ -57,22 +58,22 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => { }, [label, fieldTemplateTitle, t]); return ( - : undefined} - openDelay={HANDLE_TOOLTIP_OPEN_DELAY} + - : undefined} + openDelay={HANDLE_TOOLTIP_OPEN_DELAY} > { noOfLines={1} color={isMissingInput ? 'error.300' : 'base.300'} /> - - - - + + + + ); }); @@ -127,7 +128,15 @@ const EditableControls = memo(() => { } return ( - + ); }); From 9ff55969639db30961f2ba69d5cc0e8f0f4c4724 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 22:13:20 +1000 Subject: [PATCH 154/442] feat(ui): hide values for connected fields --- .../nodes/Invocation/fields/InputField.tsx | 2 +- .../hooks/useAnyOrDirectInputFieldNames.ts | 28 ++++++++++++++++--- .../hooks/useConnectionInputFieldNames.ts | 27 +++++++++++++++--- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx index cad4dded39..a2fce55ce3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx @@ -69,7 +69,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { ); } - if (fieldTemplate.input === 'connection') { + if (fieldTemplate.input === 'connection' || isConnected) { return ( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts index f5931db87e..7972f9eee3 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts @@ -1,4 +1,7 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; +import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; @@ -6,14 +9,31 @@ import { useMemo } from 'react'; export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => { const template = useNodeTemplate(nodeId); + const selectConnectedFieldNames = useMemo( + () => + createMemoizedSelector(selectNodesSlice, (nodesSlice) => + nodesSlice.edges + .filter((e) => e.target === nodeId) + .map((e) => e.targetHandle) + .filter(Boolean) + ), + [nodeId] + ); + const connectedFieldNames = useAppSelector(selectConnectedFieldNames); + const fieldNames = useMemo(() => { - const fields = map(template.inputs).filter( - (field) => + const fields = map(template.inputs).filter((field) => { + if (connectedFieldNames.includes(field.name)) { + return false; + } + + return ( (['any', 'direct'].includes(field.input) || field.type.isCollectionOrScalar) && keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) - ); + ); + }); return getSortedFilteredFieldNames(fields); - }, [template]); + }, [connectedFieldNames, template.inputs]); return fieldNames; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts index 84413fc9c8..0eeb592c31 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts @@ -1,4 +1,7 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; +import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; @@ -6,15 +9,31 @@ import { useMemo } from 'react'; export const useConnectionInputFieldNames = (nodeId: string): string[] => { const template = useNodeTemplate(nodeId); + const selectConnectedFieldNames = useMemo( + () => + createMemoizedSelector(selectNodesSlice, (nodesSlice) => + nodesSlice.edges + .filter((e) => e.target === nodeId) + .map((e) => e.targetHandle) + .filter(Boolean) + ), + [nodeId] + ); + const connectedFieldNames = useAppSelector(selectConnectedFieldNames); + const fieldNames = useMemo(() => { // get the visible fields - const fields = map(template.inputs).filter( - (field) => + const fields = map(template.inputs).filter((field) => { + if (connectedFieldNames.includes(field.name)) { + return true; + } + return ( (field.input === 'connection' && !field.type.isCollectionOrScalar) || !keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) - ); + ); + }); return getSortedFilteredFieldNames(fields); - }, [template]); + }, [connectedFieldNames, template.inputs]); return fieldNames; }; From a18bbac262b491e8876549f6d376de3d9ff7ca5c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 22:44:19 +1000 Subject: [PATCH 155/442] fix(ui): jank interaction between edge update and autoconnect --- .../features/nodes/components/flow/Flow.tsx | 11 +++++++---- .../src/features/nodes/hooks/useConnection.ts | 19 +++++++++++++++++-- .../src/features/nodes/store/nodesSlice.ts | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index c1542caabc..110658ffdb 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -8,6 +8,7 @@ import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { $cursorPos, $isAddNodePopoverOpen, + $isUpdatingEdge, $pendingConnection, $viewport, connectionMade, @@ -160,6 +161,7 @@ export const Flow = memo(() => { const onEdgeUpdateStart: NonNullable = useCallback( (e, edge, _handleType) => { + $isUpdatingEdge.set(true); // update mouse event edgeUpdateMouseEvent.current = e; // always delete the edge when starting an updated @@ -170,8 +172,7 @@ export const Flow = memo(() => { const onEdgeUpdate: OnEdgeUpdateFunc = useCallback( (_oldEdge, newConnection) => { - // instead of updating the edge (we deleted it earlier), we instead create - // a new one. + // Because we deleted the edge when the update started, we must create a new edge from the connection dispatch(connectionMade(newConnection)); }, [dispatch] @@ -179,8 +180,10 @@ export const Flow = memo(() => { const onEdgeUpdateEnd: NonNullable = useCallback( (e, edge, _handleType) => { - // Handle the case where user begins a drag but didn't move the cursor - - // bc we deleted the edge, we need to add it back + $isUpdatingEdge.set(false); + $pendingConnection.set(null); + // Handle the case where user begins a drag but didn't move the cursor - we deleted the edge when starting + // the edge update - we need to add it back if ( // ignore touch events !('touches' in e) && diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index 468a0bd645..06471391eb 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -1,7 +1,13 @@ import { useStore } from '@nanostores/react'; import { useAppStore } from 'app/store/storeHooks'; import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; -import { $isAddNodePopoverOpen, $pendingConnection, $templates, connectionMade } from 'features/nodes/store/nodesSlice'; +import { + $isAddNodePopoverOpen, + $isUpdatingEdge, + $pendingConnection, + $templates, + connectionMade, +} from 'features/nodes/store/nodesSlice'; import { getFirstValidConnection } from 'features/nodes/store/util/findConnectionToValidHandle'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { useCallback, useMemo } from 'react'; @@ -42,10 +48,19 @@ export const useConnection = () => { const onConnectEnd = useCallback(() => { const { dispatch } = store; const pendingConnection = $pendingConnection.get(); + const isUpdatingEdge = $isUpdatingEdge.get(); + const mouseOverNodeId = $mouseOverNode.get(); + + // If we are in the middle of an edge update, and the mouse isn't over a node, we should just bail so the edge + // update logic can finish up + if (isUpdatingEdge && !mouseOverNodeId) { + $pendingConnection.set(null); + return; + } + if (!pendingConnection) { return; } - const mouseOverNodeId = $mouseOverNode.get(); const { nodes, edges } = store.getState().nodes.present; if (mouseOverNodeId) { const candidateNode = nodes.filter(isInvocationNode).find((n) => n.id === mouseOverNodeId); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index cc3c9db3e8..cfd9fa8ef8 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -592,7 +592,7 @@ export const $templates = atom({}); export const $copiedNodes = atom([]); export const $copiedEdges = atom([]); export const $pendingConnection = atom(null); -export const $isModifyingEdge = atom(false); +export const $isUpdatingEdge = atom(false); export const $viewport = atom({ x: 0, y: 0, zoom: 1 }); export const $isAddNodePopoverOpen = atom(false); export const closeAddNodePopover = () => { From 78cb4d75ad38540ad99419c19c3499998df152f6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 22:45:55 +1000 Subject: [PATCH 156/442] fix(ui): use `elevateEdgesOnSelect` so last-selected edge is the interactable one when updating edges --- .../frontend/web/src/features/nodes/components/flow/Flow.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 110658ffdb..f1fcf24af2 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -283,6 +283,7 @@ export const Flow = memo(() => { onPaneClick={handlePaneClick} deleteKeyCode={DELETE_KEYS} selectionMode={selectionMode} + elevateEdgesOnSelect > From 76825f42618ef30244357fd688d50c211f354730 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 23:00:12 +1000 Subject: [PATCH 157/442] fix(ui): allow collect node inputs to connect to multiple fields when using lazy connect --- .../store/util/findConnectionToValidHandle.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts index dcd805d32e..adb91d048c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts @@ -169,8 +169,9 @@ export const getFirstValidConnection = ( } } else { // Connecting from a target to a source - // Ensure we there is not already an edge to the target + // Ensure we there is not already an edge to the target, except for collect nodes if ( + pendingConnection.node.data.type !== 'collect' && edges.some( (e) => e.target === pendingConnection.node.id && e.targetHandle === pendingConnection.fieldTemplate.name ) @@ -182,20 +183,13 @@ export const getFirstValidConnection = ( return null; } - if (candidateNode.data.type === 'collect') { - // Special handling for collect node - connect to the `collection` field - return { - source: candidateNode.id, - sourceHandle: 'collection', - target: pendingConnection.node.id, - targetHandle: pendingConnection.fieldTemplate.name, - }; - } // Sources/outputs can have any number of edges, we can take the first matching output field const candidateFields = map(candidateTemplate.outputs); - const candidateField = candidateFields.find((field) => - validateSourceAndTargetTypes(field.type, pendingConnection.fieldTemplate.type) - ); + const candidateField = candidateFields.find((field) => { + const isValid = validateSourceAndTargetTypes(field.type, pendingConnection.fieldTemplate.type); + const isAlreadyConnected = edges.some((e) => e.source === candidateNode.id && e.sourceHandle === field.name); + return isValid && !isAlreadyConnected; + }); if (candidateField) { return { source: candidateNode.id, From a8b042177d8dea23fe30647773855329d078cd1f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 23:42:54 +1000 Subject: [PATCH 158/442] feat(ui): connection validation for collection items types --- invokeai/frontend/web/public/locales/en.json | 1 + .../flow/AddNodePopover/AddNodePopover.tsx | 2 +- .../src/features/nodes/hooks/useConnection.ts | 2 +- .../nodes/hooks/useConnectionState.ts | 6 ++- .../nodes/hooks/useIsValidConnection.ts | 10 +++++ .../store/util/findConnectionToValidHandle.ts | 24 ++++++++---- .../util/makeIsConnectionValidSelector.ts | 38 ++++++++++++++++++- 7 files changed, 70 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 7aa4b03b8c..8c9cce794e 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -774,6 +774,7 @@ "cannotConnectOutputToOutput": "Cannot connect output to output", "cannotConnectToSelf": "Cannot connect to self", "cannotDuplicateConnection": "Cannot create duplicate connections", + "cannotMixAndMatchCollectionItemTypes": "Cannot mix and match collection item types", "nodePack": "Node pack", "collection": "Collection", "collectionFieldType": "{{name}} Collection", diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index c53bed31f5..95104c683c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -148,7 +148,7 @@ const AddNodePopover = () => { const template = templates[node.data.type]; assert(template, 'Template not found'); const { nodes, edges } = store.getState().nodes.present; - const connection = getFirstValidConnection(nodes, edges, pendingConnection, node, template); + const connection = getFirstValidConnection(templates, nodes, edges, pendingConnection, node, template); if (connection) { dispatch(connectionMade(connection)); } diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index 06471391eb..f6091e4a13 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -70,7 +70,7 @@ export const useConnection = () => { } const candidateTemplate = templates[candidateNode.data.type]; assert(candidateTemplate, `Template not found for node type: ${candidateNode.data.type}`); - const connection = getFirstValidConnection(nodes, edges, pendingConnection, candidateNode, candidateTemplate); + const connection = getFirstValidConnection(templates, nodes, edges, pendingConnection, candidateNode, candidateTemplate); if (connection) { dispatch(connectionMade(connection)); } diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index 32f6adcddb..728b492453 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -1,7 +1,7 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $pendingConnection, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $pendingConnection, $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector'; import { useMemo } from 'react'; @@ -15,6 +15,7 @@ type UseConnectionStateProps = { export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => { const pendingConnection = useStore($pendingConnection); + const templates = useStore($templates); const fieldType = useFieldType(nodeId, fieldName, kind); const selectIsConnected = useMemo( @@ -35,13 +36,14 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta const selectConnectionError = useMemo( () => makeConnectionErrorSelector( + templates, pendingConnection, nodeId, fieldName, kind === 'inputs' ? 'target' : 'source', fieldType ), - [pendingConnection, nodeId, fieldName, kind, fieldType] + [templates, pendingConnection, nodeId, fieldName, kind, fieldType] ); const isConnected = useAppSelector(selectIsConnected); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index 041faab149..6b3294f064 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -3,8 +3,10 @@ import { useStore } from '@nanostores/react'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { $templates } from 'features/nodes/store/nodesSlice'; import { getIsGraphAcyclic } from 'features/nodes/store/util/getIsGraphAcyclic'; +import { getCollectItemType } from 'features/nodes/store/util/makeIsConnectionValidSelector'; import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; +import { isEqual } from 'lodash-es'; import { useCallback } from 'react'; import type { Connection, Node } from 'reactflow'; @@ -60,6 +62,14 @@ export const useIsValidConnection = () => { return false; } + if (targetNode.data.type === 'collect' && targetFieldTemplate.name === 'item') { + // Collect nodes shouldn't mix and match field types + const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); + if (collectItemType) { + return isEqual(sourceFieldTemplate.type, collectItemType); + } + } + // Connection is invalid if target already has a connection if ( edges.find((edge) => { diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts index adb91d048c..cd69640dca 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts @@ -1,8 +1,9 @@ import type { PendingConnection, Templates } from 'features/nodes/store/types'; +import { getCollectItemType } from 'features/nodes/store/util/makeIsConnectionValidSelector'; import type { FieldInputTemplate, FieldOutputTemplate, FieldType } from 'features/nodes/types/field'; import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; -import { differenceWith, map } from 'lodash-es'; +import { differenceWith, isEqual, map } from 'lodash-es'; import type { Connection, Edge, HandleType, Node } from 'reactflow'; import { assert } from 'tsafe'; @@ -115,6 +116,7 @@ export const findConnectionToValidHandle = ( }; export const getFirstValidConnection = ( + templates: Templates, nodes: AnyNode[], edges: InvocationNodeEdge[], pendingConnection: PendingConnection, @@ -170,12 +172,11 @@ export const getFirstValidConnection = ( } else { // Connecting from a target to a source // Ensure we there is not already an edge to the target, except for collect nodes - if ( - pendingConnection.node.data.type !== 'collect' && - edges.some( - (e) => e.target === pendingConnection.node.id && e.targetHandle === pendingConnection.fieldTemplate.name - ) - ) { + const isCollect = pendingConnection.node.data.type === 'collect'; + const isTargetAlreadyConnected = edges.some( + (e) => e.target === pendingConnection.node.id && e.targetHandle === pendingConnection.fieldTemplate.name + ); + if (!isCollect && isTargetAlreadyConnected) { return null; } @@ -184,7 +185,14 @@ export const getFirstValidConnection = ( } // Sources/outputs can have any number of edges, we can take the first matching output field - const candidateFields = map(candidateTemplate.outputs); + let candidateFields = map(candidateTemplate.outputs); + if (isCollect) { + // Narrow candidates to same field type as already is connected to the collect node + const collectItemType = getCollectItemType(templates, nodes, edges, pendingConnection.node.id); + if (collectItemType) { + candidateFields = candidateFields.filter((field) => isEqual(field.type, collectItemType)); + } + } const candidateField = candidateFields.find((field) => { const isValid = validateSourceAndTargetTypes(field.type, pendingConnection.fieldTemplate.type); const isAlreadyConnected = edges.some((e) => e.source === candidateNode.id && e.sourceHandle === field.name); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts index 212a0b804a..416ff2c6a8 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts @@ -1,19 +1,44 @@ import { createSelector } from '@reduxjs/toolkit'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import type { PendingConnection } from 'features/nodes/store/types'; +import type { PendingConnection, Templates } from 'features/nodes/store/types'; import type { FieldType } from 'features/nodes/types/field'; +import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; import i18n from 'i18next'; +import { isEqual } from 'lodash-es'; import type { HandleType } from 'reactflow'; import { getIsGraphAcyclic } from './getIsGraphAcyclic'; import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; +export const getCollectItemType = ( + templates: Templates, + nodes: AnyNode[], + edges: InvocationNodeEdge[], + nodeId: string +): FieldType | null => { + const firstEdgeToCollect = edges.find((edge) => edge.target === nodeId && edge.targetHandle === 'item'); + if (!firstEdgeToCollect?.sourceHandle) { + return null; + } + const node = nodes.find((n) => n.id === firstEdgeToCollect.source); + if (!node) { + return null; + } + const template = templates[node.data.type]; + if (!template) { + return null; + } + const fieldType = template.outputs[firstEdgeToCollect.sourceHandle]?.type ?? null; + return fieldType; +}; + /** * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts` * TODO: Figure out how to do this without duplicating all the logic */ export const makeConnectionErrorSelector = ( + templates: Templates, pendingConnection: PendingConnection | null, nodeId: string, fieldName: string, @@ -72,6 +97,17 @@ export const makeConnectionErrorSelector = ( return i18n.t('nodes.cannotDuplicateConnection'); } + const targetNode = nodes.find((node) => node.id === target); + if (targetNode?.data.type === 'collect' && targetHandle === 'item') { + // Collect nodes shouldn't mix and match field types + const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); + if (collectItemType) { + if (!isEqual(sourceType, collectItemType)) { + return i18n.t('nodes.cannotMixAndMatchCollectionItemTypes'); + } + } + } + if ( edges.find((edge) => { return edge.target === target && edge.targetHandle === targetHandle; From 6791b4eaa845cc7437cfe2fac624cbcdff8bfee3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 23:44:41 +1000 Subject: [PATCH 159/442] chore(ui): lint --- .../src/features/nodes/hooks/useConnection.ts | 9 +- .../src/features/nodes/hooks/useCopyPaste.ts | 8 +- .../web/src/features/nodes/store/selectors.ts | 2 +- .../store/util/findConnectionToValidHandle.ts | 109 +----------------- .../nodes/store/workflowSettingsSlice.ts | 2 +- .../nodes/util/workflow/validateWorkflow.ts | 5 +- 6 files changed, 19 insertions(+), 116 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index f6091e4a13..df628ba5af 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -70,7 +70,14 @@ export const useConnection = () => { } const candidateTemplate = templates[candidateNode.data.type]; assert(candidateTemplate, `Template not found for node type: ${candidateNode.data.type}`); - const connection = getFirstValidConnection(templates, nodes, edges, pendingConnection, candidateNode, candidateTemplate); + const connection = getFirstValidConnection( + templates, + nodes, + edges, + pendingConnection, + candidateNode, + candidateTemplate + ); if (connection) { dispatch(connectionMade(connection)); } diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts index 727c0932f7..9acd5722cf 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts @@ -1,6 +1,12 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; -import { $copiedEdges,$copiedNodes,$cursorPos, selectionPasted, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { + $copiedEdges, + $copiedNodes, + $cursorPos, + selectionPasted, + selectNodesSlice, +} from 'features/nodes/store/nodesSlice'; import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; import { v4 as uuidv4 } from 'uuid'; diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts index be8cfafa8b..4739a77e1c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts +++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts @@ -4,7 +4,7 @@ import type { InvocationNode, InvocationNodeData } from 'features/nodes/types/in import { isInvocationNode } from 'features/nodes/types/invocation'; import { assert } from 'tsafe'; -export const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode => { +const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode => { const node = nodesSlice.nodes.find((node) => node.id === nodeId); assert(isInvocationNode(node), `Node ${nodeId} is not an invocation node`); return node; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts index cd69640dca..4c47cb15b0 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts @@ -1,120 +1,13 @@ import type { PendingConnection, Templates } from 'features/nodes/store/types'; import { getCollectItemType } from 'features/nodes/store/util/makeIsConnectionValidSelector'; -import type { FieldInputTemplate, FieldOutputTemplate, FieldType } from 'features/nodes/types/field'; import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; -import { isInvocationNode } from 'features/nodes/types/invocation'; import { differenceWith, isEqual, map } from 'lodash-es'; -import type { Connection, Edge, HandleType, Node } from 'reactflow'; +import type { Connection } from 'reactflow'; import { assert } from 'tsafe'; import { getIsGraphAcyclic } from './getIsGraphAcyclic'; import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; -const isValidConnection = ( - edges: Edge[], - handleCurrentType: HandleType, - handleCurrentFieldType: FieldType, - node: Node, - handle: FieldInputTemplate | FieldOutputTemplate -) => { - let isValidConnection = true; - if (handleCurrentType === 'source') { - if ( - edges.find((edge) => { - return edge.target === node.id && edge.targetHandle === handle.name; - }) - ) { - isValidConnection = false; - } - } else { - if ( - edges.find((edge) => { - return edge.source === node.id && edge.sourceHandle === handle.name; - }) - ) { - isValidConnection = false; - } - } - - if (!validateSourceAndTargetTypes(handleCurrentFieldType, handle.type)) { - isValidConnection = false; - } - - return isValidConnection; -}; - -export const findConnectionToValidHandle = ( - node: AnyNode, - nodes: AnyNode[], - edges: InvocationNodeEdge[], - templates: Templates, - handleCurrentNodeId: string, - handleCurrentName: string, - handleCurrentType: HandleType, - handleCurrentFieldType: FieldType -): Connection | null => { - if (node.id === handleCurrentNodeId || !isInvocationNode(node)) { - return null; - } - - const template = templates[node.data.type]; - - if (!template) { - return null; - } - - const handles = handleCurrentType === 'source' ? template.inputs : template.outputs; - - //Prioritize handles whos name matches the node we're coming from - const handle = handles[handleCurrentName]; - - if (handle) { - const sourceID = handleCurrentType === 'source' ? handleCurrentNodeId : node.id; - const targetID = handleCurrentType === 'source' ? node.id : handleCurrentNodeId; - const sourceHandle = handleCurrentType === 'source' ? handleCurrentName : handle.name; - const targetHandle = handleCurrentType === 'source' ? handle.name : handleCurrentName; - - const isGraphAcyclic = getIsGraphAcyclic(sourceID, targetID, nodes, edges); - - const valid = isValidConnection(edges, handleCurrentType, handleCurrentFieldType, node, handle); - - if (isGraphAcyclic && valid) { - return { - source: sourceID, - sourceHandle: sourceHandle, - target: targetID, - targetHandle: targetHandle, - }; - } - } - - for (const handleName in handles) { - const handle = handles[handleName]; - if (!handle) { - continue; - } - - const sourceID = handleCurrentType === 'source' ? handleCurrentNodeId : node.id; - const targetID = handleCurrentType === 'source' ? node.id : handleCurrentNodeId; - const sourceHandle = handleCurrentType === 'source' ? handleCurrentName : handle.name; - const targetHandle = handleCurrentType === 'source' ? handle.name : handleCurrentName; - - const isGraphAcyclic = getIsGraphAcyclic(sourceID, targetID, nodes, edges); - - const valid = isValidConnection(edges, handleCurrentType, handleCurrentFieldType, node, handle); - - if (isGraphAcyclic && valid) { - return { - source: sourceID, - sourceHandle: sourceHandle, - target: targetID, - targetHandle: targetHandle, - }; - } - } - return null; -}; - export const getFirstValidConnection = ( templates: Templates, nodes: AnyNode[], diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts index 7487fd488b..4a2e45abde 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts @@ -3,7 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { SelectionMode } from 'reactflow'; -export type WorkflowSettingsState = { +type WorkflowSettingsState = { _version: 1; shouldShowMinimapPanel: boolean; shouldValidateGraph: boolean; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts index c57a7213b8..d2d3d64cb0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts @@ -31,10 +31,7 @@ type ValidateWorkflowResult = { * @throws {WorkflowVersionError} If the workflow version is not recognized. * @throws {z.ZodError} If there is a validation error. */ -export const validateWorkflow = ( - workflow: unknown, - invocationTemplates: Templates -): ValidateWorkflowResult => { +export const validateWorkflow = (workflow: unknown, invocationTemplates: Templates): ValidateWorkflowResult => { // Parse the raw workflow data & migrate it to the latest version const _workflow = parseAndMigrateWorkflow(workflow); From 23ac340a3ff05837efaa43580b349f36350deb65 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 08:49:13 +1000 Subject: [PATCH 160/442] tests(ui): add test for `parseSchema` --- .../nodes/util/schema/parseSchema.test.ts | 814 ++++++++++++++++++ 1 file changed, 814 insertions(+) create mode 100644 invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts new file mode 100644 index 0000000000..b67b61d19e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts @@ -0,0 +1,814 @@ +import { parseSchema } from 'features/nodes/util/schema/parseSchema'; +import { omit, pick } from 'lodash-es'; +import type { OpenAPIV3_1 } from 'openapi-types'; +import { describe, expect, it } from 'vitest'; + +describe('parseSchema', () => { + it('should parse the schema', () => { + const templates = parseSchema(schema); + expect(templates).toEqual(expected); + }); + it('should omit denied nodes', () => { + const templates = parseSchema(schema, undefined, ['add']); + expect(templates).toEqual(omit(expected, 'add')); + }); + it('should include only allowed nodes', () => { + const templates = parseSchema(schema, ['add']); + expect(templates).toEqual(pick(expected, 'add')); + }); +}); + +const expected = { + add: { + title: 'Add Integers', + type: 'add', + version: '1.0.1', + tags: ['math', 'add'], + description: 'Adds two numbers', + outputType: 'integer_output', + inputs: { + a: { + name: 'a', + title: 'A', + required: false, + description: 'The first number', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + default: 0, + }, + b: { + name: 'b', + title: 'B', + required: false, + description: 'The second number', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + default: 0, + }, + }, + outputs: { + value: { + fieldKind: 'output', + name: 'value', + title: 'Value', + description: 'The output integer', + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', + }, + scheduler: { + title: 'Scheduler', + type: 'scheduler', + version: '1.0.0', + tags: ['scheduler'], + description: 'Selects a scheduler.', + outputType: 'scheduler_output', + inputs: { + scheduler: { + name: 'scheduler', + title: 'Scheduler', + required: false, + description: 'Scheduler to use during inference', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + ui_type: 'SchedulerField', + type: { + name: 'SchedulerField', + isCollection: false, + isCollectionOrScalar: false, + }, + default: 'euler', + }, + }, + outputs: { + scheduler: { + fieldKind: 'output', + name: 'scheduler', + title: 'Scheduler', + description: 'Scheduler to use during inference', + type: { + name: 'SchedulerField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + ui_type: 'SchedulerField', + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', + }, + main_model_loader: { + title: 'Main Model', + type: 'main_model_loader', + version: '1.0.2', + tags: ['model'], + description: 'Loads a main model, outputting its submodels.', + outputType: 'model_loader_output', + inputs: { + model: { + name: 'model', + title: 'Model', + required: true, + description: 'Main model (UNet, VAE, CLIP) to load', + fieldKind: 'input', + input: 'direct', + ui_hidden: false, + ui_type: 'MainModelField', + type: { + name: 'MainModelField', + isCollection: false, + isCollectionOrScalar: false, + }, + }, + }, + outputs: { + vae: { + fieldKind: 'output', + name: 'vae', + title: 'VAE', + description: 'VAE', + type: { + name: 'VAEField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + clip: { + fieldKind: 'output', + name: 'clip', + title: 'CLIP', + description: 'CLIP (tokenizer, text encoder, LoRAs) and skipped layer count', + type: { + name: 'CLIPField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + unet: { + fieldKind: 'output', + name: 'unet', + title: 'UNet', + description: 'UNet (scheduler, LoRAs)', + type: { + name: 'UNetField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', + }, +}; + +const schema = { + openapi: '3.1.0', + info: { + title: 'Invoke - Community Edition', + description: 'An API for invoking AI image operations', + version: '1.0.0', + }, + components: { + schemas: { + AddInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + a: { + type: 'integer', + title: 'A', + description: 'The first number', + default: 0, + field_kind: 'input', + input: 'any', + orig_default: 0, + orig_required: false, + ui_hidden: false, + }, + b: { + type: 'integer', + title: 'B', + description: 'The second number', + default: 0, + field_kind: 'input', + input: 'any', + orig_default: 0, + orig_required: false, + ui_hidden: false, + }, + type: { + type: 'string', + enum: ['add'], + const: 'add', + title: 'type', + default: 'add', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['type', 'id'], + title: 'Add Integers', + description: 'Adds two numbers', + category: 'math', + classification: 'stable', + node_pack: 'invokeai', + tags: ['math', 'add'], + version: '1.0.1', + output: { + $ref: '#/components/schemas/IntegerOutput', + }, + class: 'invocation', + }, + IntegerOutput: { + description: 'Base class for nodes that output a single integer', + properties: { + value: { + description: 'The output integer', + field_kind: 'output', + title: 'Value', + type: 'integer', + ui_hidden: false, + }, + type: { + const: 'integer_output', + default: 'integer_output', + enum: ['integer_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + }, + required: ['value', 'type', 'type'], + title: 'IntegerOutput', + type: 'object', + class: 'output', + }, + SchedulerInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + scheduler: { + type: 'string', + enum: [ + 'ddim', + 'ddpm', + 'deis', + 'lms', + 'lms_k', + 'pndm', + 'heun', + 'heun_k', + 'euler', + 'euler_k', + 'euler_a', + 'kdpm_2', + 'kdpm_2_a', + 'dpmpp_2s', + 'dpmpp_2s_k', + 'dpmpp_2m', + 'dpmpp_2m_k', + 'dpmpp_2m_sde', + 'dpmpp_2m_sde_k', + 'dpmpp_sde', + 'dpmpp_sde_k', + 'unipc', + 'lcm', + 'tcd', + ], + title: 'Scheduler', + description: 'Scheduler to use during inference', + default: 'euler', + field_kind: 'input', + input: 'any', + orig_default: 'euler', + orig_required: false, + ui_hidden: false, + ui_type: 'SchedulerField', + }, + type: { + type: 'string', + enum: ['scheduler'], + const: 'scheduler', + title: 'type', + default: 'scheduler', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['type', 'id'], + title: 'Scheduler', + description: 'Selects a scheduler.', + category: 'latents', + classification: 'stable', + node_pack: 'invokeai', + tags: ['scheduler'], + version: '1.0.0', + output: { + $ref: '#/components/schemas/SchedulerOutput', + }, + class: 'invocation', + }, + SchedulerOutput: { + properties: { + scheduler: { + description: 'Scheduler to use during inference', + enum: [ + 'ddim', + 'ddpm', + 'deis', + 'lms', + 'lms_k', + 'pndm', + 'heun', + 'heun_k', + 'euler', + 'euler_k', + 'euler_a', + 'kdpm_2', + 'kdpm_2_a', + 'dpmpp_2s', + 'dpmpp_2s_k', + 'dpmpp_2m', + 'dpmpp_2m_k', + 'dpmpp_2m_sde', + 'dpmpp_2m_sde_k', + 'dpmpp_sde', + 'dpmpp_sde_k', + 'unipc', + 'lcm', + 'tcd', + ], + field_kind: 'output', + title: 'Scheduler', + type: 'string', + ui_hidden: false, + ui_type: 'SchedulerField', + }, + type: { + const: 'scheduler_output', + default: 'scheduler_output', + enum: ['scheduler_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + }, + required: ['scheduler', 'type', 'type'], + title: 'SchedulerOutput', + type: 'object', + class: 'output', + }, + MainModelLoaderInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + model: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Main model (UNet, VAE, CLIP) to load', + field_kind: 'input', + input: 'direct', + orig_required: true, + ui_hidden: false, + ui_type: 'MainModelField', + }, + type: { + type: 'string', + enum: ['main_model_loader'], + const: 'main_model_loader', + title: 'type', + default: 'main_model_loader', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['model', 'type', 'id'], + title: 'Main Model', + description: 'Loads a main model, outputting its submodels.', + category: 'model', + classification: 'stable', + node_pack: 'invokeai', + tags: ['model'], + version: '1.0.2', + output: { + $ref: '#/components/schemas/ModelLoaderOutput', + }, + class: 'invocation', + }, + ModelIdentifierField: { + properties: { + key: { + description: "The model's unique key", + title: 'Key', + type: 'string', + }, + hash: { + description: "The model's BLAKE3 hash", + title: 'Hash', + type: 'string', + }, + name: { + description: "The model's name", + title: 'Name', + type: 'string', + }, + base: { + allOf: [ + { + $ref: '#/components/schemas/BaseModelType', + }, + ], + description: "The model's base model type", + }, + type: { + allOf: [ + { + $ref: '#/components/schemas/ModelType', + }, + ], + description: "The model's type", + }, + submodel_type: { + anyOf: [ + { + $ref: '#/components/schemas/SubModelType', + }, + { + type: 'null', + }, + ], + default: null, + description: 'The submodel to load, if this is a main model', + }, + }, + required: ['key', 'hash', 'name', 'base', 'type'], + title: 'ModelIdentifierField', + type: 'object', + }, + BaseModelType: { + description: 'Base model type.', + enum: ['any', 'sd-1', 'sd-2', 'sdxl', 'sdxl-refiner'], + title: 'BaseModelType', + type: 'string', + }, + ModelType: { + description: 'Model type.', + enum: ['onnx', 'main', 'vae', 'lora', 'controlnet', 'embedding', 'ip_adapter', 'clip_vision', 't2i_adapter'], + title: 'ModelType', + type: 'string', + }, + SubModelType: { + description: 'Submodel type.', + enum: [ + 'unet', + 'text_encoder', + 'text_encoder_2', + 'tokenizer', + 'tokenizer_2', + 'vae', + 'vae_decoder', + 'vae_encoder', + 'scheduler', + 'safety_checker', + ], + title: 'SubModelType', + type: 'string', + }, + ModelLoaderOutput: { + description: 'Model loader output', + properties: { + vae: { + allOf: [ + { + $ref: '#/components/schemas/VAEField', + }, + ], + description: 'VAE', + field_kind: 'output', + title: 'VAE', + ui_hidden: false, + }, + type: { + const: 'model_loader_output', + default: 'model_loader_output', + enum: ['model_loader_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + clip: { + allOf: [ + { + $ref: '#/components/schemas/CLIPField', + }, + ], + description: 'CLIP (tokenizer, text encoder, LoRAs) and skipped layer count', + field_kind: 'output', + title: 'CLIP', + ui_hidden: false, + }, + unet: { + allOf: [ + { + $ref: '#/components/schemas/UNetField', + }, + ], + description: 'UNet (scheduler, LoRAs)', + field_kind: 'output', + title: 'UNet', + ui_hidden: false, + }, + }, + required: ['vae', 'type', 'clip', 'unet', 'type'], + title: 'ModelLoaderOutput', + type: 'object', + class: 'output', + }, + VAEField: { + properties: { + vae: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load vae submodel', + }, + seamless_axes: { + description: 'Axes("x" and "y") to which apply seamless', + items: { + type: 'string', + }, + title: 'Seamless Axes', + type: 'array', + }, + }, + required: ['vae'], + title: 'VAEField', + type: 'object', + class: 'output', + }, + }, + UNetField: { + properties: { + unet: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load unet submodel', + }, + scheduler: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load scheduler submodel', + }, + loras: { + description: 'LoRAs to apply on model loading', + items: { + $ref: '#/components/schemas/LoRAField', + }, + title: 'Loras', + type: 'array', + }, + seamless_axes: { + description: 'Axes("x" and "y") to which apply seamless', + items: { + type: 'string', + }, + title: 'Seamless Axes', + type: 'array', + }, + freeu_config: { + anyOf: [ + { + $ref: '#/components/schemas/FreeUConfig', + }, + { + type: 'null', + }, + ], + default: null, + description: 'FreeU configuration', + }, + }, + required: ['unet', 'scheduler', 'loras'], + title: 'UNetField', + type: 'object', + class: 'output', + }, + LoRAField: { + properties: { + lora: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load lora model', + }, + weight: { + description: 'Weight to apply to lora model', + title: 'Weight', + type: 'number', + }, + }, + required: ['lora', 'weight'], + title: 'LoRAField', + type: 'object', + class: 'output', + }, + FreeUConfig: { + description: + 'Configuration for the FreeU hyperparameters.\n- https://huggingface.co/docs/diffusers/main/en/using-diffusers/freeu\n- https://github.com/ChenyangSi/FreeU', + properties: { + s1: { + description: + 'Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', + maximum: 3.0, + minimum: -1.0, + title: 'S1', + type: 'number', + }, + s2: { + description: + 'Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', + maximum: 3.0, + minimum: -1.0, + title: 'S2', + type: 'number', + }, + b1: { + description: 'Scaling factor for stage 1 to amplify the contributions of backbone features.', + maximum: 3.0, + minimum: -1.0, + title: 'B1', + type: 'number', + }, + b2: { + description: 'Scaling factor for stage 2 to amplify the contributions of backbone features.', + maximum: 3.0, + minimum: -1.0, + title: 'B2', + type: 'number', + }, + }, + required: ['s1', 's2', 'b1', 'b2'], + title: 'FreeUConfig', + type: 'object', + class: 'output', + }, + VAEField: { + properties: { + vae: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load vae submodel', + }, + seamless_axes: { + description: 'Axes("x" and "y") to which apply seamless', + items: { + type: 'string', + }, + title: 'Seamless Axes', + type: 'array', + }, + }, + required: ['vae'], + title: 'VAEField', + type: 'object', + class: 'output', + }, + CLIPField: { + properties: { + tokenizer: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load tokenizer submodel', + }, + text_encoder: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load text_encoder submodel', + }, + skipped_layers: { + description: 'Number of skipped layers in text_encoder', + title: 'Skipped Layers', + type: 'integer', + }, + loras: { + description: 'LoRAs to apply on model loading', + items: { + $ref: '#/components/schemas/LoRAField', + }, + title: 'Loras', + type: 'array', + }, + }, + required: ['tokenizer', 'text_encoder', 'skipped_layers', 'loras'], + title: 'CLIPField', + type: 'object', + class: 'output', + }, + }, +} as OpenAPIV3_1.Document; From dd42a56084d653abcac49c74e35bf9123117563a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 10:31:37 +1000 Subject: [PATCH 161/442] tests(ui): fix parseSchema test fixture The schema fixture wasn't formatted quite right - doesn't affect the test but still. --- .../nodes/util/schema/parseSchema.test.ts | 310 ++++++++---------- 1 file changed, 143 insertions(+), 167 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts index b67b61d19e..6c0a6635c7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts @@ -611,6 +611,119 @@ const schema = { type: 'object', class: 'output', }, + UNetField: { + properties: { + unet: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load unet submodel', + }, + scheduler: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load scheduler submodel', + }, + loras: { + description: 'LoRAs to apply on model loading', + items: { + $ref: '#/components/schemas/LoRAField', + }, + title: 'Loras', + type: 'array', + }, + seamless_axes: { + description: 'Axes("x" and "y") to which apply seamless', + items: { + type: 'string', + }, + title: 'Seamless Axes', + type: 'array', + }, + freeu_config: { + anyOf: [ + { + $ref: '#/components/schemas/FreeUConfig', + }, + { + type: 'null', + }, + ], + default: null, + description: 'FreeU configuration', + }, + }, + required: ['unet', 'scheduler', 'loras'], + title: 'UNetField', + type: 'object', + class: 'output', + }, + LoRAField: { + properties: { + lora: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load lora model', + }, + weight: { + description: 'Weight to apply to lora model', + title: 'Weight', + type: 'number', + }, + }, + required: ['lora', 'weight'], + title: 'LoRAField', + type: 'object', + class: 'output', + }, + FreeUConfig: { + description: + 'Configuration for the FreeU hyperparameters.\n- https://huggingface.co/docs/diffusers/main/en/using-diffusers/freeu\n- https://github.com/ChenyangSi/FreeU', + properties: { + s1: { + description: + 'Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', + maximum: 3.0, + minimum: -1.0, + title: 'S1', + type: 'number', + }, + s2: { + description: + 'Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', + maximum: 3.0, + minimum: -1.0, + title: 'S2', + type: 'number', + }, + b1: { + description: 'Scaling factor for stage 1 to amplify the contributions of backbone features.', + maximum: 3.0, + minimum: -1.0, + title: 'B1', + type: 'number', + }, + b2: { + description: 'Scaling factor for stage 2 to amplify the contributions of backbone features.', + maximum: 3.0, + minimum: -1.0, + title: 'B2', + type: 'number', + }, + }, + required: ['s1', 's2', 'b1', 'b2'], + title: 'FreeUConfig', + type: 'object', + class: 'output', + }, VAEField: { properties: { vae: { @@ -635,180 +748,43 @@ const schema = { type: 'object', class: 'output', }, - }, - UNetField: { - properties: { - unet: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load unet submodel', - }, - scheduler: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load scheduler submodel', - }, - loras: { - description: 'LoRAs to apply on model loading', - items: { - $ref: '#/components/schemas/LoRAField', + CLIPField: { + properties: { + tokenizer: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load tokenizer submodel', }, - title: 'Loras', - type: 'array', - }, - seamless_axes: { - description: 'Axes("x" and "y") to which apply seamless', - items: { - type: 'string', + text_encoder: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load text_encoder submodel', }, - title: 'Seamless Axes', - type: 'array', - }, - freeu_config: { - anyOf: [ - { - $ref: '#/components/schemas/FreeUConfig', - }, - { - type: 'null', - }, - ], - default: null, - description: 'FreeU configuration', - }, - }, - required: ['unet', 'scheduler', 'loras'], - title: 'UNetField', - type: 'object', - class: 'output', - }, - LoRAField: { - properties: { - lora: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load lora model', - }, - weight: { - description: 'Weight to apply to lora model', - title: 'Weight', - type: 'number', - }, - }, - required: ['lora', 'weight'], - title: 'LoRAField', - type: 'object', - class: 'output', - }, - FreeUConfig: { - description: - 'Configuration for the FreeU hyperparameters.\n- https://huggingface.co/docs/diffusers/main/en/using-diffusers/freeu\n- https://github.com/ChenyangSi/FreeU', - properties: { - s1: { - description: - 'Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', - maximum: 3.0, - minimum: -1.0, - title: 'S1', - type: 'number', - }, - s2: { - description: - 'Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', - maximum: 3.0, - minimum: -1.0, - title: 'S2', - type: 'number', - }, - b1: { - description: 'Scaling factor for stage 1 to amplify the contributions of backbone features.', - maximum: 3.0, - minimum: -1.0, - title: 'B1', - type: 'number', - }, - b2: { - description: 'Scaling factor for stage 2 to amplify the contributions of backbone features.', - maximum: 3.0, - minimum: -1.0, - title: 'B2', - type: 'number', - }, - }, - required: ['s1', 's2', 'b1', 'b2'], - title: 'FreeUConfig', - type: 'object', - class: 'output', - }, - VAEField: { - properties: { - vae: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load vae submodel', - }, - seamless_axes: { - description: 'Axes("x" and "y") to which apply seamless', - items: { - type: 'string', + skipped_layers: { + description: 'Number of skipped layers in text_encoder', + title: 'Skipped Layers', + type: 'integer', }, - title: 'Seamless Axes', - type: 'array', - }, - }, - required: ['vae'], - title: 'VAEField', - type: 'object', - class: 'output', - }, - CLIPField: { - properties: { - tokenizer: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', + loras: { + description: 'LoRAs to apply on model loading', + items: { + $ref: '#/components/schemas/LoRAField', }, - ], - description: 'Info to load tokenizer submodel', - }, - text_encoder: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load text_encoder submodel', - }, - skipped_layers: { - description: 'Number of skipped layers in text_encoder', - title: 'Skipped Layers', - type: 'integer', - }, - loras: { - description: 'LoRAs to apply on model loading', - items: { - $ref: '#/components/schemas/LoRAField', + title: 'Loras', + type: 'array', }, - title: 'Loras', - type: 'array', }, + required: ['tokenizer', 'text_encoder', 'skipped_layers', 'loras'], + title: 'CLIPField', + type: 'object', + class: 'output', }, - required: ['tokenizer', 'text_encoder', 'skipped_layers', 'loras'], - title: 'CLIPField', - type: 'object', - class: 'output', }, }, } as OpenAPIV3_1.Document; From d2f5103f9f13a70216f4d02e72890129ec99cdce Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 12:16:13 +1000 Subject: [PATCH 162/442] fix(ui): ignore actions from other slices in nodesSlice history --- invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index cfd9fa8ef8..2218530c31 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -651,6 +651,10 @@ export const nodesUndoableConfig: UndoableOptions = { return null; }, filter: (action, _state, _history) => { + // Ignore all actions from other slices + if (!action.type.startsWith(nodesSlice.name)) { + return false; + } if (nodesChanged.match(action)) { if (action.payload.every((change) => change.type === 'dimensions')) { return false; From ad8778df6c004246b2d7a06cc42a37f29cc45a79 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 13:21:01 +1000 Subject: [PATCH 163/442] feat(ui): extract node execution state from nodesSlice This state is ephemeral and not undoable. --- .../socketio/socketGeneratorProgress.ts | 10 ++ .../socketio/socketInvocationComplete.ts | 15 ++- .../socketio/socketInvocationError.ts | 12 +++ .../socketio/socketInvocationStarted.ts | 9 ++ .../socketio/socketQueueItemStatusChanged.ts | 19 ++++ .../features/nodes/components/flow/Flow.tsx | 2 + .../InvocationNodeStatusIndicator.tsx | 13 +-- .../flow/nodes/common/NodeWrapper.tsx | 18 +--- .../inspector/InspectorOutputsTab.tsx | 16 +-- .../features/nodes/hooks/useExecutionState.ts | 56 +++++++++++ .../src/features/nodes/store/nodesSlice.ts | 97 +------------------ .../web/src/features/nodes/store/types.ts | 2 +- 12 files changed, 141 insertions(+), 128 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts index bb113a09ee..2dd598396a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts @@ -1,5 +1,8 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; +import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; import { socketGeneratorProgress } from 'services/events/actions'; const log = logger('socketio'); @@ -9,6 +12,13 @@ export const addGeneratorProgressEventListener = (startAppListening: AppStartLis actionCreator: socketGeneratorProgress, effect: (action) => { log.trace(action.payload, `Generator progress`); + const { source_node_id, step, total_steps, progress_image } = action.payload.data; + const nes = deepClone($nodeExecutionStates.get()[source_node_id]); + if (nes) { + nes.status = zNodeStatus.enum.IN_PROGRESS; + nes.progress = (step + 1) / total_steps; + nes.progressImage = progress_image ?? null; + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index fb3a4a41c9..06dc08d846 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; import { @@ -9,7 +10,9 @@ import { isImageViewerOpenChanged, } from 'features/gallery/store/gallerySlice'; import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; +import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { isImageOutput } from 'features/nodes/types/common'; +import { zNodeStatus } from 'features/nodes/types/invocation'; import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; @@ -28,7 +31,7 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi const { data } = action.payload; log.debug({ data: parseify(data) }, `Invocation complete (${action.payload.data.node.type})`); - const { result, node, queue_batch_id } = data; + const { result, node, queue_batch_id, source_node_id } = data; // This complete event has an associated image output if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type)) { const { image_name } = result.image; @@ -110,6 +113,16 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi } } } + + const nes = deepClone($nodeExecutionStates.get()[source_node_id]); + if (nes) { + nes.status = zNodeStatus.enum.COMPLETED; + if (nes.progress !== null) { + nes.progress = 1; + } + nes.outputs.push(result); + upsertExecutionState(nes.nodeId, nes); + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts index fb898b4c7a..ce26c4dd7d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts @@ -1,5 +1,8 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; +import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; import { socketInvocationError } from 'services/events/actions'; const log = logger('socketio'); @@ -9,6 +12,15 @@ export const addInvocationErrorEventListener = (startAppListening: AppStartListe actionCreator: socketInvocationError, effect: (action) => { log.error(action.payload, `Invocation error (${action.payload.data.node.type})`); + const { source_node_id } = action.payload.data; + const nes = deepClone($nodeExecutionStates.get()[source_node_id]); + if (nes) { + nes.status = zNodeStatus.enum.FAILED; + nes.error = action.payload.data.error; + nes.progress = null; + nes.progressImage = null; + upsertExecutionState(nes.nodeId, nes); + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts index baf476a66b..9d6e0ac14d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts @@ -1,5 +1,8 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; +import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; import { socketInvocationStarted } from 'services/events/actions'; const log = logger('socketio'); @@ -9,6 +12,12 @@ export const addInvocationStartedEventListener = (startAppListening: AppStartLis actionCreator: socketInvocationStarted, effect: (action) => { log.debug(action.payload, `Invocation started (${action.payload.data.node.type})`); + const { source_node_id } = action.payload.data; + const nes = deepClone($nodeExecutionStates.get()[source_node_id]); + if (nes) { + nes.status = zNodeStatus.enum.IN_PROGRESS; + upsertExecutionState(nes.nodeId, nes); + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts index 84073bb427..2adc529766 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts @@ -1,5 +1,9 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; +import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; +import { forEach } from 'lodash-es'; import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue'; import { socketQueueItemStatusChanged } from 'services/events/actions'; @@ -54,6 +58,21 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening: dispatch( queueApi.util.invalidateTags(['CurrentSessionQueueItem', 'NextSessionQueueItem', 'InvocationCacheStatus']) ); + + if (['in_progress'].includes(action.payload.data.queue_item.status)) { + forEach($nodeExecutionStates.get(), (nes) => { + if (!nes) { + return; + } + const clone = deepClone(nes); + clone.status = zNodeStatus.enum.PENDING; + clone.error = null; + clone.progress = null; + clone.progressImage = null; + clone.outputs = []; + $nodeExecutionStates.setKey(clone.nodeId, clone); + }); + } }, }); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index f1fcf24af2..8b33323ddd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useConnection } from 'features/nodes/hooks/useConnection'; import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste'; +import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { @@ -81,6 +82,7 @@ export const Flow = memo(() => { const isValidConnection = useIsValidConnection(); const cancelConnection = useReactFlowStore(selectCancelConnection); useWorkflowWatcher(); + useSyncExecutionState(); const [borderRadius] = useToken('radii', ['base']); const flowStyles = useMemo( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx index 3138cb32fe..b58f6fe8ba 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx @@ -1,12 +1,10 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Badge, CircularProgress, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { useExecutionState } from 'features/nodes/hooks/useExecutionState'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import type { NodeExecutionState } from 'features/nodes/types/invocation'; import { zNodeStatus } from 'features/nodes/types/invocation'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCheckBold, PiDotsThreeOutlineFill, PiWarningBold } from 'react-icons/pi'; @@ -24,12 +22,7 @@ const circleStyles: SystemStyleObject = { }; const InvocationNodeStatusIndicator = ({ nodeId }: Props) => { - const selectNodeExecutionState = useMemo( - () => createMemoizedSelector(selectNodesSlice, (nodes) => nodes.nodeExecutionStates[nodeId]), - [nodeId] - ); - - const nodeExecutionState = useAppSelector(selectNodeExecutionState); + const nodeExecutionState = useExecutionState(nodeId); if (!nodeExecutionState) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx index 51649f4f82..57426982ef 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx @@ -1,14 +1,14 @@ import type { ChakraProps } from '@invoke-ai/ui-library'; import { Box, useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; +import { useExecutionState } from 'features/nodes/hooks/useExecutionState'; import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; -import { nodeExclusivelySelected, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { nodeExclusivelySelected } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from 'features/nodes/types/constants'; import { zNodeStatus } from 'features/nodes/types/invocation'; import type { MouseEvent, PropsWithChildren } from 'react'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; type NodeWrapperProps = PropsWithChildren & { nodeId: string; @@ -20,16 +20,8 @@ const NodeWrapper = (props: NodeWrapperProps) => { const { nodeId, width, children, selected } = props; const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId); - const selectIsInProgress = useMemo( - () => - createSelector( - selectNodesSlice, - (nodes) => nodes.nodeExecutionStates[nodeId]?.status === zNodeStatus.enum.IN_PROGRESS - ), - [nodeId] - ); - - const isInProgress = useAppSelector(selectIsInProgress); + const executionState = useExecutionState(nodeId); + const isInProgress = executionState?.status === zNodeStatus.enum.IN_PROGRESS; const [nodeInProgress, shadowsXl, shadowsBase] = useToken('shadows', [ 'nodeInProgress', diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx index 17a1dd33f1..d4150243b9 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx @@ -5,6 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; +import { useExecutionState } from 'features/nodes/hooks/useExecutionState'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { selectLastSelectedNode } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; @@ -23,27 +24,26 @@ const InspectorOutputsTab = () => { const lastSelectedNode = selectLastSelectedNode(nodes); const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; - const nes = nodes.nodeExecutionStates[lastSelectedNode?.id ?? '__UNKNOWN_NODE__']; - - if (!isInvocationNode(lastSelectedNode) || !nes || !lastSelectedNodeTemplate) { + if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) { return; } return { - outputs: nes.outputs, + nodeId: lastSelectedNode.id, outputType: lastSelectedNodeTemplate.outputType, }; }), [templates] ); const data = useAppSelector(selector); + const nes = useExecutionState(data?.nodeId); const { t } = useTranslation(); - if (!data) { + if (!data || !nes) { return ; } - if (data.outputs.length === 0) { + if (nes.outputs.length === 0) { return ; } @@ -52,11 +52,11 @@ const InspectorOutputsTab = () => { {data.outputType === 'image_output' ? ( - data.outputs.map((result, i) => ( + nes.outputs.map((result, i) => ( )) ) : ( - + )} diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts new file mode 100644 index 0000000000..0e5dc1ac43 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts @@ -0,0 +1,56 @@ +import { useStore } from '@nanostores/react'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { deepClone } from 'common/util/deepClone'; +import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import type { NodeExecutionStates } from 'features/nodes/store/types'; +import type { NodeExecutionState } from 'features/nodes/types/invocation'; +import { zNodeStatus } from 'features/nodes/types/invocation'; +import { map } from 'nanostores'; +import { useEffect, useMemo } from 'react'; + +export const $nodeExecutionStates = map({}); + +const initialNodeExecutionState: Omit = { + status: zNodeStatus.enum.PENDING, + error: null, + progress: null, + progressImage: null, + outputs: [], +}; + +export const useExecutionState = (nodeId?: string) => { + const executionStates = useStore($nodeExecutionStates, nodeId ? { keys: [nodeId] } : undefined); + const executionState = useMemo(() => (nodeId ? executionStates[nodeId] : undefined), [executionStates, nodeId]); + return executionState; +}; + +const removeNodeExecutionState = (nodeId: string) => { + $nodeExecutionStates.setKey(nodeId, undefined); +}; + +export const upsertExecutionState = (nodeId: string, updates?: Partial) => { + const state = $nodeExecutionStates.get()[nodeId]; + if (!state) { + $nodeExecutionStates.setKey(nodeId, { ...deepClone(initialNodeExecutionState), nodeId, ...updates }); + } else { + $nodeExecutionStates.setKey(nodeId, { ...state, ...updates }); + } +}; + +const selectNodeIds = createMemoizedSelector(selectNodesSlice, (nodesSlice) => nodesSlice.nodes.map((node) => node.id)); + +export const useSyncExecutionState = () => { + const nodeIds = useAppSelector(selectNodeIds); + useEffect(() => { + const nodeExecutionStates = $nodeExecutionStates.get(); + const nodeIdsToAdd = nodeIds.filter((id) => !nodeExecutionStates[id]); + const nodeIdsToRemove = Object.keys(nodeExecutionStates).filter((id) => !nodeIds.includes(id)); + for (const id of nodeIdsToAdd) { + upsertExecutionState(id); + } + for (const id of nodeIdsToRemove) { + removeNodeExecutionState(id); + } + }, [nodeIds]); +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 2218530c31..644287dd29 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -1,7 +1,6 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; import { workflowLoaded } from 'features/nodes/store/actions'; import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; import type { @@ -43,38 +42,21 @@ import { zT2IAdapterModelFieldValue, zVAEModelFieldValue, } from 'features/nodes/types/field'; -import type { AnyNode, InvocationNodeEdge, NodeExecutionState } from 'features/nodes/types/invocation'; -import { isInvocationNode, isNotesNode, zNodeStatus } from 'features/nodes/types/invocation'; -import { forEach } from 'lodash-es'; +import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; +import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; import { atom } from 'nanostores'; import type { Connection, Edge, EdgeChange, EdgeRemoveChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow'; import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow'; import type { UndoableOptions } from 'redux-undo'; -import { - socketGeneratorProgress, - socketInvocationComplete, - socketInvocationError, - socketInvocationStarted, - socketQueueItemStatusChanged, -} from 'services/events/actions'; import type { z } from 'zod'; import type { NodesState, PendingConnection, Templates } from './types'; import { findUnoccupiedPosition } from './util/findUnoccupiedPosition'; -const initialNodeExecutionState: Omit = { - status: zNodeStatus.enum.PENDING, - error: null, - progress: null, - progressImage: null, - outputs: [], -}; - const initialNodesState: NodesState = { _version: 1, nodes: [], edges: [], - nodeExecutionStates: {}, }; type FieldValueAction = PayloadAction<{ @@ -137,15 +119,6 @@ export const nodesSlice = createSlice({ ); state.nodes.push(node); - - if (!isInvocationNode(node)) { - return; - } - - state.nodeExecutionStates[node.id] = { - nodeId: node.id, - ...initialNodeExecutionState, - }; }, edgesChanged: (state, action: PayloadAction) => { state.edges = applyEdgeChanges(action.payload, state.edges); @@ -316,7 +289,6 @@ export const nodesSlice = createSlice({ if (!isInvocationNode(node)) { return; } - delete state.nodeExecutionStates[node.id]; }); }, nodeLabelChanged: (state, action: PayloadAction<{ nodeId: string; label: string }>) => { @@ -459,14 +431,6 @@ export const nodesSlice = createSlice({ state.nodes = applyNodeChanges(nodeChanges, state.nodes); state.edges = applyEdgeChanges(edgeChanges, state.edges); - - // Add node execution states for new nodes - nodes.forEach((node) => { - state.nodeExecutionStates[node.id] = { - nodeId: node.id, - ...deepClone(initialNodeExecutionState), - }; - }); }, undo: (state) => state, redo: (state) => state, @@ -485,63 +449,6 @@ export const nodesSlice = createSlice({ edges.map((edge) => ({ item: edge, type: 'add' })), [] ); - - state.nodeExecutionStates = nodes.reduce>((acc, node) => { - acc[node.id] = { - nodeId: node.id, - ...initialNodeExecutionState, - }; - return acc; - }, {}); - }); - - builder.addCase(socketInvocationStarted, (state, action) => { - const { source_node_id } = action.payload.data; - const node = state.nodeExecutionStates[source_node_id]; - if (node) { - node.status = zNodeStatus.enum.IN_PROGRESS; - } - }); - builder.addCase(socketInvocationComplete, (state, action) => { - const { source_node_id, result } = action.payload.data; - const nes = state.nodeExecutionStates[source_node_id]; - if (nes) { - nes.status = zNodeStatus.enum.COMPLETED; - if (nes.progress !== null) { - nes.progress = 1; - } - nes.outputs.push(result); - } - }); - builder.addCase(socketInvocationError, (state, action) => { - const { source_node_id } = action.payload.data; - const node = state.nodeExecutionStates[source_node_id]; - if (node) { - node.status = zNodeStatus.enum.FAILED; - node.error = action.payload.data.error; - node.progress = null; - node.progressImage = null; - } - }); - builder.addCase(socketGeneratorProgress, (state, action) => { - const { source_node_id, step, total_steps, progress_image } = action.payload.data; - const node = state.nodeExecutionStates[source_node_id]; - if (node) { - node.status = zNodeStatus.enum.IN_PROGRESS; - node.progress = (step + 1) / total_steps; - node.progressImage = progress_image ?? null; - } - }); - builder.addCase(socketQueueItemStatusChanged, (state, action) => { - if (['in_progress'].includes(action.payload.data.queue_item.status)) { - forEach(state.nodeExecutionStates, (nes) => { - nes.status = zNodeStatus.enum.PENDING; - nes.error = null; - nes.progress = null; - nes.progressImage = null; - nes.outputs = []; - }); - } }); }, }); diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index 090a967626..2f514bdb5b 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -14,6 +14,7 @@ import type { import type { WorkflowV3 } from 'features/nodes/types/workflow'; export type Templates = Record; +export type NodeExecutionStates = Record; export type PendingConnection = { node: InvocationNode; @@ -25,7 +26,6 @@ export type NodesState = { _version: 1; nodes: AnyNode[]; edges: InvocationNodeEdge[]; - nodeExecutionStates: Record; }; export type WorkflowMode = 'edit' | 'view'; From 575ecb4028b3821a7555f12c8c84bbbfe9a2247a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 20:08:37 +1000 Subject: [PATCH 164/442] feat(ui): prevent connections to direct-only inputs --- .../nodes/hooks/useIsValidConnection.ts | 4 +++ .../store/util/findConnectionToValidHandle.ts | 2 +- .../util/makeIsConnectionValidSelector.ts | 31 ++++++++++++------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index 6b3294f064..00b4b40176 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -45,6 +45,10 @@ export const useIsValidConnection = () => { return false; } + if (targetFieldTemplate.input === 'direct') { + return false; + } + if (!shouldValidateGraph) { // manual override! return true; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts index 4c47cb15b0..1f33c52371 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts @@ -38,7 +38,7 @@ export const getFirstValidConnection = ( }; } // Only one connection per target field is allowed - look for an unconnected target field - const candidateFields = map(candidateTemplate.inputs); + const candidateFields = map(candidateTemplate.inputs).filter((i) => i.input !== 'direct'); const candidateConnectedFields = edges .filter((edge) => edge.target === candidateNode.id) .map((edge) => { diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts index 416ff2c6a8..90e75e0d87 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts @@ -6,6 +6,7 @@ import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocatio import i18n from 'i18next'; import { isEqual } from 'lodash-es'; import type { HandleType } from 'reactflow'; +import { assert } from 'tsafe'; import { getIsGraphAcyclic } from './getIsGraphAcyclic'; import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; @@ -80,25 +81,33 @@ export const makeConnectionErrorSelector = ( } // we have to figure out which is the target and which is the source - const target = handleType === 'target' ? nodeId : connectionNodeId; - const targetHandle = handleType === 'target' ? fieldName : connectionFieldName; - const source = handleType === 'source' ? nodeId : connectionNodeId; - const sourceHandle = handleType === 'source' ? fieldName : connectionFieldName; + const targetNodeId = handleType === 'target' ? nodeId : connectionNodeId; + const targetFieldName = handleType === 'target' ? fieldName : connectionFieldName; + const sourceNodeId = handleType === 'source' ? nodeId : connectionNodeId; + const sourceFieldName = handleType === 'source' ? fieldName : connectionFieldName; if ( edges.find((edge) => { - edge.target === target && - edge.targetHandle === targetHandle && - edge.source === source && - edge.sourceHandle === sourceHandle; + edge.target === targetNodeId && + edge.targetHandle === targetFieldName && + edge.source === sourceNodeId && + edge.sourceHandle === sourceFieldName; }) ) { // We already have a connection from this source to this target return i18n.t('nodes.cannotDuplicateConnection'); } - const targetNode = nodes.find((node) => node.id === target); - if (targetNode?.data.type === 'collect' && targetHandle === 'item') { + const targetNode = nodes.find((node) => node.id === targetNodeId); + assert(targetNode, `Target node not found: ${targetNodeId}`); + const targetTemplate = templates[targetNode.data.type]; + assert(targetTemplate, `Target template not found: ${targetNode.data.type}`); + + if (targetTemplate.inputs[targetFieldName]?.input === 'direct') { + return i18n.t('nodes.cannotConnectToDirectInput'); + } + + if (targetNode.data.type === 'collect' && targetFieldName === 'item') { // Collect nodes shouldn't mix and match field types const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); if (collectItemType) { @@ -110,7 +119,7 @@ export const makeConnectionErrorSelector = ( if ( edges.find((edge) => { - return edge.target === target && edge.targetHandle === targetHandle; + return edge.target === targetNodeId && edge.targetHandle === targetFieldName; }) && // except CollectionItem inputs can have multiples targetType.name !== 'CollectionItemField' From 32dff2c4e3a3eadb4da63f7763143da584b23c2f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 18:41:59 +1000 Subject: [PATCH 165/442] feat(ui): copy/paste input edges when copying node - Copy edges to selected nodes on copy - If pasted with `ctrl/meta-shift-v`, also paste the input edges --- .../web/src/features/nodes/components/flow/Flow.tsx | 9 +++++++++ .../web/src/features/nodes/hooks/useCopyPaste.ts | 13 +++++++++++-- .../web/src/features/nodes/store/nodesSlice.ts | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 8b33323ddd..656de737c7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -231,6 +231,15 @@ export const Flow = memo(() => { ); useHotkeys(['Ctrl+v', 'Meta+v'], onPasteHotkey); + const onPasteWithEdgesToNodesHotkey = useCallback( + (e: KeyboardEvent) => { + e.preventDefault(); + pasteSelection(true); + }, + [pasteSelection] + ); + useHotkeys(['Ctrl+shift+v', 'Meta+shift+v'], onPasteWithEdgesToNodesHotkey); + const onUndoHotkey = useCallback(() => { if (mayUndo) { dispatch(undo()); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts index 9acd5722cf..08def1514c 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts @@ -4,10 +4,12 @@ import { $copiedEdges, $copiedNodes, $cursorPos, + $edgesToCopiedNodes, selectionPasted, selectNodesSlice, } from 'features/nodes/store/nodesSlice'; import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; +import { isEqual, uniqWith } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; const copySelection = () => { @@ -16,17 +18,24 @@ const copySelection = () => { const { nodes, edges } = selectNodesSlice(getState()); const selectedNodes = nodes.filter((node) => node.selected); const selectedEdges = edges.filter((edge) => edge.selected); + const edgesToSelectedNodes = edges.filter((edge) => selectedNodes.some((node) => node.id === edge.target)); $copiedNodes.set(selectedNodes); $copiedEdges.set(selectedEdges); + $edgesToCopiedNodes.set(edgesToSelectedNodes); }; -const pasteSelection = () => { +const pasteSelection = (withEdgesToCopiedNodes?: boolean) => { const { getState, dispatch } = getStore(); const currentNodes = selectNodesSlice(getState()).nodes; const cursorPos = $cursorPos.get(); const copiedNodes = deepClone($copiedNodes.get()); - const copiedEdges = deepClone($copiedEdges.get()); + let copiedEdges = deepClone($copiedEdges.get()); + + if (withEdgesToCopiedNodes) { + const edgesToCopiedNodes = deepClone($edgesToCopiedNodes.get()); + copiedEdges = uniqWith([...copiedEdges, ...edgesToCopiedNodes], isEqual); + } // Calculate an offset to reposition nodes to surround the cursor position, maintaining relative positioning const xCoords = copiedNodes.map((node) => node.position.x); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 644287dd29..1f61c77e83 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -498,6 +498,7 @@ export const $cursorPos = atom(null); export const $templates = atom({}); export const $copiedNodes = atom([]); export const $copiedEdges = atom([]); +export const $edgesToCopiedNodes = atom([]); export const $pendingConnection = atom(null); export const $isUpdatingEdge = atom(false); export const $viewport = atom({ x: 0, y: 0, zoom: 1 }); From a18d7adad497cd5f9d1149c3961c38add63c593b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 15 May 2024 06:35:18 +1000 Subject: [PATCH 166/442] fix(ui): allow image dims multiple of 32 with SDXL and T2I adapter See https://github.com/invoke-ai/InvokeAI/pull/6342#issuecomment-2109912452 for discussion. --- .../frontend/web/src/common/hooks/useIsReadyToEnqueue.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index ac4fed4c63..41d6f4607e 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -133,9 +133,12 @@ const createSelector = (templates: Templates) => } else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) { problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); } - // T2I Adapters require images have dimensions that are multiples of 64 - if (l.controlAdapter.type === 't2i_adapter' && (size.width % 64 !== 0 || size.height % 64 !== 0)) { - problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions')); + // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) + if (l.controlAdapter.type === 't2i_adapter') { + const multiple = model?.base === 'sdxl' ? 32 : 64; + if (size.width % multiple !== 0 || size.height % multiple !== 0) { + problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions')); + } } } From 07feb5ba07060288025af22c20ec3094be545921 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 07:54:42 +1000 Subject: [PATCH 167/442] Revert "feat(ui): SDXL clip skip" This reverts commit 40b4fa723819b984678f1cd868108c2506e79010. --- .../util/graph/generation/addSDXLLoRAs.ts | 6 ++--- .../generation/buildGenerationTabSDXLGraph.ts | 25 ++++--------------- .../components/Advanced/ParamClipSkip.tsx | 4 +++ .../features/parameters/types/constants.ts | 4 +-- 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts index cef2ad2f47..f38e8de570 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts @@ -11,8 +11,6 @@ export const addSDXLLoRas = ( denoise: Invocation<'denoise_latents'>, modelLoader: Invocation<'sdxl_model_loader'>, seamless: Invocation<'seamless'> | null, - clipSkip: Invocation<'clip_skip'>, - clipSkip2: Invocation<'clip_skip'>, posCond: Invocation<'sdxl_compel_prompt'>, negCond: Invocation<'sdxl_compel_prompt'> ): void => { @@ -39,8 +37,8 @@ export const addSDXLLoRas = ( g.addEdge(loraCollector, 'collection', loraCollectionLoader, 'loras'); // Use seamless as UNet input if it exists, otherwise use the model loader g.addEdge(seamless ?? modelLoader, 'unet', loraCollectionLoader, 'unet'); - g.addEdge(clipSkip, 'clip', loraCollectionLoader, 'clip'); - g.addEdge(clipSkip2, 'clip', loraCollectionLoader, 'clip2'); + g.addEdge(modelLoader, 'clip', loraCollectionLoader, 'clip'); + g.addEdge(modelLoader, 'clip2', loraCollectionLoader, 'clip2'); // Reroute UNet & CLIP connections through the LoRA collection loader g.deleteEdgesTo(denoise, ['unet']); g.deleteEdgesTo(posCond, ['clip', 'clip2']); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts index 4e8d716a11..416e81a632 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts @@ -1,7 +1,6 @@ import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { - CLIP_SKIP, LATENTS_TO_IMAGE, NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING_COLLECT, @@ -30,7 +29,6 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); - g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); - g.addEdge(modelLoader, 'clip2', clipSkip2, 'clip'); - g.addEdge(clipSkip, 'clip', posCond, 'clip'); - g.addEdge(clipSkip, 'clip', negCond, 'clip'); - g.addEdge(clipSkip2, 'clip', posCond, 'clip2'); - g.addEdge(clipSkip2, 'clip', negCond, 'clip2'); + g.addEdge(modelLoader, 'clip', posCond, 'clip'); + g.addEdge(modelLoader, 'clip', negCond, 'clip'); + g.addEdge(modelLoader, 'clip2', posCond, 'clip2'); + g.addEdge(modelLoader, 'clip2', negCond, 'clip2'); g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); @@ -146,13 +132,12 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise { return CLIP_SKIP_MAP[model.base].markers; }, [model]); + if (model?.base === 'sdxl') { + return null; + } + return ( diff --git a/invokeai/frontend/web/src/features/parameters/types/constants.ts b/invokeai/frontend/web/src/features/parameters/types/constants.ts index 05d16a7eda..6d7b4f9248 100644 --- a/invokeai/frontend/web/src/features/parameters/types/constants.ts +++ b/invokeai/frontend/web/src/features/parameters/types/constants.ts @@ -39,8 +39,8 @@ export const CLIP_SKIP_MAP = { markers: [0, 1, 2, 3, 5, 10, 15, 20, 24], }, sdxl: { - maxClip: 11, - markers: [0, 1, 2, 5, 11], + maxClip: 24, + markers: [0, 1, 2, 3, 5, 10, 15, 20, 24], }, 'sdxl-refiner': { maxClip: 24, From 6b7b0b3777e1b761cf7ac3e70a2c8863f1fef23c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 08:16:51 +1000 Subject: [PATCH 168/442] fix(ui): do not rearrange fields when connection/disconnecting --- .../hooks/useAnyOrDirectInputFieldNames.ts | 27 ++++---------- .../hooks/useConnectionInputFieldNames.ts | 37 +++++++------------ 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts index 7972f9eee3..3b7a1b74c1 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts @@ -1,7 +1,5 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; +import { EMPTY_ARRAY } from 'app/store/constants'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; @@ -9,31 +7,20 @@ import { useMemo } from 'react'; export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => { const template = useNodeTemplate(nodeId); - const selectConnectedFieldNames = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodesSlice) => - nodesSlice.edges - .filter((e) => e.target === nodeId) - .map((e) => e.targetHandle) - .filter(Boolean) - ), - [nodeId] - ); - const connectedFieldNames = useAppSelector(selectConnectedFieldNames); const fieldNames = useMemo(() => { const fields = map(template.inputs).filter((field) => { - if (connectedFieldNames.includes(field.name)) { - return false; - } - return ( (['any', 'direct'].includes(field.input) || field.type.isCollectionOrScalar) && keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) ); }); - return getSortedFilteredFieldNames(fields); - }, [connectedFieldNames, template.inputs]); + const _fieldNames = getSortedFilteredFieldNames(fields); + if (_fieldNames.length === 0) { + return EMPTY_ARRAY; + } + return _fieldNames; + }, [template.inputs]); return fieldNames; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts index 0eeb592c31..d071ac76d2 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts @@ -1,7 +1,5 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; +import { EMPTY_ARRAY } from 'app/store/constants'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; @@ -9,31 +7,22 @@ import { useMemo } from 'react'; export const useConnectionInputFieldNames = (nodeId: string): string[] => { const template = useNodeTemplate(nodeId); - const selectConnectedFieldNames = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodesSlice) => - nodesSlice.edges - .filter((e) => e.target === nodeId) - .map((e) => e.targetHandle) - .filter(Boolean) - ), - [nodeId] - ); - const connectedFieldNames = useAppSelector(selectConnectedFieldNames); - const fieldNames = useMemo(() => { // get the visible fields - const fields = map(template.inputs).filter((field) => { - if (connectedFieldNames.includes(field.name)) { - return true; - } - return ( + const fields = map(template.inputs).filter( + (field) => (field.input === 'connection' && !field.type.isCollectionOrScalar) || !keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) - ); - }); + ); + + const _fieldNames = getSortedFilteredFieldNames(fields); + + if (_fieldNames.length === 0) { + return EMPTY_ARRAY; + } + + return _fieldNames; + }, [template.inputs]); - return getSortedFilteredFieldNames(fields); - }, [connectedFieldNames, template.inputs]); return fieldNames; }; From 4e21d01c7f8e713be58484431978c4fdb936b992 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 08:17:10 +1000 Subject: [PATCH 169/442] feat(ui): dim field name when connected --- .../flow/nodes/Invocation/fields/EditableFieldTitle.tsx | 4 +++- .../components/flow/nodes/Invocation/fields/InputField.tsx | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx index 04bcd81db8..6372f6e78f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx @@ -25,10 +25,11 @@ interface Props { kind: 'inputs' | 'outputs'; isMissingInput?: boolean; withTooltip?: boolean; + shouldDim?: boolean; } const EditableFieldTitle = forwardRef((props: Props, ref) => { - const { nodeId, fieldName, kind, isMissingInput = false, withTooltip = false } = props; + const { nodeId, fieldName, kind, isMissingInput = false, withTooltip = false, shouldDim = false } = props; const label = useFieldLabel(nodeId, fieldName); const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind); const { t } = useTranslation(); @@ -80,6 +81,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => { sx={editablePreviewStyles} noOfLines={1} color={isMissingInput ? 'error.300' : 'base.300'} + opacity={shouldDim ? 0.5 : 1} /> diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx index a2fce55ce3..cd0878412b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx @@ -79,6 +79,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { kind="inputs" isMissingInput={isMissingInput} withTooltip + shouldDim /> From 5d60c3c8e1f236079216ac044943ea02e86a2742 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 08:24:05 +1000 Subject: [PATCH 170/442] fix(ui): jank when editing field title --- .../nodes/Invocation/fields/EditableFieldTitle.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx index 6372f6e78f..617b6141c8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx @@ -40,13 +40,11 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => { const handleSubmit = useCallback( async (newTitleRaw: string) => { const newTitle = newTitleRaw.trim(); - if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) { - return; - } - setLocalTitle(newTitle || fieldTemplateTitle || t('nodes.unknownField')); - dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle })); + const finalTitle = newTitle || fieldTemplateTitle || t('nodes.unknownField'); + setLocalTitle(finalTitle); + dispatch(fieldLabelChanged({ nodeId, fieldName, label: finalTitle })); }, - [label, fieldTemplateTitle, dispatch, nodeId, fieldName, t] + [fieldTemplateTitle, dispatch, nodeId, fieldName, t] ); const handleChange = useCallback((newTitle: string) => { From 822f1e1f0601d2726b86c2ec7fbe257dd8af2bbe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 13:30:51 +1000 Subject: [PATCH 171/442] feat(ui): store workflow in generation tab images --- .../listenerMiddleware/listeners/enqueueRequestedLinear.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 6ca7ee7ffa..4bef10c312 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -4,6 +4,7 @@ import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph'; import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph'; +import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow'; import { queueApi } from 'services/api/endpoints/queue'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { @@ -25,6 +26,8 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) } const batchConfig = prepareLinearUIBatch(state, graph, prepend); + const workflow = graphToWorkflow(graph); + batchConfig.batch.workflow = workflow; const req = dispatch( queueApi.endpoints.enqueueBatch.initiate(batchConfig, { From 66fc110b6484999106092ff01ecc25d91458f1fd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 16:09:47 +1000 Subject: [PATCH 172/442] Revert "feat(ui): store workflow in generation tab images" This reverts commit c9c4190fb45696088207b0ac3c69c2795d7f9694. --- .../listenerMiddleware/listeners/enqueueRequestedLinear.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 4bef10c312..6ca7ee7ffa 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -4,7 +4,6 @@ import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph'; import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph'; -import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow'; import { queueApi } from 'services/api/endpoints/queue'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { @@ -26,8 +25,6 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) } const batchConfig = prepareLinearUIBatch(state, graph, prepend); - const workflow = graphToWorkflow(graph); - batchConfig.batch.workflow = workflow; const req = dispatch( queueApi.endpoints.enqueueBatch.initiate(batchConfig, { From 922716d2ab8b28b38f5d881051c5bcbfe65ff22a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 18:10:04 +1000 Subject: [PATCH 173/442] feat(ui): store graph in image metadata The previous super-minimal implementation had a major issue - the saved workflow didn't take into account batched field values. When generating with multiple iterations or dynamic prompts, the same workflow with the first prompt, seed, etc was stored in each image. As a result, when the batch results in multiple queue items, only one of the images has the correct workflow - the others are mismatched. To work around this, we can store the _graph_ in the image metadata (alongside the workflow, if generated via workflow editor). When loading a workflow from an image, we can choose to load the workflow or the graph, preferring the workflow. Internally, we need to update images router image-saving services. The changes are minimal. To avoid pydantic errors deserializing the graph, when we extract it from the image, we will leave it as stringified JSON and let the frontend's more sophisticated and flexible parsing handle it. The worklow is also changed to just return stringified JSON, so the API is consistent. --- invokeai/app/api/routers/images.py | 15 +- .../services/image_files/image_files_base.py | 9 +- .../services/image_files/image_files_disk.py | 19 +- invokeai/app/services/images/images_base.py | 9 +- .../app/services/images/images_default.py | 18 +- .../app/services/shared/invocation_context.py | 1 + invokeai/frontend/web/public/locales/en.json | 1 + .../listeners/workflowLoadRequested.ts | 26 +- .../ImageMetadataGraphTabContent.tsx | 34 ++ .../ImageMetadataViewer.tsx | 5 + .../ImageMetadataWorkflowTabContent.tsx | 14 +- .../web/src/features/nodes/store/actions.ts | 4 +- .../LoadWorkflowFromGraphModal.tsx | 3 +- .../hooks/useGetAndLoadEmbeddedWorkflow.ts | 15 +- .../web/src/services/api/endpoints/images.ts | 6 +- .../api/hooks/useDebouncedImageWorkflow.ts | 4 +- .../frontend/web/src/services/api/schema.ts | 482 +++++++++++++----- .../frontend/web/src/services/api/types.ts | 3 + 18 files changed, 510 insertions(+), 158 deletions(-) create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent.tsx diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index dc8a04b711..36ae39e986 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -12,7 +12,7 @@ from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidato from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID, WorkflowWithoutIDValidator +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutIDValidator from ..dependencies import ApiDependencies @@ -185,14 +185,21 @@ async def get_image_metadata( raise HTTPException(status_code=404) +class WorkflowAndGraphResponse(BaseModel): + workflow: Optional[str] = Field(description="The workflow used to generate the image, as stringified JSON") + graph: Optional[str] = Field(description="The graph used to generate the image, as stringified JSON") + + @images_router.get( - "/i/{image_name}/workflow", operation_id="get_image_workflow", response_model=Optional[WorkflowWithoutID] + "/i/{image_name}/workflow", operation_id="get_image_workflow", response_model=WorkflowAndGraphResponse ) async def get_image_workflow( image_name: str = Path(description="The name of image whose workflow to get"), -) -> Optional[WorkflowWithoutID]: +) -> WorkflowAndGraphResponse: try: - return ApiDependencies.invoker.services.images.get_workflow(image_name) + workflow = ApiDependencies.invoker.services.images.get_workflow(image_name) + graph = ApiDependencies.invoker.services.images.get_graph(image_name) + return WorkflowAndGraphResponse(workflow=workflow, graph=graph) except Exception: raise HTTPException(status_code=404) diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py index f4036277b7..839c6fecb6 100644 --- a/invokeai/app/services/image_files/image_files_base.py +++ b/invokeai/app/services/image_files/image_files_base.py @@ -5,6 +5,7 @@ from typing import Optional from PIL.Image import Image as PILImageType from invokeai.app.invocations.fields import MetadataField +from invokeai.app.services.shared.graph import Graph from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID @@ -35,6 +36,7 @@ class ImageFileStorageBase(ABC): image_name: str, metadata: Optional[MetadataField] = None, workflow: Optional[WorkflowWithoutID] = None, + graph: Optional[Graph] = None, thumbnail_size: int = 256, ) -> None: """Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image name, thumbnail name, and created timestamp.""" @@ -46,6 +48,11 @@ class ImageFileStorageBase(ABC): pass @abstractmethod - def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: + def get_workflow(self, image_name: str) -> Optional[str]: """Gets the workflow of an image.""" pass + + @abstractmethod + def get_graph(self, image_name: str) -> Optional[str]: + """Gets the graph of an image.""" + pass diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py index 35fa93f81c..2b5bf433ae 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -9,6 +9,7 @@ from send2trash import send2trash from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.invoker import Invoker +from invokeai.app.services.shared.graph import Graph from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail @@ -58,6 +59,7 @@ class DiskImageFileStorage(ImageFileStorageBase): image_name: str, metadata: Optional[MetadataField] = None, workflow: Optional[WorkflowWithoutID] = None, + graph: Optional[Graph] = None, thumbnail_size: int = 256, ) -> None: try: @@ -75,6 +77,10 @@ class DiskImageFileStorage(ImageFileStorageBase): workflow_json = workflow.model_dump_json() info_dict["invokeai_workflow"] = workflow_json pnginfo.add_text("invokeai_workflow", workflow_json) + if graph is not None: + graph_json = graph.model_dump_json() + info_dict["invokeai_graph"] = graph_json + pnginfo.add_text("invokeai_graph", graph_json) # When saving the image, the image object's info field is not populated. We need to set it image.info = info_dict @@ -129,11 +135,18 @@ class DiskImageFileStorage(ImageFileStorageBase): path = path if isinstance(path, Path) else Path(path) return path.exists() - def get_workflow(self, image_name: str) -> WorkflowWithoutID | None: + def get_workflow(self, image_name: str) -> str | None: image = self.get(image_name) workflow = image.info.get("invokeai_workflow", None) - if workflow is not None: - return WorkflowWithoutID.model_validate_json(workflow) + if isinstance(workflow, str): + return workflow + return None + + def get_graph(self, image_name: str) -> str | None: + image = self.get(image_name) + graph = image.info.get("invokeai_graph", None) + if isinstance(graph, str): + return graph return None def __validate_storage_folders(self) -> None: diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py index 42c4266774..dc62e71656 100644 --- a/invokeai/app/services/images/images_base.py +++ b/invokeai/app/services/images/images_base.py @@ -11,6 +11,7 @@ from invokeai.app.services.image_records.image_records_common import ( ResourceOrigin, ) from invokeai.app.services.images.images_common import ImageDTO +from invokeai.app.services.shared.graph import Graph from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID @@ -53,6 +54,7 @@ class ImageServiceABC(ABC): is_intermediate: Optional[bool] = False, metadata: Optional[MetadataField] = None, workflow: Optional[WorkflowWithoutID] = None, + graph: Optional[Graph] = None, ) -> ImageDTO: """Creates an image, storing the file and its metadata.""" pass @@ -87,7 +89,12 @@ class ImageServiceABC(ABC): pass @abstractmethod - def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: + def get_workflow(self, image_name: str) -> Optional[str]: + """Gets an image's workflow.""" + pass + + @abstractmethod + def get_graph(self, image_name: str) -> Optional[str]: """Gets an image's workflow.""" pass diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py index adeed73811..346baf1b83 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -4,6 +4,7 @@ from PIL.Image import Image as PILImageType from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.invoker import Invoker +from invokeai.app.services.shared.graph import Graph from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID @@ -44,6 +45,7 @@ class ImageService(ImageServiceABC): is_intermediate: Optional[bool] = False, metadata: Optional[MetadataField] = None, workflow: Optional[WorkflowWithoutID] = None, + graph: Optional[Graph] = None, ) -> ImageDTO: if image_origin not in ResourceOrigin: raise InvalidOriginException @@ -64,7 +66,7 @@ class ImageService(ImageServiceABC): image_category=image_category, width=width, height=height, - has_workflow=workflow is not None, + has_workflow=workflow is not None or graph is not None, # Meta fields is_intermediate=is_intermediate, # Nullable fields @@ -75,7 +77,7 @@ class ImageService(ImageServiceABC): if board_id is not None: self.__invoker.services.board_image_records.add_image_to_board(board_id=board_id, image_name=image_name) self.__invoker.services.image_files.save( - image_name=image_name, image=image, metadata=metadata, workflow=workflow + image_name=image_name, image=image, metadata=metadata, workflow=workflow, graph=graph ) image_dto = self.get_dto(image_name) @@ -157,7 +159,7 @@ class ImageService(ImageServiceABC): self.__invoker.services.logger.error("Problem getting image metadata") raise e - def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: + def get_workflow(self, image_name: str) -> Optional[str]: try: return self.__invoker.services.image_files.get_workflow(image_name) except ImageFileNotFoundException: @@ -167,6 +169,16 @@ class ImageService(ImageServiceABC): self.__invoker.services.logger.error("Problem getting image workflow") raise + def get_graph(self, image_name: str) -> Optional[str]: + try: + return self.__invoker.services.image_files.get_graph(image_name) + except ImageFileNotFoundException: + self.__invoker.services.logger.error("Image file not found") + raise + except Exception: + self.__invoker.services.logger.error("Problem getting image graph") + raise + def get_path(self, image_name: str, thumbnail: bool = False) -> str: try: return str(self.__invoker.services.image_files.get_path(image_name, thumbnail)) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 9994d663e5..8685fd0c99 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -199,6 +199,7 @@ class ImagesInterface(InvocationContextInterface): metadata=metadata_, image_origin=ResourceOrigin.INTERNAL, workflow=self._data.queue_item.workflow, + graph=self._data.queue_item.session.graph, session_id=self._data.queue_item.session_id, node_id=self._data.invocation.id, ) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 8c9cce794e..7de7a8e01c 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -880,6 +880,7 @@ "versionUnknown": " Version Unknown", "workflow": "Workflow", "graph": "Graph", + "noGraph": "No Graph", "workflowAuthor": "Author", "workflowContact": "Contact", "workflowDescription": "Short Description", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts index 4052c75bf3..a680bbca97 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts @@ -4,31 +4,49 @@ import { parseify } from 'common/util/serialize'; import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions'; import { $templates } from 'features/nodes/store/nodesSlice'; import { $flow } from 'features/nodes/store/reactFlowInstance'; +import type { Templates } from 'features/nodes/store/types'; import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error'; +import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow'; import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import { t } from 'i18next'; +import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/types'; import { z } from 'zod'; import { fromZodError } from 'zod-validation-error'; +const getWorkflow = (data: GraphAndWorkflowResponse, templates: Templates) => { + if (data.workflow) { + // Prefer to load the workflow if it's available - it has more information + const parsed = JSON.parse(data.workflow); + return validateWorkflow(parsed, templates); + } else if (data.graph) { + // Else we fall back on the graph, using the graphToWorkflow function to convert and do layout + const parsed = JSON.parse(data.graph); + const workflow = graphToWorkflow(parsed as NonNullableGraph, true); + return validateWorkflow(workflow, templates); + } else { + throw new Error('No workflow or graph provided'); + } +}; + export const addWorkflowLoadRequestedListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: workflowLoadRequested, effect: (action, { dispatch }) => { const log = logger('nodes'); - const { workflow, asCopy } = action.payload; + const { data, asCopy } = action.payload; const nodeTemplates = $templates.get(); try { - const { workflow: validatedWorkflow, warnings } = validateWorkflow(workflow, nodeTemplates); + const { workflow, warnings } = getWorkflow(data, nodeTemplates); if (asCopy) { // If we're loading a copy, we need to remove the ID so that the backend will create a new workflow - delete validatedWorkflow.id; + delete workflow.id; } - dispatch(workflowLoaded(validatedWorkflow)); + dispatch(workflowLoaded(workflow)); if (!warnings.length) { dispatch( addToast( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent.tsx new file mode 100644 index 0000000000..9f7cac4a3e --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent.tsx @@ -0,0 +1,34 @@ +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDebouncedImageWorkflow } from 'services/api/hooks/useDebouncedImageWorkflow'; +import type { ImageDTO } from 'services/api/types'; + +import DataViewer from './DataViewer'; + +type Props = { + image: ImageDTO; +}; + +const ImageMetadataGraphTabContent = ({ image }: Props) => { + const { t } = useTranslation(); + const { currentData } = useDebouncedImageWorkflow(image); + const graph = useMemo(() => { + if (currentData?.graph) { + try { + return JSON.parse(currentData.graph); + } catch { + return null; + } + } + return null; + }, [currentData]); + + if (!graph) { + return ; + } + + return ; +}; + +export default memo(ImageMetadataGraphTabContent); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx index ccc4436452..46121f9724 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx @@ -1,6 +1,7 @@ import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import ImageMetadataGraphTabContent from 'features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent'; import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem'; import { handlers } from 'features/metadata/util/handlers'; import { memo } from 'react'; @@ -52,6 +53,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { {t('metadata.metadata')} {t('metadata.imageDetails')} {t('metadata.workflow')} + {t('nodes.graph')} @@ -81,6 +83,9 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { + + + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataWorkflowTabContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataWorkflowTabContent.tsx index 60cda21028..fe4ce3e701 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataWorkflowTabContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataWorkflowTabContent.tsx @@ -1,5 +1,5 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useDebouncedImageWorkflow } from 'services/api/hooks/useDebouncedImageWorkflow'; import type { ImageDTO } from 'services/api/types'; @@ -12,7 +12,17 @@ type Props = { const ImageMetadataWorkflowTabContent = ({ image }: Props) => { const { t } = useTranslation(); - const { workflow } = useDebouncedImageWorkflow(image); + const { currentData } = useDebouncedImageWorkflow(image); + const workflow = useMemo(() => { + if (currentData?.workflow) { + try { + return JSON.parse(currentData.workflow); + } catch { + return null; + } + } + return null; + }, [currentData]); if (!workflow) { return ; diff --git a/invokeai/frontend/web/src/features/nodes/store/actions.ts b/invokeai/frontend/web/src/features/nodes/store/actions.ts index 52f1bf6e38..080acb4d95 100644 --- a/invokeai/frontend/web/src/features/nodes/store/actions.ts +++ b/invokeai/frontend/web/src/features/nodes/store/actions.ts @@ -1,6 +1,6 @@ import { createAction, isAnyOf } from '@reduxjs/toolkit'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; -import type { Graph } from 'services/api/types'; +import type { Graph, GraphAndWorkflowResponse } from 'services/api/types'; const textToImageGraphBuilt = createAction('nodes/textToImageGraphBuilt'); const imageToImageGraphBuilt = createAction('nodes/imageToImageGraphBuilt'); @@ -15,7 +15,7 @@ export const isAnyGraphBuilt = isAnyOf( ); export const workflowLoadRequested = createAction<{ - workflow: unknown; + data: GraphAndWorkflowResponse; asCopy: boolean; }>('nodes/workflowLoadRequested'); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx index ecb4aa7dd4..6ecb51a528 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx @@ -58,8 +58,7 @@ export const LoadWorkflowFromGraphModal = () => { setWorkflowRaw(JSON.stringify(workflow, null, 2)); }, [graphRaw, shouldAutoLayout]); const loadWorkflow = useCallback(() => { - const workflow = JSON.parse(workflowRaw); - dispatch(workflowLoadRequested({ workflow, asCopy: true })); + dispatch(workflowLoadRequested({ data: { workflow: workflowRaw, graph: null }, asCopy: true })); onClose(); }, [dispatch, onClose, workflowRaw]); return ( diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts index b55a01dd6d..7ea9329540 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts @@ -27,10 +27,17 @@ export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ o const getAndLoadEmbeddedWorkflow = useCallback( async (imageName: string) => { try { - const workflow = await _getAndLoadEmbeddedWorkflow(imageName); - dispatch(workflowLoadRequested({ workflow: workflow.data, asCopy: true })); - // No toast - the listener for this action does that after the workflow is loaded - onSuccess && onSuccess(); + const { data } = await _getAndLoadEmbeddedWorkflow(imageName); + if (data) { + dispatch(workflowLoadRequested({ data, asCopy: true })); + // No toast - the listener for this action does that after the workflow is loaded + onSuccess && onSuccess(); + } else { + toaster({ + title: t('toast.problemRetrievingWorkflow'), + status: 'error', + }); + } } catch { toaster({ title: t('toast.problemRetrievingWorkflow'), diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 70358ebc8c..98c253b479 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -10,6 +10,7 @@ import { keyBy } from 'lodash-es'; import type { components, paths } from 'services/api/schema'; import type { DeleteBoardResult, + GraphAndWorkflowResponse, ImageCategory, ImageDTO, ListImagesArgs, @@ -122,10 +123,7 @@ export const imagesApi = api.injectEndpoints({ providesTags: (result, error, image_name) => [{ type: 'ImageMetadata', id: image_name }], keepUnusedDataFor: 86400, // 24 hours }), - getImageWorkflow: build.query< - paths['/api/v1/images/i/{image_name}/workflow']['get']['responses']['200']['content']['application/json'], - string - >({ + getImageWorkflow: build.query({ query: (image_name) => ({ url: buildImagesUrl(`i/${image_name}/workflow`) }), providesTags: (result, error, image_name) => [{ type: 'ImageWorkflow', id: image_name }], keepUnusedDataFor: 86400, // 24 hours diff --git a/invokeai/frontend/web/src/services/api/hooks/useDebouncedImageWorkflow.ts b/invokeai/frontend/web/src/services/api/hooks/useDebouncedImageWorkflow.ts index 2945de612c..3f303fbf10 100644 --- a/invokeai/frontend/web/src/services/api/hooks/useDebouncedImageWorkflow.ts +++ b/invokeai/frontend/web/src/services/api/hooks/useDebouncedImageWorkflow.ts @@ -9,7 +9,7 @@ export const useDebouncedImageWorkflow = (imageDTO?: ImageDTO | null) => { const [debouncedImageName] = useDebounce(imageDTO?.has_workflow ? imageDTO.image_name : null, workflowFetchDebounce); - const { data: workflow, isLoading } = useGetImageWorkflowQuery(debouncedImageName ?? skipToken); + const result = useGetImageWorkflowQuery(debouncedImageName ?? skipToken); - return { workflow, isLoading }; + return result; }; diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index b3d8a61870..c1f9486bc7 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -581,6 +581,7 @@ export type components = { * type * @default add * @constant + * @enum {string} */ type: "add"; }; @@ -618,6 +619,7 @@ export type components = { * type * @default alpha_mask_to_tensor * @constant + * @enum {string} */ type: "alpha_mask_to_tensor"; }; @@ -743,6 +745,7 @@ export type components = { * Type * @default basemetadata * @constant + * @enum {string} */ type?: "basemetadata"; }; @@ -895,6 +898,7 @@ export type components = { * type * @default blank_image * @constant + * @enum {string} */ type: "blank_image"; }; @@ -934,6 +938,7 @@ export type components = { * type * @default lblend * @constant + * @enum {string} */ type: "lblend"; }; @@ -1203,6 +1208,7 @@ export type components = { * type * @default boolean_collection * @constant + * @enum {string} */ type: "boolean_collection"; }; @@ -1220,6 +1226,7 @@ export type components = { * type * @default boolean_collection_output * @constant + * @enum {string} */ type: "boolean_collection_output"; }; @@ -1255,6 +1262,7 @@ export type components = { * type * @default boolean * @constant + * @enum {string} */ type: "boolean"; }; @@ -1272,6 +1280,7 @@ export type components = { * type * @default boolean_output * @constant + * @enum {string} */ type: "boolean_output"; }; @@ -1306,6 +1315,7 @@ export type components = { * type * @default clip_output * @constant + * @enum {string} */ type: "clip_output"; }; @@ -1346,6 +1356,7 @@ export type components = { * type * @default clip_skip * @constant + * @enum {string} */ type: "clip_skip"; }; @@ -1364,6 +1375,7 @@ export type components = { * type * @default clip_skip_output * @constant + * @enum {string} */ type: "clip_skip_output"; }; @@ -1419,6 +1431,7 @@ export type components = { /** * Format * @constant + * @enum {string} */ format: "diffusers"; /** @default */ @@ -1427,6 +1440,7 @@ export type components = { * Type * @default clip_vision * @constant + * @enum {string} */ type: "clip_vision"; }; @@ -1462,6 +1476,7 @@ export type components = { * type * @default infill_cv2 * @constant + * @enum {string} */ type: "infill_cv2"; }; @@ -1521,6 +1536,7 @@ export type components = { * type * @default calculate_image_tiles_even_split * @constant + * @enum {string} */ type: "calculate_image_tiles_even_split"; }; @@ -1580,6 +1596,7 @@ export type components = { * type * @default calculate_image_tiles * @constant + * @enum {string} */ type: "calculate_image_tiles"; }; @@ -1639,6 +1656,7 @@ export type components = { * type * @default calculate_image_tiles_min_overlap * @constant + * @enum {string} */ type: "calculate_image_tiles_min_overlap"; }; @@ -1653,6 +1671,7 @@ export type components = { * type * @default calculate_image_tiles_output * @constant + * @enum {string} */ type: "calculate_image_tiles_output"; }; @@ -1723,6 +1742,7 @@ export type components = { * type * @default canny_image_processor * @constant + * @enum {string} */ type: "canny_image_processor"; }; @@ -1768,6 +1788,7 @@ export type components = { * type * @default canvas_paste_back * @constant + * @enum {string} */ type: "canvas_paste_back"; }; @@ -1823,6 +1844,7 @@ export type components = { * type * @default img_pad_crop * @constant + * @enum {string} */ type: "img_pad_crop"; }; @@ -1874,6 +1896,7 @@ export type components = { * type * @default collect * @constant + * @enum {string} */ type: "collect"; }; @@ -1888,6 +1911,7 @@ export type components = { * type * @default collect_output * @constant + * @enum {string} */ type: "collect_output"; }; @@ -1905,6 +1929,7 @@ export type components = { * type * @default color_collection_output * @constant + * @enum {string} */ type: "color_collection_output"; }; @@ -1951,6 +1976,7 @@ export type components = { * type * @default color_correct * @constant + * @enum {string} */ type: "color_correct"; }; @@ -2016,6 +2042,7 @@ export type components = { * type * @default color * @constant + * @enum {string} */ type: "color"; }; @@ -2057,6 +2084,7 @@ export type components = { * type * @default color_map_image_processor * @constant + * @enum {string} */ type: "color_map_image_processor"; }; @@ -2071,6 +2099,7 @@ export type components = { * type * @default color_output * @constant + * @enum {string} */ type: "color_output"; }; @@ -2113,6 +2142,7 @@ export type components = { * type * @default compel * @constant + * @enum {string} */ type: "compel"; }; @@ -2148,6 +2178,7 @@ export type components = { * type * @default conditioning_collection * @constant + * @enum {string} */ type: "conditioning_collection"; }; @@ -2165,6 +2196,7 @@ export type components = { * type * @default conditioning_collection_output * @constant + * @enum {string} */ type: "conditioning_collection_output"; }; @@ -2212,6 +2244,7 @@ export type components = { * type * @default conditioning * @constant + * @enum {string} */ type: "conditioning"; }; @@ -2226,6 +2259,7 @@ export type components = { * type * @default conditioning_output * @constant + * @enum {string} */ type: "conditioning_output"; }; @@ -2291,6 +2325,7 @@ export type components = { * type * @default content_shuffle_image_processor * @constant + * @enum {string} */ type: "content_shuffle_image_processor"; }; @@ -2393,6 +2428,7 @@ export type components = { * Format * @default checkpoint * @constant + * @enum {string} */ format: "checkpoint"; /** @@ -2409,6 +2445,7 @@ export type components = { * Type * @default controlnet * @constant + * @enum {string} */ type: "controlnet"; }; @@ -2467,6 +2504,7 @@ export type components = { * Format * @default diffusers * @constant + * @enum {string} */ format: "diffusers"; /** @default */ @@ -2475,6 +2513,7 @@ export type components = { * Type * @default controlnet * @constant + * @enum {string} */ type: "controlnet"; }; @@ -2540,6 +2579,7 @@ export type components = { * type * @default controlnet * @constant + * @enum {string} */ type: "controlnet"; }; @@ -2595,6 +2635,7 @@ export type components = { * type * @default control_output * @constant + * @enum {string} */ type: "control_output"; }; @@ -2785,6 +2826,7 @@ export type components = { * type * @default core_metadata * @constant + * @enum {string} */ type: "core_metadata"; [key: string]: unknown; @@ -2833,6 +2875,7 @@ export type components = { * type * @default create_denoise_mask * @constant + * @enum {string} */ type: "create_denoise_mask"; }; @@ -2909,6 +2952,7 @@ export type components = { * type * @default create_gradient_mask * @constant + * @enum {string} */ type: "create_gradient_mask"; }; @@ -2961,6 +3005,7 @@ export type components = { * type * @default crop_latents * @constant + * @enum {string} */ type: "crop_latents"; }; @@ -3016,6 +3061,7 @@ export type components = { * type * @default cv_inpaint * @constant + * @enum {string} */ type: "cv_inpaint"; }; @@ -3072,6 +3118,7 @@ export type components = { * type * @default dw_openpose_image_processor * @constant + * @enum {string} */ type: "dw_openpose_image_processor"; }; @@ -3194,6 +3241,7 @@ export type components = { * type * @default denoise_latents * @constant + * @enum {string} */ type: "denoise_latents"; }; @@ -3231,6 +3279,7 @@ export type components = { * type * @default denoise_mask_output * @constant + * @enum {string} */ type: "denoise_mask_output"; }; @@ -3279,6 +3328,7 @@ export type components = { * type * @default depth_anything_image_processor * @constant + * @enum {string} */ type: "depth_anything_image_processor"; }; @@ -3320,6 +3370,7 @@ export type components = { * type * @default div * @constant + * @enum {string} */ type: "div"; }; @@ -3454,6 +3505,7 @@ export type components = { * type * @default dynamic_prompt * @constant + * @enum {string} */ type: "dynamic_prompt"; }; @@ -3509,6 +3561,7 @@ export type components = { * type * @default esrgan * @constant + * @enum {string} */ type: "esrgan"; }; @@ -3608,6 +3661,7 @@ export type components = { * type * @default face_identifier * @constant + * @enum {string} */ type: "face_identifier"; }; @@ -3677,6 +3731,7 @@ export type components = { * type * @default face_mask_detection * @constant + * @enum {string} */ type: "face_mask_detection"; }; @@ -3701,6 +3756,7 @@ export type components = { * type * @default face_mask_output * @constant + * @enum {string} */ type: "face_mask_output"; /** @description The output mask */ @@ -3772,6 +3828,7 @@ export type components = { * type * @default face_off * @constant + * @enum {string} */ type: "face_off"; }; @@ -3796,6 +3853,7 @@ export type components = { * type * @default face_off_output * @constant + * @enum {string} */ type: "face_off_output"; /** @description The output mask */ @@ -3843,6 +3901,7 @@ export type components = { * type * @default float_collection * @constant + * @enum {string} */ type: "float_collection"; }; @@ -3860,6 +3919,7 @@ export type components = { * type * @default float_collection_output * @constant + * @enum {string} */ type: "float_collection_output"; }; @@ -3895,6 +3955,7 @@ export type components = { * type * @default float * @constant + * @enum {string} */ type: "float"; }; @@ -3942,6 +4003,7 @@ export type components = { * type * @default float_range * @constant + * @enum {string} */ type: "float_range"; }; @@ -3990,6 +4052,7 @@ export type components = { * type * @default float_math * @constant + * @enum {string} */ type: "float_math"; }; @@ -4007,6 +4070,7 @@ export type components = { * type * @default float_output * @constant + * @enum {string} */ type: "float_output"; }; @@ -4055,6 +4119,7 @@ export type components = { * type * @default float_to_int * @constant + * @enum {string} */ type: "float_to_int"; }; @@ -4158,6 +4223,7 @@ export type components = { * type * @default freeu * @constant + * @enum {string} */ type: "freeu"; }; @@ -4174,6 +4240,7 @@ export type components = { * type * @default gradient_mask_output * @constant + * @enum {string} */ type: "gradient_mask_output"; }; @@ -4189,7 +4256,7 @@ export type components = { * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["StringReplaceInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ResizeLatentsInvocation"]; + [key: string]: components["schemas"]["IdealSizeInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["ImageScaleInvocation"]; }; /** * Edges @@ -4226,7 +4293,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["LatentsOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["String2Output"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["T2IAdapterOutput"]; + [key: string]: components["schemas"]["LoRALoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["String2Output"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["CLIPSkipInvocationOutput"]; }; /** * Errors @@ -4269,6 +4336,7 @@ export type components = { * Type * @default hf * @constant + * @enum {string} */ type?: "hf"; }; @@ -4327,6 +4395,7 @@ export type components = { * type * @default hed_image_processor * @constant + * @enum {string} */ type: "hed_image_processor"; }; @@ -4370,6 +4439,7 @@ export type components = { * type * @default heuristic_resize * @constant + * @enum {string} */ type: "heuristic_resize"; }; @@ -4392,6 +4462,7 @@ export type components = { * Type * @default huggingface * @constant + * @enum {string} */ type?: "huggingface"; /** @@ -4482,11 +4553,13 @@ export type components = { * Type * @default ip_adapter * @constant + * @enum {string} */ type: "ip_adapter"; /** * Format * @constant + * @enum {string} */ format: "checkpoint"; }; @@ -4601,6 +4674,7 @@ export type components = { * type * @default ip_adapter * @constant + * @enum {string} */ type: "ip_adapter"; }; @@ -4657,6 +4731,7 @@ export type components = { * Type * @default ip_adapter * @constant + * @enum {string} */ type: "ip_adapter"; /** Image Encoder Model Id */ @@ -4664,6 +4739,7 @@ export type components = { /** * Format * @constant + * @enum {string} */ format: "invokeai"; }; @@ -4715,6 +4791,7 @@ export type components = { * type * @default ip_adapter_output * @constant + * @enum {string} */ type: "ip_adapter_output"; }; @@ -4764,6 +4841,7 @@ export type components = { * type * @default ideal_size * @constant + * @enum {string} */ type: "ideal_size"; }; @@ -4786,6 +4864,7 @@ export type components = { * type * @default ideal_size_output * @constant + * @enum {string} */ type: "ideal_size_output"; }; @@ -4834,6 +4913,7 @@ export type components = { * type * @default img_blur * @constant + * @enum {string} */ type: "img_blur"; }; @@ -4888,6 +4968,7 @@ export type components = { * type * @default img_chan * @constant + * @enum {string} */ type: "img_chan"; }; @@ -4941,6 +5022,7 @@ export type components = { * type * @default img_channel_multiply * @constant + * @enum {string} */ type: "img_channel_multiply"; }; @@ -4988,6 +5070,7 @@ export type components = { * type * @default img_channel_offset * @constant + * @enum {string} */ type: "img_channel_offset"; }; @@ -5022,6 +5105,7 @@ export type components = { * type * @default image_collection * @constant + * @enum {string} */ type: "image_collection"; }; @@ -5039,6 +5123,7 @@ export type components = { * type * @default image_collection_output * @constant + * @enum {string} */ type: "image_collection_output"; }; @@ -5081,6 +5166,7 @@ export type components = { * type * @default img_conv * @constant + * @enum {string} */ type: "img_conv"; }; @@ -5140,6 +5226,7 @@ export type components = { * type * @default img_crop * @constant + * @enum {string} */ type: "img_crop"; }; @@ -5272,6 +5359,7 @@ export type components = { * type * @default img_hue_adjust * @constant + * @enum {string} */ type: "img_hue_adjust"; }; @@ -5319,6 +5407,7 @@ export type components = { * type * @default img_ilerp * @constant + * @enum {string} */ type: "img_ilerp"; }; @@ -5350,6 +5439,7 @@ export type components = { * type * @default image * @constant + * @enum {string} */ type: "image"; }; @@ -5397,6 +5487,7 @@ export type components = { * type * @default img_lerp * @constant + * @enum {string} */ type: "img_lerp"; }; @@ -5442,6 +5533,7 @@ export type components = { * type * @default image_mask_to_tensor * @constant + * @enum {string} */ type: "image_mask_to_tensor"; }; @@ -5479,6 +5571,7 @@ export type components = { * type * @default img_mul * @constant + * @enum {string} */ type: "img_mul"; }; @@ -5514,6 +5607,7 @@ export type components = { * type * @default img_nsfw * @constant + * @enum {string} */ type: "img_nsfw"; }; @@ -5538,6 +5632,7 @@ export type components = { * type * @default image_output * @constant + * @enum {string} */ type: "image_output"; }; @@ -5595,6 +5690,7 @@ export type components = { * type * @default img_paste * @constant + * @enum {string} */ type: "img_paste"; }; @@ -5679,6 +5775,7 @@ export type components = { * type * @default img_resize * @constant + * @enum {string} */ type: "img_resize"; }; @@ -5727,6 +5824,7 @@ export type components = { * type * @default img_scale * @constant + * @enum {string} */ type: "img_scale"; }; @@ -5772,6 +5870,7 @@ export type components = { * type * @default i2l * @constant + * @enum {string} */ type: "i2l"; }; @@ -5834,6 +5933,7 @@ export type components = { * type * @default img_watermark * @constant + * @enum {string} */ type: "img_watermark"; }; @@ -5900,6 +6000,7 @@ export type components = { * type * @default infill_rgba * @constant + * @enum {string} */ type: "infill_rgba"; }; @@ -5948,6 +6049,7 @@ export type components = { * type * @default infill_patchmatch * @constant + * @enum {string} */ type: "infill_patchmatch"; }; @@ -5995,6 +6097,7 @@ export type components = { * type * @default infill_tile * @constant + * @enum {string} */ type: "infill_tile"; }; @@ -6036,6 +6139,7 @@ export type components = { * type * @default integer_collection * @constant + * @enum {string} */ type: "integer_collection"; }; @@ -6053,6 +6157,7 @@ export type components = { * type * @default integer_collection_output * @constant + * @enum {string} */ type: "integer_collection_output"; }; @@ -6088,6 +6193,7 @@ export type components = { * type * @default integer * @constant + * @enum {string} */ type: "integer"; }; @@ -6136,6 +6242,7 @@ export type components = { * type * @default integer_math * @constant + * @enum {string} */ type: "integer_math"; }; @@ -6153,6 +6260,7 @@ export type components = { * type * @default integer_output * @constant + * @enum {string} */ type: "integer_output"; }; @@ -6184,6 +6292,7 @@ export type components = { * type * @default invert_tensor_mask * @constant + * @enum {string} */ type: "invert_tensor_mask"; }; @@ -6253,6 +6362,7 @@ export type components = { * type * @default iterate * @constant + * @enum {string} */ type: "iterate"; }; @@ -6280,6 +6390,7 @@ export type components = { * type * @default iterate_output * @constant + * @enum {string} */ type: "iterate_output"; }; @@ -6316,6 +6427,7 @@ export type components = { * type * @default infill_lama * @constant + * @enum {string} */ type: "infill_lama"; }; @@ -6350,6 +6462,7 @@ export type components = { * type * @default latents_collection * @constant + * @enum {string} */ type: "latents_collection"; }; @@ -6367,6 +6480,7 @@ export type components = { * type * @default latents_collection_output * @constant + * @enum {string} */ type: "latents_collection_output"; }; @@ -6415,6 +6529,7 @@ export type components = { * type * @default latents * @constant + * @enum {string} */ type: "latents"; }; @@ -6439,6 +6554,7 @@ export type components = { * type * @default latents_output * @constant + * @enum {string} */ type: "latents_output"; }; @@ -6488,6 +6604,7 @@ export type components = { * type * @default l2i * @constant + * @enum {string} */ type: "l2i"; }; @@ -6553,6 +6670,7 @@ export type components = { * type * @default leres_image_processor * @constant + * @enum {string} */ type: "leres_image_processor"; }; @@ -6600,6 +6718,7 @@ export type components = { * type * @default lineart_anime_image_processor * @constant + * @enum {string} */ type: "lineart_anime_image_processor"; }; @@ -6653,6 +6772,7 @@ export type components = { * type * @default lineart_image_processor * @constant + * @enum {string} */ type: "lineart_image_processor"; }; @@ -6697,6 +6817,7 @@ export type components = { * type * @default lora_collection_loader * @constant + * @enum {string} */ type: "lora_collection_loader"; }; @@ -6753,6 +6874,7 @@ export type components = { * Type * @default lora * @constant + * @enum {string} */ type: "lora"; /** @@ -6764,6 +6886,7 @@ export type components = { * Format * @default diffusers * @constant + * @enum {string} */ format: "diffusers"; }; @@ -6824,6 +6947,7 @@ export type components = { * type * @default lora_loader * @constant + * @enum {string} */ type: "lora_loader"; }; @@ -6848,6 +6972,7 @@ export type components = { * type * @default lora_loader_output * @constant + * @enum {string} */ type: "lora_loader_output"; }; @@ -6904,6 +7029,7 @@ export type components = { * Type * @default lora * @constant + * @enum {string} */ type: "lora"; /** @@ -6915,6 +7041,7 @@ export type components = { * Format * @default lycoris * @constant + * @enum {string} */ format: "lycoris"; }; @@ -6968,6 +7095,7 @@ export type components = { * type * @default lora_selector * @constant + * @enum {string} */ type: "lora_selector"; }; @@ -6985,6 +7113,7 @@ export type components = { * type * @default lora_selector_output * @constant + * @enum {string} */ type: "lora_selector_output"; }; @@ -7004,6 +7133,7 @@ export type components = { * Type * @default local * @constant + * @enum {string} */ type?: "local"; }; @@ -7065,6 +7195,7 @@ export type components = { * Type * @default main * @constant + * @enum {string} */ type: "main"; /** @@ -7080,6 +7211,7 @@ export type components = { * Format * @default checkpoint * @constant + * @enum {string} */ format: "checkpoint"; /** @@ -7153,6 +7285,7 @@ export type components = { * Type * @default main * @constant + * @enum {string} */ type: "main"; /** @@ -7168,6 +7301,7 @@ export type components = { * Format * @default diffusers * @constant + * @enum {string} */ format: "diffusers"; /** @default */ @@ -7244,6 +7378,7 @@ export type components = { * type * @default main_model_loader * @constant + * @enum {string} */ type: "main_model_loader"; }; @@ -7281,6 +7416,7 @@ export type components = { * type * @default mask_combine * @constant + * @enum {string} */ type: "mask_combine"; }; @@ -7336,6 +7472,7 @@ export type components = { * type * @default mask_edge * @constant + * @enum {string} */ type: "mask_edge"; }; @@ -7377,6 +7514,7 @@ export type components = { * type * @default tomask * @constant + * @enum {string} */ type: "tomask"; }; @@ -7426,6 +7564,7 @@ export type components = { * type * @default mask_from_id * @constant + * @enum {string} */ type: "mask_from_id"; }; @@ -7450,6 +7589,7 @@ export type components = { * type * @default mask_output * @constant + * @enum {string} */ type: "mask_output"; }; @@ -7509,6 +7649,7 @@ export type components = { * type * @default mediapipe_face_processor * @constant + * @enum {string} */ type: "mediapipe_face_processor"; }; @@ -7543,6 +7684,7 @@ export type components = { * type * @default merge_metadata * @constant + * @enum {string} */ type: "merge_metadata"; }; @@ -7594,6 +7736,7 @@ export type components = { * type * @default merge_tiles_to_image * @constant + * @enum {string} */ type: "merge_tiles_to_image"; }; @@ -7634,6 +7777,7 @@ export type components = { * type * @default metadata * @constant + * @enum {string} */ type: "metadata"; }; @@ -7686,6 +7830,7 @@ export type components = { * type * @default metadata_item * @constant + * @enum {string} */ type: "metadata_item"; }; @@ -7700,6 +7845,7 @@ export type components = { * type * @default metadata_item_output * @constant + * @enum {string} */ type: "metadata_item_output"; }; @@ -7711,6 +7857,7 @@ export type components = { * type * @default metadata_output * @constant + * @enum {string} */ type: "metadata_output"; }; @@ -7770,6 +7917,7 @@ export type components = { * type * @default midas_depth_image_processor * @constant + * @enum {string} */ type: "midas_depth_image_processor"; }; @@ -7829,6 +7977,7 @@ export type components = { * type * @default mlsd_image_processor * @constant + * @enum {string} */ type: "mlsd_image_processor"; }; @@ -7959,6 +8108,7 @@ export type components = { * type * @default model_loader_output * @constant + * @enum {string} */ type: "model_loader_output"; /** @@ -8089,6 +8239,7 @@ export type components = { * type * @default mul * @constant + * @enum {string} */ type: "mul"; }; @@ -8160,6 +8311,7 @@ export type components = { * type * @default noise * @constant + * @enum {string} */ type: "noise"; }; @@ -8184,6 +8336,7 @@ export type components = { * type * @default noise_output * @constant + * @enum {string} */ type: "noise_output"; }; @@ -8231,6 +8384,7 @@ export type components = { * type * @default normalbae_image_processor * @constant + * @enum {string} */ type: "normalbae_image_processor"; }; @@ -8338,6 +8492,7 @@ export type components = { * type * @default pair_tile_image * @constant + * @enum {string} */ type: "pair_tile_image"; }; @@ -8349,6 +8504,7 @@ export type components = { * type * @default pair_tile_image_output * @constant + * @enum {string} */ type: "pair_tile_image_output"; }; @@ -8408,6 +8564,7 @@ export type components = { * type * @default pidi_image_processor * @constant + * @enum {string} */ type: "pidi_image_processor"; }; @@ -8464,6 +8621,7 @@ export type components = { * type * @default prompt_from_file * @constant + * @enum {string} */ type: "prompt_from_file"; }; @@ -8522,6 +8680,7 @@ export type components = { * type * @default rand_float * @constant + * @enum {string} */ type: "rand_float"; }; @@ -8563,6 +8722,7 @@ export type components = { * type * @default rand_int * @constant + * @enum {string} */ type: "rand_int"; }; @@ -8616,6 +8776,7 @@ export type components = { * type * @default random_range * @constant + * @enum {string} */ type: "random_range"; }; @@ -8663,6 +8824,7 @@ export type components = { * type * @default range * @constant + * @enum {string} */ type: "range"; }; @@ -8710,6 +8872,7 @@ export type components = { * type * @default range_of_size * @constant + * @enum {string} */ type: "range_of_size"; }; @@ -8771,6 +8934,7 @@ export type components = { * type * @default rectangle_mask * @constant + * @enum {string} */ type: "rectangle_mask"; }; @@ -8861,6 +9025,7 @@ export type components = { * type * @default lresize * @constant + * @enum {string} */ type: "lresize"; }; @@ -8912,6 +9077,7 @@ export type components = { * type * @default round_float * @constant + * @enum {string} */ type: "round_float"; }; @@ -8995,6 +9161,7 @@ export type components = { * type * @default sdxl_compel_prompt * @constant + * @enum {string} */ type: "sdxl_compel_prompt"; }; @@ -9044,6 +9211,7 @@ export type components = { * type * @default sdxl_lora_collection_loader * @constant + * @enum {string} */ type: "sdxl_lora_collection_loader"; }; @@ -9099,6 +9267,7 @@ export type components = { * type * @default sdxl_lora_loader * @constant + * @enum {string} */ type: "sdxl_lora_loader"; }; @@ -9129,6 +9298,7 @@ export type components = { * type * @default sdxl_lora_loader_output * @constant + * @enum {string} */ type: "sdxl_lora_loader_output"; }; @@ -9160,6 +9330,7 @@ export type components = { * type * @default sdxl_model_loader * @constant + * @enum {string} */ type: "sdxl_model_loader"; }; @@ -9192,6 +9363,7 @@ export type components = { * type * @default sdxl_model_loader_output * @constant + * @enum {string} */ type: "sdxl_model_loader_output"; }; @@ -9255,6 +9427,7 @@ export type components = { * type * @default sdxl_refiner_compel_prompt * @constant + * @enum {string} */ type: "sdxl_refiner_compel_prompt"; }; @@ -9286,6 +9459,7 @@ export type components = { * type * @default sdxl_refiner_model_loader * @constant + * @enum {string} */ type: "sdxl_refiner_model_loader"; }; @@ -9313,6 +9487,7 @@ export type components = { * type * @default sdxl_refiner_model_loader_output * @constant + * @enum {string} */ type: "sdxl_refiner_model_loader_output"; }; @@ -9353,6 +9528,7 @@ export type components = { * type * @default save_image * @constant + * @enum {string} */ type: "save_image"; }; @@ -9402,6 +9578,7 @@ export type components = { * type * @default lscale * @constant + * @enum {string} */ type: "lscale"; }; @@ -9438,6 +9615,7 @@ export type components = { * type * @default scheduler * @constant + * @enum {string} */ type: "scheduler"; }; @@ -9453,6 +9631,7 @@ export type components = { * type * @default scheduler_output * @constant + * @enum {string} */ type: "scheduler_output"; }; @@ -9510,6 +9689,7 @@ export type components = { * type * @default seamless * @constant + * @enum {string} */ type: "seamless"; }; @@ -9534,6 +9714,7 @@ export type components = { * type * @default seamless_output * @constant + * @enum {string} */ type: "seamless_output"; }; @@ -9581,6 +9762,7 @@ export type components = { * type * @default segment_anything_processor * @constant + * @enum {string} */ type: "segment_anything_processor"; }; @@ -9822,6 +10004,7 @@ export type components = { * type * @default show_image * @constant + * @enum {string} */ type: "show_image"; }; @@ -9944,6 +10127,7 @@ export type components = { * type * @default step_param_easing * @constant + * @enum {string} */ type: "step_param_easing"; }; @@ -9966,6 +10150,7 @@ export type components = { * type * @default string_2_output * @constant + * @enum {string} */ type: "string_2_output"; }; @@ -10001,6 +10186,7 @@ export type components = { * type * @default string_collection * @constant + * @enum {string} */ type: "string_collection"; }; @@ -10018,6 +10204,7 @@ export type components = { * type * @default string_collection_output * @constant + * @enum {string} */ type: "string_collection_output"; }; @@ -10053,6 +10240,7 @@ export type components = { * type * @default string * @constant + * @enum {string} */ type: "string"; }; @@ -10094,6 +10282,7 @@ export type components = { * type * @default string_join * @constant + * @enum {string} */ type: "string_join"; }; @@ -10141,6 +10330,7 @@ export type components = { * type * @default string_join_three * @constant + * @enum {string} */ type: "string_join_three"; }; @@ -10158,6 +10348,7 @@ export type components = { * type * @default string_output * @constant + * @enum {string} */ type: "string_output"; }; @@ -10180,6 +10371,7 @@ export type components = { * type * @default string_pos_neg_output * @constant + * @enum {string} */ type: "string_pos_neg_output"; }; @@ -10233,6 +10425,7 @@ export type components = { * type * @default string_replace * @constant + * @enum {string} */ type: "string_replace"; }; @@ -10274,6 +10467,7 @@ export type components = { * type * @default string_split * @constant + * @enum {string} */ type: "string_split"; }; @@ -10309,6 +10503,7 @@ export type components = { * type * @default string_split_neg * @constant + * @enum {string} */ type: "string_split_neg"; }; @@ -10356,6 +10551,7 @@ export type components = { * type * @default sub * @constant + * @enum {string} */ type: "sub"; }; @@ -10413,6 +10609,7 @@ export type components = { /** * Format * @constant + * @enum {string} */ format: "diffusers"; /** @default */ @@ -10421,6 +10618,7 @@ export type components = { * Type * @default t2i_adapter * @constant + * @enum {string} */ type: "t2i_adapter"; }; @@ -10514,6 +10712,7 @@ export type components = { * type * @default t2i_adapter * @constant + * @enum {string} */ type: "t2i_adapter"; }; @@ -10562,6 +10761,7 @@ export type components = { * type * @default t2i_adapter_output * @constant + * @enum {string} */ type: "t2i_adapter_output"; }; @@ -10640,12 +10840,14 @@ export type components = { * Type * @default embedding * @constant + * @enum {string} */ type: "embedding"; /** * Format * @default embedding_file * @constant + * @enum {string} */ format: "embedding_file"; }; @@ -10702,12 +10904,14 @@ export type components = { * Type * @default embedding * @constant + * @enum {string} */ type: "embedding"; /** * Format * @default embedding_folder * @constant + * @enum {string} */ format: "embedding_folder"; }; @@ -10756,6 +10960,7 @@ export type components = { * type * @default tile_image_processor * @constant + * @enum {string} */ type: "tile_image_processor"; }; @@ -10787,6 +10992,7 @@ export type components = { * type * @default tile_to_properties * @constant + * @enum {string} */ type: "tile_to_properties"; }; @@ -10846,6 +11052,7 @@ export type components = { * type * @default tile_to_properties_output * @constant + * @enum {string} */ type: "tile_to_properties_output"; }; @@ -10890,6 +11097,7 @@ export type components = { * type * @default unet_output * @constant + * @enum {string} */ type: "unet_output"; }; @@ -10909,6 +11117,7 @@ export type components = { * Type * @default url * @constant + * @enum {string} */ type?: "url"; }; @@ -10956,6 +11165,7 @@ export type components = { * type * @default unsharp_mask * @constant + * @enum {string} */ type: "unsharp_mask"; }; @@ -11025,6 +11235,7 @@ export type components = { * Format * @default checkpoint * @constant + * @enum {string} */ format: "checkpoint"; /** @@ -11041,6 +11252,7 @@ export type components = { * Type * @default vae * @constant + * @enum {string} */ type: "vae"; }; @@ -11097,12 +11309,14 @@ export type components = { * Type * @default vae * @constant + * @enum {string} */ type: "vae"; /** * Format * @default diffusers * @constant + * @enum {string} */ format: "diffusers"; }; @@ -11147,6 +11361,7 @@ export type components = { * type * @default vae_loader * @constant + * @enum {string} */ type: "vae_loader"; }; @@ -11164,6 +11379,7 @@ export type components = { * type * @default vae_output * @constant + * @enum {string} */ type: "vae_output"; }; @@ -11240,6 +11456,19 @@ export type components = { */ id: string; }; + /** WorkflowAndGraphResponse */ + WorkflowAndGraphResponse: { + /** + * Workflow + * @description The workflow used to generate the image, as stringified JSON + */ + workflow: string | null; + /** + * Graph + * @description The graph used to generate the image, as stringified JSON + */ + graph: string | null; + }; /** * WorkflowCategory * @enum {string} @@ -11420,6 +11649,7 @@ export type components = { * type * @default zoe_depth_image_processor * @constant + * @enum {string} */ type: "zoe_depth_image_processor"; }; @@ -11611,143 +11841,143 @@ export type components = { */ UIType: "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "T2IAdapterModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict"; InvocationOutputMap: { - string_replace: components["schemas"]["StringOutput"]; - merge_metadata: components["schemas"]["MetadataOutput"]; - boolean: components["schemas"]["BooleanOutput"]; - face_off: components["schemas"]["FaceOffOutput"]; - prompt_from_file: components["schemas"]["StringCollectionOutput"]; + ideal_size: components["schemas"]["IdealSizeOutput"]; + lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; color_map_image_processor: components["schemas"]["ImageOutput"]; - range_of_size: components["schemas"]["IntegerCollectionOutput"]; + img_resize: components["schemas"]["ImageOutput"]; + calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; + lineart_image_processor: components["schemas"]["ImageOutput"]; + boolean_collection: components["schemas"]["BooleanCollectionOutput"]; + ip_adapter: components["schemas"]["IPAdapterOutput"]; + face_mask_detection: components["schemas"]["FaceMaskOutput"]; + string_replace: components["schemas"]["StringOutput"]; + infill_lama: components["schemas"]["ImageOutput"]; + calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; + tile_image_processor: components["schemas"]["ImageOutput"]; + calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; + img_blur: components["schemas"]["ImageOutput"]; + scheduler: components["schemas"]["SchedulerOutput"]; + range: components["schemas"]["IntegerCollectionOutput"]; + lora_selector: components["schemas"]["LoRASelectorOutput"]; + metadata: components["schemas"]["MetadataOutput"]; + clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; + rand_float: components["schemas"]["FloatOutput"]; + float_collection: components["schemas"]["FloatCollectionOutput"]; + zoe_depth_image_processor: components["schemas"]["ImageOutput"]; + create_gradient_mask: components["schemas"]["GradientMaskOutput"]; + i2l: components["schemas"]["LatentsOutput"]; + dynamic_prompt: components["schemas"]["StringCollectionOutput"]; + create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; + img_ilerp: components["schemas"]["ImageOutput"]; + tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; + infill_cv2: components["schemas"]["ImageOutput"]; + string_join_three: components["schemas"]["StringOutput"]; + denoise_latents: components["schemas"]["LatentsOutput"]; + iterate: components["schemas"]["IterateInvocationOutput"]; + step_param_easing: components["schemas"]["FloatCollectionOutput"]; + img_nsfw: components["schemas"]["ImageOutput"]; + infill_patchmatch: components["schemas"]["ImageOutput"]; + pair_tile_image: components["schemas"]["PairTileImageOutput"]; + alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; + lora_loader: components["schemas"]["LoRALoaderOutput"]; + normalbae_image_processor: components["schemas"]["ImageOutput"]; + img_hue_adjust: components["schemas"]["ImageOutput"]; + conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; + image_mask_to_tensor: components["schemas"]["MaskOutput"]; + t2i_adapter: components["schemas"]["T2IAdapterOutput"]; infill_rgba: components["schemas"]["ImageOutput"]; - compel: components["schemas"]["ConditioningOutput"]; - midas_depth_image_processor: components["schemas"]["ImageOutput"]; + vae_loader: components["schemas"]["VAEOutput"]; + blank_image: components["schemas"]["ImageOutput"]; + latents: components["schemas"]["LatentsOutput"]; + sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + boolean: components["schemas"]["BooleanOutput"]; + float_range: components["schemas"]["FloatCollectionOutput"]; + integer: components["schemas"]["IntegerOutput"]; + mul: components["schemas"]["IntegerOutput"]; + img_crop: components["schemas"]["ImageOutput"]; + face_identifier: components["schemas"]["ImageOutput"]; + main_model_loader: components["schemas"]["ModelLoaderOutput"]; + mlsd_image_processor: components["schemas"]["ImageOutput"]; + esrgan: components["schemas"]["ImageOutput"]; + integer_math: components["schemas"]["IntegerOutput"]; + sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + img_chan: components["schemas"]["ImageOutput"]; + round_float: components["schemas"]["FloatOutput"]; + random_range: components["schemas"]["IntegerCollectionOutput"]; + image_collection: components["schemas"]["ImageCollectionOutput"]; + sub: components["schemas"]["IntegerOutput"]; + lblend: components["schemas"]["LatentsOutput"]; + sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; + cv_inpaint: components["schemas"]["ImageOutput"]; + sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; + invert_tensor_mask: components["schemas"]["MaskOutput"]; image: components["schemas"]["ImageOutput"]; + img_mul: components["schemas"]["ImageOutput"]; + l2i: components["schemas"]["ImageOutput"]; + canny_image_processor: components["schemas"]["ImageOutput"]; + save_image: components["schemas"]["ImageOutput"]; + string_split: components["schemas"]["String2Output"]; + segment_anything_processor: components["schemas"]["ImageOutput"]; + heuristic_resize: components["schemas"]["ImageOutput"]; + face_off: components["schemas"]["FaceOffOutput"]; + img_channel_offset: components["schemas"]["ImageOutput"]; + img_conv: components["schemas"]["ImageOutput"]; + add: components["schemas"]["IntegerOutput"]; + infill_tile: components["schemas"]["ImageOutput"]; color: components["schemas"]["ColorOutput"]; mediapipe_face_processor: components["schemas"]["ImageOutput"]; - cv_inpaint: components["schemas"]["ImageOutput"]; - pidi_image_processor: components["schemas"]["ImageOutput"]; - i2l: components["schemas"]["LatentsOutput"]; - infill_lama: components["schemas"]["ImageOutput"]; freeu: components["schemas"]["UNetOutput"]; - segment_anything_processor: components["schemas"]["ImageOutput"]; - canny_image_processor: components["schemas"]["ImageOutput"]; - mlsd_image_processor: components["schemas"]["ImageOutput"]; - conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; - round_float: components["schemas"]["FloatOutput"]; - crop_latents: components["schemas"]["LatentsOutput"]; - tile_image_processor: components["schemas"]["ImageOutput"]; - integer_collection: components["schemas"]["IntegerCollectionOutput"]; - float_range: components["schemas"]["FloatCollectionOutput"]; - img_conv: components["schemas"]["ImageOutput"]; - sub: components["schemas"]["IntegerOutput"]; - denoise_latents: components["schemas"]["LatentsOutput"]; - string_collection: components["schemas"]["StringCollectionOutput"]; - lora_loader: components["schemas"]["LoRALoaderOutput"]; - core_metadata: components["schemas"]["MetadataOutput"]; - t2i_adapter: components["schemas"]["T2IAdapterOutput"]; - boolean_collection: components["schemas"]["BooleanCollectionOutput"]; - canvas_paste_back: components["schemas"]["ImageOutput"]; + pidi_image_processor: components["schemas"]["ImageOutput"]; + depth_anything_image_processor: components["schemas"]["ImageOutput"]; + noise: components["schemas"]["NoiseOutput"]; + collect: components["schemas"]["CollectInvocationOutput"]; + content_shuffle_image_processor: components["schemas"]["ImageOutput"]; string_split_neg: components["schemas"]["StringPosNegOutput"]; - img_nsfw: components["schemas"]["ImageOutput"]; - random_range: components["schemas"]["IntegerCollectionOutput"]; - ideal_size: components["schemas"]["IdealSizeOutput"]; - img_scale: components["schemas"]["ImageOutput"]; - dw_openpose_image_processor: components["schemas"]["ImageOutput"]; - image_collection: components["schemas"]["ImageCollectionOutput"]; - tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; - save_image: components["schemas"]["ImageOutput"]; - img_channel_offset: components["schemas"]["ImageOutput"]; - float_collection: components["schemas"]["FloatCollectionOutput"]; - lineart_anime_image_processor: components["schemas"]["ImageOutput"]; - img_blur: components["schemas"]["ImageOutput"]; - img_watermark: components["schemas"]["ImageOutput"]; - lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; - rand_int: components["schemas"]["IntegerOutput"]; - pair_tile_image: components["schemas"]["PairTileImageOutput"]; - tomask: components["schemas"]["ImageOutput"]; - sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; - mask_combine: components["schemas"]["ImageOutput"]; - create_gradient_mask: components["schemas"]["GradientMaskOutput"]; - face_identifier: components["schemas"]["ImageOutput"]; - string: components["schemas"]["StringOutput"]; - heuristic_resize: components["schemas"]["ImageOutput"]; - lineart_image_processor: components["schemas"]["ImageOutput"]; - image_mask_to_tensor: components["schemas"]["MaskOutput"]; - lscale: components["schemas"]["LatentsOutput"]; - img_channel_multiply: components["schemas"]["ImageOutput"]; - sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - infill_cv2: components["schemas"]["ImageOutput"]; - latents: components["schemas"]["LatentsOutput"]; - add: components["schemas"]["IntegerOutput"]; - sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; - img_mul: components["schemas"]["ImageOutput"]; + img_lerp: components["schemas"]["ImageOutput"]; leres_image_processor: components["schemas"]["ImageOutput"]; - ip_adapter: components["schemas"]["IPAdapterOutput"]; - string_join: components["schemas"]["StringOutput"]; - img_resize: components["schemas"]["ImageOutput"]; - img_chan: components["schemas"]["ImageOutput"]; - blank_image: components["schemas"]["ImageOutput"]; + div: components["schemas"]["IntegerOutput"]; + lscale: components["schemas"]["LatentsOutput"]; + metadata_item: components["schemas"]["MetadataItemOutput"]; + seamless: components["schemas"]["SeamlessModeOutput"]; + img_paste: components["schemas"]["ImageOutput"]; + string: components["schemas"]["StringOutput"]; + mask_combine: components["schemas"]["ImageOutput"]; + float_math: components["schemas"]["FloatOutput"]; + tomask: components["schemas"]["ImageOutput"]; + img_channel_multiply: components["schemas"]["ImageOutput"]; + sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; + mask_edge: components["schemas"]["ImageOutput"]; + merge_tiles_to_image: components["schemas"]["ImageOutput"]; + range_of_size: components["schemas"]["IntegerCollectionOutput"]; + sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; + canvas_paste_back: components["schemas"]["ImageOutput"]; + controlnet: components["schemas"]["ControlOutput"]; + dw_openpose_image_processor: components["schemas"]["ImageOutput"]; + string_collection: components["schemas"]["StringCollectionOutput"]; + float_to_int: components["schemas"]["IntegerOutput"]; + color_correct: components["schemas"]["ImageOutput"]; + unsharp_mask: components["schemas"]["ImageOutput"]; + float: components["schemas"]["FloatOutput"]; + rand_int: components["schemas"]["IntegerOutput"]; + mask_from_id: components["schemas"]["ImageOutput"]; latents_collection: components["schemas"]["LatentsCollectionOutput"]; conditioning: components["schemas"]["ConditioningOutput"]; - main_model_loader: components["schemas"]["ModelLoaderOutput"]; - img_pad_crop: components["schemas"]["ImageOutput"]; + integer_collection: components["schemas"]["IntegerCollectionOutput"]; + string_join: components["schemas"]["StringOutput"]; + compel: components["schemas"]["ConditioningOutput"]; + crop_latents: components["schemas"]["LatentsOutput"]; + img_watermark: components["schemas"]["ImageOutput"]; rectangle_mask: components["schemas"]["MaskOutput"]; - create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; - vae_loader: components["schemas"]["VAEOutput"]; - calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; - unsharp_mask: components["schemas"]["ImageOutput"]; - scheduler: components["schemas"]["SchedulerOutput"]; - integer_math: components["schemas"]["IntegerOutput"]; - mul: components["schemas"]["IntegerOutput"]; - string_join_three: components["schemas"]["StringOutput"]; - rand_float: components["schemas"]["FloatOutput"]; - calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; - controlnet: components["schemas"]["ControlOutput"]; - img_crop: components["schemas"]["ImageOutput"]; - float_math: components["schemas"]["FloatOutput"]; - div: components["schemas"]["IntegerOutput"]; - integer: components["schemas"]["IntegerOutput"]; - img_paste: components["schemas"]["ImageOutput"]; - lora_selector: components["schemas"]["LoRASelectorOutput"]; - iterate: components["schemas"]["IterateInvocationOutput"]; + prompt_from_file: components["schemas"]["StringCollectionOutput"]; + merge_metadata: components["schemas"]["MetadataOutput"]; + img_pad_crop: components["schemas"]["ImageOutput"]; + midas_depth_image_processor: components["schemas"]["ImageOutput"]; + core_metadata: components["schemas"]["MetadataOutput"]; show_image: components["schemas"]["ImageOutput"]; - collect: components["schemas"]["CollectInvocationOutput"]; - depth_anything_image_processor: components["schemas"]["ImageOutput"]; - mask_edge: components["schemas"]["ImageOutput"]; - infill_patchmatch: components["schemas"]["ImageOutput"]; - lblend: components["schemas"]["LatentsOutput"]; - metadata_item: components["schemas"]["MetadataItemOutput"]; - content_shuffle_image_processor: components["schemas"]["ImageOutput"]; - img_ilerp: components["schemas"]["ImageOutput"]; - metadata: components["schemas"]["MetadataOutput"]; - normalbae_image_processor: components["schemas"]["ImageOutput"]; - sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; - merge_tiles_to_image: components["schemas"]["ImageOutput"]; - mask_from_id: components["schemas"]["ImageOutput"]; - string_split: components["schemas"]["String2Output"]; - esrgan: components["schemas"]["ImageOutput"]; - color_correct: components["schemas"]["ImageOutput"]; - alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; - float_to_int: components["schemas"]["IntegerOutput"]; - seamless: components["schemas"]["SeamlessModeOutput"]; - float: components["schemas"]["FloatOutput"]; - dynamic_prompt: components["schemas"]["StringCollectionOutput"]; - clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; - sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; - l2i: components["schemas"]["ImageOutput"]; - infill_tile: components["schemas"]["ImageOutput"]; - invert_tensor_mask: components["schemas"]["MaskOutput"]; - face_mask_detection: components["schemas"]["FaceMaskOutput"]; - zoe_depth_image_processor: components["schemas"]["ImageOutput"]; - range: components["schemas"]["IntegerCollectionOutput"]; hed_image_processor: components["schemas"]["ImageOutput"]; - img_hue_adjust: components["schemas"]["ImageOutput"]; - step_param_easing: components["schemas"]["FloatCollectionOutput"]; - img_lerp: components["schemas"]["ImageOutput"]; - noise: components["schemas"]["NoiseOutput"]; - calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; lresize: components["schemas"]["LatentsOutput"]; + lineart_anime_image_processor: components["schemas"]["ImageOutput"]; + img_scale: components["schemas"]["ImageOutput"]; }; }; responses: never; @@ -12691,7 +12921,7 @@ export type operations = { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["WorkflowWithoutID"] | null; + "application/json": components["schemas"]["WorkflowAndGraphResponse"]; }; }; /** @description Validation Error */ diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 6e2a70264f..1160a2bee5 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -16,6 +16,9 @@ export type UpdateBoardArg = paths['/api/v1/boards/{board_id}']['patch']['parame changes: paths['/api/v1/boards/{board_id}']['patch']['requestBody']['content']['application/json']; }; +export type GraphAndWorkflowResponse = + paths['/api/v1/images/i/{image_name}/workflow']['get']['responses']['200']['content']['application/json']; + export type BatchConfig = paths['/api/v1/queue/{queue_id}/enqueue_batch']['post']['requestBody']['content']['application/json']; From 799cf06d20d4308bf72bf7cafc88fd9e66ddb758 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 18:51:34 +1000 Subject: [PATCH 174/442] fix(ui): loading library workflows --- .../workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts index ca4e72e34c..f616812175 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts @@ -27,8 +27,9 @@ export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = ({ onS const getAndLoadWorkflow = useCallback( async (workflow_id: string) => { try { - const data = await _getAndLoadWorkflow(workflow_id).unwrap(); - dispatch(workflowLoadRequested({ workflow: data.workflow, asCopy: false })); + const { workflow } = await _getAndLoadWorkflow(workflow_id).unwrap(); + // This action expects a stringified workflow, instead of updating the routes and services we will just stringify it here + dispatch(workflowLoadRequested({ data: { workflow: JSON.stringify(workflow), graph: null }, asCopy: false })); // No toast - the listener for this action does that after the workflow is loaded onSuccess && onSuccess(); } catch { From 386d552493d5c531de3fac781f5cb0b95f403b6c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 18:53:30 +1000 Subject: [PATCH 175/442] fix(ui): loading workflows from file --- .../features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx index 8610fa87e0..7a39d4ecd0 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx @@ -29,8 +29,7 @@ export const useLoadWorkflowFromFile: UseLoadWorkflowFromFile = ({ resetRef, onS const rawJSON = reader.result; try { - const parsedJSON = JSON.parse(String(rawJSON)); - dispatch(workflowLoadRequested({ workflow: parsedJSON, asCopy: true })); + dispatch(workflowLoadRequested({ data: { workflow: String(rawJSON), graph: null }, asCopy: true })); dispatch(workflowLoadedFromFile()); onSuccess && onSuccess(); } catch (e) { From 93ebc175c61c726dc0a590eb0037ce425d02efa8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 19:14:59 +1000 Subject: [PATCH 176/442] fix(app): retain graph in metadata when uploading images --- invokeai/app/api/routers/images.py | 12 +++++++++++- .../app/services/image_files/image_files_base.py | 2 +- .../app/services/image_files/image_files_disk.py | 4 ++-- invokeai/app/services/images/images_base.py | 2 +- invokeai/app/services/images/images_default.py | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 36ae39e986..8cd7e16e28 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -49,6 +49,7 @@ async def upload_image( metadata = None workflow = None + graph = None contents = await file.read() try: @@ -76,9 +77,17 @@ async def upload_image( try: workflow = WorkflowWithoutIDValidator.validate_json(workflow_raw) except ValidationError: - ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") + ApiDependencies.invoker.services.logger.warn("Failed to parse workflow for uploaded image") pass + # attempt to extract graph from image + graph_raw = pil_image.info.get("invokeai_graph", None) + if isinstance(graph_raw, str): + graph = graph_raw + else: + ApiDependencies.invoker.services.logger.warn("Failed to parse graph for uploaded image") + pass + try: image_dto = ApiDependencies.invoker.services.images.create( image=pil_image, @@ -88,6 +97,7 @@ async def upload_image( board_id=board_id, metadata=metadata, workflow=workflow, + graph=graph, is_intermediate=is_intermediate, ) diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py index 839c6fecb6..34b5054bd0 100644 --- a/invokeai/app/services/image_files/image_files_base.py +++ b/invokeai/app/services/image_files/image_files_base.py @@ -36,7 +36,7 @@ class ImageFileStorageBase(ABC): image_name: str, metadata: Optional[MetadataField] = None, workflow: Optional[WorkflowWithoutID] = None, - graph: Optional[Graph] = None, + graph: Optional[Graph | str] = None, thumbnail_size: int = 256, ) -> None: """Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image name, thumbnail name, and created timestamp.""" diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py index 2b5bf433ae..9f29d333ec 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -59,7 +59,7 @@ class DiskImageFileStorage(ImageFileStorageBase): image_name: str, metadata: Optional[MetadataField] = None, workflow: Optional[WorkflowWithoutID] = None, - graph: Optional[Graph] = None, + graph: Optional[Graph | str] = None, thumbnail_size: int = 256, ) -> None: try: @@ -78,7 +78,7 @@ class DiskImageFileStorage(ImageFileStorageBase): info_dict["invokeai_workflow"] = workflow_json pnginfo.add_text("invokeai_workflow", workflow_json) if graph is not None: - graph_json = graph.model_dump_json() + graph_json = graph.model_dump_json() if isinstance(graph, Graph) else graph info_dict["invokeai_graph"] = graph_json pnginfo.add_text("invokeai_graph", graph_json) diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py index dc62e71656..60c8c12b1b 100644 --- a/invokeai/app/services/images/images_base.py +++ b/invokeai/app/services/images/images_base.py @@ -54,7 +54,7 @@ class ImageServiceABC(ABC): is_intermediate: Optional[bool] = False, metadata: Optional[MetadataField] = None, workflow: Optional[WorkflowWithoutID] = None, - graph: Optional[Graph] = None, + graph: Optional[Graph | str] = None, ) -> ImageDTO: """Creates an image, storing the file and its metadata.""" pass diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py index 346baf1b83..a591045a72 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -45,7 +45,7 @@ class ImageService(ImageServiceABC): is_intermediate: Optional[bool] = False, metadata: Optional[MetadataField] = None, workflow: Optional[WorkflowWithoutID] = None, - graph: Optional[Graph] = None, + graph: Optional[Graph | str] = None, ) -> ImageDTO: if image_origin not in ResourceOrigin: raise InvalidOriginException From 5928ade5fde041fd3cc6e029a6bd166aa651f1a1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 19:25:04 +1000 Subject: [PATCH 177/442] feat(app): simplified create image API Graph, metadata and workflow all take stringified JSON only. This makes the API consistent and means we don't need to do a round-trip of pydantic parsing when handling this data. It also prevents a failure mode where an uploaded image's metadata, workflow or graph are old and don't match the current schema. As before, the frontend does strict validation and parsing when loading these values. --- invokeai/app/api/routers/images.py | 27 +++++++++---------- .../services/image_files/image_files_base.py | 10 +++---- .../services/image_files/image_files_disk.py | 24 +++++++---------- invokeai/app/services/images/images_base.py | 8 +++--- .../app/services/shared/invocation_context.py | 18 +++++++++---- 5 files changed, 40 insertions(+), 47 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 8cd7e16e28..9c55ff6531 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -6,13 +6,12 @@ from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, from fastapi.responses import FileResponse from fastapi.routing import APIRouter from PIL import Image -from pydantic import BaseModel, Field, ValidationError +from pydantic import BaseModel, Field -from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator +from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutIDValidator from ..dependencies import ApiDependencies @@ -64,21 +63,19 @@ async def upload_image( # TODO: retain non-invokeai metadata on upload? # attempt to parse metadata from image metadata_raw = pil_image.info.get("invokeai_metadata", None) - if metadata_raw: - try: - metadata = MetadataFieldValidator.validate_json(metadata_raw) - except ValidationError: - ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") - pass + if isinstance(metadata_raw, str): + metadata = metadata_raw + else: + ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") + pass # attempt to parse workflow from image workflow_raw = pil_image.info.get("invokeai_workflow", None) - if workflow_raw is not None: - try: - workflow = WorkflowWithoutIDValidator.validate_json(workflow_raw) - except ValidationError: - ApiDependencies.invoker.services.logger.warn("Failed to parse workflow for uploaded image") - pass + if isinstance(workflow_raw, str): + workflow = workflow_raw + else: + ApiDependencies.invoker.services.logger.warn("Failed to parse workflow for uploaded image") + pass # attempt to extract graph from image graph_raw = pil_image.info.get("invokeai_graph", None) diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py index 34b5054bd0..dc6609aa48 100644 --- a/invokeai/app/services/image_files/image_files_base.py +++ b/invokeai/app/services/image_files/image_files_base.py @@ -4,10 +4,6 @@ from typing import Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.fields import MetadataField -from invokeai.app.services.shared.graph import Graph -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID - class ImageFileStorageBase(ABC): """Low-level service responsible for storing and retrieving image files.""" @@ -34,9 +30,9 @@ class ImageFileStorageBase(ABC): self, image: PILImageType, image_name: str, - metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowWithoutID] = None, - graph: Optional[Graph | str] = None, + metadata: Optional[str] = None, + workflow: Optional[str] = None, + graph: Optional[str] = None, thumbnail_size: int = 256, ) -> None: """Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image name, thumbnail name, and created timestamp.""" diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py index 9f29d333ec..15d0be31f8 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -7,10 +7,7 @@ from PIL import Image, PngImagePlugin from PIL.Image import Image as PILImageType from send2trash import send2trash -from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.invoker import Invoker -from invokeai.app.services.shared.graph import Graph -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail from .image_files_base import ImageFileStorageBase @@ -57,9 +54,9 @@ class DiskImageFileStorage(ImageFileStorageBase): self, image: PILImageType, image_name: str, - metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowWithoutID] = None, - graph: Optional[Graph | str] = None, + metadata: Optional[str] = None, + workflow: Optional[str] = None, + graph: Optional[str] = None, thumbnail_size: int = 256, ) -> None: try: @@ -70,17 +67,14 @@ class DiskImageFileStorage(ImageFileStorageBase): info_dict = {} if metadata is not None: - metadata_json = metadata.model_dump_json() - info_dict["invokeai_metadata"] = metadata_json - pnginfo.add_text("invokeai_metadata", metadata_json) + info_dict["invokeai_metadata"] = metadata + pnginfo.add_text("invokeai_metadata", metadata) if workflow is not None: - workflow_json = workflow.model_dump_json() - info_dict["invokeai_workflow"] = workflow_json - pnginfo.add_text("invokeai_workflow", workflow_json) + info_dict["invokeai_workflow"] = workflow + pnginfo.add_text("invokeai_workflow", workflow) if graph is not None: - graph_json = graph.model_dump_json() if isinstance(graph, Graph) else graph - info_dict["invokeai_graph"] = graph_json - pnginfo.add_text("invokeai_graph", graph_json) + info_dict["invokeai_graph"] = graph + pnginfo.add_text("invokeai_graph", graph) # When saving the image, the image object's info field is not populated. We need to set it image.info = info_dict diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py index 60c8c12b1b..9175fc4809 100644 --- a/invokeai/app/services/images/images_base.py +++ b/invokeai/app/services/images/images_base.py @@ -11,9 +11,7 @@ from invokeai.app.services.image_records.image_records_common import ( ResourceOrigin, ) from invokeai.app.services.images.images_common import ImageDTO -from invokeai.app.services.shared.graph import Graph from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID class ImageServiceABC(ABC): @@ -52,9 +50,9 @@ class ImageServiceABC(ABC): session_id: Optional[str] = None, board_id: Optional[str] = None, is_intermediate: Optional[bool] = False, - metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowWithoutID] = None, - graph: Optional[Graph | str] = None, + metadata: Optional[str] = None, + workflow: Optional[str] = None, + graph: Optional[str] = None, ) -> ImageDTO: """Creates an image, storing the file and its metadata.""" pass diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 8685fd0c99..de31a42665 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -180,9 +180,9 @@ class ImagesInterface(InvocationContextInterface): # If `metadata` is provided directly, use that. Else, use the metadata provided by `WithMetadata`, falling back to None. metadata_ = None if metadata: - metadata_ = metadata - elif isinstance(self._data.invocation, WithMetadata): - metadata_ = self._data.invocation.metadata + metadata_ = metadata.model_dump_json() + elif isinstance(self._data.invocation, WithMetadata) and self._data.invocation.metadata: + metadata_ = self._data.invocation.metadata.model_dump_json() # If `board_id` is provided directly, use that. Else, use the board provided by `WithBoard`, falling back to None. board_id_ = None @@ -191,6 +191,14 @@ class ImagesInterface(InvocationContextInterface): elif isinstance(self._data.invocation, WithBoard) and self._data.invocation.board: board_id_ = self._data.invocation.board.board_id + workflow_ = None + if self._data.queue_item.workflow: + workflow_ = self._data.queue_item.workflow.model_dump_json() + + graph_ = None + if self._data.queue_item.session.graph: + graph_ = self._data.queue_item.session.graph.model_dump_json() + return self._services.images.create( image=image, is_intermediate=self._data.invocation.is_intermediate, @@ -198,8 +206,8 @@ class ImagesInterface(InvocationContextInterface): board_id=board_id_, metadata=metadata_, image_origin=ResourceOrigin.INTERNAL, - workflow=self._data.queue_item.workflow, - graph=self._data.queue_item.session.graph, + workflow=workflow_, + graph=graph_, session_id=self._data.queue_item.session_id, node_id=self._data.invocation.id, ) From 985ef898256f4d0a6f3886e38aa3c458a20a3729 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 08:54:32 +1000 Subject: [PATCH 178/442] fix(app): type annotations in images service --- invokeai/app/services/images/images_default.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py index a591045a72..1206526bd5 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -4,9 +4,7 @@ from PIL.Image import Image as PILImageType from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.invoker import Invoker -from invokeai.app.services.shared.graph import Graph from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from ..image_files.image_files_common import ( ImageFileDeleteException, @@ -43,9 +41,9 @@ class ImageService(ImageServiceABC): session_id: Optional[str] = None, board_id: Optional[str] = None, is_intermediate: Optional[bool] = False, - metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowWithoutID] = None, - graph: Optional[Graph | str] = None, + metadata: Optional[str] = None, + workflow: Optional[str] = None, + graph: Optional[str] = None, ) -> ImageDTO: if image_origin not in ResourceOrigin: raise InvalidOriginException From b0cfca9d24f9fda6825b3a97fef5f1269364387b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 08:55:15 +1000 Subject: [PATCH 179/442] fix(app): pass image metadata as stringified json --- invokeai/app/services/image_records/image_records_base.py | 2 +- invokeai/app/services/image_records/image_records_sqlite.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/image_records/image_records_base.py b/invokeai/app/services/image_records/image_records_base.py index 7b7b261eca..45c0705090 100644 --- a/invokeai/app/services/image_records/image_records_base.py +++ b/invokeai/app/services/image_records/image_records_base.py @@ -80,7 +80,7 @@ class ImageRecordStorageBase(ABC): starred: Optional[bool] = False, session_id: Optional[str] = None, node_id: Optional[str] = None, - metadata: Optional[MetadataField] = None, + metadata: Optional[str] = None, ) -> datetime: """Saves an image record.""" pass diff --git a/invokeai/app/services/image_records/image_records_sqlite.py b/invokeai/app/services/image_records/image_records_sqlite.py index 5b37913c8f..ef73e79fa1 100644 --- a/invokeai/app/services/image_records/image_records_sqlite.py +++ b/invokeai/app/services/image_records/image_records_sqlite.py @@ -328,10 +328,9 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): starred: Optional[bool] = False, session_id: Optional[str] = None, node_id: Optional[str] = None, - metadata: Optional[MetadataField] = None, + metadata: Optional[str] = None, ) -> datetime: try: - metadata_json = metadata.model_dump_json() if metadata is not None else None self._lock.acquire() self._cursor.execute( """--sql @@ -358,7 +357,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): height, node_id, session_id, - metadata_json, + metadata, is_intermediate, starred, has_workflow, From 5f3e7afd45fb250d6a0a4a10052ddb556a684851 Mon Sep 17 00:00:00 2001 From: maryhipp Date: Fri, 17 May 2024 12:41:21 -0400 Subject: [PATCH 180/442] add nullable user to invocation error events --- invokeai/app/services/events/events_base.py | 2 ++ .../app/services/session_processor/session_processor_default.py | 1 + 2 files changed, 3 insertions(+) diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 6373ec1f78..100e5c024f 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -122,6 +122,7 @@ class EventServiceBase: source_node_id: str, error_type: str, error: str, + user_id: str | None, ) -> None: """Emitted when an invocation has completed""" self.__emit_queue_event( @@ -135,6 +136,7 @@ class EventServiceBase: "source_node_id": source_node_id, "error_type": error_type, "error": error, + "user_id": user_id }, ) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index 61270e0879..de963867d5 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -237,6 +237,7 @@ class DefaultSessionProcessor(SessionProcessorBase): source_node_id=source_invocation_id, error_type=e.__class__.__name__, error=error, + user_id=None ) pass From b6b7e737e0a9a0cbf3cfc2e234c4d9017d030d96 Mon Sep 17 00:00:00 2001 From: maryhipp Date: Fri, 17 May 2024 12:46:24 -0400 Subject: [PATCH 181/442] ruff --- invokeai/app/services/events/events_base.py | 2 +- .../app/services/session_processor/session_processor_default.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 100e5c024f..62dfb69f0e 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -136,7 +136,7 @@ class EventServiceBase: "source_node_id": source_node_id, "error_type": error_type, "error": error, - "user_id": user_id + "user_id": user_id, }, ) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index de963867d5..05f6a0496d 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -237,7 +237,7 @@ class DefaultSessionProcessor(SessionProcessorBase): source_node_id=source_invocation_id, error_type=e.__class__.__name__, error=error, - user_id=None + user_id=None, ) pass From 84e031edc27e812581f1f8b0b46604433889c3f8 Mon Sep 17 00:00:00 2001 From: maryhipp Date: Fri, 17 May 2024 16:14:04 -0400 Subject: [PATCH 182/442] add nulable project also --- invokeai/app/services/events/events_base.py | 2 ++ .../app/services/session_processor/session_processor_default.py | 1 + 2 files changed, 3 insertions(+) diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 62dfb69f0e..5fbef3d86d 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -123,6 +123,7 @@ class EventServiceBase: error_type: str, error: str, user_id: str | None, + project_id: str | None, ) -> None: """Emitted when an invocation has completed""" self.__emit_queue_event( @@ -137,6 +138,7 @@ class EventServiceBase: "error_type": error_type, "error": error, "user_id": user_id, + "project_id": project_id }, ) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index 05f6a0496d..ee3ce7da15 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -238,6 +238,7 @@ class DefaultSessionProcessor(SessionProcessorBase): error_type=e.__class__.__name__, error=error, user_id=None, + project_id=None ) pass From 17e1fc5254c15167b721ef6e3f174e9c928b5eb2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 09:08:48 +1000 Subject: [PATCH 183/442] chore(app): ruff --- invokeai/app/services/events/events_base.py | 2 +- .../app/services/session_processor/session_processor_default.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 5fbef3d86d..aa91cdaec8 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -138,7 +138,7 @@ class EventServiceBase: "error_type": error_type, "error": error, "user_id": user_id, - "project_id": project_id + "project_id": project_id, }, ) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index ee3ce7da15..894996b1e6 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -238,7 +238,7 @@ class DefaultSessionProcessor(SessionProcessorBase): error_type=e.__class__.__name__, error=error, user_id=None, - project_id=None + project_id=None, ) pass From 811d0da0f0fb6cceda0eb5b148bdb2bd93852b0d Mon Sep 17 00:00:00 2001 From: Shukri Date: Sat, 18 May 2024 03:37:34 +0200 Subject: [PATCH 184/442] docs: fix link to. install reqs --- docs/installation/020_INSTALL_MANUAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/020_INSTALL_MANUAL.md b/docs/installation/020_INSTALL_MANUAL.md index 36859a5795..0d7150387c 100644 --- a/docs/installation/020_INSTALL_MANUAL.md +++ b/docs/installation/020_INSTALL_MANUAL.md @@ -10,7 +10,7 @@ InvokeAI is distributed as a python package on PyPI, installable with `pip`. The ### Requirements -Before you start, go through the [installation requirements]. +Before you start, go through the [installation requirements](./INSTALL_REQUIREMENTS.md). ### Installation Walkthrough From a5d08c981b800f565ea4cc7e9137466868338142 Mon Sep 17 00:00:00 2001 From: Shukri Date: Sat, 18 May 2024 03:53:30 +0200 Subject: [PATCH 185/442] docs: fix typo in --root arg of invokeai-web --- docs/installation/020_INSTALL_MANUAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/020_INSTALL_MANUAL.md b/docs/installation/020_INSTALL_MANUAL.md index 0d7150387c..a3868c8fcb 100644 --- a/docs/installation/020_INSTALL_MANUAL.md +++ b/docs/installation/020_INSTALL_MANUAL.md @@ -116,4 +116,4 @@ Before you start, go through the [installation requirements](./INSTALL_REQUIREME !!! warning - If the virtual environment is _not_ inside the root directory, then you _must_ specify the path to the root directory with `--root_dir \path\to\invokeai` or the `INVOKEAI_ROOT` environment variable. + If the virtual environment is _not_ inside the root directory, then you _must_ specify the path to the root directory with `--root \path\to\invokeai` or the `INVOKEAI_ROOT` environment variable. From e8387d75239f7ee6e52aa9cf5e978d01277f1eba Mon Sep 17 00:00:00 2001 From: Shukri Date: Sat, 18 May 2024 03:55:49 +0200 Subject: [PATCH 186/442] docs: add link to tool on pytorch website --- docs/installation/020_INSTALL_MANUAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/020_INSTALL_MANUAL.md b/docs/installation/020_INSTALL_MANUAL.md index a3868c8fcb..f589848b05 100644 --- a/docs/installation/020_INSTALL_MANUAL.md +++ b/docs/installation/020_INSTALL_MANUAL.md @@ -79,7 +79,7 @@ Before you start, go through the [installation requirements](./INSTALL_REQUIREME 1. Install the InvokeAI Package. The base command is `pip install InvokeAI --use-pep517`, but you may need to change this depending on your system and the desired features. - - You may need to provide an [extra index URL]. Select your platform configuration using [this tool on the PyTorch website]. Copy the `--extra-index-url` string from this and append it to your install command. + - You may need to provide an [extra index URL]. Select your platform configuration using [this tool on the PyTorch website](https://pytorch.org/get-started/locally/). Copy the `--extra-index-url` string from this and append it to your install command. !!! example "Install with an extra index URL" From 124d34a8cc9155db06052f1f3e806088a2e7e3d1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 14:25:19 +1000 Subject: [PATCH 187/442] docs: add link for `--extra-index-url` --- docs/installation/020_INSTALL_MANUAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/020_INSTALL_MANUAL.md b/docs/installation/020_INSTALL_MANUAL.md index f589848b05..059834eb45 100644 --- a/docs/installation/020_INSTALL_MANUAL.md +++ b/docs/installation/020_INSTALL_MANUAL.md @@ -79,7 +79,7 @@ Before you start, go through the [installation requirements](./INSTALL_REQUIREME 1. Install the InvokeAI Package. The base command is `pip install InvokeAI --use-pep517`, but you may need to change this depending on your system and the desired features. - - You may need to provide an [extra index URL]. Select your platform configuration using [this tool on the PyTorch website](https://pytorch.org/get-started/locally/). Copy the `--extra-index-url` string from this and append it to your install command. + - You may need to provide an [extra index URL](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-extra-index-url). Select your platform configuration using [this tool on the PyTorch website](https://pytorch.org/get-started/locally/). Copy the `--extra-index-url` string from this and append it to your install command. !!! example "Install with an extra index URL" From 5127fd6320c44ef77f0bb8b074b5468ae4470141 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 14:16:58 +1000 Subject: [PATCH 188/442] fix(ui): control adapter autoprocess jank If you change the control model and the new model has the same default processor, we would still re-process the image, even if there was no need to do so. With this change, if the image and processor config are unchanged, we bail out. --- .../listeners/controlAdapterPreprocessor.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index 2a59cc0317..ad464249df 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -16,6 +16,7 @@ import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { isImageOutput } from 'features/nodes/types/common'; import { addToast } from 'features/system/store/systemSlice'; import { t } from 'i18next'; +import { isEqual } from 'lodash-es'; import { getImageDTO } from 'services/api/endpoints/images'; import { queueApi } from 'services/api/endpoints/queue'; import type { BatchConfig } from 'services/api/types'; @@ -47,8 +48,10 @@ const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batc export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => { startAppListening({ matcher, - effect: async (action, { dispatch, getState, cancelActiveListeners, delay, take, signal }) => { + effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => { const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId; + const state = getState(); + const originalState = getOriginalState(); // Cancel any in-progress instances of this listener cancelActiveListeners(); @@ -57,18 +60,27 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // Delay before starting actual work await delay(DEBOUNCE_MS); - // Double-check that we are still eligible for processing - const state = getState(); const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); - // If we have no image or there is no processor config, bail if (!layer) { return; } + // We should only process if the processor settings or image have changed + const originalLayer = originalState.controlLayers.present.layers + .filter(isControlAdapterLayer) + .find((l) => l.id === layerId); + const originalImage = originalLayer?.controlAdapter.image; + const originalConfig = originalLayer?.controlAdapter.processorConfig; + const image = layer.controlAdapter.image; const config = layer.controlAdapter.processorConfig; + if (isEqual(config, originalConfig) && isEqual(image, originalImage)) { + // Neither config nor image have changed, we can bail + return; + } + if (!image || !config) { // The user has reset the image or config, so we should clear the processed image dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); From af3fd26d4e74b4aa005ab363c0badd13bd30a616 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 14:19:54 +1000 Subject: [PATCH 189/442] fix(ui): bug when clearing processor When clearing the processor config, we shouldn't re-process the image. This logic wasn't handled correctly, but coincidentally the bug didn't cause a user-facing issue. Without a config, we had a runtime error when trying to build the node for the processor graph and the listener failed. So while we didn't re-process the image, it was because there was an error, not because the logic was correct. Fix this by bailing if there is no image or config. --- .../listeners/controlAdapterPreprocessor.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index ad464249df..3dc8db93f9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -82,8 +82,11 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni } if (!image || !config) { - // The user has reset the image or config, so we should clear the processed image + // - If we have no image, we have nothing to process + // - If we have no processor config, we have nothing to process + // Clear the processed image and bail dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); + return; } // At this point, the user has stopped fiddling with the processor settings and there is a processor selected. @@ -93,8 +96,8 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni cancelProcessorBatch(dispatch, layerId, layer.controlAdapter.processorPendingBatchId); } - // @ts-expect-error: TS isn't able to narrow the typing of buildNode and `config` will error... - const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config); + // TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now + const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config as never); const enqueueBatchArg: BatchConfig = { prepend: true, batch: { From 85a5a7c47a76a75ba3244bf449084886b03c414e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 20:08:32 +1000 Subject: [PATCH 190/442] feat(ui): add `originalType` to FieldType, improved connection validation We now keep track of the original field type, derived from the python type annotation in addition to the override type provided by `ui_type`. This makes `ui_type` work more like it sound like it should work - change the UI input component only. Connection validation is extend to also check the original types. If there is any match between two fields' "final" or original types, we consider the connection valid.This change is backwards-compatible; there is no workflow migration needed. --- .../nodes/hooks/useIsValidConnection.ts | 5 +- .../store/util/findConnectionToValidHandle.ts | 6 +- .../util/makeIsConnectionValidSelector.ts | 5 +- .../util/validateSourceAndTargetTypes.ts | 26 +- .../web/src/features/nodes/types/field.ts | 231 ++++++++++++------ .../util/schema/buildFieldInputTemplate.ts | 188 ++++---------- .../util/schema/buildFieldOutputTemplate.ts | 4 +- .../nodes/util/schema/parseFieldType.test.ts | 6 +- .../nodes/util/schema/parseFieldType.ts | 25 +- .../nodes/util/schema/parseSchema.test.ts | 152 ++++++++++++ .../features/nodes/util/schema/parseSchema.ts | 200 ++++++++------- 11 files changed, 502 insertions(+), 346 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index 00b4b40176..14a7a728e0 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -4,9 +4,8 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { $templates } from 'features/nodes/store/nodesSlice'; import { getIsGraphAcyclic } from 'features/nodes/store/util/getIsGraphAcyclic'; import { getCollectItemType } from 'features/nodes/store/util/makeIsConnectionValidSelector'; -import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; +import { areTypesEqual, validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; -import { isEqual } from 'lodash-es'; import { useCallback } from 'react'; import type { Connection, Node } from 'reactflow'; @@ -70,7 +69,7 @@ export const useIsValidConnection = () => { // Collect nodes shouldn't mix and match field types const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); if (collectItemType) { - return isEqual(sourceFieldTemplate.type, collectItemType); + return areTypesEqual(sourceFieldTemplate.type, collectItemType); } } diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts index 1f33c52371..e0411ee67e 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts @@ -1,12 +1,12 @@ import type { PendingConnection, Templates } from 'features/nodes/store/types'; import { getCollectItemType } from 'features/nodes/store/util/makeIsConnectionValidSelector'; import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; -import { differenceWith, isEqual, map } from 'lodash-es'; +import { differenceWith, map } from 'lodash-es'; import type { Connection } from 'reactflow'; import { assert } from 'tsafe'; import { getIsGraphAcyclic } from './getIsGraphAcyclic'; -import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; +import { areTypesEqual, validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; export const getFirstValidConnection = ( templates: Templates, @@ -83,7 +83,7 @@ export const getFirstValidConnection = ( // Narrow candidates to same field type as already is connected to the collect node const collectItemType = getCollectItemType(templates, nodes, edges, pendingConnection.node.id); if (collectItemType) { - candidateFields = candidateFields.filter((field) => isEqual(field.type, collectItemType)); + candidateFields = candidateFields.filter((field) => areTypesEqual(field.type, collectItemType)); } } const candidateField = candidateFields.find((field) => { diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts index 90e75e0d87..e7f659508f 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts @@ -4,12 +4,11 @@ import type { PendingConnection, Templates } from 'features/nodes/store/types'; import type { FieldType } from 'features/nodes/types/field'; import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; import i18n from 'i18next'; -import { isEqual } from 'lodash-es'; import type { HandleType } from 'reactflow'; import { assert } from 'tsafe'; import { getIsGraphAcyclic } from './getIsGraphAcyclic'; -import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; +import { areTypesEqual, validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; export const getCollectItemType = ( templates: Templates, @@ -111,7 +110,7 @@ export const makeConnectionErrorSelector = ( // Collect nodes shouldn't mix and match field types const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); if (collectItemType) { - if (!isEqual(sourceType, collectItemType)) { + if (!areTypesEqual(sourceType, collectItemType)) { return i18n.t('nodes.cannotMixAndMatchCollectionItemTypes'); } } diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts index 3cbfb5b89c..cc5a6bb596 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts @@ -1,5 +1,25 @@ -import type { FieldType } from 'features/nodes/types/field'; -import { isEqual } from 'lodash-es'; +import { type FieldType, isStatefulFieldType } from 'features/nodes/types/field'; +import { isEqual, omit } from 'lodash-es'; + +export const areTypesEqual = (sourceType: FieldType, targetType: FieldType) => { + const _sourceType = isStatefulFieldType(sourceType) ? omit(sourceType, 'originalType') : sourceType; + const _targetType = isStatefulFieldType(targetType) ? omit(targetType, 'originalType') : targetType; + const _sourceTypeOriginal = isStatefulFieldType(sourceType) ? sourceType.originalType : sourceType; + const _targetTypeOriginal = isStatefulFieldType(targetType) ? targetType.originalType : targetType; + if (isEqual(_sourceType, _targetType)) { + return true; + } + if (isEqual(_sourceType, _targetTypeOriginal)) { + return true; + } + if (isEqual(_sourceTypeOriginal, _targetType)) { + return true; + } + if (isEqual(_sourceTypeOriginal, _targetTypeOriginal)) { + return true; + } + return false; +}; /** * Validates that the source and target types are compatible for a connection. @@ -15,7 +35,7 @@ export const validateSourceAndTargetTypes = (sourceType: FieldType, targetType: return false; } - if (isEqual(sourceType, targetType)) { + if (areTypesEqual(sourceType, targetType)) { return true; } diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts index 87b0839bc3..37e2a26397 100644 --- a/invokeai/frontend/web/src/features/nodes/types/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/field.ts @@ -66,16 +66,114 @@ export const zFieldIdentifier = z.object({ export type FieldIdentifier = z.infer; // #endregion -// #region IntegerField +// #region Field Types +const zStatelessFieldType = zFieldTypeBase.extend({ + name: z.string().min(1), // stateless --> we accept the field's name as the type +}); const zIntegerFieldType = zFieldTypeBase.extend({ name: z.literal('IntegerField'), + originalType: zStatelessFieldType.optional(), }); +const zFloatFieldType = zFieldTypeBase.extend({ + name: z.literal('FloatField'), + originalType: zStatelessFieldType.optional(), +}); +const zStringFieldType = zFieldTypeBase.extend({ + name: z.literal('StringField'), + originalType: zStatelessFieldType.optional(), +}); +const zBooleanFieldType = zFieldTypeBase.extend({ + name: z.literal('BooleanField'), + originalType: zStatelessFieldType.optional(), +}); +const zEnumFieldType = zFieldTypeBase.extend({ + name: z.literal('EnumField'), + originalType: zStatelessFieldType.optional(), +}); +const zImageFieldType = zFieldTypeBase.extend({ + name: z.literal('ImageField'), + originalType: zStatelessFieldType.optional(), +}); +const zBoardFieldType = zFieldTypeBase.extend({ + name: z.literal('BoardField'), + originalType: zStatelessFieldType.optional(), +}); +const zColorFieldType = zFieldTypeBase.extend({ + name: z.literal('ColorField'), + originalType: zStatelessFieldType.optional(), +}); +const zMainModelFieldType = zFieldTypeBase.extend({ + name: z.literal('MainModelField'), + originalType: zStatelessFieldType.optional(), +}); +const zSDXLMainModelFieldType = zFieldTypeBase.extend({ + name: z.literal('SDXLMainModelField'), + originalType: zStatelessFieldType.optional(), +}); +const zSDXLRefinerModelFieldType = zFieldTypeBase.extend({ + name: z.literal('SDXLRefinerModelField'), + originalType: zStatelessFieldType.optional(), +}); +const zVAEModelFieldType = zFieldTypeBase.extend({ + name: z.literal('VAEModelField'), + originalType: zStatelessFieldType.optional(), +}); +const zLoRAModelFieldType = zFieldTypeBase.extend({ + name: z.literal('LoRAModelField'), + originalType: zStatelessFieldType.optional(), +}); +const zControlNetModelFieldType = zFieldTypeBase.extend({ + name: z.literal('ControlNetModelField'), + originalType: zStatelessFieldType.optional(), +}); +const zIPAdapterModelFieldType = zFieldTypeBase.extend({ + name: z.literal('IPAdapterModelField'), + originalType: zStatelessFieldType.optional(), +}); +const zT2IAdapterModelFieldType = zFieldTypeBase.extend({ + name: z.literal('T2IAdapterModelField'), + originalType: zStatelessFieldType.optional(), +}); +const zSchedulerFieldType = zFieldTypeBase.extend({ + name: z.literal('SchedulerField'), + originalType: zStatelessFieldType.optional(), +}); +const zStatefulFieldType = z.union([ + zIntegerFieldType, + zFloatFieldType, + zStringFieldType, + zBooleanFieldType, + zEnumFieldType, + zImageFieldType, + zBoardFieldType, + zMainModelFieldType, + zSDXLMainModelFieldType, + zSDXLRefinerModelFieldType, + zVAEModelFieldType, + zLoRAModelFieldType, + zControlNetModelFieldType, + zIPAdapterModelFieldType, + zT2IAdapterModelFieldType, + zColorFieldType, + zSchedulerFieldType, +]); +export type StatefulFieldType = z.infer; +const statefulFieldTypeNames = zStatefulFieldType.options.map((o) => o.shape.name.value); +export const isStatefulFieldType = (fieldType: FieldType): fieldType is StatefulFieldType => + statefulFieldTypeNames.includes(fieldType.name as any); +const zFieldType = z.union([zStatefulFieldType, zStatelessFieldType]); +export type FieldType = z.infer; +// #endregion + +// #region IntegerField + export const zIntegerFieldValue = z.number().int(); const zIntegerFieldInputInstance = zFieldInputInstanceBase.extend({ value: zIntegerFieldValue, }); const zIntegerFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zIntegerFieldType, + originalType: zFieldType.optional(), default: zIntegerFieldValue, multipleOf: z.number().int().optional(), maximum: z.number().int().optional(), @@ -85,6 +183,7 @@ const zIntegerFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zIntegerFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zIntegerFieldType, + originalType: zFieldType.optional(), }); export type IntegerFieldValue = z.infer; export type IntegerFieldInputInstance = z.infer; @@ -96,15 +195,14 @@ export const isIntegerFieldInputTemplate = (val: unknown): val is IntegerFieldIn // #endregion // #region FloatField -const zFloatFieldType = zFieldTypeBase.extend({ - name: z.literal('FloatField'), -}); + export const zFloatFieldValue = z.number(); const zFloatFieldInputInstance = zFieldInputInstanceBase.extend({ value: zFloatFieldValue, }); const zFloatFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zFloatFieldType, + originalType: zFieldType.optional(), default: zFloatFieldValue, multipleOf: z.number().optional(), maximum: z.number().optional(), @@ -114,6 +212,7 @@ const zFloatFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zFloatFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zFloatFieldType, + originalType: zFieldType.optional(), }); export type FloatFieldValue = z.infer; export type FloatFieldInputInstance = z.infer; @@ -125,21 +224,21 @@ export const isFloatFieldInputTemplate = (val: unknown): val is FloatFieldInputT // #endregion // #region StringField -const zStringFieldType = zFieldTypeBase.extend({ - name: z.literal('StringField'), -}); + export const zStringFieldValue = z.string(); const zStringFieldInputInstance = zFieldInputInstanceBase.extend({ value: zStringFieldValue, }); const zStringFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zStringFieldType, + originalType: zFieldType.optional(), default: zStringFieldValue, maxLength: z.number().int().optional(), minLength: z.number().int().optional(), }); const zStringFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zStringFieldType, + originalType: zFieldType.optional(), }); export type StringFieldValue = z.infer; @@ -152,19 +251,19 @@ export const isStringFieldInputTemplate = (val: unknown): val is StringFieldInpu // #endregion // #region BooleanField -const zBooleanFieldType = zFieldTypeBase.extend({ - name: z.literal('BooleanField'), -}); + export const zBooleanFieldValue = z.boolean(); const zBooleanFieldInputInstance = zFieldInputInstanceBase.extend({ value: zBooleanFieldValue, }); const zBooleanFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zBooleanFieldType, + originalType: zFieldType.optional(), default: zBooleanFieldValue, }); const zBooleanFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zBooleanFieldType, + originalType: zFieldType.optional(), }); export type BooleanFieldValue = z.infer; export type BooleanFieldInputInstance = z.infer; @@ -176,21 +275,21 @@ export const isBooleanFieldInputTemplate = (val: unknown): val is BooleanFieldIn // #endregion // #region EnumField -const zEnumFieldType = zFieldTypeBase.extend({ - name: z.literal('EnumField'), -}); + export const zEnumFieldValue = z.string(); const zEnumFieldInputInstance = zFieldInputInstanceBase.extend({ value: zEnumFieldValue, }); const zEnumFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zEnumFieldType, + originalType: zFieldType.optional(), default: zEnumFieldValue, options: z.array(z.string()), labels: z.record(z.string()).optional(), }); const zEnumFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zEnumFieldType, + originalType: zFieldType.optional(), }); export type EnumFieldValue = z.infer; export type EnumFieldInputInstance = z.infer; @@ -202,19 +301,19 @@ export const isEnumFieldInputTemplate = (val: unknown): val is EnumFieldInputTem // #endregion // #region ImageField -const zImageFieldType = zFieldTypeBase.extend({ - name: z.literal('ImageField'), -}); + export const zImageFieldValue = zImageField.optional(); const zImageFieldInputInstance = zFieldInputInstanceBase.extend({ value: zImageFieldValue, }); const zImageFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zImageFieldType, + originalType: zFieldType.optional(), default: zImageFieldValue, }); const zImageFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zImageFieldType, + originalType: zFieldType.optional(), }); export type ImageFieldValue = z.infer; export type ImageFieldInputInstance = z.infer; @@ -226,19 +325,19 @@ export const isImageFieldInputTemplate = (val: unknown): val is ImageFieldInputT // #endregion // #region BoardField -const zBoardFieldType = zFieldTypeBase.extend({ - name: z.literal('BoardField'), -}); + export const zBoardFieldValue = zBoardField.optional(); const zBoardFieldInputInstance = zFieldInputInstanceBase.extend({ value: zBoardFieldValue, }); const zBoardFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zBoardFieldType, + originalType: zFieldType.optional(), default: zBoardFieldValue, }); const zBoardFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zBoardFieldType, + originalType: zFieldType.optional(), }); export type BoardFieldValue = z.infer; export type BoardFieldInputInstance = z.infer; @@ -250,19 +349,19 @@ export const isBoardFieldInputTemplate = (val: unknown): val is BoardFieldInputT // #endregion // #region ColorField -const zColorFieldType = zFieldTypeBase.extend({ - name: z.literal('ColorField'), -}); + export const zColorFieldValue = zColorField.optional(); const zColorFieldInputInstance = zFieldInputInstanceBase.extend({ value: zColorFieldValue, }); const zColorFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zColorFieldType, + originalType: zFieldType.optional(), default: zColorFieldValue, }); const zColorFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zColorFieldType, + originalType: zFieldType.optional(), }); export type ColorFieldValue = z.infer; export type ColorFieldInputInstance = z.infer; @@ -274,19 +373,19 @@ export const isColorFieldInputTemplate = (val: unknown): val is ColorFieldInputT // #endregion // #region MainModelField -const zMainModelFieldType = zFieldTypeBase.extend({ - name: z.literal('MainModelField'), -}); + export const zMainModelFieldValue = zModelIdentifierField.optional(); const zMainModelFieldInputInstance = zFieldInputInstanceBase.extend({ value: zMainModelFieldValue, }); const zMainModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zMainModelFieldType, + originalType: zFieldType.optional(), default: zMainModelFieldValue, }); const zMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zMainModelFieldType, + originalType: zFieldType.optional(), }); export type MainModelFieldValue = z.infer; export type MainModelFieldInputInstance = z.infer; @@ -298,19 +397,19 @@ export const isMainModelFieldInputTemplate = (val: unknown): val is MainModelFie // #endregion // #region SDXLMainModelField -const zSDXLMainModelFieldType = zFieldTypeBase.extend({ - name: z.literal('SDXLMainModelField'), -}); + const zSDXLMainModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL models only. const zSDXLMainModelFieldInputInstance = zFieldInputInstanceBase.extend({ value: zSDXLMainModelFieldValue, }); const zSDXLMainModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zSDXLMainModelFieldType, + originalType: zFieldType.optional(), default: zSDXLMainModelFieldValue, }); const zSDXLMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zSDXLMainModelFieldType, + originalType: zFieldType.optional(), }); export type SDXLMainModelFieldInputInstance = z.infer; export type SDXLMainModelFieldInputTemplate = z.infer; @@ -321,9 +420,7 @@ export const isSDXLMainModelFieldInputTemplate = (val: unknown): val is SDXLMain // #endregion // #region SDXLRefinerModelField -const zSDXLRefinerModelFieldType = zFieldTypeBase.extend({ - name: z.literal('SDXLRefinerModelField'), -}); + /** @alias */ // tells knip to ignore this duplicate export export const zSDXLRefinerModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL Refiner models only. const zSDXLRefinerModelFieldInputInstance = zFieldInputInstanceBase.extend({ @@ -331,10 +428,12 @@ const zSDXLRefinerModelFieldInputInstance = zFieldInputInstanceBase.extend({ }); const zSDXLRefinerModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zSDXLRefinerModelFieldType, + originalType: zFieldType.optional(), default: zSDXLRefinerModelFieldValue, }); const zSDXLRefinerModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zSDXLRefinerModelFieldType, + originalType: zFieldType.optional(), }); export type SDXLRefinerModelFieldValue = z.infer; export type SDXLRefinerModelFieldInputInstance = z.infer; @@ -346,19 +445,19 @@ export const isSDXLRefinerModelFieldInputTemplate = (val: unknown): val is SDXLR // #endregion // #region VAEModelField -const zVAEModelFieldType = zFieldTypeBase.extend({ - name: z.literal('VAEModelField'), -}); + export const zVAEModelFieldValue = zModelIdentifierField.optional(); const zVAEModelFieldInputInstance = zFieldInputInstanceBase.extend({ value: zVAEModelFieldValue, }); const zVAEModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zVAEModelFieldType, + originalType: zFieldType.optional(), default: zVAEModelFieldValue, }); const zVAEModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zVAEModelFieldType, + originalType: zFieldType.optional(), }); export type VAEModelFieldValue = z.infer; export type VAEModelFieldInputInstance = z.infer; @@ -370,19 +469,19 @@ export const isVAEModelFieldInputTemplate = (val: unknown): val is VAEModelField // #endregion // #region LoRAModelField -const zLoRAModelFieldType = zFieldTypeBase.extend({ - name: z.literal('LoRAModelField'), -}); + export const zLoRAModelFieldValue = zModelIdentifierField.optional(); const zLoRAModelFieldInputInstance = zFieldInputInstanceBase.extend({ value: zLoRAModelFieldValue, }); const zLoRAModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zLoRAModelFieldType, + originalType: zFieldType.optional(), default: zLoRAModelFieldValue, }); const zLoRAModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zLoRAModelFieldType, + originalType: zFieldType.optional(), }); export type LoRAModelFieldValue = z.infer; export type LoRAModelFieldInputInstance = z.infer; @@ -394,19 +493,19 @@ export const isLoRAModelFieldInputTemplate = (val: unknown): val is LoRAModelFie // #endregion // #region ControlNetModelField -const zControlNetModelFieldType = zFieldTypeBase.extend({ - name: z.literal('ControlNetModelField'), -}); + export const zControlNetModelFieldValue = zModelIdentifierField.optional(); const zControlNetModelFieldInputInstance = zFieldInputInstanceBase.extend({ value: zControlNetModelFieldValue, }); const zControlNetModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zControlNetModelFieldType, + originalType: zFieldType.optional(), default: zControlNetModelFieldValue, }); const zControlNetModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zControlNetModelFieldType, + originalType: zFieldType.optional(), }); export type ControlNetModelFieldValue = z.infer; export type ControlNetModelFieldInputInstance = z.infer; @@ -418,19 +517,19 @@ export const isControlNetModelFieldInputTemplate = (val: unknown): val is Contro // #endregion // #region IPAdapterModelField -const zIPAdapterModelFieldType = zFieldTypeBase.extend({ - name: z.literal('IPAdapterModelField'), -}); + export const zIPAdapterModelFieldValue = zModelIdentifierField.optional(); const zIPAdapterModelFieldInputInstance = zFieldInputInstanceBase.extend({ value: zIPAdapterModelFieldValue, }); const zIPAdapterModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zIPAdapterModelFieldType, + originalType: zFieldType.optional(), default: zIPAdapterModelFieldValue, }); const zIPAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zIPAdapterModelFieldType, + originalType: zFieldType.optional(), }); export type IPAdapterModelFieldValue = z.infer; export type IPAdapterModelFieldInputInstance = z.infer; @@ -442,19 +541,19 @@ export const isIPAdapterModelFieldInputTemplate = (val: unknown): val is IPAdapt // #endregion // #region T2IAdapterField -const zT2IAdapterModelFieldType = zFieldTypeBase.extend({ - name: z.literal('T2IAdapterModelField'), -}); + export const zT2IAdapterModelFieldValue = zModelIdentifierField.optional(); const zT2IAdapterModelFieldInputInstance = zFieldInputInstanceBase.extend({ value: zT2IAdapterModelFieldValue, }); const zT2IAdapterModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zT2IAdapterModelFieldType, + originalType: zFieldType.optional(), default: zT2IAdapterModelFieldValue, }); const zT2IAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zT2IAdapterModelFieldType, + originalType: zFieldType.optional(), }); export type T2IAdapterModelFieldValue = z.infer; export type T2IAdapterModelFieldInputInstance = z.infer; @@ -466,19 +565,19 @@ export const isT2IAdapterModelFieldInputTemplate = (val: unknown): val is T2IAda // #endregion // #region SchedulerField -const zSchedulerFieldType = zFieldTypeBase.extend({ - name: z.literal('SchedulerField'), -}); + export const zSchedulerFieldValue = zSchedulerField.optional(); const zSchedulerFieldInputInstance = zFieldInputInstanceBase.extend({ value: zSchedulerFieldValue, }); const zSchedulerFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zSchedulerFieldType, + originalType: zFieldType.optional(), default: zSchedulerFieldValue, }); const zSchedulerFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zSchedulerFieldType, + originalType: zFieldType.optional(), }); export type SchedulerFieldValue = z.infer; export type SchedulerFieldInputInstance = z.infer; @@ -501,20 +600,20 @@ export const isSchedulerFieldInputTemplate = (val: unknown): val is SchedulerFie * - Reserved fields like IsIntermediate * - Any other field we don't have full-on schemas for */ -const zStatelessFieldType = zFieldTypeBase.extend({ - name: z.string().min(1), // stateless --> we accept the field's name as the type -}); + const zStatelessFieldValue = z.undefined().catch(undefined); // stateless --> no value, but making this z.never() introduces a lot of extra TS fanagling const zStatelessFieldInputInstance = zFieldInputInstanceBase.extend({ value: zStatelessFieldValue, }); const zStatelessFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zStatelessFieldType, + originalType: zFieldType.optional(), default: zStatelessFieldValue, input: z.literal('connection'), // stateless --> only accepts connection inputs }); const zStatelessFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zStatelessFieldType, + originalType: zFieldType.optional(), }); export type StatelessFieldInputTemplate = z.infer; @@ -535,34 +634,6 @@ export type StatelessFieldInputTemplate = z.infer; -export const isStatefulFieldType = (val: unknown): val is StatefulFieldType => - zStatefulFieldType.safeParse(val).success; - -const zFieldType = z.union([zStatefulFieldType, zStatelessFieldType]); -export type FieldType = z.infer; -// #endregion - // #region StatefulFieldValue & FieldValue export const zStatefulFieldValue = z.union([ zIntegerFieldValue, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts index 3e8278ea6a..6b4c4d8b29 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts @@ -30,26 +30,16 @@ import { isNumber, startCase } from 'lodash-es'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type FieldInputTemplateBuilder = // valid `any`! - (arg: { - schemaObject: InvocationFieldSchema; - baseField: Omit; - isCollection: boolean; - isCollectionOrScalar: boolean; - }) => T; + (arg: { schemaObject: InvocationFieldSchema; baseField: Omit; fieldType: T['type'] }) => T; const buildIntegerFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: IntegerFieldInputTemplate = { ...baseField, - type: { - name: 'IntegerField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? 0, }; @@ -79,16 +69,11 @@ const buildIntegerFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: FloatFieldInputTemplate = { ...baseField, - type: { - name: 'FloatField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? 0, }; @@ -118,16 +103,11 @@ const buildFloatFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: StringFieldInputTemplate = { ...baseField, - type: { - name: 'StringField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? '', }; @@ -145,16 +125,11 @@ const buildStringFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: BooleanFieldInputTemplate = { ...baseField, - type: { - name: 'BooleanField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? false, }; @@ -164,16 +139,11 @@ const buildBooleanFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: MainModelFieldInputTemplate = { ...baseField, - type: { - name: 'MainModelField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -183,16 +153,11 @@ const buildMainModelFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: SDXLMainModelFieldInputTemplate = { ...baseField, - type: { - name: 'SDXLMainModelField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -202,16 +167,11 @@ const buildSDXLMainModelFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: SDXLRefinerModelFieldInputTemplate = { ...baseField, - type: { - name: 'SDXLRefinerModelField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -221,16 +181,11 @@ const buildRefinerModelFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: VAEModelFieldInputTemplate = { ...baseField, - type: { - name: 'VAEModelField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -240,16 +195,11 @@ const buildVAEModelFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: LoRAModelFieldInputTemplate = { ...baseField, - type: { - name: 'LoRAModelField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -259,16 +209,11 @@ const buildLoRAModelFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: ControlNetModelFieldInputTemplate = { ...baseField, - type: { - name: 'ControlNetModelField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -278,16 +223,11 @@ const buildControlNetModelFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: IPAdapterModelFieldInputTemplate = { ...baseField, - type: { - name: 'IPAdapterModelField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -297,16 +237,11 @@ const buildIPAdapterModelFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: T2IAdapterModelFieldInputTemplate = { ...baseField, - type: { - name: 'T2IAdapterModelField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -316,16 +251,11 @@ const buildT2IAdapterModelFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: BoardFieldInputTemplate = { ...baseField, - type: { - name: 'BoardField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -335,16 +265,11 @@ const buildBoardFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: ImageFieldInputTemplate = { ...baseField, - type: { - name: 'ImageField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? undefined, }; @@ -354,8 +279,7 @@ const buildImageFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { let options: EnumFieldInputTemplate['options'] = []; if (schemaObject.anyOf) { @@ -383,11 +307,7 @@ const buildEnumFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: ColorFieldInputTemplate = { ...baseField, - type: { - name: 'ColorField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? { r: 127, g: 127, b: 127, a: 255 }, }; @@ -418,16 +333,11 @@ const buildColorFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, - isCollection, - isCollectionOrScalar, + fieldType, }) => { const template: SchedulerFieldInputTemplate = { ...baseField, - type: { - name: 'SchedulerField', - isCollection, - isCollectionOrScalar, - }, + type: fieldType, default: schemaObject.default ?? 'euler', }; @@ -452,7 +362,7 @@ export const TEMPLATE_BUILDER_MAP: Record connection only inputs - type: fieldType, - default: undefined, // stateless --> no default value - }; - return template; + return template; + } else { + // This is a StatelessField, create it directly. + const template: StatelessFieldInputTemplate = { + ...baseField, + input: 'connection', // stateless --> connection only inputs + type: fieldType, + default: undefined, // stateless --> no default value + }; + + return template; + } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldOutputTemplate.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldOutputTemplate.ts index 8c789493ad..abbe2c3488 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldOutputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldOutputTemplate.ts @@ -9,7 +9,7 @@ export const buildFieldOutputTemplate = ( ): FieldOutputTemplate => { const { title, description, ui_hidden, ui_type, ui_order } = fieldSchema; - const fieldOutputTemplate: FieldOutputTemplate = { + const template: FieldOutputTemplate = { fieldKind: 'output', name: fieldName, title: title ?? (fieldName ? startCase(fieldName) : ''), @@ -20,5 +20,5 @@ export const buildFieldOutputTemplate = ( ui_order, }; - return fieldOutputTemplate; + return template; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts index d7011ad6f8..cc12b45aa6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts @@ -244,7 +244,7 @@ const specialCases: ParseFieldTypeTestCase[] = [ enum: ['ddim', 'ddpm', 'deis'], ui_type: 'SchedulerField', }, - expected: { name: 'SchedulerField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, }, { name: 'Explicit ui_type (AnyField)', @@ -253,7 +253,7 @@ const specialCases: ParseFieldTypeTestCase[] = [ enum: ['ddim', 'ddpm', 'deis'], ui_type: 'AnyField', }, - expected: { name: 'AnyField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, }, { name: 'Explicit ui_type (CollectionField)', @@ -262,7 +262,7 @@ const specialCases: ParseFieldTypeTestCase[] = [ enum: ['ddim', 'ddpm', 'deis'], ui_type: 'CollectionField', }, - expected: { name: 'CollectionField', isCollection: true, isCollectionOrScalar: false }, + expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, }, ]; diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts index 13da6b3831..6f6ecaa5bb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts @@ -6,14 +6,8 @@ import { UnsupportedUnionError, } from 'features/nodes/types/error'; import type { FieldType } from 'features/nodes/types/field'; -import type { InvocationFieldSchema, OpenAPIV3_1SchemaOrRef } from 'features/nodes/types/openapi'; -import { - isArraySchemaObject, - isInvocationFieldSchema, - isNonArraySchemaObject, - isRefObject, - isSchemaObject, -} from 'features/nodes/types/openapi'; +import type { OpenAPIV3_1SchemaOrRef } from 'features/nodes/types/openapi'; +import { isArraySchemaObject, isNonArraySchemaObject, isRefObject, isSchemaObject } from 'features/nodes/types/openapi'; import { t } from 'i18next'; import { isArray } from 'lodash-es'; import type { OpenAPIV3_1 } from 'openapi-types'; @@ -35,7 +29,7 @@ const OPENAPI_TO_FIELD_TYPE_MAP: Record = { boolean: 'BooleanField', }; -const isCollectionFieldType = (fieldType: string) => { +export const isCollectionFieldType = (fieldType: string) => { /** * CollectionField is `list[Any]` in the pydantic schema, but we need to distinguish between * it and other `list[Any]` fields, due to its special internal handling. @@ -48,18 +42,7 @@ const isCollectionFieldType = (fieldType: string) => { return false; }; -export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef | InvocationFieldSchema): FieldType => { - if (isInvocationFieldSchema(schemaObject)) { - // Check if this field has an explicit type provided by the node schema - const { ui_type } = schemaObject; - if (ui_type) { - return { - name: ui_type, - isCollection: isCollectionFieldType(ui_type), - isCollectionOrScalar: false, - }; - } - } +export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType => { if (isSchemaObject(schemaObject)) { if (schemaObject.const) { // Fields with a single const value are defined as `Literal["value"]` in the pydantic schema - it's actually an enum diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts index 6c0a6635c7..480387a8a4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts @@ -97,6 +97,11 @@ const expected = { name: 'SchedulerField', isCollection: false, isCollectionOrScalar: false, + originalType: { + name: 'EnumField', + isCollection: false, + isCollectionOrScalar: false, + }, }, default: 'euler', }, @@ -111,6 +116,11 @@ const expected = { name: 'SchedulerField', isCollection: false, isCollectionOrScalar: false, + originalType: { + name: 'EnumField', + isCollection: false, + isCollectionOrScalar: false, + }, }, ui_hidden: false, ui_type: 'SchedulerField', @@ -141,6 +151,11 @@ const expected = { name: 'MainModelField', isCollection: false, isCollectionOrScalar: false, + originalType: { + name: 'ModelIdentifierField', + isCollection: false, + isCollectionOrScalar: false, + }, }, }, }, @@ -186,6 +201,48 @@ const expected = { nodePack: 'invokeai', classification: 'stable', }, + collect: { + title: 'Collect', + type: 'collect', + version: '1.0.0', + tags: [], + description: 'Collects values into a collection', + outputType: 'collect_output', + inputs: { + item: { + name: 'item', + title: 'Collection Item', + required: false, + description: 'The item to collect (all inputs must be of the same type)', + fieldKind: 'input', + input: 'connection', + ui_hidden: false, + ui_type: 'CollectionItemField', + type: { + name: 'CollectionItemField', + isCollection: false, + isCollectionOrScalar: false, + }, + }, + }, + outputs: { + collection: { + fieldKind: 'output', + name: 'collection', + title: 'Collection', + description: 'The collection of input items', + type: { + name: 'CollectionField', + isCollection: true, + isCollectionOrScalar: false, + }, + ui_hidden: false, + ui_type: 'CollectionField', + }, + }, + useCache: true, + classification: 'stable', + }, }; const schema = { @@ -785,6 +842,101 @@ const schema = { type: 'object', class: 'output', }, + CollectInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + item: { + anyOf: [ + {}, + { + type: 'null', + }, + ], + title: 'Collection Item', + description: 'The item to collect (all inputs must be of the same type)', + field_kind: 'input', + input: 'connection', + orig_required: false, + ui_hidden: false, + ui_type: 'CollectionItemField', + }, + collection: { + items: {}, + type: 'array', + title: 'Collection', + description: 'The collection, will be provided on execution', + default: [], + field_kind: 'input', + input: 'any', + orig_default: [], + orig_required: false, + ui_hidden: true, + }, + type: { + type: 'string', + enum: ['collect'], + const: 'collect', + title: 'type', + default: 'collect', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['type', 'id'], + title: 'CollectInvocation', + description: 'Collects values into a collection', + classification: 'stable', + version: '1.0.0', + output: { + $ref: '#/components/schemas/CollectInvocationOutput', + }, + class: 'invocation', + }, + CollectInvocationOutput: { + properties: { + collection: { + description: 'The collection of input items', + field_kind: 'output', + items: {}, + title: 'Collection', + type: 'array', + ui_hidden: false, + ui_type: 'CollectionField', + }, + type: { + const: 'collect_output', + default: 'collect_output', + enum: ['collect_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + }, + required: ['collection', 'type', 'type'], + title: 'CollectInvocationOutput', + type: 'object', + class: 'output', + }, }, }, } as OpenAPIV3_1.Document; diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts index 3178209f93..0638a52954 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts @@ -1,23 +1,29 @@ import { logger } from 'app/logging/logger'; +import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; import type { Templates } from 'features/nodes/store/types'; import { FieldParseError } from 'features/nodes/types/error'; -import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; +import { + type FieldInputTemplate, + type FieldOutputTemplate, + type FieldType, + isStatefulFieldType, +} from 'features/nodes/types/field'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; -import type { InvocationSchemaObject } from 'features/nodes/types/openapi'; +import type { InvocationFieldSchema, InvocationSchemaObject } from 'features/nodes/types/openapi'; import { isInvocationFieldSchema, isInvocationOutputSchemaObject, isInvocationSchemaObject, } from 'features/nodes/types/openapi'; import { t } from 'i18next'; -import { reduce } from 'lodash-es'; +import { isEqual, reduce } from 'lodash-es'; import type { OpenAPIV3_1 } from 'openapi-types'; import { serializeError } from 'serialize-error'; import { buildFieldInputTemplate } from './buildFieldInputTemplate'; import { buildFieldOutputTemplate } from './buildFieldOutputTemplate'; -import { parseFieldType } from './parseFieldType'; +import { isCollectionFieldType, parseFieldType } from './parseFieldType'; const RESERVED_INPUT_FIELD_NAMES = ['id', 'type', 'use_cache']; const RESERVED_OUTPUT_FIELD_NAMES = ['type']; @@ -94,51 +100,43 @@ export const parseSchema = ( return inputsAccumulator; } - try { - const fieldType = parseFieldType(property); + const fieldTypeOverride = property.ui_type + ? { + name: property.ui_type, + isCollection: isCollectionFieldType(property.ui_type), + isCollectionOrScalar: false, + } + : null; - if (isReservedFieldType(fieldType.name)) { - logger('nodes').trace( - { node: type, field: propertyName, schema: parseify(property) }, - 'Skipped reserved input field' - ); - return inputsAccumulator; - } + const originalFieldType = getFieldType(property, propertyName, type, 'input'); - const fieldInputTemplate = buildFieldInputTemplate(property, propertyName, fieldType); - - inputsAccumulator[propertyName] = fieldInputTemplate; - } catch (e) { - if (e instanceof FieldParseError) { - logger('nodes').warn( - { - node: type, - field: propertyName, - schema: parseify(property), - }, - t('nodes.inputFieldTypeParseError', { - node: type, - field: propertyName, - message: e.message, - }) - ); - } else { - logger('nodes').warn( - { - node: type, - field: propertyName, - schema: parseify(property), - error: serializeError(e), - }, - t('nodes.inputFieldTypeParseError', { - node: type, - field: propertyName, - message: 'unknown error', - }) - ); - } + const fieldType = fieldTypeOverride ?? originalFieldType; + if (!fieldType) { + logger('nodes').trace( + { node: type, field: propertyName, schema: parseify(property) }, + 'Unable to parse field type' + ); + return inputsAccumulator; } + if (isReservedFieldType(fieldType.name)) { + logger('nodes').trace( + { node: type, field: propertyName, schema: parseify(property) }, + 'Skipped reserved input field' + ); + return inputsAccumulator; + } + + if (isStatefulFieldType(fieldType) && originalFieldType && !isEqual(originalFieldType, fieldType)) { + console.log('STATEFUL WITH ORIGINAL'); + fieldType.originalType = deepClone(originalFieldType); + console.log(fieldType); + } + + const fieldInputTemplate = buildFieldInputTemplate(property, propertyName, fieldType); + console.log(fieldInputTemplate); + inputsAccumulator[propertyName] = fieldInputTemplate; + return inputsAccumulator; }, {} @@ -183,54 +181,34 @@ export const parseSchema = ( return outputsAccumulator; } - try { - const fieldType = parseFieldType(property); + const fieldTypeOverride = property.ui_type + ? { + name: property.ui_type, + isCollection: isCollectionFieldType(property.ui_type), + isCollectionOrScalar: false, + } + : null; - if (!fieldType) { - logger('nodes').warn( - { - node: type, - field: propertyName, - schema: parseify(property), - }, - 'Missing output field type' - ); - return outputsAccumulator; - } + const originalFieldType = getFieldType(property, propertyName, type, 'output'); - const fieldOutputTemplate = buildFieldOutputTemplate(property, propertyName, fieldType); - - outputsAccumulator[propertyName] = fieldOutputTemplate; - } catch (e) { - if (e instanceof FieldParseError) { - logger('nodes').warn( - { - node: type, - field: propertyName, - schema: parseify(property), - }, - t('nodes.outputFieldTypeParseError', { - node: type, - field: propertyName, - message: e.message, - }) - ); - } else { - logger('nodes').warn( - { - node: type, - field: propertyName, - schema: parseify(property), - error: serializeError(e), - }, - t('nodes.outputFieldTypeParseError', { - node: type, - field: propertyName, - message: 'unknown error', - }) - ); - } + const fieldType = fieldTypeOverride ?? originalFieldType; + if (!fieldType) { + logger('nodes').trace( + { node: type, field: propertyName, schema: parseify(property) }, + 'Unable to parse field type' + ); + return outputsAccumulator; } + + if (isStatefulFieldType(fieldType) && originalFieldType && !isEqual(originalFieldType, fieldType)) { + console.log('STATEFUL WITH ORIGINAL'); + fieldType.originalType = deepClone(originalFieldType); + console.log(fieldType); + } + + const fieldOutputTemplate = buildFieldOutputTemplate(property, propertyName, fieldType); + + outputsAccumulator[propertyName] = fieldOutputTemplate; return outputsAccumulator; }, {} as Record @@ -259,3 +237,45 @@ export const parseSchema = ( return invocations; }; + +const getFieldType = ( + property: InvocationFieldSchema, + propertyName: string, + type: string, + kind: 'input' | 'output' +): FieldType | null => { + try { + return parseFieldType(property); + } catch (e) { + const tKey = kind === 'input' ? 'nodes.inputFieldTypeParseError' : 'nodes.outputFieldTypeParseError'; + if (e instanceof FieldParseError) { + logger('nodes').warn( + { + node: type, + field: propertyName, + schema: parseify(property), + }, + t(tKey, { + node: type, + field: propertyName, + message: e.message, + }) + ); + } else { + logger('nodes').warn( + { + node: type, + field: propertyName, + schema: parseify(property), + error: serializeError(e), + }, + t(tKey, { + node: type, + field: propertyName, + message: 'unknown error', + }) + ); + } + return null; + } +}; From fe7ed72c9c12334c46f0ada4bf449b7beca04ce0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 20:15:04 +1000 Subject: [PATCH 191/442] feat(nodes): make all `ModelIdentifierField` inputs accept connections --- .../controlnet_image_processors.py | 5 ++--- invokeai/app/invocations/ip_adapter.py | 5 ++--- invokeai/app/invocations/model.py | 22 +++++++++---------- invokeai/app/invocations/sdxl.py | 10 ++++----- invokeai/app/invocations/t2i_adapter.py | 5 ++--- 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index d2f01622b2..f5edd49874 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -24,7 +24,6 @@ from pydantic import BaseModel, Field, field_validator, model_validator from invokeai.app.invocations.fields import ( FieldDescriptions, ImageField, - Input, InputField, OutputField, UIType, @@ -80,13 +79,13 @@ class ControlOutput(BaseInvocationOutput): control: ControlField = OutputField(description=FieldDescriptions.control) -@invocation("controlnet", title="ControlNet", tags=["controlnet"], category="controlnet", version="1.1.1") +@invocation("controlnet", title="ControlNet", tags=["controlnet"], category="controlnet", version="1.1.2") class ControlNetInvocation(BaseInvocation): """Collects ControlNet info to pass to other nodes""" image: ImageField = InputField(description="The control image") control_model: ModelIdentifierField = InputField( - description=FieldDescriptions.controlnet_model, input=Input.Direct, ui_type=UIType.ControlNetModel + description=FieldDescriptions.controlnet_model, ui_type=UIType.ControlNetModel ) control_weight: Union[float, List[float]] = InputField( default=1.0, ge=-1, le=2, description="The weight given to the ControlNet" diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index 34a30628da..de40879eef 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Field, field_validator, model_validator from typing_extensions import Self from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, TensorField, UIType +from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, TensorField, UIType from invokeai.app.invocations.model import ModelIdentifierField from invokeai.app.invocations.primitives import ImageField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights @@ -58,7 +58,7 @@ class IPAdapterOutput(BaseInvocationOutput): CLIP_VISION_MODEL_MAP = {"ViT-H": "ip_adapter_sd_image_encoder", "ViT-G": "ip_adapter_sdxl_image_encoder"} -@invocation("ip_adapter", title="IP-Adapter", tags=["ip_adapter", "control"], category="ip_adapter", version="1.4.0") +@invocation("ip_adapter", title="IP-Adapter", tags=["ip_adapter", "control"], category="ip_adapter", version="1.4.1") class IPAdapterInvocation(BaseInvocation): """Collects IP-Adapter info to pass to other nodes.""" @@ -67,7 +67,6 @@ class IPAdapterInvocation(BaseInvocation): ip_adapter_model: ModelIdentifierField = InputField( description="The IP-Adapter model.", title="IP-Adapter Model", - input=Input.Direct, ui_order=-1, ui_type=UIType.IPAdapterModel, ) diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index 245034c481..05f451b957 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -98,14 +98,12 @@ class ModelLoaderOutput(UNetOutput, CLIPOutput, VAEOutput): title="Main Model", tags=["model"], category="model", - version="1.0.2", + version="1.0.3", ) class MainModelLoaderInvocation(BaseInvocation): """Loads a main model, outputting its submodels.""" - model: ModelIdentifierField = InputField( - description=FieldDescriptions.main_model, input=Input.Direct, ui_type=UIType.MainModel - ) + model: ModelIdentifierField = InputField(description=FieldDescriptions.main_model, ui_type=UIType.MainModel) # TODO: precision? def invoke(self, context: InvocationContext) -> ModelLoaderOutput: @@ -134,12 +132,12 @@ class LoRALoaderOutput(BaseInvocationOutput): clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP") -@invocation("lora_loader", title="LoRA", tags=["model"], category="model", version="1.0.2") +@invocation("lora_loader", title="LoRA", tags=["model"], category="model", version="1.0.3") class LoRALoaderInvocation(BaseInvocation): """Apply selected lora to unet and text_encoder.""" lora: ModelIdentifierField = InputField( - description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA", ui_type=UIType.LoRAModel + description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel ) weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) unet: Optional[UNetField] = InputField( @@ -197,12 +195,12 @@ class LoRASelectorOutput(BaseInvocationOutput): lora: LoRAField = OutputField(description="LoRA model and weight", title="LoRA") -@invocation("lora_selector", title="LoRA Selector", tags=["model"], category="model", version="1.0.0") +@invocation("lora_selector", title="LoRA Selector", tags=["model"], category="model", version="1.0.1") class LoRASelectorInvocation(BaseInvocation): """Selects a LoRA model and weight.""" lora: ModelIdentifierField = InputField( - description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA", ui_type=UIType.LoRAModel + description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel ) weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) @@ -273,13 +271,13 @@ class SDXLLoRALoaderOutput(BaseInvocationOutput): title="SDXL LoRA", tags=["lora", "model"], category="model", - version="1.0.2", + version="1.0.3", ) class SDXLLoRALoaderInvocation(BaseInvocation): """Apply selected lora to unet and text_encoder.""" lora: ModelIdentifierField = InputField( - description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA", ui_type=UIType.LoRAModel + description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel ) weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) unet: Optional[UNetField] = InputField( @@ -414,12 +412,12 @@ class SDXLLoRACollectionLoader(BaseInvocation): return output -@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.2") +@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.3") class VAELoaderInvocation(BaseInvocation): """Loads a VAE model, outputting a VaeLoaderOutput""" vae_model: ModelIdentifierField = InputField( - description=FieldDescriptions.vae_model, input=Input.Direct, title="VAE", ui_type=UIType.VAEModel + description=FieldDescriptions.vae_model, title="VAE", ui_type=UIType.VAEModel ) def invoke(self, context: InvocationContext) -> VAEOutput: diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py index 9b1ee90350..1c0817cb92 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -1,4 +1,4 @@ -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType +from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, UIType from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.model_manager import SubModelType @@ -30,12 +30,12 @@ class SDXLRefinerModelLoaderOutput(BaseInvocationOutput): vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE") -@invocation("sdxl_model_loader", title="SDXL Main Model", tags=["model", "sdxl"], category="model", version="1.0.2") +@invocation("sdxl_model_loader", title="SDXL Main Model", tags=["model", "sdxl"], category="model", version="1.0.3") class SDXLModelLoaderInvocation(BaseInvocation): """Loads an sdxl base model, outputting its submodels.""" model: ModelIdentifierField = InputField( - description=FieldDescriptions.sdxl_main_model, input=Input.Direct, ui_type=UIType.SDXLMainModel + description=FieldDescriptions.sdxl_main_model, ui_type=UIType.SDXLMainModel ) # TODO: precision? @@ -67,13 +67,13 @@ class SDXLModelLoaderInvocation(BaseInvocation): title="SDXL Refiner Model", tags=["model", "sdxl", "refiner"], category="model", - version="1.0.2", + version="1.0.3", ) class SDXLRefinerModelLoaderInvocation(BaseInvocation): """Loads an sdxl refiner model, outputting its submodels.""" model: ModelIdentifierField = InputField( - description=FieldDescriptions.sdxl_refiner_model, input=Input.Direct, ui_type=UIType.SDXLRefinerModel + description=FieldDescriptions.sdxl_refiner_model, ui_type=UIType.SDXLRefinerModel ) # TODO: precision? diff --git a/invokeai/app/invocations/t2i_adapter.py b/invokeai/app/invocations/t2i_adapter.py index b22a089d3f..04f9a6c695 100644 --- a/invokeai/app/invocations/t2i_adapter.py +++ b/invokeai/app/invocations/t2i_adapter.py @@ -8,7 +8,7 @@ from invokeai.app.invocations.baseinvocation import ( invocation, invocation_output, ) -from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField, UIType +from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField, UIType from invokeai.app.invocations.model import ModelIdentifierField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights from invokeai.app.services.shared.invocation_context import InvocationContext @@ -45,7 +45,7 @@ class T2IAdapterOutput(BaseInvocationOutput): @invocation( - "t2i_adapter", title="T2I-Adapter", tags=["t2i_adapter", "control"], category="t2i_adapter", version="1.0.2" + "t2i_adapter", title="T2I-Adapter", tags=["t2i_adapter", "control"], category="t2i_adapter", version="1.0.3" ) class T2IAdapterInvocation(BaseInvocation): """Collects T2I-Adapter info to pass to other nodes.""" @@ -55,7 +55,6 @@ class T2IAdapterInvocation(BaseInvocation): t2i_adapter_model: ModelIdentifierField = InputField( description="The T2I-Adapter model.", title="T2I-Adapter Model", - input=Input.Direct, ui_order=-1, ui_type=UIType.T2IAdapterModel, ) From 2cbf7d9221bdce7e7dee5072000e1cf601236318 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 20:18:20 +1000 Subject: [PATCH 192/442] fix(ui): stupid ts --- invokeai/frontend/web/src/features/nodes/types/field.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts index 37e2a26397..4dcc478352 100644 --- a/invokeai/frontend/web/src/features/nodes/types/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/field.ts @@ -160,7 +160,7 @@ const zStatefulFieldType = z.union([ export type StatefulFieldType = z.infer; const statefulFieldTypeNames = zStatefulFieldType.options.map((o) => o.shape.name.value); export const isStatefulFieldType = (fieldType: FieldType): fieldType is StatefulFieldType => - statefulFieldTypeNames.includes(fieldType.name as any); + (statefulFieldTypeNames as string[]).includes(fieldType.name); const zFieldType = z.union([zStatefulFieldType, zStatelessFieldType]); export type FieldType = z.infer; // #endregion From 6a2c53f6c5e9ad8d7c742b5e7e762ccc48e221c8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 20:33:44 +1000 Subject: [PATCH 193/442] fix(ui): do not allow comparison between undefined original types --- .../nodes/store/util/validateSourceAndTargetTypes.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts index cc5a6bb596..45b771b5b4 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts @@ -9,13 +9,13 @@ export const areTypesEqual = (sourceType: FieldType, targetType: FieldType) => { if (isEqual(_sourceType, _targetType)) { return true; } - if (isEqual(_sourceType, _targetTypeOriginal)) { + if (_targetTypeOriginal && isEqual(_sourceType, _targetTypeOriginal)) { return true; } - if (isEqual(_sourceTypeOriginal, _targetType)) { + if (_sourceTypeOriginal && isEqual(_sourceTypeOriginal, _targetType)) { return true; } - if (isEqual(_sourceTypeOriginal, _targetTypeOriginal)) { + if (_sourceTypeOriginal && _targetTypeOriginal && isEqual(_sourceTypeOriginal, _targetTypeOriginal)) { return true; } return false; From a012bb6e071ab3dbd1d023d45068f698155531ff Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 20:47:00 +1000 Subject: [PATCH 194/442] feat(ui): add ModelIdentifierField field type This new field type accepts _any_ model. A field renderer lets the user select any available model. --- .../Invocation/fields/InputFieldRenderer.tsx | 7 ++ .../ModelIdentifierFieldInputComponent.tsx | 68 +++++++++++++++++++ .../src/features/nodes/store/nodesSlice.ts | 6 ++ .../web/src/features/nodes/types/field.ts | 32 +++++++++ .../util/schema/buildFieldInputInstance.ts | 1 + .../util/schema/buildFieldInputTemplate.ts | 16 +++++ 6 files changed, 130 insertions(+) create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index b6e331c114..99937ceec4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -1,3 +1,4 @@ +import ModelIdentifierFieldInputComponent from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent'; import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance'; import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate'; import { @@ -23,6 +24,8 @@ import { isLoRAModelFieldInputTemplate, isMainModelFieldInputInstance, isMainModelFieldInputTemplate, + isModelIdentifierFieldInputInstance, + isModelIdentifierFieldInputTemplate, isSchedulerFieldInputInstance, isSchedulerFieldInputTemplate, isSDXLMainModelFieldInputInstance, @@ -95,6 +98,10 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => { return ; } + if (isModelIdentifierFieldInputInstance(fieldInstance) && isModelIdentifierFieldInputTemplate(fieldTemplate)) { + return ; + } + if (isSDXLRefinerModelFieldInputInstance(fieldInstance) && isSDXLRefinerModelFieldInputTemplate(fieldTemplate)) { return ; } 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 new file mode 100644 index 0000000000..6a0c9b63fa --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent.tsx @@ -0,0 +1,68 @@ +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 { fieldModelIdentifierValueChanged } from 'features/nodes/store/nodesSlice'; +import type { ModelIdentifierFieldInputInstance, ModelIdentifierFieldInputTemplate } from 'features/nodes/types/field'; +import { memo, useCallback, useMemo } from 'react'; +import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/types'; + +import type { FieldComponentProps } from './types'; + +type Props = FieldComponentProps; + +const ModelIdentifierFieldInputComponent = (props: Props) => { + const { nodeId, field } = props; + const dispatch = useAppDispatch(); + const { data, isLoading } = useGetModelConfigsQuery(); + const _onChange = useCallback( + (value: AnyModelConfig | null) => { + if (!value) { + return; + } + dispatch( + fieldModelIdentifierValueChanged({ + nodeId, + fieldName: field.name, + value, + }) + ); + }, + [dispatch, field.name, nodeId] + ); + + const modelConfigs = useMemo(() => { + if (!data) { + return EMPTY_ARRAY; + } + + return modelConfigsAdapterSelectors.selectAll(data); + }, [data]); + + console.log(modelConfigs); + + const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({ + modelConfigs, + onChange: _onChange, + isLoading, + selectedModel: field.value, + groupByType: true, + }); + + return ( + + + + + + ); +}; + +export default memo(ModelIdentifierFieldInputComponent); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 1f61c77e83..cec13e8df4 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -16,6 +16,7 @@ import type { IPAdapterModelFieldValue, LoRAModelFieldValue, MainModelFieldValue, + ModelIdentifierFieldValue, SchedulerFieldValue, SDXLRefinerModelFieldValue, StatefulFieldValue, @@ -35,6 +36,7 @@ import { zIPAdapterModelFieldValue, zLoRAModelFieldValue, zMainModelFieldValue, + zModelIdentifierFieldValue, zSchedulerFieldValue, zSDXLRefinerModelFieldValue, zStatefulFieldValue, @@ -344,6 +346,9 @@ export const nodesSlice = createSlice({ fieldMainModelValueChanged: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zMainModelFieldValue); }, + fieldModelIdentifierValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zModelIdentifierFieldValue); + }, fieldRefinerModelValueChanged: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zSDXLRefinerModelFieldValue); }, @@ -469,6 +474,7 @@ export const { fieldT2IAdapterModelValueChanged, fieldLabelChanged, fieldLoRAModelValueChanged, + fieldModelIdentifierValueChanged, fieldMainModelValueChanged, fieldNumberValueChanged, fieldRefinerModelValueChanged, diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts index 4dcc478352..a98f773c7e 100644 --- a/invokeai/frontend/web/src/features/nodes/types/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/field.ts @@ -106,6 +106,10 @@ const zMainModelFieldType = zFieldTypeBase.extend({ name: z.literal('MainModelField'), originalType: zStatelessFieldType.optional(), }); +const zModelIdentifierFieldType = zFieldTypeBase.extend({ + name: z.literal('ModelIdentifierField'), + originalType: zStatelessFieldType.optional(), +}); const zSDXLMainModelFieldType = zFieldTypeBase.extend({ name: z.literal('SDXLMainModelField'), originalType: zStatelessFieldType.optional(), @@ -146,6 +150,7 @@ const zStatefulFieldType = z.union([ zEnumFieldType, zImageFieldType, zBoardFieldType, + zModelIdentifierFieldType, zMainModelFieldType, zSDXLMainModelFieldType, zSDXLRefinerModelFieldType, @@ -396,6 +401,29 @@ export const isMainModelFieldInputTemplate = (val: unknown): val is MainModelFie zMainModelFieldInputTemplate.safeParse(val).success; // #endregion +// #region ModelIdentifierField +export const zModelIdentifierFieldValue = zModelIdentifierField.optional(); +const zModelIdentifierFieldInputInstance = zFieldInputInstanceBase.extend({ + value: zModelIdentifierFieldValue, +}); +const zModelIdentifierFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zModelIdentifierFieldType, + originalType: zFieldType.optional(), + default: zModelIdentifierFieldValue, +}); +const zModelIdentifierFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zModelIdentifierFieldType, + originalType: zFieldType.optional(), +}); +export type ModelIdentifierFieldValue = z.infer; +export type ModelIdentifierFieldInputInstance = z.infer; +export type ModelIdentifierFieldInputTemplate = z.infer; +export const isModelIdentifierFieldInputInstance = (val: unknown): val is ModelIdentifierFieldInputInstance => + zModelIdentifierFieldInputInstance.safeParse(val).success; +export const isModelIdentifierFieldInputTemplate = (val: unknown): val is ModelIdentifierFieldInputTemplate => + zModelIdentifierFieldInputTemplate.safeParse(val).success; +// #endregion + // #region SDXLMainModelField const zSDXLMainModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL models only. @@ -643,6 +671,7 @@ export const zStatefulFieldValue = z.union([ zEnumFieldValue, zImageFieldValue, zBoardFieldValue, + zModelIdentifierFieldValue, zMainModelFieldValue, zSDXLMainModelFieldValue, zSDXLRefinerModelFieldValue, @@ -669,6 +698,7 @@ const zStatefulFieldInputInstance = z.union([ zEnumFieldInputInstance, zImageFieldInputInstance, zBoardFieldInputInstance, + zModelIdentifierFieldInputInstance, zMainModelFieldInputInstance, zSDXLMainModelFieldInputInstance, zSDXLRefinerModelFieldInputInstance, @@ -696,6 +726,7 @@ const zStatefulFieldInputTemplate = z.union([ zEnumFieldInputTemplate, zImageFieldInputTemplate, zBoardFieldInputTemplate, + zModelIdentifierFieldInputTemplate, zMainModelFieldInputTemplate, zSDXLMainModelFieldInputTemplate, zSDXLRefinerModelFieldInputTemplate, @@ -724,6 +755,7 @@ const zStatefulFieldOutputTemplate = z.union([ zEnumFieldOutputTemplate, zImageFieldOutputTemplate, zBoardFieldOutputTemplate, + zModelIdentifierFieldOutputTemplate, zMainModelFieldOutputTemplate, zSDXLMainModelFieldOutputTemplate, zSDXLRefinerModelFieldOutputTemplate, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts index f8097566c9..597779fd61 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts @@ -11,6 +11,7 @@ const FIELD_VALUE_FALLBACK_MAP: Record = IntegerField: 0, IPAdapterModelField: undefined, LoRAModelField: undefined, + ModelIdentifierField: undefined, MainModelField: undefined, SchedulerField: 'euler', SDXLMainModelField: undefined, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts index 6b4c4d8b29..2b77274526 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts @@ -13,6 +13,7 @@ import type { IPAdapterModelFieldInputTemplate, LoRAModelFieldInputTemplate, MainModelFieldInputTemplate, + ModelIdentifierFieldInputTemplate, SchedulerFieldInputTemplate, SDXLMainModelFieldInputTemplate, SDXLRefinerModelFieldInputTemplate, @@ -136,6 +137,20 @@ const buildBooleanFieldInputTemplate: FieldInputTemplateBuilder = ({ + schemaObject, + baseField, + fieldType, +}) => { + const template: ModelIdentifierFieldInputTemplate = { + ...baseField, + type: fieldType, + default: schemaObject.default ?? undefined, + }; + + return template; +}; + const buildMainModelFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, @@ -355,6 +370,7 @@ export const TEMPLATE_BUILDER_MAP: Record Date: Fri, 17 May 2024 20:47:46 +1000 Subject: [PATCH 195/442] feat(nodes): add `ModelIdentifierInvocation` This node allows a user to select _any_ model, outputting a `ModelIdentifierField` for that model. --- invokeai/app/invocations/model.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index 05f451b957..6f78cf43bf 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -93,6 +93,32 @@ class ModelLoaderOutput(UNetOutput, CLIPOutput, VAEOutput): pass +@invocation_output("model_identifier_output") +class ModelIdentifierOutput(BaseInvocationOutput): + """Model identifier output""" + + model: ModelIdentifierField = OutputField(description="Model identifier", title="Model") + + +@invocation( + "model_identifier", + title="Model identifier", + tags=["model"], + category="model", + version="1.0.0", +) +class ModelIdentifierInvocation(BaseInvocation): + """Selects any model, outputting it.""" + + model: ModelIdentifierField = InputField(description="The model to select", title="Model") + + def invoke(self, context: InvocationContext) -> ModelIdentifierOutput: + if not context.models.exists(self.model.key): + raise Exception(f"Unknown model {self.model.key}") + + return ModelIdentifierOutput(model=self.model) + + @invocation( "main_model_loader", title="Main Model", From de1ea50e6dc24bff75a8c01741f6ad4ec41aea3c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 21:05:41 +1000 Subject: [PATCH 196/442] fix(ui): rebase resolution --- .../features/nodes/store/util/makeIsConnectionValidSelector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts index e7f659508f..5a5972a376 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts @@ -106,7 +106,7 @@ export const makeConnectionErrorSelector = ( return i18n.t('nodes.cannotConnectToDirectInput'); } - if (targetNode.data.type === 'collect' && targetFieldName === 'item') { + if (targetNode?.data.type === 'collect' && targetFieldName === 'item') { // Collect nodes shouldn't mix and match field types const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); if (collectItemType) { From af7b194bec0c8ab108c32c59c4e640f453378dae Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 17 May 2024 21:05:53 +1000 Subject: [PATCH 197/442] chore(ui): lint --- .../fields/inputs/ModelIdentifierFieldInputComponent.tsx | 2 -- .../web/src/features/nodes/util/schema/parseSchema.ts | 5 ----- 2 files changed, 7 deletions(-) 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 6a0c9b63fa..4019689978 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 @@ -40,8 +40,6 @@ const ModelIdentifierFieldInputComponent = (props: Props) => { return modelConfigsAdapterSelectors.selectAll(data); }, [data]); - console.log(modelConfigs); - const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({ modelConfigs, onChange: _onChange, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts index 0638a52954..f9b93382f9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts @@ -128,13 +128,10 @@ export const parseSchema = ( } if (isStatefulFieldType(fieldType) && originalFieldType && !isEqual(originalFieldType, fieldType)) { - console.log('STATEFUL WITH ORIGINAL'); fieldType.originalType = deepClone(originalFieldType); - console.log(fieldType); } const fieldInputTemplate = buildFieldInputTemplate(property, propertyName, fieldType); - console.log(fieldInputTemplate); inputsAccumulator[propertyName] = fieldInputTemplate; return inputsAccumulator; @@ -201,9 +198,7 @@ export const parseSchema = ( } if (isStatefulFieldType(fieldType) && originalFieldType && !isEqual(originalFieldType, fieldType)) { - console.log('STATEFUL WITH ORIGINAL'); fieldType.originalType = deepClone(originalFieldType); - console.log(fieldType); } const fieldOutputTemplate = buildFieldOutputTemplate(property, propertyName, fieldType); From 6658897210c12335cc7107466a03edecb89d028f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 17:22:29 +1000 Subject: [PATCH 198/442] tidy(ui): tidy connection validation functions and logic --- .../flow/AddNodePopover/AddNodePopover.tsx | 3 +- .../src/features/nodes/hooks/useConnection.ts | 2 +- .../nodes/hooks/useConnectionState.ts | 2 +- .../nodes/hooks/useIsValidConnection.ts | 11 +- .../nodes/store/util/connectionValidation.ts | 386 ++++++++++++++++++ .../store/util/findConnectionToValidHandle.ts | 105 ----- .../nodes/store/util/getIsGraphAcyclic.ts | 21 - .../util/makeIsConnectionValidSelector.ts | 146 ------- .../util/validateSourceAndTargetTypes.ts | 90 ---- 9 files changed, 396 insertions(+), 370 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/store/util/getIsGraphAcyclic.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 95104c683c..40fa13320a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -17,8 +17,7 @@ import { nodeAdded, openAddNodePopover, } from 'features/nodes/store/nodesSlice'; -import { getFirstValidConnection } from 'features/nodes/store/util/findConnectionToValidHandle'; -import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; +import { getFirstValidConnection, validateSourceAndTargetTypes } from 'features/nodes/store/util/connectionValidation'; import type { AnyNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { filter, map, memoize, some } from 'lodash-es'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index df628ba5af..81eea993be 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -8,7 +8,7 @@ import { $templates, connectionMade, } from 'features/nodes/store/nodesSlice'; -import { getFirstValidConnection } from 'features/nodes/store/util/findConnectionToValidHandle'; +import { getFirstValidConnection } from 'features/nodes/store/util/connectionValidation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { useCallback, useMemo } from 'react'; import type { OnConnect, OnConnectEnd, OnConnectStart } from 'reactflow'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index 728b492453..dfa8b0cf36 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { $pendingConnection, $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector'; +import { makeConnectionErrorSelector } from 'features/nodes/store/util/connectionValidation.js'; import { useMemo } from 'react'; import { useFieldType } from './useFieldType.ts'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index 14a7a728e0..b92114bab2 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -2,9 +2,12 @@ import { useStore } from '@nanostores/react'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { $templates } from 'features/nodes/store/nodesSlice'; -import { getIsGraphAcyclic } from 'features/nodes/store/util/getIsGraphAcyclic'; -import { getCollectItemType } from 'features/nodes/store/util/makeIsConnectionValidSelector'; -import { areTypesEqual, validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; +import { + areTypesEqual, + getCollectItemType, + getHasCycles, + validateSourceAndTargetTypes, +} from 'features/nodes/store/util/connectionValidation'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { useCallback } from 'react'; import type { Connection, Node } from 'reactflow'; @@ -90,7 +93,7 @@ export const useIsValidConnection = () => { } // Graphs much be acyclic (no loops!) - return getIsGraphAcyclic(source, target, nodes, edges); + return !getHasCycles(source, target, nodes, edges); }, [shouldValidateGraph, templates, store] ); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts b/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts new file mode 100644 index 0000000000..98de4284ad --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts @@ -0,0 +1,386 @@ +import graphlib from '@dagrejs/graphlib'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import type { PendingConnection, Templates } from 'features/nodes/store/types'; +import { type FieldType, isStatefulFieldType } from 'features/nodes/types/field'; +import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; +import i18n from 'i18next'; +import { differenceWith, isEqual, map, omit } from 'lodash-es'; +import type { Connection, Edge, HandleType, Node } from 'reactflow'; +import { assert } from 'tsafe'; + +/** + * Finds the first valid field for a pending connection between two nodes. + * @param templates The invocation templates + * @param nodes The current nodes + * @param edges The current edges + * @param pendingConnection The pending connection + * @param candidateNode The candidate node to which the connection is being made + * @param candidateTemplate The candidate template for the candidate node + * @returns The first valid connection, or null if no valid connection is found + */ +export const getFirstValidConnection = ( + templates: Templates, + nodes: AnyNode[], + edges: InvocationNodeEdge[], + pendingConnection: PendingConnection, + candidateNode: InvocationNode, + candidateTemplate: InvocationTemplate +): Connection | null => { + if (pendingConnection.node.id === candidateNode.id) { + // Cannot connect to self + return null; + } + + const pendingFieldKind = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; + + if (pendingFieldKind === 'source') { + // Connecting from a source to a target + if (getHasCycles(pendingConnection.node.id, candidateNode.id, nodes, edges)) { + return null; + } + if (candidateNode.data.type === 'collect') { + // Special handling for collect node - the `item` field takes any number of connections + return { + source: pendingConnection.node.id, + sourceHandle: pendingConnection.fieldTemplate.name, + target: candidateNode.id, + targetHandle: 'item', + }; + } + // Only one connection per target field is allowed - look for an unconnected target field + const candidateFields = map(candidateTemplate.inputs); + const candidateConnectedFields = edges + .filter((edge) => edge.target === candidateNode.id) + .map((edge) => { + // Edges must always have a targetHandle, safe to assert here + assert(edge.targetHandle); + return edge.targetHandle; + }); + const candidateUnconnectedFields = differenceWith( + candidateFields, + candidateConnectedFields, + (field, connectedFieldName) => field.name === connectedFieldName + ); + const candidateField = candidateUnconnectedFields.find((field) => + validateSourceAndTargetTypes(pendingConnection.fieldTemplate.type, field.type) + ); + if (candidateField) { + return { + source: pendingConnection.node.id, + sourceHandle: pendingConnection.fieldTemplate.name, + target: candidateNode.id, + targetHandle: candidateField.name, + }; + } + } else { + // Connecting from a target to a source + // Ensure we there is not already an edge to the target, except for collect nodes + const isCollect = pendingConnection.node.data.type === 'collect'; + const isTargetAlreadyConnected = edges.some( + (e) => e.target === pendingConnection.node.id && e.targetHandle === pendingConnection.fieldTemplate.name + ); + if (!isCollect && isTargetAlreadyConnected) { + return null; + } + + if (getHasCycles(candidateNode.id, pendingConnection.node.id, nodes, edges)) { + return null; + } + + // Sources/outputs can have any number of edges, we can take the first matching output field + let candidateFields = map(candidateTemplate.outputs); + if (isCollect) { + // Narrow candidates to same field type as already is connected to the collect node + const collectItemType = getCollectItemType(templates, nodes, edges, pendingConnection.node.id); + if (collectItemType) { + candidateFields = candidateFields.filter((field) => areTypesEqual(field.type, collectItemType)); + } + } + const candidateField = candidateFields.find((field) => { + const isValid = validateSourceAndTargetTypes(field.type, pendingConnection.fieldTemplate.type); + const isAlreadyConnected = edges.some((e) => e.source === candidateNode.id && e.sourceHandle === field.name); + return isValid && !isAlreadyConnected; + }); + if (candidateField) { + return { + source: candidateNode.id, + sourceHandle: candidateField.name, + target: pendingConnection.node.id, + targetHandle: pendingConnection.fieldTemplate.name, + }; + } + } + + return null; +}; + +/** + * Check if adding an edge between the source and target nodes would create a cycle in the graph. + * @param source The source node id + * @param target The target node id + * @param nodes The graph's current nodes + * @param edges The graph's current edges + * @returns True if the graph would be acyclic after adding the edge, false otherwise + */ +export const getHasCycles = (source: string, target: string, nodes: Node[], edges: Edge[]) => { + // construct graphlib graph from editor state + const g = new graphlib.Graph(); + + nodes.forEach((n) => { + g.setNode(n.id); + }); + + edges.forEach((e) => { + g.setEdge(e.source, e.target); + }); + + // add the candidate edge + g.setEdge(source, target); + + // check if the graph is acyclic + return !graphlib.alg.isAcyclic(g); +}; + +/** + * Given a collect node, return the type of the items it collects. The graph is traversed to find the first node and + * field connected to the collector's `item` input. The field type of that field is returned, else null if there is no + * input field. + * @param templates The current invocation templates + * @param nodes The current nodes + * @param edges The current edges + * @param nodeId The collect node's id + * @returns The type of the items the collect node collects, or null if there is no input field + */ +export const getCollectItemType = ( + templates: Templates, + nodes: AnyNode[], + edges: InvocationNodeEdge[], + nodeId: string +): FieldType | null => { + const firstEdgeToCollect = edges.find((edge) => edge.target === nodeId && edge.targetHandle === 'item'); + if (!firstEdgeToCollect?.sourceHandle) { + return null; + } + const node = nodes.find((n) => n.id === firstEdgeToCollect.source); + if (!node) { + return null; + } + const template = templates[node.data.type]; + if (!template) { + return null; + } + const fieldType = template.outputs[firstEdgeToCollect.sourceHandle]?.type ?? null; + return fieldType; +}; + +/** + * Creates a selector that validates a pending connection. + * + * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts` + * TODO: Figure out how to do this without duplicating all the logic + * + * @param templates The invocation templates + * @param pendingConnection The current pending connection (if there is one) + * @param nodeId The id of the node for which the selector is being created + * @param fieldName The name of the field for which the selector is being created + * @param handleType The type of the handle for which the selector is being created + * @param fieldType The type of the field for which the selector is being created + * @returns + */ +export const makeConnectionErrorSelector = ( + templates: Templates, + pendingConnection: PendingConnection | null, + nodeId: string, + fieldName: string, + handleType: HandleType, + fieldType: FieldType +) => { + return createMemoizedSelector(selectNodesSlice, (nodesSlice) => { + const { nodes, edges } = nodesSlice; + + if (!pendingConnection) { + return i18n.t('nodes.noConnectionInProgress'); + } + + const connectionNodeId = pendingConnection.node.id; + const connectionFieldName = pendingConnection.fieldTemplate.name; + const connectionHandleType = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; + const connectionStartFieldType = pendingConnection.fieldTemplate.type; + + if (!connectionHandleType || !connectionNodeId || !connectionFieldName) { + return i18n.t('nodes.noConnectionData'); + } + + const targetType = handleType === 'target' ? fieldType : connectionStartFieldType; + const sourceType = handleType === 'source' ? fieldType : connectionStartFieldType; + + if (nodeId === connectionNodeId) { + return i18n.t('nodes.cannotConnectToSelf'); + } + + if (handleType === connectionHandleType) { + if (handleType === 'source') { + return i18n.t('nodes.cannotConnectOutputToOutput'); + } + return i18n.t('nodes.cannotConnectInputToInput'); + } + + // we have to figure out which is the target and which is the source + const targetNodeId = handleType === 'target' ? nodeId : connectionNodeId; + const targetFieldName = handleType === 'target' ? fieldName : connectionFieldName; + const sourceNodeId = handleType === 'source' ? nodeId : connectionNodeId; + const sourceFieldName = handleType === 'source' ? fieldName : connectionFieldName; + + if ( + edges.find((edge) => { + edge.target === targetNodeId && + edge.targetHandle === targetFieldName && + edge.source === sourceNodeId && + edge.sourceHandle === sourceFieldName; + }) + ) { + // We already have a connection from this source to this target + return i18n.t('nodes.cannotDuplicateConnection'); + } + + const targetNode = nodes.find((node) => node.id === targetNodeId); + assert(targetNode, `Target node not found: ${targetNodeId}`); + const targetTemplate = templates[targetNode.data.type]; + assert(targetTemplate, `Target template not found: ${targetNode.data.type}`); + + if (targetTemplate.inputs[targetFieldName]?.input === 'direct') { + return i18n.t('nodes.cannotConnectToDirectInput'); + } + if (targetNode.data.type === 'collect' && targetFieldName === 'item') { + // Collect nodes shouldn't mix and match field types + const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); + if (collectItemType) { + if (!areTypesEqual(sourceType, collectItemType)) { + return i18n.t('nodes.cannotMixAndMatchCollectionItemTypes'); + } + } + } + + if ( + edges.find((edge) => { + return edge.target === targetNodeId && edge.targetHandle === targetFieldName; + }) && + // except CollectionItem inputs can have multiples + targetType.name !== 'CollectionItemField' + ) { + return i18n.t('nodes.inputMayOnlyHaveOneConnection'); + } + + if (!validateSourceAndTargetTypes(sourceType, targetType)) { + return i18n.t('nodes.fieldTypesMustMatch'); + } + + const hasCycles = getHasCycles( + connectionHandleType === 'source' ? connectionNodeId : nodeId, + connectionHandleType === 'source' ? nodeId : connectionNodeId, + nodes, + edges + ); + + if (hasCycles) { + return i18n.t('nodes.connectionWouldCreateCycle'); + } + + return; + }); +}; + +/** + * Validates that the source and target types are compatible for a connection. + * @param sourceType The type of the source field. + * @param targetType The type of the target field. + * @returns True if the connection is valid, false otherwise. + */ +export const validateSourceAndTargetTypes = (sourceType: FieldType, targetType: FieldType) => { + // TODO: There's a bug with Collect -> Iterate nodes: + // https://github.com/invoke-ai/InvokeAI/issues/3956 + // Once this is resolved, we can remove this check. + if (sourceType.name === 'CollectionField' && targetType.name === 'CollectionField') { + return false; + } + + if (areTypesEqual(sourceType, targetType)) { + return true; + } + + /** + * Connection types must be the same for a connection, with exceptions: + * - CollectionItem can connect to any non-Collection + * - Non-Collections can connect to CollectionItem + * - Anything (non-Collections, Collections, CollectionOrScalar) can connect to CollectionOrScalar of the same base type + * - Generic Collection can connect to any other Collection or CollectionOrScalar + * - Any Collection can connect to a Generic Collection + */ + const isCollectionItemToNonCollection = sourceType.name === 'CollectionItemField' && !targetType.isCollection; + + const isNonCollectionToCollectionItem = + targetType.name === 'CollectionItemField' && !sourceType.isCollection && !sourceType.isCollectionOrScalar; + + const isAnythingToCollectionOrScalarOfSameBaseType = + targetType.isCollectionOrScalar && sourceType.name === targetType.name; + + const isGenericCollectionToAnyCollectionOrCollectionOrScalar = + sourceType.name === 'CollectionField' && (targetType.isCollection || targetType.isCollectionOrScalar); + + const isCollectionToGenericCollection = targetType.name === 'CollectionField' && sourceType.isCollection; + + const areBothTypesSingle = + !sourceType.isCollection && + !sourceType.isCollectionOrScalar && + !targetType.isCollection && + !targetType.isCollectionOrScalar; + + const isIntToFloat = areBothTypesSingle && sourceType.name === 'IntegerField' && targetType.name === 'FloatField'; + + const isIntOrFloatToString = + areBothTypesSingle && + (sourceType.name === 'IntegerField' || sourceType.name === 'FloatField') && + targetType.name === 'StringField'; + + const isTargetAnyType = targetType.name === 'AnyField'; + + // One of these must be true for the connection to be valid + return ( + isCollectionItemToNonCollection || + isNonCollectionToCollectionItem || + isAnythingToCollectionOrScalarOfSameBaseType || + isGenericCollectionToAnyCollectionOrCollectionOrScalar || + isCollectionToGenericCollection || + isIntToFloat || + isIntOrFloatToString || + isTargetAnyType + ); +}; + +/** + * Checks if two types are equal. If the field types have original types, those are also compared. Any match is + * considered equal. For example, if the source type and original target type match, the types are considered equal. + * @param sourceType The type of the source field. + * @param targetType The type of the target field. + * @returns True if the types are equal, false otherwise. + */ +export const areTypesEqual = (sourceType: FieldType, targetType: FieldType) => { + const _sourceType = isStatefulFieldType(sourceType) ? omit(sourceType, 'originalType') : sourceType; + const _targetType = isStatefulFieldType(targetType) ? omit(targetType, 'originalType') : targetType; + const _sourceTypeOriginal = isStatefulFieldType(sourceType) ? sourceType.originalType : sourceType; + const _targetTypeOriginal = isStatefulFieldType(targetType) ? targetType.originalType : targetType; + if (isEqual(_sourceType, _targetType)) { + return true; + } + if (_targetTypeOriginal && isEqual(_sourceType, _targetTypeOriginal)) { + return true; + } + if (_sourceTypeOriginal && isEqual(_sourceTypeOriginal, _targetType)) { + return true; + } + if (_sourceTypeOriginal && _targetTypeOriginal && isEqual(_sourceTypeOriginal, _targetTypeOriginal)) { + return true; + } + return false; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts deleted file mode 100644 index e0411ee67e..0000000000 --- a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { PendingConnection, Templates } from 'features/nodes/store/types'; -import { getCollectItemType } from 'features/nodes/store/util/makeIsConnectionValidSelector'; -import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; -import { differenceWith, map } from 'lodash-es'; -import type { Connection } from 'reactflow'; -import { assert } from 'tsafe'; - -import { getIsGraphAcyclic } from './getIsGraphAcyclic'; -import { areTypesEqual, validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; - -export const getFirstValidConnection = ( - templates: Templates, - nodes: AnyNode[], - edges: InvocationNodeEdge[], - pendingConnection: PendingConnection, - candidateNode: InvocationNode, - candidateTemplate: InvocationTemplate -): Connection | null => { - if (pendingConnection.node.id === candidateNode.id) { - // Cannot connect to self - return null; - } - - const pendingFieldKind = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; - - if (pendingFieldKind === 'source') { - // Connecting from a source to a target - if (!getIsGraphAcyclic(pendingConnection.node.id, candidateNode.id, nodes, edges)) { - return null; - } - if (candidateNode.data.type === 'collect') { - // Special handling for collect node - the `item` field takes any number of connections - return { - source: pendingConnection.node.id, - sourceHandle: pendingConnection.fieldTemplate.name, - target: candidateNode.id, - targetHandle: 'item', - }; - } - // Only one connection per target field is allowed - look for an unconnected target field - const candidateFields = map(candidateTemplate.inputs).filter((i) => i.input !== 'direct'); - const candidateConnectedFields = edges - .filter((edge) => edge.target === candidateNode.id) - .map((edge) => { - // Edges must always have a targetHandle, safe to assert here - assert(edge.targetHandle); - return edge.targetHandle; - }); - const candidateUnconnectedFields = differenceWith( - candidateFields, - candidateConnectedFields, - (field, connectedFieldName) => field.name === connectedFieldName - ); - const candidateField = candidateUnconnectedFields.find((field) => - validateSourceAndTargetTypes(pendingConnection.fieldTemplate.type, field.type) - ); - if (candidateField) { - return { - source: pendingConnection.node.id, - sourceHandle: pendingConnection.fieldTemplate.name, - target: candidateNode.id, - targetHandle: candidateField.name, - }; - } - } else { - // Connecting from a target to a source - // Ensure we there is not already an edge to the target, except for collect nodes - const isCollect = pendingConnection.node.data.type === 'collect'; - const isTargetAlreadyConnected = edges.some( - (e) => e.target === pendingConnection.node.id && e.targetHandle === pendingConnection.fieldTemplate.name - ); - if (!isCollect && isTargetAlreadyConnected) { - return null; - } - - if (!getIsGraphAcyclic(candidateNode.id, pendingConnection.node.id, nodes, edges)) { - return null; - } - - // Sources/outputs can have any number of edges, we can take the first matching output field - let candidateFields = map(candidateTemplate.outputs); - if (isCollect) { - // Narrow candidates to same field type as already is connected to the collect node - const collectItemType = getCollectItemType(templates, nodes, edges, pendingConnection.node.id); - if (collectItemType) { - candidateFields = candidateFields.filter((field) => areTypesEqual(field.type, collectItemType)); - } - } - const candidateField = candidateFields.find((field) => { - const isValid = validateSourceAndTargetTypes(field.type, pendingConnection.fieldTemplate.type); - const isAlreadyConnected = edges.some((e) => e.source === candidateNode.id && e.sourceHandle === field.name); - return isValid && !isAlreadyConnected; - }); - if (candidateField) { - return { - source: candidateNode.id, - sourceHandle: candidateField.name, - target: pendingConnection.node.id, - targetHandle: pendingConnection.fieldTemplate.name, - }; - } - } - - return null; -}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getIsGraphAcyclic.ts b/invokeai/frontend/web/src/features/nodes/store/util/getIsGraphAcyclic.ts deleted file mode 100644 index 2ef1c64c0e..0000000000 --- a/invokeai/frontend/web/src/features/nodes/store/util/getIsGraphAcyclic.ts +++ /dev/null @@ -1,21 +0,0 @@ -import graphlib from '@dagrejs/graphlib'; -import type { Edge, Node } from 'reactflow'; - -export const getIsGraphAcyclic = (source: string, target: string, nodes: Node[], edges: Edge[]) => { - // construct graphlib graph from editor state - const g = new graphlib.Graph(); - - nodes.forEach((n) => { - g.setNode(n.id); - }); - - edges.forEach((e) => { - g.setEdge(e.source, e.target); - }); - - // add the candidate edge - g.setEdge(source, target); - - // check if the graph is acyclic - return graphlib.alg.isAcyclic(g); -}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts deleted file mode 100644 index 5a5972a376..0000000000 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import type { PendingConnection, Templates } from 'features/nodes/store/types'; -import type { FieldType } from 'features/nodes/types/field'; -import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; -import i18n from 'i18next'; -import type { HandleType } from 'reactflow'; -import { assert } from 'tsafe'; - -import { getIsGraphAcyclic } from './getIsGraphAcyclic'; -import { areTypesEqual, validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; - -export const getCollectItemType = ( - templates: Templates, - nodes: AnyNode[], - edges: InvocationNodeEdge[], - nodeId: string -): FieldType | null => { - const firstEdgeToCollect = edges.find((edge) => edge.target === nodeId && edge.targetHandle === 'item'); - if (!firstEdgeToCollect?.sourceHandle) { - return null; - } - const node = nodes.find((n) => n.id === firstEdgeToCollect.source); - if (!node) { - return null; - } - const template = templates[node.data.type]; - if (!template) { - return null; - } - const fieldType = template.outputs[firstEdgeToCollect.sourceHandle]?.type ?? null; - return fieldType; -}; - -/** - * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts` - * TODO: Figure out how to do this without duplicating all the logic - */ - -export const makeConnectionErrorSelector = ( - templates: Templates, - pendingConnection: PendingConnection | null, - nodeId: string, - fieldName: string, - handleType: HandleType, - fieldType?: FieldType | null -) => { - return createSelector(selectNodesSlice, (nodesSlice) => { - const { nodes, edges } = nodesSlice; - - if (!fieldType) { - return i18n.t('nodes.noFieldType'); - } - - if (!pendingConnection) { - return i18n.t('nodes.noConnectionInProgress'); - } - - const connectionNodeId = pendingConnection.node.id; - const connectionFieldName = pendingConnection.fieldTemplate.name; - const connectionHandleType = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; - const connectionStartFieldType = pendingConnection.fieldTemplate.type; - - if (!connectionHandleType || !connectionNodeId || !connectionFieldName) { - return i18n.t('nodes.noConnectionData'); - } - - const targetType = handleType === 'target' ? fieldType : connectionStartFieldType; - const sourceType = handleType === 'source' ? fieldType : connectionStartFieldType; - - if (nodeId === connectionNodeId) { - return i18n.t('nodes.cannotConnectToSelf'); - } - - if (handleType === connectionHandleType) { - if (handleType === 'source') { - return i18n.t('nodes.cannotConnectOutputToOutput'); - } - return i18n.t('nodes.cannotConnectInputToInput'); - } - - // we have to figure out which is the target and which is the source - const targetNodeId = handleType === 'target' ? nodeId : connectionNodeId; - const targetFieldName = handleType === 'target' ? fieldName : connectionFieldName; - const sourceNodeId = handleType === 'source' ? nodeId : connectionNodeId; - const sourceFieldName = handleType === 'source' ? fieldName : connectionFieldName; - - if ( - edges.find((edge) => { - edge.target === targetNodeId && - edge.targetHandle === targetFieldName && - edge.source === sourceNodeId && - edge.sourceHandle === sourceFieldName; - }) - ) { - // We already have a connection from this source to this target - return i18n.t('nodes.cannotDuplicateConnection'); - } - - const targetNode = nodes.find((node) => node.id === targetNodeId); - assert(targetNode, `Target node not found: ${targetNodeId}`); - const targetTemplate = templates[targetNode.data.type]; - assert(targetTemplate, `Target template not found: ${targetNode.data.type}`); - - if (targetTemplate.inputs[targetFieldName]?.input === 'direct') { - return i18n.t('nodes.cannotConnectToDirectInput'); - } - - if (targetNode?.data.type === 'collect' && targetFieldName === 'item') { - // Collect nodes shouldn't mix and match field types - const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); - if (collectItemType) { - if (!areTypesEqual(sourceType, collectItemType)) { - return i18n.t('nodes.cannotMixAndMatchCollectionItemTypes'); - } - } - } - - if ( - edges.find((edge) => { - return edge.target === targetNodeId && edge.targetHandle === targetFieldName; - }) && - // except CollectionItem inputs can have multiples - targetType.name !== 'CollectionItemField' - ) { - return i18n.t('nodes.inputMayOnlyHaveOneConnection'); - } - - if (!validateSourceAndTargetTypes(sourceType, targetType)) { - return i18n.t('nodes.fieldTypesMustMatch'); - } - - const isGraphAcyclic = getIsGraphAcyclic( - connectionHandleType === 'source' ? connectionNodeId : nodeId, - connectionHandleType === 'source' ? nodeId : connectionNodeId, - nodes, - edges - ); - - if (!isGraphAcyclic) { - return i18n.t('nodes.connectionWouldCreateCycle'); - } - - return; - }); -}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts deleted file mode 100644 index 45b771b5b4..0000000000 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { type FieldType, isStatefulFieldType } from 'features/nodes/types/field'; -import { isEqual, omit } from 'lodash-es'; - -export const areTypesEqual = (sourceType: FieldType, targetType: FieldType) => { - const _sourceType = isStatefulFieldType(sourceType) ? omit(sourceType, 'originalType') : sourceType; - const _targetType = isStatefulFieldType(targetType) ? omit(targetType, 'originalType') : targetType; - const _sourceTypeOriginal = isStatefulFieldType(sourceType) ? sourceType.originalType : sourceType; - const _targetTypeOriginal = isStatefulFieldType(targetType) ? targetType.originalType : targetType; - if (isEqual(_sourceType, _targetType)) { - return true; - } - if (_targetTypeOriginal && isEqual(_sourceType, _targetTypeOriginal)) { - return true; - } - if (_sourceTypeOriginal && isEqual(_sourceTypeOriginal, _targetType)) { - return true; - } - if (_sourceTypeOriginal && _targetTypeOriginal && isEqual(_sourceTypeOriginal, _targetTypeOriginal)) { - return true; - } - return false; -}; - -/** - * Validates that the source and target types are compatible for a connection. - * @param sourceType The type of the source field. - * @param targetType The type of the target field. - * @returns True if the connection is valid, false otherwise. - */ -export const validateSourceAndTargetTypes = (sourceType: FieldType, targetType: FieldType) => { - // TODO: There's a bug with Collect -> Iterate nodes: - // https://github.com/invoke-ai/InvokeAI/issues/3956 - // Once this is resolved, we can remove this check. - if (sourceType.name === 'CollectionField' && targetType.name === 'CollectionField') { - return false; - } - - if (areTypesEqual(sourceType, targetType)) { - return true; - } - - /** - * Connection types must be the same for a connection, with exceptions: - * - CollectionItem can connect to any non-Collection - * - Non-Collections can connect to CollectionItem - * - Anything (non-Collections, Collections, CollectionOrScalar) can connect to CollectionOrScalar of the same base type - * - Generic Collection can connect to any other Collection or CollectionOrScalar - * - Any Collection can connect to a Generic Collection - */ - - const isCollectionItemToNonCollection = sourceType.name === 'CollectionItemField' && !targetType.isCollection; - - const isNonCollectionToCollectionItem = - targetType.name === 'CollectionItemField' && !sourceType.isCollection && !sourceType.isCollectionOrScalar; - - const isAnythingToCollectionOrScalarOfSameBaseType = - targetType.isCollectionOrScalar && sourceType.name === targetType.name; - - const isGenericCollectionToAnyCollectionOrCollectionOrScalar = - sourceType.name === 'CollectionField' && (targetType.isCollection || targetType.isCollectionOrScalar); - - const isCollectionToGenericCollection = targetType.name === 'CollectionField' && sourceType.isCollection; - - const areBothTypesSingle = - !sourceType.isCollection && - !sourceType.isCollectionOrScalar && - !targetType.isCollection && - !targetType.isCollectionOrScalar; - - const isIntToFloat = areBothTypesSingle && sourceType.name === 'IntegerField' && targetType.name === 'FloatField'; - - const isIntOrFloatToString = - areBothTypesSingle && - (sourceType.name === 'IntegerField' || sourceType.name === 'FloatField') && - targetType.name === 'StringField'; - - const isTargetAnyType = targetType.name === 'AnyField'; - - // One of these must be true for the connection to be valid - return ( - isCollectionItemToNonCollection || - isNonCollectionToCollectionItem || - isAnythingToCollectionOrScalarOfSameBaseType || - isGenericCollectionToAnyCollectionOrCollectionOrScalar || - isCollectionToGenericCollection || - isIntToFloat || - isIntOrFloatToString || - isTargetAnyType - ); -}; From 9d127fee6bc7ca2ac93ec91a65fbfb4fb27fbc39 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 17:28:56 +1000 Subject: [PATCH 199/442] feat(ui): makeConnectionErrorSelector now creates a parameterized selector --- .../nodes/hooks/useConnectionState.ts | 14 +- .../nodes/store/util/connectionValidation.ts | 176 +++++++++--------- 2 files changed, 93 insertions(+), 97 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index dfa8b0cf36..9571ce2ee2 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -34,16 +34,8 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta ); const selectConnectionError = useMemo( - () => - makeConnectionErrorSelector( - templates, - pendingConnection, - nodeId, - fieldName, - kind === 'inputs' ? 'target' : 'source', - fieldType - ), - [templates, pendingConnection, nodeId, fieldName, kind, fieldType] + () => makeConnectionErrorSelector(templates, nodeId, fieldName, kind === 'inputs' ? 'target' : 'source', fieldType), + [templates, nodeId, fieldName, kind, fieldType] ); const isConnected = useAppSelector(selectIsConnected); @@ -58,7 +50,7 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta pendingConnection.fieldTemplate.fieldKind === { inputs: 'input', outputs: 'output' }[kind] ); }, [fieldName, kind, nodeId, pendingConnection]); - const connectionError = useAppSelector(selectConnectionError); + const connectionError = useAppSelector((s) => selectConnectionError(s, pendingConnection)); const shouldDim = useMemo( () => Boolean(isConnectionInProgress && connectionError && !isConnectionStartField), diff --git a/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts b/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts index 98de4284ad..907426b51d 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts @@ -1,7 +1,8 @@ import graphlib from '@dagrejs/graphlib'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import type { RootState } from 'app/store/store'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import type { PendingConnection, Templates } from 'features/nodes/store/types'; +import type { NodesState, PendingConnection, Templates } from 'features/nodes/store/types'; import { type FieldType, isStatefulFieldType } from 'features/nodes/types/field'; import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; import i18n from 'i18next'; @@ -190,105 +191,108 @@ export const getCollectItemType = ( */ export const makeConnectionErrorSelector = ( templates: Templates, - pendingConnection: PendingConnection | null, nodeId: string, fieldName: string, handleType: HandleType, fieldType: FieldType ) => { - return createMemoizedSelector(selectNodesSlice, (nodesSlice) => { - const { nodes, edges } = nodesSlice; + return createMemoizedSelector( + selectNodesSlice, + (state: RootState, pendingConnection: PendingConnection | null) => pendingConnection, + (nodesSlice: NodesState, pendingConnection: PendingConnection | null) => { + const { nodes, edges } = nodesSlice; - if (!pendingConnection) { - return i18n.t('nodes.noConnectionInProgress'); - } - - const connectionNodeId = pendingConnection.node.id; - const connectionFieldName = pendingConnection.fieldTemplate.name; - const connectionHandleType = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; - const connectionStartFieldType = pendingConnection.fieldTemplate.type; - - if (!connectionHandleType || !connectionNodeId || !connectionFieldName) { - return i18n.t('nodes.noConnectionData'); - } - - const targetType = handleType === 'target' ? fieldType : connectionStartFieldType; - const sourceType = handleType === 'source' ? fieldType : connectionStartFieldType; - - if (nodeId === connectionNodeId) { - return i18n.t('nodes.cannotConnectToSelf'); - } - - if (handleType === connectionHandleType) { - if (handleType === 'source') { - return i18n.t('nodes.cannotConnectOutputToOutput'); + if (!pendingConnection) { + return i18n.t('nodes.noConnectionInProgress'); } - return i18n.t('nodes.cannotConnectInputToInput'); - } - // we have to figure out which is the target and which is the source - const targetNodeId = handleType === 'target' ? nodeId : connectionNodeId; - const targetFieldName = handleType === 'target' ? fieldName : connectionFieldName; - const sourceNodeId = handleType === 'source' ? nodeId : connectionNodeId; - const sourceFieldName = handleType === 'source' ? fieldName : connectionFieldName; + const connectionNodeId = pendingConnection.node.id; + const connectionFieldName = pendingConnection.fieldTemplate.name; + const connectionHandleType = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; + const connectionStartFieldType = pendingConnection.fieldTemplate.type; - if ( - edges.find((edge) => { - edge.target === targetNodeId && - edge.targetHandle === targetFieldName && - edge.source === sourceNodeId && - edge.sourceHandle === sourceFieldName; - }) - ) { - // We already have a connection from this source to this target - return i18n.t('nodes.cannotDuplicateConnection'); - } + if (!connectionHandleType || !connectionNodeId || !connectionFieldName) { + return i18n.t('nodes.noConnectionData'); + } - const targetNode = nodes.find((node) => node.id === targetNodeId); - assert(targetNode, `Target node not found: ${targetNodeId}`); - const targetTemplate = templates[targetNode.data.type]; - assert(targetTemplate, `Target template not found: ${targetNode.data.type}`); + const targetType = handleType === 'target' ? fieldType : connectionStartFieldType; + const sourceType = handleType === 'source' ? fieldType : connectionStartFieldType; - if (targetTemplate.inputs[targetFieldName]?.input === 'direct') { - return i18n.t('nodes.cannotConnectToDirectInput'); - } - if (targetNode.data.type === 'collect' && targetFieldName === 'item') { - // Collect nodes shouldn't mix and match field types - const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); - if (collectItemType) { - if (!areTypesEqual(sourceType, collectItemType)) { - return i18n.t('nodes.cannotMixAndMatchCollectionItemTypes'); + if (nodeId === connectionNodeId) { + return i18n.t('nodes.cannotConnectToSelf'); + } + + if (handleType === connectionHandleType) { + if (handleType === 'source') { + return i18n.t('nodes.cannotConnectOutputToOutput'); + } + return i18n.t('nodes.cannotConnectInputToInput'); + } + + // we have to figure out which is the target and which is the source + const targetNodeId = handleType === 'target' ? nodeId : connectionNodeId; + const targetFieldName = handleType === 'target' ? fieldName : connectionFieldName; + const sourceNodeId = handleType === 'source' ? nodeId : connectionNodeId; + const sourceFieldName = handleType === 'source' ? fieldName : connectionFieldName; + + if ( + edges.find((edge) => { + edge.target === targetNodeId && + edge.targetHandle === targetFieldName && + edge.source === sourceNodeId && + edge.sourceHandle === sourceFieldName; + }) + ) { + // We already have a connection from this source to this target + return i18n.t('nodes.cannotDuplicateConnection'); + } + + const targetNode = nodes.find((node) => node.id === targetNodeId); + assert(targetNode, `Target node not found: ${targetNodeId}`); + const targetTemplate = templates[targetNode.data.type]; + assert(targetTemplate, `Target template not found: ${targetNode.data.type}`); + + if (targetTemplate.inputs[targetFieldName]?.input === 'direct') { + return i18n.t('nodes.cannotConnectToDirectInput'); + } + if (targetNode.data.type === 'collect' && targetFieldName === 'item') { + // Collect nodes shouldn't mix and match field types + const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); + if (collectItemType) { + if (!areTypesEqual(sourceType, collectItemType)) { + return i18n.t('nodes.cannotMixAndMatchCollectionItemTypes'); + } } } + + if ( + edges.find((edge) => { + return edge.target === targetNodeId && edge.targetHandle === targetFieldName; + }) && + // except CollectionItem inputs can have multiples + targetType.name !== 'CollectionItemField' + ) { + return i18n.t('nodes.inputMayOnlyHaveOneConnection'); + } + + if (!validateSourceAndTargetTypes(sourceType, targetType)) { + return i18n.t('nodes.fieldTypesMustMatch'); + } + + const hasCycles = getHasCycles( + connectionHandleType === 'source' ? connectionNodeId : nodeId, + connectionHandleType === 'source' ? nodeId : connectionNodeId, + nodes, + edges + ); + + if (hasCycles) { + return i18n.t('nodes.connectionWouldCreateCycle'); + } + + return; } - - if ( - edges.find((edge) => { - return edge.target === targetNodeId && edge.targetHandle === targetFieldName; - }) && - // except CollectionItem inputs can have multiples - targetType.name !== 'CollectionItemField' - ) { - return i18n.t('nodes.inputMayOnlyHaveOneConnection'); - } - - if (!validateSourceAndTargetTypes(sourceType, targetType)) { - return i18n.t('nodes.fieldTypesMustMatch'); - } - - const hasCycles = getHasCycles( - connectionHandleType === 'source' ? connectionNodeId : nodeId, - connectionHandleType === 'source' ? nodeId : connectionNodeId, - nodes, - edges - ); - - if (hasCycles) { - return i18n.t('nodes.connectionWouldCreateCycle'); - } - - return; - }); + ); }; /** From 468644ab189edb82dfb3cb9cedcd4e94df705a2e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 17:37:28 +1000 Subject: [PATCH 200/442] fix(ui): rebase conflict --- .../web/src/features/nodes/store/util/connectionValidation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts b/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts index 907426b51d..a2f723fcfe 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts @@ -255,6 +255,7 @@ export const makeConnectionErrorSelector = ( if (targetTemplate.inputs[targetFieldName]?.input === 'direct') { return i18n.t('nodes.cannotConnectToDirectInput'); } + if (targetNode.data.type === 'collect' && targetFieldName === 'item') { // Collect nodes shouldn't mix and match field types const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); From 9f7841a04bb09403dd744c001dbb0b661df03388 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 18:46:03 +1000 Subject: [PATCH 201/442] tidy(ui): clean up addnodepopover hotkeys --- .../flow/AddNodePopover/AddNodePopover.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 40fa13320a..214fc069f9 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -21,7 +21,6 @@ import { getFirstValidConnection, validateSourceAndTargetTypes } from 'features/ import type { AnyNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { filter, map, memoize, some } from 'lodash-es'; -import type { KeyboardEventHandler } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; import { flushSync } from 'react-dom'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -159,25 +158,24 @@ const AddNodePopover = () => { ); const handleHotkeyOpen: HotkeyCallback = useCallback((e) => { - e.preventDefault(); - openAddNodePopover(); - flushSync(() => { - selectRef.current?.inputRef?.focus(); - }); + if (!$isAddNodePopoverOpen.get()) { + e.preventDefault(); + openAddNodePopover(); + flushSync(() => { + selectRef.current?.inputRef?.focus(); + }); + } }, []); const handleHotkeyClose: HotkeyCallback = useCallback(() => { - closeAddNodePopover(); - }, []); - - useHotkeys(['shift+a', 'space'], handleHotkeyOpen); - useHotkeys(['escape'], handleHotkeyClose); - const onKeyDown: KeyboardEventHandler = useCallback((e) => { - if (e.key === 'Escape') { + if ($isAddNodePopoverOpen.get()) { closeAddNodePopover(); } }, []); + useHotkeys(['shift+a', 'space'], handleHotkeyOpen); + useHotkeys(['escape'], handleHotkeyClose, { enableOnFormTags: ['TEXTAREA'] }); + const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]); return ( @@ -214,7 +212,6 @@ const AddNodePopover = () => { filterOption={filterOption} onChange={onChange} onMenuClose={closeAddNodePopover} - onKeyDown={onKeyDown} inputRef={inputRef} closeMenuOnSelect={false} /> From 6b4e464d1780b53b6d7c05edefdeec2efa958085 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 18:55:37 +1000 Subject: [PATCH 202/442] fix(ui): rework edge update logic --- .../features/nodes/components/flow/Flow.tsx | 76 ++++++++++--------- .../src/features/nodes/store/nodesSlice.ts | 11 ++- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 656de737c7..501513919a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -8,12 +8,13 @@ import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection' import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { $cursorPos, + $didUpdateEdge, $isAddNodePopoverOpen, $isUpdatingEdge, + $lastEdgeUpdateMouseEvent, $pendingConnection, $viewport, connectionMade, - edgeAdded, edgeDeleted, edgesChanged, edgesDeleted, @@ -24,6 +25,7 @@ import { undo, } from 'features/nodes/store/nodesSlice'; import { $flow } from 'features/nodes/store/reactFlowInstance'; +import { isString } from 'lodash-es'; import type { CSSProperties, MouseEvent } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -39,7 +41,7 @@ import type { ReactFlowProps, ReactFlowState, } from 'reactflow'; -import { Background, ReactFlow, useStore as useReactFlowStore } from 'reactflow'; +import { Background, ReactFlow, useStore as useReactFlowStore, useUpdateNodeInternals } from 'reactflow'; import CustomConnectionLine from './connectionLines/CustomConnectionLine'; import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge'; @@ -81,6 +83,7 @@ export const Flow = memo(() => { const flowWrapper = useRef(null); const isValidConnection = useIsValidConnection(); const cancelConnection = useReactFlowStore(selectCancelConnection); + const updateNodeInternals = useUpdateNodeInternals(); useWorkflowWatcher(); useSyncExecutionState(); const [borderRadius] = useToken('radii', ['base']); @@ -157,45 +160,46 @@ export const Flow = memo(() => { * where the edge is deleted if you click it accidentally). */ - // We have a ref for cursor position, but it is the *projected* cursor position. - // Easiest to just keep track of the last mouse event for this particular feature - const edgeUpdateMouseEvent = useRef(); - - const onEdgeUpdateStart: NonNullable = useCallback( - (e, edge, _handleType) => { - $isUpdatingEdge.set(true); - // update mouse event - edgeUpdateMouseEvent.current = e; - // always delete the edge when starting an updated - dispatch(edgeDeleted(edge.id)); - }, - [dispatch] - ); + const onEdgeUpdateStart: NonNullable = useCallback((e, _edge, _handleType) => { + $isUpdatingEdge.set(true); + $didUpdateEdge.set(false); + $lastEdgeUpdateMouseEvent.set(e); + }, []); const onEdgeUpdate: OnEdgeUpdateFunc = useCallback( - (_oldEdge, newConnection) => { - // Because we deleted the edge when the update started, we must create a new edge from the connection + (edge, newConnection) => { + // This event is fired when an edge update is successful + $didUpdateEdge.set(true); + // When an edge update is successful, we need to delete the old edge and create a new one + dispatch(edgeDeleted(edge.id)); dispatch(connectionMade(newConnection)); + // Because we shift the position of handles depending on whether a field is connected or not, we must use + // updateNodeInternals to tell reactflow to recalculate the positions of the handles + const nodesToUpdate = [edge.source, edge.target, newConnection.source, newConnection.target].filter(isString); + updateNodeInternals(nodesToUpdate); }, - [dispatch] + [dispatch, updateNodeInternals] ); const onEdgeUpdateEnd: NonNullable = useCallback( (e, edge, _handleType) => { - $isUpdatingEdge.set(false); - $pendingConnection.set(null); - // Handle the case where user begins a drag but didn't move the cursor - we deleted the edge when starting - // the edge update - we need to add it back - if ( - // ignore touch events - !('touches' in e) && - edgeUpdateMouseEvent.current?.clientX === e.clientX && - edgeUpdateMouseEvent.current?.clientY === e.clientY - ) { - dispatch(edgeAdded(edge)); + const didUpdateEdge = $didUpdateEdge.get(); + // Fall back to a reasonable default event + const lastEvent = $lastEdgeUpdateMouseEvent.get() ?? { clientX: 0, clientY: 0 }; + // We have to narrow this event down to MouseEvents - could be TouchEvent + const didMouseMove = + !('touches' in e) && Math.hypot(e.clientX - lastEvent.clientX, e.clientY - lastEvent.clientY) > 5; + + // If we got this far and did not successfully update an edge, and the mouse moved away from the handle, + // the user probably intended to delete the edge + if (!didUpdateEdge && didMouseMove) { + dispatch(edgeDeleted(edge.id)); } - // reset mouse event - edgeUpdateMouseEvent.current = undefined; + + $isUpdatingEdge.set(false); + $didUpdateEdge.set(false); + $pendingConnection.set(null); + $lastEdgeUpdateMouseEvent.set(null); }, [dispatch] ); @@ -255,9 +259,11 @@ export const Flow = memo(() => { useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey); const onEscapeHotkey = useCallback(() => { - $pendingConnection.set(null); - $isAddNodePopoverOpen.set(false); - cancelConnection(); + if (!$isUpdatingEdge.get()) { + $pendingConnection.set(null); + $isAddNodePopoverOpen.set(false); + cancelConnection(); + } }, [cancelConnection]); useHotkeys('esc', onEscapeHotkey); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index cec13e8df4..83632c16e1 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -47,6 +47,7 @@ import { import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; import { atom } from 'nanostores'; +import type { MouseEvent } from 'react'; import type { Connection, Edge, EdgeChange, EdgeRemoveChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow'; import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow'; import type { UndoableOptions } from 'redux-undo'; @@ -125,9 +126,6 @@ export const nodesSlice = createSlice({ edgesChanged: (state, action: PayloadAction) => { state.edges = applyEdgeChanges(action.payload, state.edges); }, - edgeAdded: (state, action: PayloadAction) => { - state.edges = addEdge(action.payload, state.edges); - }, connectionMade: (state, action: PayloadAction) => { state.edges = addEdge({ ...action.payload, type: 'default' }, state.edges); }, @@ -495,7 +493,6 @@ export const { notesNodeValueChanged, selectedAll, selectionPasted, - edgeAdded, undo, redo, } = nodesSlice.actions; @@ -507,6 +504,9 @@ export const $copiedEdges = atom([]); export const $edgesToCopiedNodes = atom([]); export const $pendingConnection = atom(null); export const $isUpdatingEdge = atom(false); +export const $didUpdateEdge = atom(false); +export const $lastEdgeUpdateMouseEvent = atom(null); + export const $viewport = atom({ x: 0, y: 0, zoom: 1 }); export const $isAddNodePopoverOpen = atom(false); export const closeAddNodePopover = () => { @@ -609,6 +609,5 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( nodesDeleted, nodeUseCacheChanged, notesNodeValueChanged, - selectionPasted, - edgeAdded + selectionPasted ); From 6f7160b9fd70d1cccefd2fb3ddd397e8580ed86d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 18 May 2024 18:59:14 +1000 Subject: [PATCH 203/442] fix(ui): call updateNodeInternals when making connections --- .../web/src/features/nodes/hooks/useConnection.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index 81eea993be..f0dba67bf5 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -10,13 +10,15 @@ import { } from 'features/nodes/store/nodesSlice'; import { getFirstValidConnection } from 'features/nodes/store/util/connectionValidation'; import { isInvocationNode } from 'features/nodes/types/invocation'; +import { isString } from 'lodash-es'; import { useCallback, useMemo } from 'react'; -import type { OnConnect, OnConnectEnd, OnConnectStart } from 'reactflow'; +import { type OnConnect, type OnConnectEnd, type OnConnectStart, useUpdateNodeInternals } from 'reactflow'; import { assert } from 'tsafe'; export const useConnection = () => { const store = useAppStore(); const templates = useStore($templates); + const updateNodeInternals = useUpdateNodeInternals(); const onConnectStart = useCallback( (event, params) => { @@ -41,9 +43,11 @@ export const useConnection = () => { (connection) => { const { dispatch } = store; dispatch(connectionMade(connection)); + const nodesToUpdate = [connection.source, connection.target].filter(isString); + updateNodeInternals(nodesToUpdate); $pendingConnection.set(null); }, - [store] + [store, updateNodeInternals] ); const onConnectEnd = useCallback(() => { const { dispatch } = store; @@ -80,13 +84,15 @@ export const useConnection = () => { ); if (connection) { dispatch(connectionMade(connection)); + const nodesToUpdate = [connection.source, connection.target].filter(isString); + updateNodeInternals(nodesToUpdate); } $pendingConnection.set(null); } else { // The mouse is not over a node - we should open the add node popover $isAddNodePopoverOpen.set(true); } - }, [store, templates]); + }, [store, templates, updateNodeInternals]); const api = useMemo(() => ({ onConnectStart, onConnect, onConnectEnd }), [onConnectStart, onConnect, onConnectEnd]); return api; From 3fcb2720d73c6ad13e972ad5f525448579f5796e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 00:11:15 +1000 Subject: [PATCH 204/442] tests(ui): add tests for consolidated connection validation --- invokeai/frontend/web/public/locales/en.json | 3 + .../flow/AddNodePopover/AddNodePopover.tsx | 5 +- .../src/features/nodes/hooks/useConnection.ts | 2 +- .../nodes/hooks/useIsValidConnection.ts | 12 +- .../nodes/store/util/areTypesEqual.test.ts | 101 ++ .../nodes/store/util/areTypesEqual.ts | 30 + .../nodes/store/util/connectionValidation.ts | 271 +---- .../store/util/getCollectItemType.test.ts | 16 + .../nodes/store/util/getCollectItemType.ts | 35 + .../store/util/getFirstValidConnection.ts | 116 ++ .../nodes/store/util/getHasCycles.test.ts | 23 + .../features/nodes/store/util/getHasCycles.ts | 30 + .../features/nodes/store/util/testUtils.ts | 1073 +++++++++++++++++ .../store/util/validateConnection.test.ts | 149 +++ .../nodes/store/util/validateConnection.ts | 109 ++ .../util/validateConnectionTypes.test.ts | 222 ++++ .../store/util/validateConnectionTypes.ts | 69 ++ .../web/src/features/nodes/types/field.ts | 19 - .../nodes/util/schema/parseSchema.test.ts | 937 +------------- 19 files changed, 1999 insertions(+), 1223 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.test.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.test.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 7de7a8e01c..1f44e641fc 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -775,6 +775,9 @@ "cannotConnectToSelf": "Cannot connect to self", "cannotDuplicateConnection": "Cannot create duplicate connections", "cannotMixAndMatchCollectionItemTypes": "Cannot mix and match collection item types", + "missingNode": "Missing invocation node", + "missingInvocationTemplate": "Missing invocation template", + "missingFieldTemplate": "Missing field template", "nodePack": "Node pack", "collection": "Collection", "collectionFieldType": "{{name}} Collection", diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 214fc069f9..14d69b4720 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -17,7 +17,8 @@ import { nodeAdded, openAddNodePopover, } from 'features/nodes/store/nodesSlice'; -import { getFirstValidConnection, validateSourceAndTargetTypes } from 'features/nodes/store/util/connectionValidation'; +import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; +import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; import type { AnyNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { filter, map, memoize, some } from 'lodash-es'; @@ -77,7 +78,7 @@ const AddNodePopover = () => { return some(fields, (field) => { const sourceType = pendingFieldKind === 'input' ? field.type : pendingConnection.fieldTemplate.type; const targetType = pendingFieldKind === 'output' ? field.type : pendingConnection.fieldTemplate.type; - return validateSourceAndTargetTypes(sourceType, targetType); + return validateConnectionTypes(sourceType, targetType); }); }); }, [templates, pendingConnection]); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index f0dba67bf5..0190a0b29e 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -8,7 +8,7 @@ import { $templates, connectionMade, } from 'features/nodes/store/nodesSlice'; -import { getFirstValidConnection } from 'features/nodes/store/util/connectionValidation'; +import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { isString } from 'lodash-es'; import { useCallback, useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index b92114bab2..77c4e3c75b 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -2,12 +2,10 @@ import { useStore } from '@nanostores/react'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { $templates } from 'features/nodes/store/nodesSlice'; -import { - areTypesEqual, - getCollectItemType, - getHasCycles, - validateSourceAndTargetTypes, -} from 'features/nodes/store/util/connectionValidation'; +import { areTypesEqual } from 'features/nodes/store/util/areTypesEqual'; +import { getCollectItemType } from 'features/nodes/store/util/getCollectItemType'; +import { getHasCycles } from 'features/nodes/store/util/getHasCycles'; +import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { useCallback } from 'react'; import type { Connection, Node } from 'reactflow'; @@ -88,7 +86,7 @@ export const useIsValidConnection = () => { } // Must use the originalType here if it exists - if (!validateSourceAndTargetTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { + if (!validateConnectionTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { return false; } diff --git a/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.test.ts new file mode 100644 index 0000000000..7be307d07e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; + +import { areTypesEqual } from './areTypesEqual'; + +describe(areTypesEqual.name, () => { + it('should handle equal source and target type', () => { + const sourceType = { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'Foo', + isCollection: false, + isCollectionOrScalar: false, + }, + }; + const targetType = { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'Bar', + isCollection: false, + isCollectionOrScalar: false, + }, + }; + expect(areTypesEqual(sourceType, targetType)).toBe(true); + }); + + it('should handle equal source type and original target type', () => { + const sourceType = { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'Foo', + isCollection: false, + isCollectionOrScalar: false, + }, + }; + const targetType = { + name: 'Bar', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + }; + expect(areTypesEqual(sourceType, targetType)).toBe(true); + }); + + it('should handle equal original source type and target type', () => { + const sourceType = { + name: 'Foo', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + }; + const targetType = { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'Bar', + isCollection: false, + isCollectionOrScalar: false, + }, + }; + expect(areTypesEqual(sourceType, targetType)).toBe(true); + }); + + it('should handle equal original source type and original target type', () => { + const sourceType = { + name: 'Foo', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + }; + const targetType = { + name: 'Bar', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + }; + expect(areTypesEqual(sourceType, targetType)).toBe(true); + }); +}); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.ts b/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.ts new file mode 100644 index 0000000000..e01b48b972 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.ts @@ -0,0 +1,30 @@ +import type { FieldType } from 'features/nodes/types/field'; +import { isEqual, omit } from 'lodash-es'; + +/** + * Checks if two types are equal. If the field types have original types, those are also compared. Any match is + * considered equal. For example, if the source type and original target type match, the types are considered equal. + * @param sourceType The type of the source field. + * @param targetType The type of the target field. + * @returns True if the types are equal, false otherwise. + */ + +export const areTypesEqual = (sourceType: FieldType, targetType: FieldType) => { + const _sourceType = 'originalType' in sourceType ? omit(sourceType, 'originalType') : sourceType; + const _targetType = 'originalType' in targetType ? omit(targetType, 'originalType') : targetType; + const _sourceTypeOriginal = 'originalType' in sourceType ? sourceType.originalType : null; + const _targetTypeOriginal = 'originalType' in targetType ? targetType.originalType : null; + if (isEqual(_sourceType, _targetType)) { + return true; + } + if (_targetTypeOriginal && isEqual(_sourceType, _targetTypeOriginal)) { + return true; + } + if (_sourceTypeOriginal && isEqual(_sourceTypeOriginal, _targetType)) { + return true; + } + if (_sourceTypeOriginal && _targetTypeOriginal && isEqual(_sourceTypeOriginal, _targetTypeOriginal)) { + return true; + } + return false; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts b/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts index a2f723fcfe..7819221f8a 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts @@ -1,179 +1,16 @@ -import graphlib from '@dagrejs/graphlib'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import type { RootState } from 'app/store/store'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { NodesState, PendingConnection, Templates } from 'features/nodes/store/types'; -import { type FieldType, isStatefulFieldType } from 'features/nodes/types/field'; -import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; +import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; +import type { FieldType } from 'features/nodes/types/field'; import i18n from 'i18next'; -import { differenceWith, isEqual, map, omit } from 'lodash-es'; -import type { Connection, Edge, HandleType, Node } from 'reactflow'; +import type { HandleType } from 'reactflow'; import { assert } from 'tsafe'; -/** - * Finds the first valid field for a pending connection between two nodes. - * @param templates The invocation templates - * @param nodes The current nodes - * @param edges The current edges - * @param pendingConnection The pending connection - * @param candidateNode The candidate node to which the connection is being made - * @param candidateTemplate The candidate template for the candidate node - * @returns The first valid connection, or null if no valid connection is found - */ -export const getFirstValidConnection = ( - templates: Templates, - nodes: AnyNode[], - edges: InvocationNodeEdge[], - pendingConnection: PendingConnection, - candidateNode: InvocationNode, - candidateTemplate: InvocationTemplate -): Connection | null => { - if (pendingConnection.node.id === candidateNode.id) { - // Cannot connect to self - return null; - } - - const pendingFieldKind = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; - - if (pendingFieldKind === 'source') { - // Connecting from a source to a target - if (getHasCycles(pendingConnection.node.id, candidateNode.id, nodes, edges)) { - return null; - } - if (candidateNode.data.type === 'collect') { - // Special handling for collect node - the `item` field takes any number of connections - return { - source: pendingConnection.node.id, - sourceHandle: pendingConnection.fieldTemplate.name, - target: candidateNode.id, - targetHandle: 'item', - }; - } - // Only one connection per target field is allowed - look for an unconnected target field - const candidateFields = map(candidateTemplate.inputs); - const candidateConnectedFields = edges - .filter((edge) => edge.target === candidateNode.id) - .map((edge) => { - // Edges must always have a targetHandle, safe to assert here - assert(edge.targetHandle); - return edge.targetHandle; - }); - const candidateUnconnectedFields = differenceWith( - candidateFields, - candidateConnectedFields, - (field, connectedFieldName) => field.name === connectedFieldName - ); - const candidateField = candidateUnconnectedFields.find((field) => - validateSourceAndTargetTypes(pendingConnection.fieldTemplate.type, field.type) - ); - if (candidateField) { - return { - source: pendingConnection.node.id, - sourceHandle: pendingConnection.fieldTemplate.name, - target: candidateNode.id, - targetHandle: candidateField.name, - }; - } - } else { - // Connecting from a target to a source - // Ensure we there is not already an edge to the target, except for collect nodes - const isCollect = pendingConnection.node.data.type === 'collect'; - const isTargetAlreadyConnected = edges.some( - (e) => e.target === pendingConnection.node.id && e.targetHandle === pendingConnection.fieldTemplate.name - ); - if (!isCollect && isTargetAlreadyConnected) { - return null; - } - - if (getHasCycles(candidateNode.id, pendingConnection.node.id, nodes, edges)) { - return null; - } - - // Sources/outputs can have any number of edges, we can take the first matching output field - let candidateFields = map(candidateTemplate.outputs); - if (isCollect) { - // Narrow candidates to same field type as already is connected to the collect node - const collectItemType = getCollectItemType(templates, nodes, edges, pendingConnection.node.id); - if (collectItemType) { - candidateFields = candidateFields.filter((field) => areTypesEqual(field.type, collectItemType)); - } - } - const candidateField = candidateFields.find((field) => { - const isValid = validateSourceAndTargetTypes(field.type, pendingConnection.fieldTemplate.type); - const isAlreadyConnected = edges.some((e) => e.source === candidateNode.id && e.sourceHandle === field.name); - return isValid && !isAlreadyConnected; - }); - if (candidateField) { - return { - source: candidateNode.id, - sourceHandle: candidateField.name, - target: pendingConnection.node.id, - targetHandle: pendingConnection.fieldTemplate.name, - }; - } - } - - return null; -}; - -/** - * Check if adding an edge between the source and target nodes would create a cycle in the graph. - * @param source The source node id - * @param target The target node id - * @param nodes The graph's current nodes - * @param edges The graph's current edges - * @returns True if the graph would be acyclic after adding the edge, false otherwise - */ -export const getHasCycles = (source: string, target: string, nodes: Node[], edges: Edge[]) => { - // construct graphlib graph from editor state - const g = new graphlib.Graph(); - - nodes.forEach((n) => { - g.setNode(n.id); - }); - - edges.forEach((e) => { - g.setEdge(e.source, e.target); - }); - - // add the candidate edge - g.setEdge(source, target); - - // check if the graph is acyclic - return !graphlib.alg.isAcyclic(g); -}; - -/** - * Given a collect node, return the type of the items it collects. The graph is traversed to find the first node and - * field connected to the collector's `item` input. The field type of that field is returned, else null if there is no - * input field. - * @param templates The current invocation templates - * @param nodes The current nodes - * @param edges The current edges - * @param nodeId The collect node's id - * @returns The type of the items the collect node collects, or null if there is no input field - */ -export const getCollectItemType = ( - templates: Templates, - nodes: AnyNode[], - edges: InvocationNodeEdge[], - nodeId: string -): FieldType | null => { - const firstEdgeToCollect = edges.find((edge) => edge.target === nodeId && edge.targetHandle === 'item'); - if (!firstEdgeToCollect?.sourceHandle) { - return null; - } - const node = nodes.find((n) => n.id === firstEdgeToCollect.source); - if (!node) { - return null; - } - const template = templates[node.data.type]; - if (!template) { - return null; - } - const fieldType = template.outputs[firstEdgeToCollect.sourceHandle]?.type ?? null; - return fieldType; -}; +import { areTypesEqual } from './areTypesEqual'; +import { getCollectItemType } from './getCollectItemType'; +import { getHasCycles } from './getHasCycles'; /** * Creates a selector that validates a pending connection. @@ -276,7 +113,7 @@ export const makeConnectionErrorSelector = ( return i18n.t('nodes.inputMayOnlyHaveOneConnection'); } - if (!validateSourceAndTargetTypes(sourceType, targetType)) { + if (!validateConnectionTypes(sourceType, targetType)) { return i18n.t('nodes.fieldTypesMustMatch'); } @@ -295,97 +132,3 @@ export const makeConnectionErrorSelector = ( } ); }; - -/** - * Validates that the source and target types are compatible for a connection. - * @param sourceType The type of the source field. - * @param targetType The type of the target field. - * @returns True if the connection is valid, false otherwise. - */ -export const validateSourceAndTargetTypes = (sourceType: FieldType, targetType: FieldType) => { - // TODO: There's a bug with Collect -> Iterate nodes: - // https://github.com/invoke-ai/InvokeAI/issues/3956 - // Once this is resolved, we can remove this check. - if (sourceType.name === 'CollectionField' && targetType.name === 'CollectionField') { - return false; - } - - if (areTypesEqual(sourceType, targetType)) { - return true; - } - - /** - * Connection types must be the same for a connection, with exceptions: - * - CollectionItem can connect to any non-Collection - * - Non-Collections can connect to CollectionItem - * - Anything (non-Collections, Collections, CollectionOrScalar) can connect to CollectionOrScalar of the same base type - * - Generic Collection can connect to any other Collection or CollectionOrScalar - * - Any Collection can connect to a Generic Collection - */ - const isCollectionItemToNonCollection = sourceType.name === 'CollectionItemField' && !targetType.isCollection; - - const isNonCollectionToCollectionItem = - targetType.name === 'CollectionItemField' && !sourceType.isCollection && !sourceType.isCollectionOrScalar; - - const isAnythingToCollectionOrScalarOfSameBaseType = - targetType.isCollectionOrScalar && sourceType.name === targetType.name; - - const isGenericCollectionToAnyCollectionOrCollectionOrScalar = - sourceType.name === 'CollectionField' && (targetType.isCollection || targetType.isCollectionOrScalar); - - const isCollectionToGenericCollection = targetType.name === 'CollectionField' && sourceType.isCollection; - - const areBothTypesSingle = - !sourceType.isCollection && - !sourceType.isCollectionOrScalar && - !targetType.isCollection && - !targetType.isCollectionOrScalar; - - const isIntToFloat = areBothTypesSingle && sourceType.name === 'IntegerField' && targetType.name === 'FloatField'; - - const isIntOrFloatToString = - areBothTypesSingle && - (sourceType.name === 'IntegerField' || sourceType.name === 'FloatField') && - targetType.name === 'StringField'; - - const isTargetAnyType = targetType.name === 'AnyField'; - - // One of these must be true for the connection to be valid - return ( - isCollectionItemToNonCollection || - isNonCollectionToCollectionItem || - isAnythingToCollectionOrScalarOfSameBaseType || - isGenericCollectionToAnyCollectionOrCollectionOrScalar || - isCollectionToGenericCollection || - isIntToFloat || - isIntOrFloatToString || - isTargetAnyType - ); -}; - -/** - * Checks if two types are equal. If the field types have original types, those are also compared. Any match is - * considered equal. For example, if the source type and original target type match, the types are considered equal. - * @param sourceType The type of the source field. - * @param targetType The type of the target field. - * @returns True if the types are equal, false otherwise. - */ -export const areTypesEqual = (sourceType: FieldType, targetType: FieldType) => { - const _sourceType = isStatefulFieldType(sourceType) ? omit(sourceType, 'originalType') : sourceType; - const _targetType = isStatefulFieldType(targetType) ? omit(targetType, 'originalType') : targetType; - const _sourceTypeOriginal = isStatefulFieldType(sourceType) ? sourceType.originalType : sourceType; - const _targetTypeOriginal = isStatefulFieldType(targetType) ? targetType.originalType : targetType; - if (isEqual(_sourceType, _targetType)) { - return true; - } - if (_targetTypeOriginal && isEqual(_sourceType, _targetTypeOriginal)) { - return true; - } - if (_sourceTypeOriginal && isEqual(_sourceTypeOriginal, _targetType)) { - return true; - } - if (_sourceTypeOriginal && _targetTypeOriginal && isEqual(_sourceTypeOriginal, _targetTypeOriginal)) { - return true; - } - return false; -}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts new file mode 100644 index 0000000000..93c63b6f41 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts @@ -0,0 +1,16 @@ +import { getCollectItemType } from 'features/nodes/store/util/getCollectItemType'; +import { add, buildEdge, collect, position, templates } from 'features/nodes/store/util/testUtils'; +import type { FieldType } from 'features/nodes/types/field'; +import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode'; +import { describe, expect, it } from 'vitest'; + +describe(getCollectItemType.name, () => { + it('should return the type of the items the collect node collects', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, collect); + const nodes = [n1, n2]; + const edges = [buildEdge(n1.id, 'value', n2.id, 'item')]; + const result = getCollectItemType(templates, nodes, edges, n2.id); + expect(result).toEqual({ name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }); + }); +}); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts new file mode 100644 index 0000000000..9e0ce0fbee --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts @@ -0,0 +1,35 @@ +import type { Templates } from 'features/nodes/store/types'; +import type { FieldType } from 'features/nodes/types/field'; +import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; + +/** + * Given a collect node, return the type of the items it collects. The graph is traversed to find the first node and + * field connected to the collector's `item` input. The field type of that field is returned, else null if there is no + * input field. + * @param templates The current invocation templates + * @param nodes The current nodes + * @param edges The current edges + * @param nodeId The collect node's id + * @returns The type of the items the collect node collects, or null if there is no input field + */ +export const getCollectItemType = ( + templates: Templates, + nodes: AnyNode[], + edges: InvocationNodeEdge[], + nodeId: string +): FieldType | null => { + const firstEdgeToCollect = edges.find((edge) => edge.target === nodeId && edge.targetHandle === 'item'); + if (!firstEdgeToCollect?.sourceHandle) { + return null; + } + const node = nodes.find((n) => n.id === firstEdgeToCollect.source); + if (!node) { + return null; + } + const template = templates[node.data.type]; + if (!template) { + return null; + } + const fieldType = template.outputs[firstEdgeToCollect.sourceHandle]?.type ?? null; + return fieldType; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts new file mode 100644 index 0000000000..98155f0c20 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts @@ -0,0 +1,116 @@ +import type { PendingConnection, Templates } from 'features/nodes/store/types'; +import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; +import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; +import { differenceWith, map } from 'lodash-es'; +import type { Connection } from 'reactflow'; +import { assert } from 'tsafe'; + +import { areTypesEqual } from './areTypesEqual'; +import { getCollectItemType } from './getCollectItemType'; +import { getHasCycles } from './getHasCycles'; + +/** + * Finds the first valid field for a pending connection between two nodes. + * @param templates The invocation templates + * @param nodes The current nodes + * @param edges The current edges + * @param pendingConnection The pending connection + * @param candidateNode The candidate node to which the connection is being made + * @param candidateTemplate The candidate template for the candidate node + * @returns The first valid connection, or null if no valid connection is found + */ + +export const getFirstValidConnection = ( + templates: Templates, + nodes: AnyNode[], + edges: InvocationNodeEdge[], + pendingConnection: PendingConnection, + candidateNode: InvocationNode, + candidateTemplate: InvocationTemplate +): Connection | null => { + if (pendingConnection.node.id === candidateNode.id) { + // Cannot connect to self + return null; + } + + const pendingFieldKind = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; + + if (pendingFieldKind === 'source') { + // Connecting from a source to a target + if (getHasCycles(pendingConnection.node.id, candidateNode.id, nodes, edges)) { + return null; + } + if (candidateNode.data.type === 'collect') { + // Special handling for collect node - the `item` field takes any number of connections + return { + source: pendingConnection.node.id, + sourceHandle: pendingConnection.fieldTemplate.name, + target: candidateNode.id, + targetHandle: 'item', + }; + } + // Only one connection per target field is allowed - look for an unconnected target field + const candidateFields = map(candidateTemplate.inputs); + const candidateConnectedFields = edges + .filter((edge) => edge.target === candidateNode.id) + .map((edge) => { + // Edges must always have a targetHandle, safe to assert here + assert(edge.targetHandle); + return edge.targetHandle; + }); + const candidateUnconnectedFields = differenceWith( + candidateFields, + candidateConnectedFields, + (field, connectedFieldName) => field.name === connectedFieldName + ); + const candidateField = candidateUnconnectedFields.find((field) => validateConnectionTypes(pendingConnection.fieldTemplate.type, field.type) + ); + if (candidateField) { + return { + source: pendingConnection.node.id, + sourceHandle: pendingConnection.fieldTemplate.name, + target: candidateNode.id, + targetHandle: candidateField.name, + }; + } + } else { + // Connecting from a target to a source + // Ensure we there is not already an edge to the target, except for collect nodes + const isCollect = pendingConnection.node.data.type === 'collect'; + const isTargetAlreadyConnected = edges.some( + (e) => e.target === pendingConnection.node.id && e.targetHandle === pendingConnection.fieldTemplate.name + ); + if (!isCollect && isTargetAlreadyConnected) { + return null; + } + + if (getHasCycles(candidateNode.id, pendingConnection.node.id, nodes, edges)) { + return null; + } + + // Sources/outputs can have any number of edges, we can take the first matching output field + let candidateFields = map(candidateTemplate.outputs); + if (isCollect) { + // Narrow candidates to same field type as already is connected to the collect node + const collectItemType = getCollectItemType(templates, nodes, edges, pendingConnection.node.id); + if (collectItemType) { + candidateFields = candidateFields.filter((field) => areTypesEqual(field.type, collectItemType)); + } + } + const candidateField = candidateFields.find((field) => { + const isValid = validateConnectionTypes(field.type, pendingConnection.fieldTemplate.type); + const isAlreadyConnected = edges.some((e) => e.source === candidateNode.id && e.sourceHandle === field.name); + return isValid && !isAlreadyConnected; + }); + if (candidateField) { + return { + source: candidateNode.id, + sourceHandle: candidateField.name, + target: pendingConnection.node.id, + targetHandle: pendingConnection.fieldTemplate.name, + }; + } + } + + return null; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.test.ts new file mode 100644 index 0000000000..872da36998 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.test.ts @@ -0,0 +1,23 @@ +import { getHasCycles } from 'features/nodes/store/util/getHasCycles'; +import { add, buildEdge, position } from 'features/nodes/store/util/testUtils'; +import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode'; +import { describe, expect, it } from 'vitest'; + +describe(getHasCycles.name, () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, add); + const n3 = buildInvocationNode(position, add); + const nodes = [n1, n2, n3]; + + it('should return true if the graph WOULD have cycles after adding the edge', () => { + const edges = [buildEdge(n1.id, 'value', n2.id, 'a'), buildEdge(n2.id, 'value', n3.id, 'a')]; + const result = getHasCycles(n3.id, n1.id, nodes, edges); + expect(result).toBe(true); + }); + + it('should return false if the graph WOULD NOT have cycles after adding the edge', () => { + const edges = [buildEdge(n1.id, 'value', n2.id, 'a')]; + const result = getHasCycles(n2.id, n3.id, nodes, edges); + expect(result).toBe(false); + }); +}); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.ts b/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.ts new file mode 100644 index 0000000000..c1a4e51f0c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.ts @@ -0,0 +1,30 @@ +import graphlib from '@dagrejs/graphlib'; +import type { Edge, Node } from 'reactflow'; + +/** + * Check if adding an edge between the source and target nodes would create a cycle in the graph. + * @param source The source node id + * @param target The target node id + * @param nodes The graph's current nodes + * @param edges The graph's current edges + * @returns True if the graph would be acyclic after adding the edge, false otherwise + */ + +export const getHasCycles = (source: string, target: string, nodes: Node[], edges: Edge[]) => { + // construct graphlib graph from editor state + const g = new graphlib.Graph(); + + nodes.forEach((n) => { + g.setNode(n.id); + }); + + edges.forEach((e) => { + g.setEdge(e.source, e.target); + }); + + // add the candidate edge + g.setEdge(source, target); + + // check if the graph is acyclic + return !graphlib.alg.isAcyclic(g); +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts new file mode 100644 index 0000000000..efde3336e2 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts @@ -0,0 +1,1073 @@ +import type { Templates } from 'features/nodes/store/types'; +import type { InvocationTemplate } from 'features/nodes/types/invocation'; +import type { OpenAPIV3_1 } from 'openapi-types'; +import type { Edge, XYPosition } from 'reactflow'; + +export const buildEdge = (source: string, sourceHandle: string, target: string, targetHandle: string): Edge => ({ + source, + sourceHandle, + target, + targetHandle, + type: 'default', + id: `reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`, +}); + +export const position: XYPosition = { x: 0, y: 0 }; + +export const add: InvocationTemplate = { + title: 'Add Integers', + type: 'add', + version: '1.0.1', + tags: ['math', 'add'], + description: 'Adds two numbers', + outputType: 'integer_output', + inputs: { + a: { + name: 'a', + title: 'A', + required: false, + description: 'The first number', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + default: 0, + }, + b: { + name: 'b', + title: 'B', + required: false, + description: 'The second number', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + default: 0, + }, + }, + outputs: { + value: { + fieldKind: 'output', + name: 'value', + title: 'Value', + description: 'The output integer', + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', +}; + +export const sub: InvocationTemplate = { + title: 'Subtract Integers', + type: 'sub', + version: '1.0.1', + tags: ['math', 'subtract'], + description: 'Subtracts two numbers', + outputType: 'integer_output', + inputs: { + a: { + name: 'a', + title: 'A', + required: false, + description: 'The first number', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + default: 0, + }, + b: { + name: 'b', + title: 'B', + required: false, + description: 'The second number', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + default: 0, + }, + }, + outputs: { + value: { + fieldKind: 'output', + name: 'value', + title: 'Value', + description: 'The output integer', + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', +}; + +export const collect: InvocationTemplate = { + title: 'Collect', + type: 'collect', + version: '1.0.0', + tags: [], + description: 'Collects values into a collection', + outputType: 'collect_output', + inputs: { + item: { + name: 'item', + title: 'Collection Item', + required: false, + description: 'The item to collect (all inputs must be of the same type)', + fieldKind: 'input', + input: 'connection', + ui_hidden: false, + ui_type: 'CollectionItemField', + type: { + name: 'CollectionItemField', + isCollection: false, + isCollectionOrScalar: false, + }, + }, + }, + outputs: { + collection: { + fieldKind: 'output', + name: 'collection', + title: 'Collection', + description: 'The collection of input items', + type: { + name: 'CollectionField', + isCollection: true, + isCollectionOrScalar: false, + }, + ui_hidden: false, + ui_type: 'CollectionField', + }, + }, + useCache: true, + classification: 'stable', +}; + +export const scheduler: InvocationTemplate = { + title: 'Scheduler', + type: 'scheduler', + version: '1.0.0', + tags: ['scheduler'], + description: 'Selects a scheduler.', + outputType: 'scheduler_output', + inputs: { + scheduler: { + name: 'scheduler', + title: 'Scheduler', + required: false, + description: 'Scheduler to use during inference', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + ui_type: 'SchedulerField', + type: { + name: 'SchedulerField', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'EnumField', + isCollection: false, + isCollectionOrScalar: false, + }, + }, + default: 'euler', + }, + }, + outputs: { + scheduler: { + fieldKind: 'output', + name: 'scheduler', + title: 'Scheduler', + description: 'Scheduler to use during inference', + type: { + name: 'SchedulerField', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'EnumField', + isCollection: false, + isCollectionOrScalar: false, + }, + }, + ui_hidden: false, + ui_type: 'SchedulerField', + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', +}; + +export const main_model_loader: InvocationTemplate = { + title: 'Main Model', + type: 'main_model_loader', + version: '1.0.2', + tags: ['model'], + description: 'Loads a main model, outputting its submodels.', + outputType: 'model_loader_output', + inputs: { + model: { + name: 'model', + title: 'Model', + required: true, + description: 'Main model (UNet, VAE, CLIP) to load', + fieldKind: 'input', + input: 'direct', + ui_hidden: false, + ui_type: 'MainModelField', + type: { + name: 'MainModelField', + isCollection: false, + isCollectionOrScalar: false, + originalType: { + name: 'ModelIdentifierField', + isCollection: false, + isCollectionOrScalar: false, + }, + }, + }, + }, + outputs: { + vae: { + fieldKind: 'output', + name: 'vae', + title: 'VAE', + description: 'VAE', + type: { + name: 'VAEField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + clip: { + fieldKind: 'output', + name: 'clip', + title: 'CLIP', + description: 'CLIP (tokenizer, text encoder, LoRAs) and skipped layer count', + type: { + name: 'CLIPField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + unet: { + fieldKind: 'output', + name: 'unet', + title: 'UNet', + description: 'UNet (scheduler, LoRAs)', + type: { + name: 'UNetField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', +} + +export const templates: Templates = { + add, + sub, + collect, + scheduler, + main_model_loader, +}; + +export const schema = { + openapi: '3.1.0', + info: { + title: 'Invoke - Community Edition', + description: 'An API for invoking AI image operations', + version: '1.0.0', + }, + components: { + schemas: { + AddInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + a: { + type: 'integer', + title: 'A', + description: 'The first number', + default: 0, + field_kind: 'input', + input: 'any', + orig_default: 0, + orig_required: false, + ui_hidden: false, + }, + b: { + type: 'integer', + title: 'B', + description: 'The second number', + default: 0, + field_kind: 'input', + input: 'any', + orig_default: 0, + orig_required: false, + ui_hidden: false, + }, + type: { + type: 'string', + enum: ['add'], + const: 'add', + title: 'type', + default: 'add', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['type', 'id'], + title: 'Add Integers', + description: 'Adds two numbers', + category: 'math', + classification: 'stable', + node_pack: 'invokeai', + tags: ['math', 'add'], + version: '1.0.1', + output: { + $ref: '#/components/schemas/IntegerOutput', + }, + class: 'invocation', + }, + IntegerOutput: { + description: 'Base class for nodes that output a single integer', + properties: { + value: { + description: 'The output integer', + field_kind: 'output', + title: 'Value', + type: 'integer', + ui_hidden: false, + }, + type: { + const: 'integer_output', + default: 'integer_output', + enum: ['integer_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + }, + required: ['value', 'type', 'type'], + title: 'IntegerOutput', + type: 'object', + class: 'output', + }, + SchedulerInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + scheduler: { + type: 'string', + enum: [ + 'ddim', + 'ddpm', + 'deis', + 'lms', + 'lms_k', + 'pndm', + 'heun', + 'heun_k', + 'euler', + 'euler_k', + 'euler_a', + 'kdpm_2', + 'kdpm_2_a', + 'dpmpp_2s', + 'dpmpp_2s_k', + 'dpmpp_2m', + 'dpmpp_2m_k', + 'dpmpp_2m_sde', + 'dpmpp_2m_sde_k', + 'dpmpp_sde', + 'dpmpp_sde_k', + 'unipc', + 'lcm', + 'tcd', + ], + title: 'Scheduler', + description: 'Scheduler to use during inference', + default: 'euler', + field_kind: 'input', + input: 'any', + orig_default: 'euler', + orig_required: false, + ui_hidden: false, + ui_type: 'SchedulerField', + }, + type: { + type: 'string', + enum: ['scheduler'], + const: 'scheduler', + title: 'type', + default: 'scheduler', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['type', 'id'], + title: 'Scheduler', + description: 'Selects a scheduler.', + category: 'latents', + classification: 'stable', + node_pack: 'invokeai', + tags: ['scheduler'], + version: '1.0.0', + output: { + $ref: '#/components/schemas/SchedulerOutput', + }, + class: 'invocation', + }, + SchedulerOutput: { + properties: { + scheduler: { + description: 'Scheduler to use during inference', + enum: [ + 'ddim', + 'ddpm', + 'deis', + 'lms', + 'lms_k', + 'pndm', + 'heun', + 'heun_k', + 'euler', + 'euler_k', + 'euler_a', + 'kdpm_2', + 'kdpm_2_a', + 'dpmpp_2s', + 'dpmpp_2s_k', + 'dpmpp_2m', + 'dpmpp_2m_k', + 'dpmpp_2m_sde', + 'dpmpp_2m_sde_k', + 'dpmpp_sde', + 'dpmpp_sde_k', + 'unipc', + 'lcm', + 'tcd', + ], + field_kind: 'output', + title: 'Scheduler', + type: 'string', + ui_hidden: false, + ui_type: 'SchedulerField', + }, + type: { + const: 'scheduler_output', + default: 'scheduler_output', + enum: ['scheduler_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + }, + required: ['scheduler', 'type', 'type'], + title: 'SchedulerOutput', + type: 'object', + class: 'output', + }, + MainModelLoaderInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + model: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Main model (UNet, VAE, CLIP) to load', + field_kind: 'input', + input: 'direct', + orig_required: true, + ui_hidden: false, + ui_type: 'MainModelField', + }, + type: { + type: 'string', + enum: ['main_model_loader'], + const: 'main_model_loader', + title: 'type', + default: 'main_model_loader', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['model', 'type', 'id'], + title: 'Main Model', + description: 'Loads a main model, outputting its submodels.', + category: 'model', + classification: 'stable', + node_pack: 'invokeai', + tags: ['model'], + version: '1.0.2', + output: { + $ref: '#/components/schemas/ModelLoaderOutput', + }, + class: 'invocation', + }, + ModelIdentifierField: { + properties: { + key: { + description: "The model's unique key", + title: 'Key', + type: 'string', + }, + hash: { + description: "The model's BLAKE3 hash", + title: 'Hash', + type: 'string', + }, + name: { + description: "The model's name", + title: 'Name', + type: 'string', + }, + base: { + allOf: [ + { + $ref: '#/components/schemas/BaseModelType', + }, + ], + description: "The model's base model type", + }, + type: { + allOf: [ + { + $ref: '#/components/schemas/ModelType', + }, + ], + description: "The model's type", + }, + submodel_type: { + anyOf: [ + { + $ref: '#/components/schemas/SubModelType', + }, + { + type: 'null', + }, + ], + default: null, + description: 'The submodel to load, if this is a main model', + }, + }, + required: ['key', 'hash', 'name', 'base', 'type'], + title: 'ModelIdentifierField', + type: 'object', + }, + BaseModelType: { + description: 'Base model type.', + enum: ['any', 'sd-1', 'sd-2', 'sdxl', 'sdxl-refiner'], + title: 'BaseModelType', + type: 'string', + }, + ModelType: { + description: 'Model type.', + enum: ['onnx', 'main', 'vae', 'lora', 'controlnet', 'embedding', 'ip_adapter', 'clip_vision', 't2i_adapter'], + title: 'ModelType', + type: 'string', + }, + SubModelType: { + description: 'Submodel type.', + enum: [ + 'unet', + 'text_encoder', + 'text_encoder_2', + 'tokenizer', + 'tokenizer_2', + 'vae', + 'vae_decoder', + 'vae_encoder', + 'scheduler', + 'safety_checker', + ], + title: 'SubModelType', + type: 'string', + }, + ModelLoaderOutput: { + description: 'Model loader output', + properties: { + vae: { + allOf: [ + { + $ref: '#/components/schemas/VAEField', + }, + ], + description: 'VAE', + field_kind: 'output', + title: 'VAE', + ui_hidden: false, + }, + type: { + const: 'model_loader_output', + default: 'model_loader_output', + enum: ['model_loader_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + clip: { + allOf: [ + { + $ref: '#/components/schemas/CLIPField', + }, + ], + description: 'CLIP (tokenizer, text encoder, LoRAs) and skipped layer count', + field_kind: 'output', + title: 'CLIP', + ui_hidden: false, + }, + unet: { + allOf: [ + { + $ref: '#/components/schemas/UNetField', + }, + ], + description: 'UNet (scheduler, LoRAs)', + field_kind: 'output', + title: 'UNet', + ui_hidden: false, + }, + }, + required: ['vae', 'type', 'clip', 'unet', 'type'], + title: 'ModelLoaderOutput', + type: 'object', + class: 'output', + }, + UNetField: { + properties: { + unet: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load unet submodel', + }, + scheduler: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load scheduler submodel', + }, + loras: { + description: 'LoRAs to apply on model loading', + items: { + $ref: '#/components/schemas/LoRAField', + }, + title: 'Loras', + type: 'array', + }, + seamless_axes: { + description: 'Axes("x" and "y") to which apply seamless', + items: { + type: 'string', + }, + title: 'Seamless Axes', + type: 'array', + }, + freeu_config: { + anyOf: [ + { + $ref: '#/components/schemas/FreeUConfig', + }, + { + type: 'null', + }, + ], + default: null, + description: 'FreeU configuration', + }, + }, + required: ['unet', 'scheduler', 'loras'], + title: 'UNetField', + type: 'object', + class: 'output', + }, + LoRAField: { + properties: { + lora: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load lora model', + }, + weight: { + description: 'Weight to apply to lora model', + title: 'Weight', + type: 'number', + }, + }, + required: ['lora', 'weight'], + title: 'LoRAField', + type: 'object', + class: 'output', + }, + FreeUConfig: { + description: + 'Configuration for the FreeU hyperparameters.\n- https://huggingface.co/docs/diffusers/main/en/using-diffusers/freeu\n- https://github.com/ChenyangSi/FreeU', + properties: { + s1: { + description: + 'Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', + maximum: 3, + minimum: -1, + title: 'S1', + type: 'number', + }, + s2: { + description: + 'Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', + maximum: 3, + minimum: -1, + title: 'S2', + type: 'number', + }, + b1: { + description: 'Scaling factor for stage 1 to amplify the contributions of backbone features.', + maximum: 3, + minimum: -1, + title: 'B1', + type: 'number', + }, + b2: { + description: 'Scaling factor for stage 2 to amplify the contributions of backbone features.', + maximum: 3, + minimum: -1, + title: 'B2', + type: 'number', + }, + }, + required: ['s1', 's2', 'b1', 'b2'], + title: 'FreeUConfig', + type: 'object', + class: 'output', + }, + VAEField: { + properties: { + vae: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load vae submodel', + }, + seamless_axes: { + description: 'Axes("x" and "y") to which apply seamless', + items: { + type: 'string', + }, + title: 'Seamless Axes', + type: 'array', + }, + }, + required: ['vae'], + title: 'VAEField', + type: 'object', + class: 'output', + }, + CLIPField: { + properties: { + tokenizer: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load tokenizer submodel', + }, + text_encoder: { + allOf: [ + { + $ref: '#/components/schemas/ModelIdentifierField', + }, + ], + description: 'Info to load text_encoder submodel', + }, + skipped_layers: { + description: 'Number of skipped layers in text_encoder', + title: 'Skipped Layers', + type: 'integer', + }, + loras: { + description: 'LoRAs to apply on model loading', + items: { + $ref: '#/components/schemas/LoRAField', + }, + title: 'Loras', + type: 'array', + }, + }, + required: ['tokenizer', 'text_encoder', 'skipped_layers', 'loras'], + title: 'CLIPField', + type: 'object', + class: 'output', + }, + CollectInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + item: { + anyOf: [ + {}, + { + type: 'null', + }, + ], + title: 'Collection Item', + description: 'The item to collect (all inputs must be of the same type)', + field_kind: 'input', + input: 'connection', + orig_required: false, + ui_hidden: false, + ui_type: 'CollectionItemField', + }, + collection: { + items: {}, + type: 'array', + title: 'Collection', + description: 'The collection, will be provided on execution', + default: [], + field_kind: 'input', + input: 'any', + orig_default: [], + orig_required: false, + ui_hidden: true, + }, + type: { + type: 'string', + enum: ['collect'], + const: 'collect', + title: 'type', + default: 'collect', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['type', 'id'], + title: 'CollectInvocation', + description: 'Collects values into a collection', + classification: 'stable', + version: '1.0.0', + output: { + $ref: '#/components/schemas/CollectInvocationOutput', + }, + class: 'invocation', + }, + CollectInvocationOutput: { + properties: { + collection: { + description: 'The collection of input items', + field_kind: 'output', + items: {}, + title: 'Collection', + type: 'array', + ui_hidden: false, + ui_type: 'CollectionField', + }, + type: { + const: 'collect_output', + default: 'collect_output', + enum: ['collect_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + }, + required: ['collection', 'type', 'type'], + title: 'CollectInvocationOutput', + type: 'object', + class: 'output', + }, + SubtractInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + a: { + type: 'integer', + title: 'A', + description: 'The first number', + default: 0, + field_kind: 'input', + input: 'any', + orig_default: 0, + orig_required: false, + ui_hidden: false, + }, + b: { + type: 'integer', + title: 'B', + description: 'The second number', + default: 0, + field_kind: 'input', + input: 'any', + orig_default: 0, + orig_required: false, + ui_hidden: false, + }, + type: { + type: 'string', + enum: ['sub'], + const: 'sub', + title: 'type', + default: 'sub', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['type', 'id'], + title: 'Subtract Integers', + description: 'Subtracts two numbers', + category: 'math', + classification: 'stable', + node_pack: 'invokeai', + tags: ['math', 'subtract'], + version: '1.0.1', + output: { + $ref: '#/components/schemas/IntegerOutput', + }, + class: 'invocation', + }, + }, + }, +} as OpenAPIV3_1.Document; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts new file mode 100644 index 0000000000..5d10ef368b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts @@ -0,0 +1,149 @@ +import { deepClone } from 'common/util/deepClone'; +import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode'; +import { set } from 'lodash-es'; +import { describe, expect, it } from 'vitest'; + +import { add, buildEdge, collect, main_model_loader, position, sub, templates } from './testUtils'; +import { buildAcceptResult, buildRejectResult, validateConnection } from './validateConnection'; + +describe(validateConnection.name, () => { + it('should reject invalid connection to self', () => { + const c = { source: 'add', sourceHandle: 'value', target: 'add', targetHandle: 'a' }; + const r = validateConnection(c, [], [], templates, null); + expect(r).toEqual(buildRejectResult('nodes.cannotConnectToSelf')); + }); + + describe('missing nodes', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, sub); + const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; + + it('should reject missing source node', () => { + const r = validateConnection(c, [n2], [], templates, null); + expect(r).toEqual(buildRejectResult('nodes.missingNode')); + }); + + it('should reject missing target node', () => { + const r = validateConnection(c, [n1], [], templates, null); + expect(r).toEqual(buildRejectResult('nodes.missingNode')); + }); + }); + + describe('missing invocation templates', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, sub); + const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; + const nodes = [n1, n2]; + + it('should reject missing source template', () => { + const r = validateConnection(c, nodes, [], { sub }, null); + expect(r).toEqual(buildRejectResult('nodes.missingInvocationTemplate')); + }); + + it('should reject missing target template', () => { + const r = validateConnection(c, nodes, [], { add }, null); + expect(r).toEqual(buildRejectResult('nodes.missingInvocationTemplate')); + }); + }); + + describe('missing field templates', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, sub); + const nodes = [n1, n2]; + + it('should reject missing source field template', () => { + const c = { source: n1.id, sourceHandle: 'invalid', target: n2.id, targetHandle: 'a' }; + const r = validateConnection(c, nodes, [], templates, null); + expect(r).toEqual(buildRejectResult('nodes.missingFieldTemplate')); + }); + + it('should reject missing target field template', () => { + const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'invalid' }; + const r = validateConnection(c, nodes, [], templates, null); + expect(r).toEqual(buildRejectResult('nodes.missingFieldTemplate')); + }); + }); + + describe('duplicate connections', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, sub); + it('should accept non-duplicate connections', () => { + const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; + const r = validateConnection(c, [n1, n2], [], templates, null); + expect(r).toEqual(buildAcceptResult()); + }); + it('should reject duplicate connections', () => { + const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; + const e = buildEdge(n1.id, 'value', n2.id, 'a'); + const r = validateConnection(c, [n1, n2], [e], templates, null); + expect(r).toEqual(buildRejectResult('nodes.cannotDuplicateConnection')); + }); + it('should accept duplicate connections if the duplicate is an ignored edge', () => { + const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; + const e = buildEdge(n1.id, 'value', n2.id, 'a'); + const r = validateConnection(c, [n1, n2], [e], templates, e); + expect(r).toEqual(buildAcceptResult()); + }); + }); + + it('should reject connection to direct input', () => { + // Create cloned add template w/ a direct input + const addWithDirectAField = deepClone(add); + set(addWithDirectAField, 'inputs.a.input', 'direct'); + set(addWithDirectAField, 'type', 'addWithDirectAField'); + + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, addWithDirectAField); + const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; + const r = validateConnection(c, [n1, n2], [], { add, addWithDirectAField }, null); + expect(r).toEqual(buildRejectResult('nodes.cannotConnectToDirectInput')); + }); + + it('should reject connection to a collect node with mismatched item types', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, collect); + const n3 = buildInvocationNode(position, main_model_loader); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'value', n2.id, 'item'); + const edges = [e1]; + const c = { source: n3.id, sourceHandle: 'vae', target: n2.id, targetHandle: 'item' }; + const r = validateConnection(c, nodes, edges, templates, null); + expect(r).toEqual(buildRejectResult('nodes.cannotMixAndMatchCollectionItemTypes')); + }); + + it('should accept connection to a collect node with matching item types', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, collect); + const n3 = buildInvocationNode(position, sub); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'value', n2.id, 'item'); + const edges = [e1]; + const c = { source: n3.id, sourceHandle: 'value', target: n2.id, targetHandle: 'item' }; + const r = validateConnection(c, nodes, edges, templates, null); + expect(r).toEqual(buildAcceptResult()); + }); + + it('should reject connections to target field that is already connected', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, add); + const n3 = buildInvocationNode(position, add); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'value', n2.id, 'a'); + const edges = [e1]; + const c = { source: n3.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; + const r = validateConnection(c, nodes, edges, templates, null); + expect(r).toEqual(buildRejectResult('nodes.inputMayOnlyHaveOneConnection')); + }); + + it('should accept connections to target field that is already connected (ignored edge)', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, add); + const n3 = buildInvocationNode(position, add); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'value', n2.id, 'a'); + const edges = [e1]; + const c = { source: n3.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; + const r = validateConnection(c, nodes, edges, templates, e1); + expect(r).toEqual(buildAcceptResult()); + }); +}); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts new file mode 100644 index 0000000000..d45a75ab9f --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -0,0 +1,109 @@ +import type { Templates } from 'features/nodes/store/types'; +import { areTypesEqual } from 'features/nodes/store/util/areTypesEqual'; +import { getCollectItemType } from 'features/nodes/store/util/getCollectItemType'; +import type { AnyNode } from 'features/nodes/types/invocation'; +import type { Connection as NullableConnection, Edge } from 'reactflow'; +import type { O } from 'ts-toolbelt'; + +type Connection = O.NonNullable; + +export type ValidateConnectionResult = { + isValid: boolean; + messageTKey?: string; +}; + +export type ValidateConnectionFunc = ( + connection: Connection, + nodes: AnyNode[], + edges: Edge[], + templates: Templates, + ignoreEdge: Edge | null +) => ValidateConnectionResult; + +export const buildResult = (isValid: boolean, messageTKey?: string): ValidateConnectionResult => ({ + isValid, + messageTKey, +}); + +const getEqualityPredicate = + (c: Connection) => + (e: Edge): boolean => { + return ( + e.target === c.target && + e.targetHandle === c.targetHandle && + e.source === c.source && + e.sourceHandle === c.sourceHandle + ); + }; + +export const buildAcceptResult = (): ValidateConnectionResult => ({ isValid: true }); +export const buildRejectResult = (messageTKey: string): ValidateConnectionResult => ({ isValid: false, messageTKey }); + +export const validateConnection: ValidateConnectionFunc = (c, nodes, edges, templates, ignoreEdge) => { + if (c.source === c.target) { + return buildRejectResult('nodes.cannotConnectToSelf'); + } + + const filteredEdges = edges.filter((e) => e.id !== ignoreEdge?.id); + + if (filteredEdges.some(getEqualityPredicate(c))) { + // We already have a connection from this source to this target + return buildRejectResult('nodes.cannotDuplicateConnection'); + } + + const sourceNode = nodes.find((n) => n.id === c.source); + if (!sourceNode) { + return buildRejectResult('nodes.missingNode'); + } + + const targetNode = nodes.find((n) => n.id === c.target); + if (!targetNode) { + return buildRejectResult('nodes.missingNode'); + } + + const sourceTemplate = templates[sourceNode.data.type]; + if (!sourceTemplate) { + return buildRejectResult('nodes.missingInvocationTemplate'); + } + + const targetTemplate = templates[targetNode.data.type]; + if (!targetTemplate) { + return buildRejectResult('nodes.missingInvocationTemplate'); + } + + const sourceFieldTemplate = sourceTemplate.outputs[c.sourceHandle]; + if (!sourceFieldTemplate) { + return buildRejectResult('nodes.missingFieldTemplate'); + } + + const targetFieldTemplate = targetTemplate.inputs[c.targetHandle]; + if (!targetFieldTemplate) { + return buildRejectResult('nodes.missingFieldTemplate'); + } + + if (targetFieldTemplate.input === 'direct') { + return buildRejectResult('nodes.cannotConnectToDirectInput'); + } + + if (targetNode.data.type === 'collect' && c.targetHandle === 'item') { + // Collect nodes shouldn't mix and match field types + const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); + if (collectItemType) { + if (!areTypesEqual(sourceFieldTemplate.type, collectItemType)) { + return buildRejectResult('nodes.cannotMixAndMatchCollectionItemTypes'); + } + } + } + + if ( + edges.find((e) => { + return e.target === c.target && e.targetHandle === c.targetHandle; + }) && + // except CollectionItem inputs can have multiples + targetFieldTemplate.type.name !== 'CollectionItemField' + ) { + return buildRejectResult('nodes.inputMayOnlyHaveOneConnection'); + } + + return buildAcceptResult(); +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts new file mode 100644 index 0000000000..d953fd973f --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts @@ -0,0 +1,222 @@ +import { describe, expect, it } from 'vitest'; + +import { validateConnectionTypes } from './validateConnectionTypes'; + +describe(validateConnectionTypes.name, () => { + describe('generic cases', () => { + it('should accept Scalar to Scalar of same type', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, + { name: 'FooField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + it('should accept Collection to Collection of same type', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: true, isCollectionOrScalar: false }, + { name: 'FooField', isCollection: true, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + it('should accept Scalar to CollectionOrScalar of same type', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, + { name: 'FooField', isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + it('should accept Collection to CollectionOrScalar of same type', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: true, isCollectionOrScalar: false }, + { name: 'FooField', isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + it('should reject Collection to Scalar of same type', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: true, isCollectionOrScalar: false }, + { name: 'FooField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(false); + }); + it('should reject CollectionOrScalar to Scalar of same type', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: false, isCollectionOrScalar: true }, + { name: 'FooField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(false); + }); + it('should reject mismatched types', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, + { name: 'BarField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(false); + }); + }); + + describe('special cases', () => { + it('should reject a collection input to a collection input', () => { + const r = validateConnectionTypes( + { name: 'CollectionField', isCollection: true, isCollectionOrScalar: false }, + { name: 'CollectionField', isCollection: true, isCollectionOrScalar: false } + ); + expect(r).toBe(false); + }); + + it('should accept equal types', () => { + const r = validateConnectionTypes( + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }, + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + + describe('CollectionItemField', () => { + it('should accept CollectionItemField to any Scalar target', () => { + const r = validateConnectionTypes( + { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false }, + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + it('should accept CollectionItemField to any CollectionOrScalar target', () => { + const r = validateConnectionTypes( + { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false }, + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + it('should accept any non-Collection to CollectionItemField', () => { + const r = validateConnectionTypes( + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }, + { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + it('should reject any Collection to CollectionItemField', () => { + const r = validateConnectionTypes( + { name: 'IntegerField', isCollection: true, isCollectionOrScalar: false }, + { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(false); + }); + it('should reject any CollectionOrScalar to CollectionItemField', () => { + const r = validateConnectionTypes( + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true }, + { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(false); + }); + }); + + describe('CollectionOrScalar', () => { + it('should accept any Scalar of same type to CollectionOrScalar', () => { + const r = validateConnectionTypes( + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }, + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + it('should accept any Collection of same type to CollectionOrScalar', () => { + const r = validateConnectionTypes( + { name: 'IntegerField', isCollection: true, isCollectionOrScalar: false }, + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + it('should accept any CollectionOrScalar of same type to CollectionOrScalar', () => { + const r = validateConnectionTypes( + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true }, + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + }); + + describe('CollectionField', () => { + it('should accept any CollectionField to any Collection type', () => { + const r = validateConnectionTypes( + { name: 'CollectionField', isCollection: false, isCollectionOrScalar: false }, + { name: 'IntegerField', isCollection: true, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + it('should accept any CollectionField to any CollectionOrScalar type', () => { + const r = validateConnectionTypes( + { name: 'CollectionField', isCollection: false, isCollectionOrScalar: false }, + { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + }); + + describe('subtype handling', () => { + type TypePair = { t1: string; t2: string }; + const typePairs = [ + { t1: 'IntegerField', t2: 'FloatField' }, + { t1: 'IntegerField', t2: 'StringField' }, + { t1: 'FloatField', t2: 'StringField' }, + ]; + it.each(typePairs)('should accept Scalar $t1 to Scalar $t2', ({ t1, t2 }: TypePair) => { + const r = validateConnectionTypes( + { name: t1, isCollection: false, isCollectionOrScalar: false }, + { name: t2, isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + it.each(typePairs)('should accept Scalar $t1 to CollectionOrScalar $t2', ({ t1, t2 }: TypePair) => { + const r = validateConnectionTypes( + { name: t1, isCollection: false, isCollectionOrScalar: false }, + { name: t2, isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + it.each(typePairs)('should accept Collection $t1 to Collection $t2', ({ t1, t2 }: TypePair) => { + const r = validateConnectionTypes( + { name: t1, isCollection: true, isCollectionOrScalar: false }, + { name: t2, isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + it.each(typePairs)('should accept Collection $t1 to CollectionOrScalar $t2', ({ t1, t2 }: TypePair) => { + const r = validateConnectionTypes( + { name: t1, isCollection: true, isCollectionOrScalar: false }, + { name: t2, isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + it.each(typePairs)('should accept CollectionOrScalar $t1 to CollectionOrScalar $t2', ({ t1, t2 }: TypePair) => { + const r = validateConnectionTypes( + { name: t1, isCollection: false, isCollectionOrScalar: true }, + { name: t2, isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + }); + + describe('AnyField', () => { + it('should accept any Scalar type to AnyField', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, + { name: 'AnyField', isCollection: false, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + it('should accept any Collection type to AnyField', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, + { name: 'AnyField', isCollection: true, isCollectionOrScalar: false } + ); + expect(r).toBe(true); + }); + it('should accept any CollectionOrScalar type to AnyField', () => { + const r = validateConnectionTypes( + { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, + { name: 'AnyField', isCollection: false, isCollectionOrScalar: true } + ); + expect(r).toBe(true); + }); + }); + }); +}); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts new file mode 100644 index 0000000000..092279e315 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts @@ -0,0 +1,69 @@ +import { areTypesEqual } from 'features/nodes/store/util/areTypesEqual'; +import type { FieldType } from 'features/nodes/types/field'; + +/** + * Validates that the source and target types are compatible for a connection. + * @param sourceType The type of the source field. + * @param targetType The type of the target field. + * @returns True if the connection is valid, false otherwise. + */ +export const validateConnectionTypes = (sourceType: FieldType, targetType: FieldType) => { + // TODO: There's a bug with Collect -> Iterate nodes: + // https://github.com/invoke-ai/InvokeAI/issues/3956 + // Once this is resolved, we can remove this check. + if (sourceType.name === 'CollectionField' && targetType.name === 'CollectionField') { + return false; + } + + if (areTypesEqual(sourceType, targetType)) { + return true; + } + + /** + * Connection types must be the same for a connection, with exceptions: + * - CollectionItem can connect to any non-Collection + * - Non-Collections can connect to CollectionItem + * - Anything (non-Collections, Collections, CollectionOrScalar) can connect to CollectionOrScalar of the same base type + * - Generic Collection can connect to any other Collection or CollectionOrScalar + * - Any Collection can connect to a Generic Collection + */ + const isCollectionItemToNonCollection = sourceType.name === 'CollectionItemField' && !targetType.isCollection; + + const isNonCollectionToCollectionItem = + targetType.name === 'CollectionItemField' && !sourceType.isCollection && !sourceType.isCollectionOrScalar; + + const isAnythingToCollectionOrScalarOfSameBaseType = + targetType.isCollectionOrScalar && sourceType.name === targetType.name; + + const isGenericCollectionToAnyCollectionOrCollectionOrScalar = + sourceType.name === 'CollectionField' && (targetType.isCollection || targetType.isCollectionOrScalar); + + const isCollectionToGenericCollection = targetType.name === 'CollectionField' && sourceType.isCollection; + + const areBothTypesSingle = + !sourceType.isCollection && + !sourceType.isCollectionOrScalar && + !targetType.isCollection && + !targetType.isCollectionOrScalar; + + const isIntToFloat = areBothTypesSingle && sourceType.name === 'IntegerField' && targetType.name === 'FloatField'; + + const isIntOrFloatToString = + areBothTypesSingle && + (sourceType.name === 'IntegerField' || sourceType.name === 'FloatField') && + targetType.name === 'StringField'; + + const isTargetAnyType = targetType.name === 'AnyField'; + + // One of these must be true for the connection to be valid + return ( + isCollectionItemToNonCollection || + isNonCollectionToCollectionItem || + isAnythingToCollectionOrScalarOfSameBaseType || + isGenericCollectionToAnyCollectionOrCollectionOrScalar || + isCollectionToGenericCollection || + isIntToFloat || + isIntOrFloatToString || + isTargetAnyType + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts index a98f773c7e..8a1a0b5039 100644 --- a/invokeai/frontend/web/src/features/nodes/types/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/field.ts @@ -188,7 +188,6 @@ const zIntegerFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zIntegerFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zIntegerFieldType, - originalType: zFieldType.optional(), }); export type IntegerFieldValue = z.infer; export type IntegerFieldInputInstance = z.infer; @@ -217,7 +216,6 @@ const zFloatFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zFloatFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zFloatFieldType, - originalType: zFieldType.optional(), }); export type FloatFieldValue = z.infer; export type FloatFieldInputInstance = z.infer; @@ -243,7 +241,6 @@ const zStringFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zStringFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zStringFieldType, - originalType: zFieldType.optional(), }); export type StringFieldValue = z.infer; @@ -268,7 +265,6 @@ const zBooleanFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zBooleanFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zBooleanFieldType, - originalType: zFieldType.optional(), }); export type BooleanFieldValue = z.infer; export type BooleanFieldInputInstance = z.infer; @@ -294,7 +290,6 @@ const zEnumFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zEnumFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zEnumFieldType, - originalType: zFieldType.optional(), }); export type EnumFieldValue = z.infer; export type EnumFieldInputInstance = z.infer; @@ -318,7 +313,6 @@ const zImageFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zImageFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zImageFieldType, - originalType: zFieldType.optional(), }); export type ImageFieldValue = z.infer; export type ImageFieldInputInstance = z.infer; @@ -342,7 +336,6 @@ const zBoardFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zBoardFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zBoardFieldType, - originalType: zFieldType.optional(), }); export type BoardFieldValue = z.infer; export type BoardFieldInputInstance = z.infer; @@ -366,7 +359,6 @@ const zColorFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zColorFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zColorFieldType, - originalType: zFieldType.optional(), }); export type ColorFieldValue = z.infer; export type ColorFieldInputInstance = z.infer; @@ -390,7 +382,6 @@ const zMainModelFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zMainModelFieldType, - originalType: zFieldType.optional(), }); export type MainModelFieldValue = z.infer; export type MainModelFieldInputInstance = z.infer; @@ -413,7 +404,6 @@ const zModelIdentifierFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zModelIdentifierFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zModelIdentifierFieldType, - originalType: zFieldType.optional(), }); export type ModelIdentifierFieldValue = z.infer; export type ModelIdentifierFieldInputInstance = z.infer; @@ -437,7 +427,6 @@ const zSDXLMainModelFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zSDXLMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zSDXLMainModelFieldType, - originalType: zFieldType.optional(), }); export type SDXLMainModelFieldInputInstance = z.infer; export type SDXLMainModelFieldInputTemplate = z.infer; @@ -461,7 +450,6 @@ const zSDXLRefinerModelFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zSDXLRefinerModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zSDXLRefinerModelFieldType, - originalType: zFieldType.optional(), }); export type SDXLRefinerModelFieldValue = z.infer; export type SDXLRefinerModelFieldInputInstance = z.infer; @@ -485,7 +473,6 @@ const zVAEModelFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zVAEModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zVAEModelFieldType, - originalType: zFieldType.optional(), }); export type VAEModelFieldValue = z.infer; export type VAEModelFieldInputInstance = z.infer; @@ -509,7 +496,6 @@ const zLoRAModelFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zLoRAModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zLoRAModelFieldType, - originalType: zFieldType.optional(), }); export type LoRAModelFieldValue = z.infer; export type LoRAModelFieldInputInstance = z.infer; @@ -533,7 +519,6 @@ const zControlNetModelFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zControlNetModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zControlNetModelFieldType, - originalType: zFieldType.optional(), }); export type ControlNetModelFieldValue = z.infer; export type ControlNetModelFieldInputInstance = z.infer; @@ -557,7 +542,6 @@ const zIPAdapterModelFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zIPAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zIPAdapterModelFieldType, - originalType: zFieldType.optional(), }); export type IPAdapterModelFieldValue = z.infer; export type IPAdapterModelFieldInputInstance = z.infer; @@ -581,7 +565,6 @@ const zT2IAdapterModelFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zT2IAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zT2IAdapterModelFieldType, - originalType: zFieldType.optional(), }); export type T2IAdapterModelFieldValue = z.infer; export type T2IAdapterModelFieldInputInstance = z.infer; @@ -605,7 +588,6 @@ const zSchedulerFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zSchedulerFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zSchedulerFieldType, - originalType: zFieldType.optional(), }); export type SchedulerFieldValue = z.infer; export type SchedulerFieldInputInstance = z.infer; @@ -641,7 +623,6 @@ const zStatelessFieldInputTemplate = zFieldInputTemplateBase.extend({ }); const zStatelessFieldOutputTemplate = zFieldOutputTemplateBase.extend({ type: zStatelessFieldType, - originalType: zFieldType.optional(), }); export type StatelessFieldInputTemplate = z.infer; diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts index 480387a8a4..656bdc9d64 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts @@ -1,942 +1,19 @@ +import { schema, templates } from 'features/nodes/store/util/testUtils'; import { parseSchema } from 'features/nodes/util/schema/parseSchema'; import { omit, pick } from 'lodash-es'; -import type { OpenAPIV3_1 } from 'openapi-types'; import { describe, expect, it } from 'vitest'; describe('parseSchema', () => { it('should parse the schema', () => { - const templates = parseSchema(schema); - expect(templates).toEqual(expected); + const parsed = parseSchema(schema); + expect(parsed).toEqual(templates); }); it('should omit denied nodes', () => { - const templates = parseSchema(schema, undefined, ['add']); - expect(templates).toEqual(omit(expected, 'add')); + const parsed = parseSchema(schema, undefined, ['add']); + expect(parsed).toEqual(omit(templates, 'add')); }); it('should include only allowed nodes', () => { - const templates = parseSchema(schema, ['add']); - expect(templates).toEqual(pick(expected, 'add')); + const parsed = parseSchema(schema, ['add']); + expect(parsed).toEqual(pick(templates, 'add')); }); }); - -const expected = { - add: { - title: 'Add Integers', - type: 'add', - version: '1.0.1', - tags: ['math', 'add'], - description: 'Adds two numbers', - outputType: 'integer_output', - inputs: { - a: { - name: 'a', - title: 'A', - required: false, - description: 'The first number', - fieldKind: 'input', - input: 'any', - ui_hidden: false, - type: { - name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, - }, - default: 0, - }, - b: { - name: 'b', - title: 'B', - required: false, - description: 'The second number', - fieldKind: 'input', - input: 'any', - ui_hidden: false, - type: { - name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, - }, - default: 0, - }, - }, - outputs: { - value: { - fieldKind: 'output', - name: 'value', - title: 'Value', - description: 'The output integer', - type: { - name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, - }, - ui_hidden: false, - }, - }, - useCache: true, - nodePack: 'invokeai', - classification: 'stable', - }, - scheduler: { - title: 'Scheduler', - type: 'scheduler', - version: '1.0.0', - tags: ['scheduler'], - description: 'Selects a scheduler.', - outputType: 'scheduler_output', - inputs: { - scheduler: { - name: 'scheduler', - title: 'Scheduler', - required: false, - description: 'Scheduler to use during inference', - fieldKind: 'input', - input: 'any', - ui_hidden: false, - ui_type: 'SchedulerField', - type: { - name: 'SchedulerField', - isCollection: false, - isCollectionOrScalar: false, - originalType: { - name: 'EnumField', - isCollection: false, - isCollectionOrScalar: false, - }, - }, - default: 'euler', - }, - }, - outputs: { - scheduler: { - fieldKind: 'output', - name: 'scheduler', - title: 'Scheduler', - description: 'Scheduler to use during inference', - type: { - name: 'SchedulerField', - isCollection: false, - isCollectionOrScalar: false, - originalType: { - name: 'EnumField', - isCollection: false, - isCollectionOrScalar: false, - }, - }, - ui_hidden: false, - ui_type: 'SchedulerField', - }, - }, - useCache: true, - nodePack: 'invokeai', - classification: 'stable', - }, - main_model_loader: { - title: 'Main Model', - type: 'main_model_loader', - version: '1.0.2', - tags: ['model'], - description: 'Loads a main model, outputting its submodels.', - outputType: 'model_loader_output', - inputs: { - model: { - name: 'model', - title: 'Model', - required: true, - description: 'Main model (UNet, VAE, CLIP) to load', - fieldKind: 'input', - input: 'direct', - ui_hidden: false, - ui_type: 'MainModelField', - type: { - name: 'MainModelField', - isCollection: false, - isCollectionOrScalar: false, - originalType: { - name: 'ModelIdentifierField', - isCollection: false, - isCollectionOrScalar: false, - }, - }, - }, - }, - outputs: { - vae: { - fieldKind: 'output', - name: 'vae', - title: 'VAE', - description: 'VAE', - type: { - name: 'VAEField', - isCollection: false, - isCollectionOrScalar: false, - }, - ui_hidden: false, - }, - clip: { - fieldKind: 'output', - name: 'clip', - title: 'CLIP', - description: 'CLIP (tokenizer, text encoder, LoRAs) and skipped layer count', - type: { - name: 'CLIPField', - isCollection: false, - isCollectionOrScalar: false, - }, - ui_hidden: false, - }, - unet: { - fieldKind: 'output', - name: 'unet', - title: 'UNet', - description: 'UNet (scheduler, LoRAs)', - type: { - name: 'UNetField', - isCollection: false, - isCollectionOrScalar: false, - }, - ui_hidden: false, - }, - }, - useCache: true, - nodePack: 'invokeai', - classification: 'stable', - }, - collect: { - title: 'Collect', - type: 'collect', - version: '1.0.0', - tags: [], - description: 'Collects values into a collection', - outputType: 'collect_output', - inputs: { - item: { - name: 'item', - title: 'Collection Item', - required: false, - description: 'The item to collect (all inputs must be of the same type)', - fieldKind: 'input', - input: 'connection', - ui_hidden: false, - ui_type: 'CollectionItemField', - type: { - name: 'CollectionItemField', - isCollection: false, - isCollectionOrScalar: false, - }, - }, - }, - outputs: { - collection: { - fieldKind: 'output', - name: 'collection', - title: 'Collection', - description: 'The collection of input items', - type: { - name: 'CollectionField', - isCollection: true, - isCollectionOrScalar: false, - }, - ui_hidden: false, - ui_type: 'CollectionField', - }, - }, - useCache: true, - classification: 'stable', - }, -}; - -const schema = { - openapi: '3.1.0', - info: { - title: 'Invoke - Community Edition', - description: 'An API for invoking AI image operations', - version: '1.0.0', - }, - components: { - schemas: { - AddInvocation: { - properties: { - id: { - type: 'string', - title: 'Id', - description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', - field_kind: 'node_attribute', - }, - is_intermediate: { - type: 'boolean', - title: 'Is Intermediate', - description: 'Whether or not this is an intermediate invocation.', - default: false, - field_kind: 'node_attribute', - ui_type: 'IsIntermediate', - }, - use_cache: { - type: 'boolean', - title: 'Use Cache', - description: 'Whether or not to use the cache', - default: true, - field_kind: 'node_attribute', - }, - a: { - type: 'integer', - title: 'A', - description: 'The first number', - default: 0, - field_kind: 'input', - input: 'any', - orig_default: 0, - orig_required: false, - ui_hidden: false, - }, - b: { - type: 'integer', - title: 'B', - description: 'The second number', - default: 0, - field_kind: 'input', - input: 'any', - orig_default: 0, - orig_required: false, - ui_hidden: false, - }, - type: { - type: 'string', - enum: ['add'], - const: 'add', - title: 'type', - default: 'add', - field_kind: 'node_attribute', - }, - }, - type: 'object', - required: ['type', 'id'], - title: 'Add Integers', - description: 'Adds two numbers', - category: 'math', - classification: 'stable', - node_pack: 'invokeai', - tags: ['math', 'add'], - version: '1.0.1', - output: { - $ref: '#/components/schemas/IntegerOutput', - }, - class: 'invocation', - }, - IntegerOutput: { - description: 'Base class for nodes that output a single integer', - properties: { - value: { - description: 'The output integer', - field_kind: 'output', - title: 'Value', - type: 'integer', - ui_hidden: false, - }, - type: { - const: 'integer_output', - default: 'integer_output', - enum: ['integer_output'], - field_kind: 'node_attribute', - title: 'type', - type: 'string', - }, - }, - required: ['value', 'type', 'type'], - title: 'IntegerOutput', - type: 'object', - class: 'output', - }, - SchedulerInvocation: { - properties: { - id: { - type: 'string', - title: 'Id', - description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', - field_kind: 'node_attribute', - }, - is_intermediate: { - type: 'boolean', - title: 'Is Intermediate', - description: 'Whether or not this is an intermediate invocation.', - default: false, - field_kind: 'node_attribute', - ui_type: 'IsIntermediate', - }, - use_cache: { - type: 'boolean', - title: 'Use Cache', - description: 'Whether or not to use the cache', - default: true, - field_kind: 'node_attribute', - }, - scheduler: { - type: 'string', - enum: [ - 'ddim', - 'ddpm', - 'deis', - 'lms', - 'lms_k', - 'pndm', - 'heun', - 'heun_k', - 'euler', - 'euler_k', - 'euler_a', - 'kdpm_2', - 'kdpm_2_a', - 'dpmpp_2s', - 'dpmpp_2s_k', - 'dpmpp_2m', - 'dpmpp_2m_k', - 'dpmpp_2m_sde', - 'dpmpp_2m_sde_k', - 'dpmpp_sde', - 'dpmpp_sde_k', - 'unipc', - 'lcm', - 'tcd', - ], - title: 'Scheduler', - description: 'Scheduler to use during inference', - default: 'euler', - field_kind: 'input', - input: 'any', - orig_default: 'euler', - orig_required: false, - ui_hidden: false, - ui_type: 'SchedulerField', - }, - type: { - type: 'string', - enum: ['scheduler'], - const: 'scheduler', - title: 'type', - default: 'scheduler', - field_kind: 'node_attribute', - }, - }, - type: 'object', - required: ['type', 'id'], - title: 'Scheduler', - description: 'Selects a scheduler.', - category: 'latents', - classification: 'stable', - node_pack: 'invokeai', - tags: ['scheduler'], - version: '1.0.0', - output: { - $ref: '#/components/schemas/SchedulerOutput', - }, - class: 'invocation', - }, - SchedulerOutput: { - properties: { - scheduler: { - description: 'Scheduler to use during inference', - enum: [ - 'ddim', - 'ddpm', - 'deis', - 'lms', - 'lms_k', - 'pndm', - 'heun', - 'heun_k', - 'euler', - 'euler_k', - 'euler_a', - 'kdpm_2', - 'kdpm_2_a', - 'dpmpp_2s', - 'dpmpp_2s_k', - 'dpmpp_2m', - 'dpmpp_2m_k', - 'dpmpp_2m_sde', - 'dpmpp_2m_sde_k', - 'dpmpp_sde', - 'dpmpp_sde_k', - 'unipc', - 'lcm', - 'tcd', - ], - field_kind: 'output', - title: 'Scheduler', - type: 'string', - ui_hidden: false, - ui_type: 'SchedulerField', - }, - type: { - const: 'scheduler_output', - default: 'scheduler_output', - enum: ['scheduler_output'], - field_kind: 'node_attribute', - title: 'type', - type: 'string', - }, - }, - required: ['scheduler', 'type', 'type'], - title: 'SchedulerOutput', - type: 'object', - class: 'output', - }, - MainModelLoaderInvocation: { - properties: { - id: { - type: 'string', - title: 'Id', - description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', - field_kind: 'node_attribute', - }, - is_intermediate: { - type: 'boolean', - title: 'Is Intermediate', - description: 'Whether or not this is an intermediate invocation.', - default: false, - field_kind: 'node_attribute', - ui_type: 'IsIntermediate', - }, - use_cache: { - type: 'boolean', - title: 'Use Cache', - description: 'Whether or not to use the cache', - default: true, - field_kind: 'node_attribute', - }, - model: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Main model (UNet, VAE, CLIP) to load', - field_kind: 'input', - input: 'direct', - orig_required: true, - ui_hidden: false, - ui_type: 'MainModelField', - }, - type: { - type: 'string', - enum: ['main_model_loader'], - const: 'main_model_loader', - title: 'type', - default: 'main_model_loader', - field_kind: 'node_attribute', - }, - }, - type: 'object', - required: ['model', 'type', 'id'], - title: 'Main Model', - description: 'Loads a main model, outputting its submodels.', - category: 'model', - classification: 'stable', - node_pack: 'invokeai', - tags: ['model'], - version: '1.0.2', - output: { - $ref: '#/components/schemas/ModelLoaderOutput', - }, - class: 'invocation', - }, - ModelIdentifierField: { - properties: { - key: { - description: "The model's unique key", - title: 'Key', - type: 'string', - }, - hash: { - description: "The model's BLAKE3 hash", - title: 'Hash', - type: 'string', - }, - name: { - description: "The model's name", - title: 'Name', - type: 'string', - }, - base: { - allOf: [ - { - $ref: '#/components/schemas/BaseModelType', - }, - ], - description: "The model's base model type", - }, - type: { - allOf: [ - { - $ref: '#/components/schemas/ModelType', - }, - ], - description: "The model's type", - }, - submodel_type: { - anyOf: [ - { - $ref: '#/components/schemas/SubModelType', - }, - { - type: 'null', - }, - ], - default: null, - description: 'The submodel to load, if this is a main model', - }, - }, - required: ['key', 'hash', 'name', 'base', 'type'], - title: 'ModelIdentifierField', - type: 'object', - }, - BaseModelType: { - description: 'Base model type.', - enum: ['any', 'sd-1', 'sd-2', 'sdxl', 'sdxl-refiner'], - title: 'BaseModelType', - type: 'string', - }, - ModelType: { - description: 'Model type.', - enum: ['onnx', 'main', 'vae', 'lora', 'controlnet', 'embedding', 'ip_adapter', 'clip_vision', 't2i_adapter'], - title: 'ModelType', - type: 'string', - }, - SubModelType: { - description: 'Submodel type.', - enum: [ - 'unet', - 'text_encoder', - 'text_encoder_2', - 'tokenizer', - 'tokenizer_2', - 'vae', - 'vae_decoder', - 'vae_encoder', - 'scheduler', - 'safety_checker', - ], - title: 'SubModelType', - type: 'string', - }, - ModelLoaderOutput: { - description: 'Model loader output', - properties: { - vae: { - allOf: [ - { - $ref: '#/components/schemas/VAEField', - }, - ], - description: 'VAE', - field_kind: 'output', - title: 'VAE', - ui_hidden: false, - }, - type: { - const: 'model_loader_output', - default: 'model_loader_output', - enum: ['model_loader_output'], - field_kind: 'node_attribute', - title: 'type', - type: 'string', - }, - clip: { - allOf: [ - { - $ref: '#/components/schemas/CLIPField', - }, - ], - description: 'CLIP (tokenizer, text encoder, LoRAs) and skipped layer count', - field_kind: 'output', - title: 'CLIP', - ui_hidden: false, - }, - unet: { - allOf: [ - { - $ref: '#/components/schemas/UNetField', - }, - ], - description: 'UNet (scheduler, LoRAs)', - field_kind: 'output', - title: 'UNet', - ui_hidden: false, - }, - }, - required: ['vae', 'type', 'clip', 'unet', 'type'], - title: 'ModelLoaderOutput', - type: 'object', - class: 'output', - }, - UNetField: { - properties: { - unet: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load unet submodel', - }, - scheduler: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load scheduler submodel', - }, - loras: { - description: 'LoRAs to apply on model loading', - items: { - $ref: '#/components/schemas/LoRAField', - }, - title: 'Loras', - type: 'array', - }, - seamless_axes: { - description: 'Axes("x" and "y") to which apply seamless', - items: { - type: 'string', - }, - title: 'Seamless Axes', - type: 'array', - }, - freeu_config: { - anyOf: [ - { - $ref: '#/components/schemas/FreeUConfig', - }, - { - type: 'null', - }, - ], - default: null, - description: 'FreeU configuration', - }, - }, - required: ['unet', 'scheduler', 'loras'], - title: 'UNetField', - type: 'object', - class: 'output', - }, - LoRAField: { - properties: { - lora: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load lora model', - }, - weight: { - description: 'Weight to apply to lora model', - title: 'Weight', - type: 'number', - }, - }, - required: ['lora', 'weight'], - title: 'LoRAField', - type: 'object', - class: 'output', - }, - FreeUConfig: { - description: - 'Configuration for the FreeU hyperparameters.\n- https://huggingface.co/docs/diffusers/main/en/using-diffusers/freeu\n- https://github.com/ChenyangSi/FreeU', - properties: { - s1: { - description: - 'Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', - maximum: 3.0, - minimum: -1.0, - title: 'S1', - type: 'number', - }, - s2: { - description: - 'Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.', - maximum: 3.0, - minimum: -1.0, - title: 'S2', - type: 'number', - }, - b1: { - description: 'Scaling factor for stage 1 to amplify the contributions of backbone features.', - maximum: 3.0, - minimum: -1.0, - title: 'B1', - type: 'number', - }, - b2: { - description: 'Scaling factor for stage 2 to amplify the contributions of backbone features.', - maximum: 3.0, - minimum: -1.0, - title: 'B2', - type: 'number', - }, - }, - required: ['s1', 's2', 'b1', 'b2'], - title: 'FreeUConfig', - type: 'object', - class: 'output', - }, - VAEField: { - properties: { - vae: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load vae submodel', - }, - seamless_axes: { - description: 'Axes("x" and "y") to which apply seamless', - items: { - type: 'string', - }, - title: 'Seamless Axes', - type: 'array', - }, - }, - required: ['vae'], - title: 'VAEField', - type: 'object', - class: 'output', - }, - CLIPField: { - properties: { - tokenizer: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load tokenizer submodel', - }, - text_encoder: { - allOf: [ - { - $ref: '#/components/schemas/ModelIdentifierField', - }, - ], - description: 'Info to load text_encoder submodel', - }, - skipped_layers: { - description: 'Number of skipped layers in text_encoder', - title: 'Skipped Layers', - type: 'integer', - }, - loras: { - description: 'LoRAs to apply on model loading', - items: { - $ref: '#/components/schemas/LoRAField', - }, - title: 'Loras', - type: 'array', - }, - }, - required: ['tokenizer', 'text_encoder', 'skipped_layers', 'loras'], - title: 'CLIPField', - type: 'object', - class: 'output', - }, - CollectInvocation: { - properties: { - id: { - type: 'string', - title: 'Id', - description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', - field_kind: 'node_attribute', - }, - is_intermediate: { - type: 'boolean', - title: 'Is Intermediate', - description: 'Whether or not this is an intermediate invocation.', - default: false, - field_kind: 'node_attribute', - ui_type: 'IsIntermediate', - }, - use_cache: { - type: 'boolean', - title: 'Use Cache', - description: 'Whether or not to use the cache', - default: true, - field_kind: 'node_attribute', - }, - item: { - anyOf: [ - {}, - { - type: 'null', - }, - ], - title: 'Collection Item', - description: 'The item to collect (all inputs must be of the same type)', - field_kind: 'input', - input: 'connection', - orig_required: false, - ui_hidden: false, - ui_type: 'CollectionItemField', - }, - collection: { - items: {}, - type: 'array', - title: 'Collection', - description: 'The collection, will be provided on execution', - default: [], - field_kind: 'input', - input: 'any', - orig_default: [], - orig_required: false, - ui_hidden: true, - }, - type: { - type: 'string', - enum: ['collect'], - const: 'collect', - title: 'type', - default: 'collect', - field_kind: 'node_attribute', - }, - }, - type: 'object', - required: ['type', 'id'], - title: 'CollectInvocation', - description: 'Collects values into a collection', - classification: 'stable', - version: '1.0.0', - output: { - $ref: '#/components/schemas/CollectInvocationOutput', - }, - class: 'invocation', - }, - CollectInvocationOutput: { - properties: { - collection: { - description: 'The collection of input items', - field_kind: 'output', - items: {}, - title: 'Collection', - type: 'array', - ui_hidden: false, - ui_type: 'CollectionField', - }, - type: { - const: 'collect_output', - default: 'collect_output', - enum: ['collect_output'], - field_kind: 'node_attribute', - title: 'type', - type: 'string', - }, - }, - required: ['collection', 'type', 'type'], - title: 'CollectInvocationOutput', - type: 'object', - class: 'output', - }, - }, - }, -} as OpenAPIV3_1.Document; From 04a596179b3ffdd69997b8d66b42faac349b5f4c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 00:25:58 +1000 Subject: [PATCH 205/442] tests(ui): finish test cases for validateConnection --- .../features/nodes/store/util/testUtils.ts | 344 +++++++++++++++++- .../store/util/validateConnection.test.ts | 22 +- .../nodes/store/util/validateConnection.ts | 28 +- 3 files changed, 388 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts index efde3336e2..b68ff8bef6 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts @@ -298,7 +298,149 @@ export const main_model_loader: InvocationTemplate = { useCache: true, nodePack: 'invokeai', classification: 'stable', -} +}; + +export const img_resize: InvocationTemplate = { + title: 'Resize Image', + type: 'img_resize', + version: '1.2.2', + tags: ['image', 'resize'], + description: 'Resizes an image to specific dimensions', + outputType: 'image_output', + inputs: { + board: { + name: 'board', + title: 'Board', + required: false, + description: 'The board to save the image to', + fieldKind: 'input', + input: 'direct', + ui_hidden: false, + type: { + name: 'BoardField', + isCollection: false, + isCollectionOrScalar: false, + }, + }, + metadata: { + name: 'metadata', + title: 'Metadata', + required: false, + description: 'Optional metadata to be saved with the image', + fieldKind: 'input', + input: 'connection', + ui_hidden: false, + type: { + name: 'MetadataField', + isCollection: false, + isCollectionOrScalar: false, + }, + }, + image: { + name: 'image', + title: 'Image', + required: true, + description: 'The image to resize', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'ImageField', + isCollection: false, + isCollectionOrScalar: false, + }, + }, + width: { + name: 'width', + title: 'Width', + required: false, + description: 'The width to resize to (px)', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + default: 512, + exclusiveMinimum: 0, + }, + height: { + name: 'height', + title: 'Height', + required: false, + description: 'The height to resize to (px)', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + default: 512, + exclusiveMinimum: 0, + }, + resample_mode: { + name: 'resample_mode', + title: 'Resample Mode', + required: false, + description: 'The resampling mode', + fieldKind: 'input', + input: 'any', + ui_hidden: false, + type: { + name: 'EnumField', + isCollection: false, + isCollectionOrScalar: false, + }, + options: ['nearest', 'box', 'bilinear', 'hamming', 'bicubic', 'lanczos'], + default: 'bicubic', + }, + }, + outputs: { + image: { + fieldKind: 'output', + name: 'image', + title: 'Image', + description: 'The output image', + type: { + name: 'ImageField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + width: { + fieldKind: 'output', + name: 'width', + title: 'Width', + description: 'The width of the image in pixels', + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + height: { + fieldKind: 'output', + name: 'height', + title: 'Height', + description: 'The height of the image in pixels', + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', +}; export const templates: Templates = { add, @@ -306,6 +448,7 @@ export const templates: Templates = { collect, scheduler, main_model_loader, + img_resize, }; export const schema = { @@ -1068,6 +1211,205 @@ export const schema = { }, class: 'invocation', }, + ImageResizeInvocation: { + properties: { + board: { + anyOf: [ + { + $ref: '#/components/schemas/BoardField', + }, + { + type: 'null', + }, + ], + description: 'The board to save the image to', + field_kind: 'internal', + input: 'direct', + orig_required: false, + ui_hidden: false, + }, + metadata: { + anyOf: [ + { + $ref: '#/components/schemas/MetadataField', + }, + { + type: 'null', + }, + ], + description: 'Optional metadata to be saved with the image', + field_kind: 'internal', + input: 'connection', + orig_required: false, + ui_hidden: false, + }, + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + image: { + allOf: [ + { + $ref: '#/components/schemas/ImageField', + }, + ], + description: 'The image to resize', + field_kind: 'input', + input: 'any', + orig_required: true, + ui_hidden: false, + }, + width: { + type: 'integer', + exclusiveMinimum: 0, + title: 'Width', + description: 'The width to resize to (px)', + default: 512, + field_kind: 'input', + input: 'any', + orig_default: 512, + orig_required: false, + ui_hidden: false, + }, + height: { + type: 'integer', + exclusiveMinimum: 0, + title: 'Height', + description: 'The height to resize to (px)', + default: 512, + field_kind: 'input', + input: 'any', + orig_default: 512, + orig_required: false, + ui_hidden: false, + }, + resample_mode: { + type: 'string', + enum: ['nearest', 'box', 'bilinear', 'hamming', 'bicubic', 'lanczos'], + title: 'Resample Mode', + description: 'The resampling mode', + default: 'bicubic', + field_kind: 'input', + input: 'any', + orig_default: 'bicubic', + orig_required: false, + ui_hidden: false, + }, + type: { + type: 'string', + enum: ['img_resize'], + const: 'img_resize', + title: 'type', + default: 'img_resize', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['type', 'id'], + title: 'Resize Image', + description: 'Resizes an image to specific dimensions', + category: 'image', + classification: 'stable', + node_pack: 'invokeai', + tags: ['image', 'resize'], + version: '1.2.2', + output: { + $ref: '#/components/schemas/ImageOutput', + }, + class: 'invocation', + }, + ImageField: { + description: 'An image primitive field', + properties: { + image_name: { + description: 'The name of the image', + title: 'Image Name', + type: 'string', + }, + }, + required: ['image_name'], + title: 'ImageField', + type: 'object', + class: 'output', + }, + ImageOutput: { + description: 'Base class for nodes that output a single image', + properties: { + image: { + allOf: [ + { + $ref: '#/components/schemas/ImageField', + }, + ], + description: 'The output image', + field_kind: 'output', + ui_hidden: false, + }, + width: { + description: 'The width of the image in pixels', + field_kind: 'output', + title: 'Width', + type: 'integer', + ui_hidden: false, + }, + height: { + description: 'The height of the image in pixels', + field_kind: 'output', + title: 'Height', + type: 'integer', + ui_hidden: false, + }, + type: { + const: 'image_output', + default: 'image_output', + enum: ['image_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + }, + required: ['image', 'width', 'height', 'type', 'type'], + title: 'ImageOutput', + type: 'object', + class: 'output', + }, + MetadataField: { + description: + 'Pydantic model for metadata with custom root of type dict[str, Any].\nMetadata is stored without a strict schema.', + title: 'MetadataField', + type: 'object', + class: 'output', + }, + BoardField: { + properties: { + board_id: { + type: 'string', + title: 'Board Id', + description: 'The id of the board', + }, + }, + type: 'object', + required: ['board_id'], + title: 'BoardField', + description: 'A board primitive field', + }, }, }, } as OpenAPIV3_1.Document; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts index 5d10ef368b..cf05b4deb6 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts @@ -3,7 +3,7 @@ import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNod import { set } from 'lodash-es'; import { describe, expect, it } from 'vitest'; -import { add, buildEdge, collect, main_model_loader, position, sub, templates } from './testUtils'; +import { add, buildEdge, collect, img_resize, main_model_loader, position, sub, templates } from './testUtils'; import { buildAcceptResult, buildRejectResult, validateConnection } from './validateConnection'; describe(validateConnection.name, () => { @@ -146,4 +146,24 @@ describe(validateConnection.name, () => { const r = validateConnection(c, nodes, edges, templates, e1); expect(r).toEqual(buildAcceptResult()); }); + + it('should reject connections between invalid types', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, img_resize); + const nodes = [n1, n2]; + const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'image' }; + const r = validateConnection(c, nodes, [], templates, null); + expect(r).toEqual(buildRejectResult('nodes.fieldTypesMustMatch')); + }); + + it('should reject connections that would create cycles', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, sub); + const nodes = [n1, n2]; + const e1 = buildEdge(n1.id, 'value', n2.id, 'a'); + const edges = [e1]; + const c = { source: n2.id, sourceHandle: 'value', target: n1.id, targetHandle: 'a' }; + const r = validateConnection(c, nodes, edges, templates, null); + expect(r).toEqual(buildRejectResult('nodes.connectionWouldCreateCycle')); + }); }); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts index d45a75ab9f..db8b7b737e 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -1,6 +1,8 @@ import type { Templates } from 'features/nodes/store/types'; import { areTypesEqual } from 'features/nodes/store/util/areTypesEqual'; import { getCollectItemType } from 'features/nodes/store/util/getCollectItemType'; +import { getHasCycles } from 'features/nodes/store/util/getHasCycles'; +import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; import type { AnyNode } from 'features/nodes/types/invocation'; import type { Connection as NullableConnection, Edge } from 'reactflow'; import type { O } from 'ts-toolbelt'; @@ -36,6 +38,12 @@ const getEqualityPredicate = ); }; +const getTargetEqualityPredicate = + (c: Connection) => + (e: Edge): boolean => { + return e.target === c.target && e.targetHandle === c.targetHandle; + }; + export const buildAcceptResult = (): ValidateConnectionResult => ({ isValid: true }); export const buildRejectResult = (messageTKey: string): ValidateConnectionResult => ({ isValid: false, messageTKey }); @@ -44,6 +52,12 @@ export const validateConnection: ValidateConnectionFunc = (c, nodes, edges, temp return buildRejectResult('nodes.cannotConnectToSelf'); } + /** + * We may need to ignore an edge when validating a connection. + * + * For example, while an edge is being updated, it still exists in the array of edges. As we validate the new connection, + * the user experience should be that the edge is temporarily removed from the graph, so we need to ignore it. + */ const filteredEdges = edges.filter((e) => e.id !== ignoreEdge?.id); if (filteredEdges.some(getEqualityPredicate(c))) { @@ -96,14 +110,20 @@ export const validateConnection: ValidateConnectionFunc = (c, nodes, edges, temp } if ( - edges.find((e) => { - return e.target === c.target && e.targetHandle === c.targetHandle; - }) && - // except CollectionItem inputs can have multiples + filteredEdges.find(getTargetEqualityPredicate(c)) && + // except CollectionItem inputs can have multiple input connections targetFieldTemplate.type.name !== 'CollectionItemField' ) { return buildRejectResult('nodes.inputMayOnlyHaveOneConnection'); } + if (!validateConnectionTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { + return buildRejectResult('nodes.fieldTypesMustMatch'); + } + + if (getHasCycles(c.source, c.target, nodes, edges)) { + return buildRejectResult('nodes.connectionWouldCreateCycle'); + } + return buildAcceptResult(); }; From 00c2d8f95d6f966feb631942e66d3b09f387b203 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 00:30:26 +1000 Subject: [PATCH 206/442] tidy(ui): areTypesEqual var names --- .../nodes/store/util/areTypesEqual.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.ts b/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.ts index e01b48b972..8502cb563c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.ts @@ -3,27 +3,26 @@ import { isEqual, omit } from 'lodash-es'; /** * Checks if two types are equal. If the field types have original types, those are also compared. Any match is - * considered equal. For example, if the source type and original target type match, the types are considered equal. - * @param sourceType The type of the source field. - * @param targetType The type of the target field. + * considered equal. For example, if the first type and original second type match, the types are considered equal. + * @param firstType The first type to compare. + * @param secondType The second type to compare. * @returns True if the types are equal, false otherwise. */ - -export const areTypesEqual = (sourceType: FieldType, targetType: FieldType) => { - const _sourceType = 'originalType' in sourceType ? omit(sourceType, 'originalType') : sourceType; - const _targetType = 'originalType' in targetType ? omit(targetType, 'originalType') : targetType; - const _sourceTypeOriginal = 'originalType' in sourceType ? sourceType.originalType : null; - const _targetTypeOriginal = 'originalType' in targetType ? targetType.originalType : null; - if (isEqual(_sourceType, _targetType)) { +export const areTypesEqual = (firstType: FieldType, secondType: FieldType) => { + const _firstType = 'originalType' in firstType ? omit(firstType, 'originalType') : firstType; + const _secondType = 'originalType' in secondType ? omit(secondType, 'originalType') : secondType; + const _originalFirstType = 'originalType' in firstType ? firstType.originalType : null; + const _originalSecondType = 'originalType' in secondType ? secondType.originalType : null; + if (isEqual(_firstType, _secondType)) { return true; } - if (_targetTypeOriginal && isEqual(_sourceType, _targetTypeOriginal)) { + if (_originalSecondType && isEqual(_firstType, _originalSecondType)) { return true; } - if (_sourceTypeOriginal && isEqual(_sourceTypeOriginal, _targetType)) { + if (_originalFirstType && isEqual(_originalFirstType, _secondType)) { return true; } - if (_sourceTypeOriginal && _targetTypeOriginal && isEqual(_sourceTypeOriginal, _targetTypeOriginal)) { + if (_originalFirstType && _originalSecondType && isEqual(_originalFirstType, _originalSecondType)) { return true; } return false; From 059d5a682c8d5556d1dc3634d2b5026901d0aeeb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 00:30:49 +1000 Subject: [PATCH 207/442] tidy(ui): validateConnection code clarity --- .../nodes/store/util/validateConnection.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts index db8b7b737e..debf294557 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -56,7 +56,8 @@ export const validateConnection: ValidateConnectionFunc = (c, nodes, edges, temp * We may need to ignore an edge when validating a connection. * * For example, while an edge is being updated, it still exists in the array of edges. As we validate the new connection, - * the user experience should be that the edge is temporarily removed from the graph, so we need to ignore it. + * the user experience should be that the edge is temporarily removed from the graph, so we need to ignore it, else + * the validation will fail unexpectedly. */ const filteredEdges = edges.filter((e) => e.id !== ignoreEdge?.id); @@ -100,21 +101,18 @@ export const validateConnection: ValidateConnectionFunc = (c, nodes, edges, temp } if (targetNode.data.type === 'collect' && c.targetHandle === 'item') { - // Collect nodes shouldn't mix and match field types + // Collect nodes shouldn't mix and match field types. const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); - if (collectItemType) { - if (!areTypesEqual(sourceFieldTemplate.type, collectItemType)) { - return buildRejectResult('nodes.cannotMixAndMatchCollectionItemTypes'); - } + if (collectItemType && !areTypesEqual(sourceFieldTemplate.type, collectItemType)) { + return buildRejectResult('nodes.cannotMixAndMatchCollectionItemTypes'); } } - if ( - filteredEdges.find(getTargetEqualityPredicate(c)) && - // except CollectionItem inputs can have multiple input connections - targetFieldTemplate.type.name !== 'CollectionItemField' - ) { - return buildRejectResult('nodes.inputMayOnlyHaveOneConnection'); + if (filteredEdges.find(getTargetEqualityPredicate(c))) { + // CollectionItemField inputs can have multiple input connections + if (targetFieldTemplate.type.name !== 'CollectionItemField') { + return buildRejectResult('nodes.inputMayOnlyHaveOneConnection'); + } } if (!validateConnectionTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { From 8074a802d6549b32ed60d5ee973c50c397018dac Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 00:53:27 +1000 Subject: [PATCH 208/442] tests(ui): coverage for validateConnectionTypes --- .../util/validateConnectionTypes.test.ts | 2 +- .../store/util/validateConnectionTypes.ts | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts index d953fd973f..10344dd349 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts @@ -175,7 +175,7 @@ describe(validateConnectionTypes.name, () => { it.each(typePairs)('should accept Collection $t1 to Collection $t2', ({ t1, t2 }: TypePair) => { const r = validateConnectionTypes( { name: t1, isCollection: true, isCollectionOrScalar: false }, - { name: t2, isCollection: false, isCollectionOrScalar: false } + { name: t2, isCollection: true, isCollectionOrScalar: false } ); expect(r).toBe(true); }); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts index 092279e315..778b33a7b1 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts @@ -40,18 +40,25 @@ export const validateConnectionTypes = (sourceType: FieldType, targetType: Field const isCollectionToGenericCollection = targetType.name === 'CollectionField' && sourceType.isCollection; - const areBothTypesSingle = - !sourceType.isCollection && - !sourceType.isCollectionOrScalar && - !targetType.isCollection && - !targetType.isCollectionOrScalar; + const isSourceScalar = !sourceType.isCollection && !sourceType.isCollectionOrScalar; + const isTargetScalar = !targetType.isCollection && !targetType.isCollectionOrScalar; + const isScalarToScalar = isSourceScalar && isTargetScalar; + const isScalarToCollectionOrScalar = isSourceScalar && targetType.isCollectionOrScalar; + const isCollectionToCollection = sourceType.isCollection && targetType.isCollection; + const isCollectionToCollectionOrScalar = sourceType.isCollection && targetType.isCollectionOrScalar; + const isCollectionOrScalarToCollectionOrScalar = sourceType.isCollectionOrScalar && targetType.isCollectionOrScalar; + const isPluralityMatch = + isScalarToScalar || + isCollectionToCollection || + isCollectionToCollectionOrScalar || + isCollectionOrScalarToCollectionOrScalar || + isScalarToCollectionOrScalar; - const isIntToFloat = areBothTypesSingle && sourceType.name === 'IntegerField' && targetType.name === 'FloatField'; + const isIntToFloat = sourceType.name === 'IntegerField' && targetType.name === 'FloatField'; + const isIntToString = sourceType.name === 'IntegerField' && targetType.name === 'StringField'; + const isFloatToString = sourceType.name === 'FloatField' && targetType.name === 'StringField'; - const isIntOrFloatToString = - areBothTypesSingle && - (sourceType.name === 'IntegerField' || sourceType.name === 'FloatField') && - targetType.name === 'StringField'; + const isSubTypeMatch = isPluralityMatch && (isIntToFloat || isIntToString || isFloatToString); const isTargetAnyType = targetType.name === 'AnyField'; @@ -62,8 +69,7 @@ export const validateConnectionTypes = (sourceType: FieldType, targetType: Field isAnythingToCollectionOrScalarOfSameBaseType || isGenericCollectionToAnyCollectionOrCollectionOrScalar || isCollectionToGenericCollection || - isIntToFloat || - isIntOrFloatToString || + isSubTypeMatch || isTargetAnyType ); }; From 857889d1faf0cc2e7fd5b51d34ff3ac46b47b18e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 00:56:16 +1000 Subject: [PATCH 209/442] tests(ui): coverage for getCollectItemType --- .../features/nodes/store/util/getCollectItemType.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts index 93c63b6f41..7f0a96bf33 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts @@ -13,4 +13,10 @@ describe(getCollectItemType.name, () => { const result = getCollectItemType(templates, nodes, edges, n2.id); expect(result).toEqual({ name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }); }); + it('should return null if the collect node does not have any connections', () => { + const n1 = buildInvocationNode(position, collect); + const nodes = [n1]; + const result = getCollectItemType(templates, nodes, [], n1.id); + expect(result).toBeNull(); + }); }); From 972398d203ca0a7bc67477713419428d4dc148f8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 01:04:58 +1000 Subject: [PATCH 210/442] tests(ui): add iterate to test schema --- .../features/nodes/store/util/testUtils.ts | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts index b68ff8bef6..470236a82e 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts @@ -442,10 +442,78 @@ export const img_resize: InvocationTemplate = { classification: 'stable', }; +const iterate: InvocationTemplate = { + title: 'Iterate', + type: 'iterate', + version: '1.1.0', + tags: [], + description: 'Iterates over a list of items', + outputType: 'iterate_output', + inputs: { + collection: { + name: 'collection', + title: 'Collection', + required: false, + description: 'The list of items to iterate over', + fieldKind: 'input', + input: 'connection', + ui_hidden: false, + ui_type: 'CollectionField', + type: { + name: 'CollectionField', + isCollection: true, + isCollectionOrScalar: false, + }, + }, + }, + outputs: { + item: { + fieldKind: 'output', + name: 'item', + title: 'Collection Item', + description: 'The item being iterated over', + type: { + name: 'CollectionItemField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + ui_type: 'CollectionItemField', + }, + index: { + fieldKind: 'output', + name: 'index', + title: 'Index', + description: 'The index of the item', + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + total: { + fieldKind: 'output', + name: 'total', + title: 'Total', + description: 'The total number of items', + type: { + name: 'IntegerField', + isCollection: false, + isCollectionOrScalar: false, + }, + ui_hidden: false, + }, + }, + useCache: true, + classification: 'stable', +}; + export const templates: Templates = { add, sub, collect, + iterate, scheduler, main_model_loader, img_resize, @@ -1410,6 +1478,111 @@ export const schema = { title: 'BoardField', description: 'A board primitive field', }, + IterateInvocation: { + properties: { + id: { + type: 'string', + title: 'Id', + description: 'The id of this instance of an invocation. Must be unique among all instances of invocations.', + field_kind: 'node_attribute', + }, + is_intermediate: { + type: 'boolean', + title: 'Is Intermediate', + description: 'Whether or not this is an intermediate invocation.', + default: false, + field_kind: 'node_attribute', + ui_type: 'IsIntermediate', + }, + use_cache: { + type: 'boolean', + title: 'Use Cache', + description: 'Whether or not to use the cache', + default: true, + field_kind: 'node_attribute', + }, + collection: { + items: {}, + type: 'array', + title: 'Collection', + description: 'The list of items to iterate over', + default: [], + field_kind: 'input', + input: 'any', + orig_default: [], + orig_required: false, + ui_hidden: false, + ui_type: 'CollectionField', + }, + index: { + type: 'integer', + title: 'Index', + description: 'The index, will be provided on executed iterators', + default: 0, + field_kind: 'input', + input: 'any', + orig_default: 0, + orig_required: false, + ui_hidden: true, + }, + type: { + type: 'string', + enum: ['iterate'], + const: 'iterate', + title: 'type', + default: 'iterate', + field_kind: 'node_attribute', + }, + }, + type: 'object', + required: ['type', 'id'], + title: 'IterateInvocation', + description: 'Iterates over a list of items', + classification: 'stable', + version: '1.1.0', + output: { + $ref: '#/components/schemas/IterateInvocationOutput', + }, + class: 'invocation', + }, + IterateInvocationOutput: { + description: 'Used to connect iteration outputs. Will be expanded to a specific output.', + properties: { + item: { + description: 'The item being iterated over', + field_kind: 'output', + title: 'Collection Item', + ui_hidden: false, + ui_type: 'CollectionItemField', + }, + index: { + description: 'The index of the item', + field_kind: 'output', + title: 'Index', + type: 'integer', + ui_hidden: false, + }, + total: { + description: 'The total number of items', + field_kind: 'output', + title: 'Total', + type: 'integer', + ui_hidden: false, + }, + type: { + const: 'iterate_output', + default: 'iterate_output', + enum: ['iterate_output'], + field_kind: 'node_attribute', + title: 'type', + type: 'string', + }, + }, + required: ['item', 'index', 'total', 'type', 'type'], + title: 'IterateInvocationOutput', + type: 'object', + class: 'output', + }, }, }, } as OpenAPIV3_1.Document; From 78f9f3ee95bbdd3472bb0e517eaccb7278f8df6b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 01:10:49 +1000 Subject: [PATCH 211/442] feat(ui): better types for validateConnection --- .../nodes/store/util/validateConnection.ts | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts index debf294557..b6b5a43d37 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -6,13 +6,19 @@ import { validateConnectionTypes } from 'features/nodes/store/util/validateConne import type { AnyNode } from 'features/nodes/types/invocation'; import type { Connection as NullableConnection, Edge } from 'reactflow'; import type { O } from 'ts-toolbelt'; +import { assert } from 'tsafe'; type Connection = O.NonNullable; -export type ValidateConnectionResult = { - isValid: boolean; - messageTKey?: string; -}; +export type ValidateConnectionResult = + | { + isValid: true; + messageTKey?: string; + } + | { + isValid: false; + messageTKey: string; + }; export type ValidateConnectionFunc = ( connection: Connection, @@ -22,10 +28,20 @@ export type ValidateConnectionFunc = ( ignoreEdge: Edge | null ) => ValidateConnectionResult; -export const buildResult = (isValid: boolean, messageTKey?: string): ValidateConnectionResult => ({ - isValid, - messageTKey, -}); +export const buildResult = (isValid: boolean, messageTKey?: string): ValidateConnectionResult => { + if (isValid) { + return { + isValid, + messageTKey, + }; + } else { + assert(messageTKey !== undefined); + return { + isValid, + messageTKey, + }; + } +}; const getEqualityPredicate = (c: Connection) => From 6ad01d824d689319232bd6654bf722962766cfa0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 01:26:43 +1000 Subject: [PATCH 212/442] feat(ui): add strict mode to validateConnection --- .../store/util/validateConnection.test.ts | 26 ++++ .../nodes/store/util/validateConnection.ts | 127 +++++++++--------- 2 files changed, 91 insertions(+), 62 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts index cf05b4deb6..108839a499 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts @@ -166,4 +166,30 @@ describe(validateConnection.name, () => { const r = validateConnection(c, nodes, edges, templates, null); expect(r).toEqual(buildRejectResult('nodes.connectionWouldCreateCycle')); }); + + describe('non-strict mode', () => { + it('should reject connections from self to self in non-strict mode', () => { + const c = { source: 'add', sourceHandle: 'value', target: 'add', targetHandle: 'a' }; + const r = validateConnection(c, [], [], templates, null, false); + expect(r).toEqual(buildRejectResult('nodes.cannotConnectToSelf')); + }); + it('should reject connections that create cycles in non-strict mode', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, sub); + const nodes = [n1, n2]; + const e1 = buildEdge(n1.id, 'value', n2.id, 'a'); + const edges = [e1]; + const c = { source: n2.id, sourceHandle: 'value', target: n1.id, targetHandle: 'a' }; + const r = validateConnection(c, nodes, edges, templates, null, false); + expect(r).toEqual(buildRejectResult('nodes.connectionWouldCreateCycle')); + }); + it('should otherwise allow invalid connections in non-strict mode', () => { + const n1 = buildInvocationNode(position, add); + const n2 = buildInvocationNode(position, img_resize); + const nodes = [n1, n2]; + const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'image' }; + const r = validateConnection(c, nodes, [], templates, null, false); + expect(r).toEqual(buildAcceptResult()); + }); + }); }); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts index b6b5a43d37..edb8ac5ecb 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -25,7 +25,8 @@ export type ValidateConnectionFunc = ( nodes: AnyNode[], edges: Edge[], templates: Templates, - ignoreEdge: Edge | null + ignoreEdge: Edge | null, + strict?: boolean ) => ValidateConnectionResult; export const buildResult = (isValid: boolean, messageTKey?: string): ValidateConnectionResult => { @@ -63,76 +64,78 @@ const getTargetEqualityPredicate = export const buildAcceptResult = (): ValidateConnectionResult => ({ isValid: true }); export const buildRejectResult = (messageTKey: string): ValidateConnectionResult => ({ isValid: false, messageTKey }); -export const validateConnection: ValidateConnectionFunc = (c, nodes, edges, templates, ignoreEdge) => { +export const validateConnection: ValidateConnectionFunc = (c, nodes, edges, templates, ignoreEdge, strict = true) => { if (c.source === c.target) { return buildRejectResult('nodes.cannotConnectToSelf'); } - /** - * We may need to ignore an edge when validating a connection. - * - * For example, while an edge is being updated, it still exists in the array of edges. As we validate the new connection, - * the user experience should be that the edge is temporarily removed from the graph, so we need to ignore it, else - * the validation will fail unexpectedly. - */ - const filteredEdges = edges.filter((e) => e.id !== ignoreEdge?.id); + if (strict) { + /** + * We may need to ignore an edge when validating a connection. + * + * For example, while an edge is being updated, it still exists in the array of edges. As we validate the new connection, + * the user experience should be that the edge is temporarily removed from the graph, so we need to ignore it, else + * the validation will fail unexpectedly. + */ + const filteredEdges = edges.filter((e) => e.id !== ignoreEdge?.id); - if (filteredEdges.some(getEqualityPredicate(c))) { - // We already have a connection from this source to this target - return buildRejectResult('nodes.cannotDuplicateConnection'); - } - - const sourceNode = nodes.find((n) => n.id === c.source); - if (!sourceNode) { - return buildRejectResult('nodes.missingNode'); - } - - const targetNode = nodes.find((n) => n.id === c.target); - if (!targetNode) { - return buildRejectResult('nodes.missingNode'); - } - - const sourceTemplate = templates[sourceNode.data.type]; - if (!sourceTemplate) { - return buildRejectResult('nodes.missingInvocationTemplate'); - } - - const targetTemplate = templates[targetNode.data.type]; - if (!targetTemplate) { - return buildRejectResult('nodes.missingInvocationTemplate'); - } - - const sourceFieldTemplate = sourceTemplate.outputs[c.sourceHandle]; - if (!sourceFieldTemplate) { - return buildRejectResult('nodes.missingFieldTemplate'); - } - - const targetFieldTemplate = targetTemplate.inputs[c.targetHandle]; - if (!targetFieldTemplate) { - return buildRejectResult('nodes.missingFieldTemplate'); - } - - if (targetFieldTemplate.input === 'direct') { - return buildRejectResult('nodes.cannotConnectToDirectInput'); - } - - if (targetNode.data.type === 'collect' && c.targetHandle === 'item') { - // Collect nodes shouldn't mix and match field types. - const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); - if (collectItemType && !areTypesEqual(sourceFieldTemplate.type, collectItemType)) { - return buildRejectResult('nodes.cannotMixAndMatchCollectionItemTypes'); + if (filteredEdges.some(getEqualityPredicate(c))) { + // We already have a connection from this source to this target + return buildRejectResult('nodes.cannotDuplicateConnection'); } - } - if (filteredEdges.find(getTargetEqualityPredicate(c))) { - // CollectionItemField inputs can have multiple input connections - if (targetFieldTemplate.type.name !== 'CollectionItemField') { - return buildRejectResult('nodes.inputMayOnlyHaveOneConnection'); + const sourceNode = nodes.find((n) => n.id === c.source); + if (!sourceNode) { + return buildRejectResult('nodes.missingNode'); } - } - if (!validateConnectionTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { - return buildRejectResult('nodes.fieldTypesMustMatch'); + const targetNode = nodes.find((n) => n.id === c.target); + if (!targetNode) { + return buildRejectResult('nodes.missingNode'); + } + + const sourceTemplate = templates[sourceNode.data.type]; + if (!sourceTemplate) { + return buildRejectResult('nodes.missingInvocationTemplate'); + } + + const targetTemplate = templates[targetNode.data.type]; + if (!targetTemplate) { + return buildRejectResult('nodes.missingInvocationTemplate'); + } + + const sourceFieldTemplate = sourceTemplate.outputs[c.sourceHandle]; + if (!sourceFieldTemplate) { + return buildRejectResult('nodes.missingFieldTemplate'); + } + + const targetFieldTemplate = targetTemplate.inputs[c.targetHandle]; + if (!targetFieldTemplate) { + return buildRejectResult('nodes.missingFieldTemplate'); + } + + if (targetFieldTemplate.input === 'direct') { + return buildRejectResult('nodes.cannotConnectToDirectInput'); + } + + if (targetNode.data.type === 'collect' && c.targetHandle === 'item') { + // Collect nodes shouldn't mix and match field types. + const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); + if (collectItemType && !areTypesEqual(sourceFieldTemplate.type, collectItemType)) { + return buildRejectResult('nodes.cannotMixAndMatchCollectionItemTypes'); + } + } + + if (filteredEdges.find(getTargetEqualityPredicate(c))) { + // CollectionItemField inputs can have multiple input connections + if (targetFieldTemplate.type.name !== 'CollectionItemField') { + return buildRejectResult('nodes.inputMayOnlyHaveOneConnection'); + } + } + + if (!validateConnectionTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { + return buildRejectResult('nodes.fieldTypesMustMatch'); + } } if (getHasCycles(c.source, c.target, nodes, edges)) { From fc31dddbf7c1deb1c10b621bc21281e5bead39d4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 01:27:25 +1000 Subject: [PATCH 213/442] feat(ui): use new validateConnection --- .../nodes/hooks/useConnectionState.ts | 9 +- .../nodes/hooks/useIsValidConnection.ts | 84 ++--------- .../nodes/store/util/connectionValidation.ts | 134 ------------------ .../store/util/makeConnectionErrorSelector.ts | 72 ++++++++++ 4 files changed, 88 insertions(+), 211 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index 9571ce2ee2..5dcb7a28b5 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -2,11 +2,9 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { $pendingConnection, $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { makeConnectionErrorSelector } from 'features/nodes/store/util/connectionValidation.js'; +import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector'; import { useMemo } from 'react'; -import { useFieldType } from './useFieldType.ts'; - type UseConnectionStateProps = { nodeId: string; fieldName: string; @@ -16,7 +14,6 @@ type UseConnectionStateProps = { export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => { const pendingConnection = useStore($pendingConnection); const templates = useStore($templates); - const fieldType = useFieldType(nodeId, fieldName, kind); const selectIsConnected = useMemo( () => @@ -34,8 +31,8 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta ); const selectConnectionError = useMemo( - () => makeConnectionErrorSelector(templates, nodeId, fieldName, kind === 'inputs' ? 'target' : 'source', fieldType), - [templates, nodeId, fieldName, kind, fieldType] + () => makeConnectionErrorSelector(templates, nodeId, fieldName, kind === 'inputs' ? 'target' : 'source'), + [templates, nodeId, fieldName, kind] ); const isConnected = useAppSelector(selectIsConnected); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index 77c4e3c75b..0f8609d2ff 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -2,13 +2,9 @@ import { useStore } from '@nanostores/react'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { $templates } from 'features/nodes/store/nodesSlice'; -import { areTypesEqual } from 'features/nodes/store/util/areTypesEqual'; -import { getCollectItemType } from 'features/nodes/store/util/getCollectItemType'; -import { getHasCycles } from 'features/nodes/store/util/getHasCycles'; -import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; -import type { InvocationNodeData } from 'features/nodes/types/invocation'; +import { validateConnection } from 'features/nodes/store/util/validateConnection'; import { useCallback } from 'react'; -import type { Connection, Node } from 'reactflow'; +import type { Connection } from 'reactflow'; /** * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts` @@ -26,74 +22,20 @@ export const useIsValidConnection = () => { return false; } - if (source === target) { - // Don't allow nodes to connect to themselves, even if validation is disabled - return false; - } + const { nodes, edges } = store.getState().nodes.present; - const state = store.getState(); - const { nodes, edges } = state.nodes.present; + const validationResult = validateConnection( + { source, sourceHandle, target, targetHandle }, + nodes, + edges, + templates, + null, + shouldValidateGraph + ); - // Find the source and target nodes - const sourceNode = nodes.find((node) => node.id === source) as Node; - const targetNode = nodes.find((node) => node.id === target) as Node; - const sourceFieldTemplate = templates[sourceNode.data.type]?.outputs[sourceHandle]; - const targetFieldTemplate = templates[targetNode.data.type]?.inputs[targetHandle]; - - // Conditional guards against undefined nodes/handles - if (!(sourceFieldTemplate && targetFieldTemplate)) { - return false; - } - - if (targetFieldTemplate.input === 'direct') { - return false; - } - - if (!shouldValidateGraph) { - // manual override! - return true; - } - - if ( - edges.find((edge) => { - edge.target === target && - edge.targetHandle === targetHandle && - edge.source === source && - edge.sourceHandle === sourceHandle; - }) - ) { - // We already have a connection from this source to this target - return false; - } - - if (targetNode.data.type === 'collect' && targetFieldTemplate.name === 'item') { - // Collect nodes shouldn't mix and match field types - const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); - if (collectItemType) { - return areTypesEqual(sourceFieldTemplate.type, collectItemType); - } - } - - // Connection is invalid if target already has a connection - if ( - edges.find((edge) => { - return edge.target === target && edge.targetHandle === targetHandle; - }) && - // except CollectionItem inputs can have multiples - targetFieldTemplate.type.name !== 'CollectionItemField' - ) { - return false; - } - - // Must use the originalType here if it exists - if (!validateConnectionTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { - return false; - } - - // Graphs much be acyclic (no loops!) - return !getHasCycles(source, target, nodes, edges); + return validationResult.isValid; }, - [shouldValidateGraph, templates, store] + [templates, shouldValidateGraph, store] ); return isValidConnection; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts b/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts deleted file mode 100644 index 7819221f8a..0000000000 --- a/invokeai/frontend/web/src/features/nodes/store/util/connectionValidation.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import type { RootState } from 'app/store/store'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import type { NodesState, PendingConnection, Templates } from 'features/nodes/store/types'; -import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; -import type { FieldType } from 'features/nodes/types/field'; -import i18n from 'i18next'; -import type { HandleType } from 'reactflow'; -import { assert } from 'tsafe'; - -import { areTypesEqual } from './areTypesEqual'; -import { getCollectItemType } from './getCollectItemType'; -import { getHasCycles } from './getHasCycles'; - -/** - * Creates a selector that validates a pending connection. - * - * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts` - * TODO: Figure out how to do this without duplicating all the logic - * - * @param templates The invocation templates - * @param pendingConnection The current pending connection (if there is one) - * @param nodeId The id of the node for which the selector is being created - * @param fieldName The name of the field for which the selector is being created - * @param handleType The type of the handle for which the selector is being created - * @param fieldType The type of the field for which the selector is being created - * @returns - */ -export const makeConnectionErrorSelector = ( - templates: Templates, - nodeId: string, - fieldName: string, - handleType: HandleType, - fieldType: FieldType -) => { - return createMemoizedSelector( - selectNodesSlice, - (state: RootState, pendingConnection: PendingConnection | null) => pendingConnection, - (nodesSlice: NodesState, pendingConnection: PendingConnection | null) => { - const { nodes, edges } = nodesSlice; - - if (!pendingConnection) { - return i18n.t('nodes.noConnectionInProgress'); - } - - const connectionNodeId = pendingConnection.node.id; - const connectionFieldName = pendingConnection.fieldTemplate.name; - const connectionHandleType = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; - const connectionStartFieldType = pendingConnection.fieldTemplate.type; - - if (!connectionHandleType || !connectionNodeId || !connectionFieldName) { - return i18n.t('nodes.noConnectionData'); - } - - const targetType = handleType === 'target' ? fieldType : connectionStartFieldType; - const sourceType = handleType === 'source' ? fieldType : connectionStartFieldType; - - if (nodeId === connectionNodeId) { - return i18n.t('nodes.cannotConnectToSelf'); - } - - if (handleType === connectionHandleType) { - if (handleType === 'source') { - return i18n.t('nodes.cannotConnectOutputToOutput'); - } - return i18n.t('nodes.cannotConnectInputToInput'); - } - - // we have to figure out which is the target and which is the source - const targetNodeId = handleType === 'target' ? nodeId : connectionNodeId; - const targetFieldName = handleType === 'target' ? fieldName : connectionFieldName; - const sourceNodeId = handleType === 'source' ? nodeId : connectionNodeId; - const sourceFieldName = handleType === 'source' ? fieldName : connectionFieldName; - - if ( - edges.find((edge) => { - edge.target === targetNodeId && - edge.targetHandle === targetFieldName && - edge.source === sourceNodeId && - edge.sourceHandle === sourceFieldName; - }) - ) { - // We already have a connection from this source to this target - return i18n.t('nodes.cannotDuplicateConnection'); - } - - const targetNode = nodes.find((node) => node.id === targetNodeId); - assert(targetNode, `Target node not found: ${targetNodeId}`); - const targetTemplate = templates[targetNode.data.type]; - assert(targetTemplate, `Target template not found: ${targetNode.data.type}`); - - if (targetTemplate.inputs[targetFieldName]?.input === 'direct') { - return i18n.t('nodes.cannotConnectToDirectInput'); - } - - if (targetNode.data.type === 'collect' && targetFieldName === 'item') { - // Collect nodes shouldn't mix and match field types - const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); - if (collectItemType) { - if (!areTypesEqual(sourceType, collectItemType)) { - return i18n.t('nodes.cannotMixAndMatchCollectionItemTypes'); - } - } - } - - if ( - edges.find((edge) => { - return edge.target === targetNodeId && edge.targetHandle === targetFieldName; - }) && - // except CollectionItem inputs can have multiples - targetType.name !== 'CollectionItemField' - ) { - return i18n.t('nodes.inputMayOnlyHaveOneConnection'); - } - - if (!validateConnectionTypes(sourceType, targetType)) { - return i18n.t('nodes.fieldTypesMustMatch'); - } - - const hasCycles = getHasCycles( - connectionHandleType === 'source' ? connectionNodeId : nodeId, - connectionHandleType === 'source' ? nodeId : connectionNodeId, - nodes, - edges - ); - - if (hasCycles) { - return i18n.t('nodes.connectionWouldCreateCycle'); - } - - return; - } - ); -}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts new file mode 100644 index 0000000000..3cefb6815f --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts @@ -0,0 +1,72 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import type { RootState } from 'app/store/store'; +import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import type { NodesState, PendingConnection, Templates } from 'features/nodes/store/types'; +import { validateConnection } from 'features/nodes/store/util/validateConnection'; +import i18n from 'i18next'; +import type { HandleType } from 'reactflow'; + +/** + * Creates a selector that validates a pending connection. + * + * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts` + * TODO: Figure out how to do this without duplicating all the logic + * + * @param templates The invocation templates + * @param nodeId The id of the node for which the selector is being created + * @param fieldName The name of the field for which the selector is being created + * @param handleType The type of the handle for which the selector is being created + * @returns + */ +export const makeConnectionErrorSelector = ( + templates: Templates, + nodeId: string, + fieldName: string, + handleType: HandleType +) => { + return createMemoizedSelector( + selectNodesSlice, + (state: RootState, pendingConnection: PendingConnection | null) => pendingConnection, + (nodesSlice: NodesState, pendingConnection: PendingConnection | null) => { + const { nodes, edges } = nodesSlice; + + if (!pendingConnection) { + return i18n.t('nodes.noConnectionInProgress'); + } + + const connectionNodeId = pendingConnection.node.id; + const connectionFieldName = pendingConnection.fieldTemplate.name; + const connectionHandleType = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; + + if (handleType === connectionHandleType) { + if (handleType === 'source') { + return i18n.t('nodes.cannotConnectOutputToOutput'); + } + return i18n.t('nodes.cannotConnectInputToInput'); + } + + // we have to figure out which is the target and which is the source + const source = handleType === 'source' ? nodeId : connectionNodeId; + const sourceHandle = handleType === 'source' ? fieldName : connectionFieldName; + const target = handleType === 'target' ? nodeId : connectionNodeId; + const targetHandle = handleType === 'target' ? fieldName : connectionFieldName; + + const validationResult = validateConnection( + { + source, + sourceHandle, + target, + targetHandle, + }, + nodes, + edges, + templates, + null + ); + + if (!validationResult.isValid) { + return i18n.t(validationResult.messageTKey); + } + } + ); +}; From 3605b6b1a36c27ef2299de6892ae0aff424a3c68 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 01:37:54 +1000 Subject: [PATCH 214/442] fix(ui): handling for in-progress edge updates during conection validation --- .../web/src/features/nodes/components/flow/Flow.tsx | 10 +++++----- .../web/src/features/nodes/hooks/useConnection.ts | 9 +++++---- .../web/src/features/nodes/hooks/useConnectionState.ts | 5 +++-- .../src/features/nodes/hooks/useIsValidConnection.ts | 6 +++--- .../web/src/features/nodes/store/nodesSlice.ts | 2 +- .../nodes/store/util/getFirstValidConnection.ts | 10 ++++++---- .../nodes/store/util/makeConnectionErrorSelector.ts | 8 +++++--- 7 files changed, 28 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 501513919a..18bbac0b44 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -9,8 +9,8 @@ import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { $cursorPos, $didUpdateEdge, + $edgePendingUpdate, $isAddNodePopoverOpen, - $isUpdatingEdge, $lastEdgeUpdateMouseEvent, $pendingConnection, $viewport, @@ -160,8 +160,8 @@ export const Flow = memo(() => { * where the edge is deleted if you click it accidentally). */ - const onEdgeUpdateStart: NonNullable = useCallback((e, _edge, _handleType) => { - $isUpdatingEdge.set(true); + const onEdgeUpdateStart: NonNullable = useCallback((e, edge, _handleType) => { + $edgePendingUpdate.set(edge); $didUpdateEdge.set(false); $lastEdgeUpdateMouseEvent.set(e); }, []); @@ -196,7 +196,7 @@ export const Flow = memo(() => { dispatch(edgeDeleted(edge.id)); } - $isUpdatingEdge.set(false); + $edgePendingUpdate.set(null); $didUpdateEdge.set(false); $pendingConnection.set(null); $lastEdgeUpdateMouseEvent.set(null); @@ -259,7 +259,7 @@ export const Flow = memo(() => { useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey); const onEscapeHotkey = useCallback(() => { - if (!$isUpdatingEdge.get()) { + if (!$edgePendingUpdate.get()) { $pendingConnection.set(null); $isAddNodePopoverOpen.set(false); cancelConnection(); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index 0190a0b29e..d81a9e5807 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -2,8 +2,8 @@ import { useStore } from '@nanostores/react'; import { useAppStore } from 'app/store/storeHooks'; import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { + $edgePendingUpdate, $isAddNodePopoverOpen, - $isUpdatingEdge, $pendingConnection, $templates, connectionMade, @@ -52,12 +52,12 @@ export const useConnection = () => { const onConnectEnd = useCallback(() => { const { dispatch } = store; const pendingConnection = $pendingConnection.get(); - const isUpdatingEdge = $isUpdatingEdge.get(); + const edgePendingUpdate = $edgePendingUpdate.get(); const mouseOverNodeId = $mouseOverNode.get(); // If we are in the middle of an edge update, and the mouse isn't over a node, we should just bail so the edge // update logic can finish up - if (isUpdatingEdge && !mouseOverNodeId) { + if (edgePendingUpdate && !mouseOverNodeId) { $pendingConnection.set(null); return; } @@ -80,7 +80,8 @@ export const useConnection = () => { edges, pendingConnection, candidateNode, - candidateTemplate + candidateTemplate, + edgePendingUpdate ); if (connection) { dispatch(connectionMade(connection)); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index 5dcb7a28b5..7649209863 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -1,7 +1,7 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $pendingConnection, $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $edgePendingUpdate, $pendingConnection, $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector'; import { useMemo } from 'react'; @@ -14,6 +14,7 @@ type UseConnectionStateProps = { export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => { const pendingConnection = useStore($pendingConnection); const templates = useStore($templates); + const edgePendingUpdate = useStore($edgePendingUpdate); const selectIsConnected = useMemo( () => @@ -47,7 +48,7 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta pendingConnection.fieldTemplate.fieldKind === { inputs: 'input', outputs: 'output' }[kind] ); }, [fieldName, kind, nodeId, pendingConnection]); - const connectionError = useAppSelector((s) => selectConnectionError(s, pendingConnection)); + const connectionError = useAppSelector((s) => selectConnectionError(s, pendingConnection, edgePendingUpdate)); const shouldDim = useMemo( () => Boolean(isConnectionInProgress && connectionError && !isConnectionStartField), diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index 0f8609d2ff..9a978b09a8 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -1,7 +1,7 @@ // TODO: enable this at some point import { useStore } from '@nanostores/react'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; -import { $templates } from 'features/nodes/store/nodesSlice'; +import { $edgePendingUpdate, $templates } from 'features/nodes/store/nodesSlice'; import { validateConnection } from 'features/nodes/store/util/validateConnection'; import { useCallback } from 'react'; import type { Connection } from 'reactflow'; @@ -21,7 +21,7 @@ export const useIsValidConnection = () => { if (!(source && sourceHandle && target && targetHandle)) { return false; } - + const edgePendingUpdate = $edgePendingUpdate.get(); const { nodes, edges } = store.getState().nodes.present; const validationResult = validateConnection( @@ -29,7 +29,7 @@ export const useIsValidConnection = () => { nodes, edges, templates, - null, + edgePendingUpdate, shouldValidateGraph ); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 83632c16e1..7915d3608c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -503,7 +503,7 @@ export const $copiedNodes = atom([]); export const $copiedEdges = atom([]); export const $edgesToCopiedNodes = atom([]); export const $pendingConnection = atom(null); -export const $isUpdatingEdge = atom(false); +export const $edgePendingUpdate = atom(null); export const $didUpdateEdge = atom(false); export const $lastEdgeUpdateMouseEvent = atom(null); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts index 98155f0c20..00899c065d 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts @@ -2,7 +2,7 @@ import type { PendingConnection, Templates } from 'features/nodes/store/types'; import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; import { differenceWith, map } from 'lodash-es'; -import type { Connection } from 'reactflow'; +import type { Connection, Edge } from 'reactflow'; import { assert } from 'tsafe'; import { areTypesEqual } from './areTypesEqual'; @@ -26,7 +26,8 @@ export const getFirstValidConnection = ( edges: InvocationNodeEdge[], pendingConnection: PendingConnection, candidateNode: InvocationNode, - candidateTemplate: InvocationTemplate + candidateTemplate: InvocationTemplate, + edgePendingUpdate: Edge | null ): Connection | null => { if (pendingConnection.node.id === candidateNode.id) { // Cannot connect to self @@ -52,7 +53,7 @@ export const getFirstValidConnection = ( // Only one connection per target field is allowed - look for an unconnected target field const candidateFields = map(candidateTemplate.inputs); const candidateConnectedFields = edges - .filter((edge) => edge.target === candidateNode.id) + .filter((edge) => edge.target === candidateNode.id || edge.id === edgePendingUpdate?.id) .map((edge) => { // Edges must always have a targetHandle, safe to assert here assert(edge.targetHandle); @@ -63,7 +64,8 @@ export const getFirstValidConnection = ( candidateConnectedFields, (field, connectedFieldName) => field.name === connectedFieldName ); - const candidateField = candidateUnconnectedFields.find((field) => validateConnectionTypes(pendingConnection.fieldTemplate.type, field.type) + const candidateField = candidateUnconnectedFields.find((field) => + validateConnectionTypes(pendingConnection.fieldTemplate.type, field.type) ); if (candidateField) { return { diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts index 3cefb6815f..fb7ed49d41 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts @@ -4,7 +4,7 @@ import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { NodesState, PendingConnection, Templates } from 'features/nodes/store/types'; import { validateConnection } from 'features/nodes/store/util/validateConnection'; import i18n from 'i18next'; -import type { HandleType } from 'reactflow'; +import type { Edge, HandleType } from 'reactflow'; /** * Creates a selector that validates a pending connection. @@ -27,7 +27,9 @@ export const makeConnectionErrorSelector = ( return createMemoizedSelector( selectNodesSlice, (state: RootState, pendingConnection: PendingConnection | null) => pendingConnection, - (nodesSlice: NodesState, pendingConnection: PendingConnection | null) => { + (state: RootState, pendingConnection: PendingConnection | null, edgePendingUpdate: Edge | null) => + edgePendingUpdate, + (nodesSlice: NodesState, pendingConnection: PendingConnection | null, edgePendingUpdate: Edge | null) => { const { nodes, edges } = nodesSlice; if (!pendingConnection) { @@ -61,7 +63,7 @@ export const makeConnectionErrorSelector = ( nodes, edges, templates, - null + edgePendingUpdate ); if (!validationResult.isValid) { From ea97ae5ae8ee31b4cb0718b63db10b61c73aef76 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 07:45:53 +1000 Subject: [PATCH 215/442] tidy(ui): extraneous vars in makeConnectionErrorSelector --- .../nodes/store/util/makeConnectionErrorSelector.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts index fb7ed49d41..e1a443a60e 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts @@ -36,8 +36,6 @@ export const makeConnectionErrorSelector = ( return i18n.t('nodes.noConnectionInProgress'); } - const connectionNodeId = pendingConnection.node.id; - const connectionFieldName = pendingConnection.fieldTemplate.name; const connectionHandleType = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; if (handleType === connectionHandleType) { @@ -48,10 +46,10 @@ export const makeConnectionErrorSelector = ( } // we have to figure out which is the target and which is the source - const source = handleType === 'source' ? nodeId : connectionNodeId; - const sourceHandle = handleType === 'source' ? fieldName : connectionFieldName; - const target = handleType === 'target' ? nodeId : connectionNodeId; - const targetHandle = handleType === 'target' ? fieldName : connectionFieldName; + const source = handleType === 'source' ? nodeId : pendingConnection.node.id; + const sourceHandle = handleType === 'source' ? fieldName : pendingConnection.fieldTemplate.name; + const target = handleType === 'target' ? nodeId : pendingConnection.node.id; + const targetHandle = handleType === 'target' ? fieldName : pendingConnection.fieldTemplate.name; const validationResult = validateConnection( { From fe3980a3698e315f82b3929087ba093c8c411489 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 07:49:03 +1000 Subject: [PATCH 216/442] tests(ui): add buildNode convenience wrapper for buildInvocationNode --- .../store/util/getCollectItemType.test.ts | 9 ++- .../nodes/store/util/getHasCycles.test.ts | 9 ++- .../features/nodes/store/util/testUtils.ts | 3 + .../store/util/validateConnection.test.ts | 63 +++++++++---------- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts index 7f0a96bf33..2fed41c14c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts @@ -1,20 +1,19 @@ import { getCollectItemType } from 'features/nodes/store/util/getCollectItemType'; -import { add, buildEdge, collect, position, templates } from 'features/nodes/store/util/testUtils'; +import { add, buildEdge, buildNode, collect, templates } from 'features/nodes/store/util/testUtils'; import type { FieldType } from 'features/nodes/types/field'; -import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode'; import { describe, expect, it } from 'vitest'; describe(getCollectItemType.name, () => { it('should return the type of the items the collect node collects', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, collect); + const n1 = buildNode(add); + const n2 = buildNode(collect); const nodes = [n1, n2]; const edges = [buildEdge(n1.id, 'value', n2.id, 'item')]; const result = getCollectItemType(templates, nodes, edges, n2.id); expect(result).toEqual({ name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }); }); it('should return null if the collect node does not have any connections', () => { - const n1 = buildInvocationNode(position, collect); + const n1 = buildNode(collect); const nodes = [n1]; const result = getCollectItemType(templates, nodes, [], n1.id); expect(result).toBeNull(); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.test.ts index 872da36998..5b3a31de09 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getHasCycles.test.ts @@ -1,12 +1,11 @@ import { getHasCycles } from 'features/nodes/store/util/getHasCycles'; -import { add, buildEdge, position } from 'features/nodes/store/util/testUtils'; -import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode'; +import { add, buildEdge, buildNode } from 'features/nodes/store/util/testUtils'; import { describe, expect, it } from 'vitest'; describe(getHasCycles.name, () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, add); - const n3 = buildInvocationNode(position, add); + const n1 = buildNode(add); + const n2 = buildNode(add); + const n3 = buildNode(add); const nodes = [n1, n2, n3]; it('should return true if the graph WOULD have cycles after adding the edge', () => { diff --git a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts index 470236a82e..f351083bc5 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts @@ -1,5 +1,6 @@ import type { Templates } from 'features/nodes/store/types'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; +import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode'; import type { OpenAPIV3_1 } from 'openapi-types'; import type { Edge, XYPosition } from 'reactflow'; @@ -14,6 +15,8 @@ export const buildEdge = (source: string, sourceHandle: string, target: string, export const position: XYPosition = { x: 0, y: 0 }; +export const buildNode = (template: InvocationTemplate) => buildInvocationNode({ x: 0, y: 0 }, template); + export const add: InvocationTemplate = { title: 'Add Integers', type: 'add', diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts index 108839a499..19035afd54 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts @@ -1,9 +1,8 @@ import { deepClone } from 'common/util/deepClone'; -import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode'; import { set } from 'lodash-es'; import { describe, expect, it } from 'vitest'; -import { add, buildEdge, collect, img_resize, main_model_loader, position, sub, templates } from './testUtils'; +import { add, buildEdge, buildNode, collect, img_resize, main_model_loader, sub, templates } from './testUtils'; import { buildAcceptResult, buildRejectResult, validateConnection } from './validateConnection'; describe(validateConnection.name, () => { @@ -14,8 +13,8 @@ describe(validateConnection.name, () => { }); describe('missing nodes', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, sub); + const n1 = buildNode(add); + const n2 = buildNode(sub); const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; it('should reject missing source node', () => { @@ -30,8 +29,8 @@ describe(validateConnection.name, () => { }); describe('missing invocation templates', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, sub); + const n1 = buildNode(add); + const n2 = buildNode(sub); const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; const nodes = [n1, n2]; @@ -47,8 +46,8 @@ describe(validateConnection.name, () => { }); describe('missing field templates', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, sub); + const n1 = buildNode(add); + const n2 = buildNode(sub); const nodes = [n1, n2]; it('should reject missing source field template', () => { @@ -65,8 +64,8 @@ describe(validateConnection.name, () => { }); describe('duplicate connections', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, sub); + const n1 = buildNode(add); + const n2 = buildNode(sub); it('should accept non-duplicate connections', () => { const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; const r = validateConnection(c, [n1, n2], [], templates, null); @@ -92,17 +91,17 @@ describe(validateConnection.name, () => { set(addWithDirectAField, 'inputs.a.input', 'direct'); set(addWithDirectAField, 'type', 'addWithDirectAField'); - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, addWithDirectAField); + const n1 = buildNode(add); + const n2 = buildNode(addWithDirectAField); const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'a' }; const r = validateConnection(c, [n1, n2], [], { add, addWithDirectAField }, null); expect(r).toEqual(buildRejectResult('nodes.cannotConnectToDirectInput')); }); it('should reject connection to a collect node with mismatched item types', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, collect); - const n3 = buildInvocationNode(position, main_model_loader); + const n1 = buildNode(add); + const n2 = buildNode(collect); + const n3 = buildNode(main_model_loader); const nodes = [n1, n2, n3]; const e1 = buildEdge(n1.id, 'value', n2.id, 'item'); const edges = [e1]; @@ -112,9 +111,9 @@ describe(validateConnection.name, () => { }); it('should accept connection to a collect node with matching item types', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, collect); - const n3 = buildInvocationNode(position, sub); + const n1 = buildNode(add); + const n2 = buildNode(collect); + const n3 = buildNode(sub); const nodes = [n1, n2, n3]; const e1 = buildEdge(n1.id, 'value', n2.id, 'item'); const edges = [e1]; @@ -124,9 +123,9 @@ describe(validateConnection.name, () => { }); it('should reject connections to target field that is already connected', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, add); - const n3 = buildInvocationNode(position, add); + const n1 = buildNode(add); + const n2 = buildNode(add); + const n3 = buildNode(add); const nodes = [n1, n2, n3]; const e1 = buildEdge(n1.id, 'value', n2.id, 'a'); const edges = [e1]; @@ -136,9 +135,9 @@ describe(validateConnection.name, () => { }); it('should accept connections to target field that is already connected (ignored edge)', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, add); - const n3 = buildInvocationNode(position, add); + const n1 = buildNode(add); + const n2 = buildNode(add); + const n3 = buildNode(add); const nodes = [n1, n2, n3]; const e1 = buildEdge(n1.id, 'value', n2.id, 'a'); const edges = [e1]; @@ -148,8 +147,8 @@ describe(validateConnection.name, () => { }); it('should reject connections between invalid types', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, img_resize); + const n1 = buildNode(add); + const n2 = buildNode(img_resize); const nodes = [n1, n2]; const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'image' }; const r = validateConnection(c, nodes, [], templates, null); @@ -157,8 +156,8 @@ describe(validateConnection.name, () => { }); it('should reject connections that would create cycles', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, sub); + const n1 = buildNode(add); + const n2 = buildNode(sub); const nodes = [n1, n2]; const e1 = buildEdge(n1.id, 'value', n2.id, 'a'); const edges = [e1]; @@ -174,8 +173,8 @@ describe(validateConnection.name, () => { expect(r).toEqual(buildRejectResult('nodes.cannotConnectToSelf')); }); it('should reject connections that create cycles in non-strict mode', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, sub); + const n1 = buildNode(add); + const n2 = buildNode(sub); const nodes = [n1, n2]; const e1 = buildEdge(n1.id, 'value', n2.id, 'a'); const edges = [e1]; @@ -184,8 +183,8 @@ describe(validateConnection.name, () => { expect(r).toEqual(buildRejectResult('nodes.connectionWouldCreateCycle')); }); it('should otherwise allow invalid connections in non-strict mode', () => { - const n1 = buildInvocationNode(position, add); - const n2 = buildInvocationNode(position, img_resize); + const n1 = buildNode(add); + const n2 = buildNode(img_resize); const nodes = [n1, n2]; const c = { source: n1.id, sourceHandle: 'value', target: n2.id, targetHandle: 'image' }; const r = validateConnection(c, nodes, [], templates, null, false); From ce2ad5903c8878d80f400028d8e3f69690eced98 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 09:09:42 +1000 Subject: [PATCH 217/442] feat(ui): extract logic for finding candidate fields to own function --- .../store/util/getFirstValidConnection.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts index 00899c065d..1449a3298a 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts @@ -116,3 +116,75 @@ export const getFirstValidConnection = ( return null; }; + +export const getTargetCandidateFields = ( + source: string, + sourceHandle: string, + target: string, + nodes: AnyNode[], + edges: Edge[], + templates: Templates, + edgePendingUpdate: Edge | null +): FieldInputTemplate[] => { + const sourceNode = nodes.find((n) => n.id === source); + const targetNode = nodes.find((n) => n.id === target); + if (!sourceNode || !targetNode) { + return []; + } + + const sourceTemplate = templates[sourceNode.data.type]; + const targetTemplate = templates[targetNode.data.type]; + if (!sourceTemplate || !targetTemplate) { + return []; + } + + const sourceField = sourceTemplate.outputs[sourceHandle]; + + if (!sourceField) { + return []; + } + + const targetCandidateFields = map(targetTemplate.inputs).filter((field) => { + const c = { source, sourceHandle, target, targetHandle: field.name }; + const r = validateConnection(c, nodes, edges, templates, edgePendingUpdate, true); + return r.isValid; + }); + + return targetCandidateFields; +}; + +export const getSourceCandidateFields = ( + target: string, + targetHandle: string, + source: string, + nodes: AnyNode[], + edges: Edge[], + templates: Templates, + edgePendingUpdate: Edge | null +): FieldOutputTemplate[] => { + const targetNode = nodes.find((n) => n.id === target); + const sourceNode = nodes.find((n) => n.id === source); + if (!sourceNode || !targetNode) { + return []; + } + + const sourceTemplate = templates[sourceNode.data.type]; + const targetTemplate = templates[targetNode.data.type]; + if (!sourceTemplate || !targetTemplate) { + return []; + } + + const targetField = targetTemplate.inputs[targetHandle]; + + if (!targetField) { + return []; + } + + const sourceCandidateFields = map(sourceTemplate.outputs).filter((field) => { + const c = { source, sourceHandle: field.name, target, targetHandle }; + const r = validateConnection(c, nodes, edges, templates, edgePendingUpdate, true); + return r.isValid; + }); + + return sourceCandidateFields; +}; From c98205d0d745e69108488469fb8d6e20df3710c9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 09:10:12 +1000 Subject: [PATCH 218/442] tests(ui): candidate fields, getFirstValidConnection (wip) --- .../util/getFirstValidConnection.test.ts | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.test.ts diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.test.ts new file mode 100644 index 0000000000..59d5bbadf6 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.test.ts @@ -0,0 +1,148 @@ +import { deepClone } from 'common/util/deepClone'; +import type { PendingConnection } from 'features/nodes/store/types'; +import { + getFirstValidConnection, + getSourceCandidateFields, + getTargetCandidateFields, +} from 'features/nodes/store/util/getFirstValidConnection'; +import { add, buildEdge, buildNode, img_resize, templates } from 'features/nodes/store/util/testUtils'; +import { unset } from 'lodash-es'; +import { describe, expect, it } from 'vitest'; + +describe('getFirstValidConnection', () => { + it('should return null if the pending and candidate nodes are the same node', () => { + const pc: PendingConnection = { node: buildNode(add), template: add, fieldTemplate: add.inputs['a']! }; + const candidateNode = pc.node; + expect(getFirstValidConnection(templates, [pc.node], [], pc, candidateNode, add, null)).toBe(null); + }); + + describe('connecting from a source to a target', () => { + const pc: PendingConnection = { + node: buildNode(img_resize), + template: img_resize, + fieldTemplate: img_resize.outputs['width']!, + }; + const candidateNode = buildNode(img_resize); + + it('should return the first valid connection if there are no connected fields', () => { + const r = getFirstValidConnection(templates, [pc.node, candidateNode], [], pc, candidateNode, img_resize, null); + const c = { + source: pc.node.id, + sourceHandle: pc.fieldTemplate.name, + target: candidateNode.id, + targetHandle: 'width', + }; + expect(r).toEqual(c); + }); + it('should return the first valid connection if there is a connected field', () => { + const r = getFirstValidConnection( + templates, + [pc.node, candidateNode], + [buildEdge(pc.node.id, 'width', candidateNode.id, 'width')], + pc, + candidateNode, + img_resize, + null + ); + const c = { + source: pc.node.id, + sourceHandle: pc.fieldTemplate.name, + target: candidateNode.id, + targetHandle: 'height', + }; + expect(r).toEqual(c); + }); + it('should return the first valid connection if there is an edgePendingUpdate', () => { + const e = buildEdge(pc.node.id, 'width', candidateNode.id, 'width'); + const r = getFirstValidConnection(templates, [pc.node, candidateNode], [e], pc, candidateNode, img_resize, e); + const c = { + source: pc.node.id, + sourceHandle: pc.fieldTemplate.name, + target: candidateNode.id, + targetHandle: 'width', + }; + expect(r).toEqual(c); + }); + }); + describe('connecting from a target to a source', () => {}); +}); + +describe('getTargetCandidateFields', () => { + it('should return an empty array if the nodes canot be found', () => { + const r = getTargetCandidateFields('missing', 'value', 'missing', [], [], templates, null); + expect(r).toEqual([]); + }); + it('should return an empty array if the templates cannot be found', () => { + const n1 = buildNode(add); + const n2 = buildNode(add); + const nodes = [n1, n2]; + const r = getTargetCandidateFields(n1.id, 'value', n2.id, nodes, [], {}, null); + expect(r).toEqual([]); + }); + it('should return an empty array if the source field template cannot be found', () => { + const n1 = buildNode(add); + const n2 = buildNode(add); + const nodes = [n1, n2]; + + const addWithoutOutputValue = deepClone(add); + unset(addWithoutOutputValue, 'outputs.value'); + + const r = getTargetCandidateFields(n1.id, 'value', n2.id, nodes, [], { add: addWithoutOutputValue }, null); + expect(r).toEqual([]); + }); + it('should return all valid target fields if there are no connected fields', () => { + const n1 = buildNode(img_resize); + const n2 = buildNode(img_resize); + const nodes = [n1, n2]; + const r = getTargetCandidateFields(n1.id, 'width', n2.id, nodes, [], templates, null); + expect(r).toEqual([img_resize.inputs['width'], img_resize.inputs['height']]); + }); + it('should ignore the edgePendingUpdate if provided', () => { + const n1 = buildNode(img_resize); + const n2 = buildNode(img_resize); + const nodes = [n1, n2]; + const edgePendingUpdate = buildEdge(n1.id, 'width', n2.id, 'width'); + const r = getTargetCandidateFields(n1.id, 'width', n2.id, nodes, [], templates, edgePendingUpdate); + expect(r).toEqual([img_resize.inputs['width'], img_resize.inputs['height']]); + }); +}); + +describe('getSourceCandidateFields', () => { + it('should return an empty array if the nodes canot be found', () => { + const r = getSourceCandidateFields('missing', 'value', 'missing', [], [], templates, null); + expect(r).toEqual([]); + }); + it('should return an empty array if the templates cannot be found', () => { + const n1 = buildNode(add); + const n2 = buildNode(add); + const nodes = [n1, n2]; + const r = getSourceCandidateFields(n2.id, 'a', n1.id, nodes, [], {}, null); + expect(r).toEqual([]); + }); + it('should return an empty array if the source field template cannot be found', () => { + const n1 = buildNode(add); + const n2 = buildNode(add); + const nodes = [n1, n2]; + + const addWithoutInputA = deepClone(add); + unset(addWithoutInputA, 'inputs.a'); + + const r = getSourceCandidateFields(n1.id, 'a', n2.id, nodes, [], { add: addWithoutInputA }, null); + expect(r).toEqual([]); + }); + it('should return all valid source fields if there are no connected fields', () => { + const n1 = buildNode(img_resize); + const n2 = buildNode(img_resize); + const nodes = [n1, n2]; + const r = getSourceCandidateFields(n2.id, 'width', n1.id, nodes, [], templates, null); + expect(r).toEqual([img_resize.outputs['width'], img_resize.outputs['height']]); + }); + it('should ignore the edgePendingUpdate if provided', () => { + const n1 = buildNode(img_resize); + const n2 = buildNode(img_resize); + const nodes = [n1, n2]; + const edgePendingUpdate = buildEdge(n1.id, 'width', n2.id, 'width'); + const r = getSourceCandidateFields(n2.id, 'width', n1.id, nodes, [], templates, edgePendingUpdate); + expect(r).toEqual([img_resize.outputs['width'], img_resize.outputs['height']]); + }); +}); From 83000a4190ac180f74679e49888018f8a61b732a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 09:59:29 +1000 Subject: [PATCH 219/442] feat(ui): rework getFirstValidConnection with new helpers --- .../store/util/getFirstValidConnection.ts | 143 +++++++----------- 1 file changed, 51 insertions(+), 92 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts index 1449a3298a..adc51341d7 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.ts @@ -1,117 +1,76 @@ -import type { PendingConnection, Templates } from 'features/nodes/store/types'; -import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; -import type { AnyNode, InvocationNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; -import { differenceWith, map } from 'lodash-es'; +import type { Templates } from 'features/nodes/store/types'; +import { validateConnection } from 'features/nodes/store/util/validateConnection'; +import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; +import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; +import { map } from 'lodash-es'; import type { Connection, Edge } from 'reactflow'; -import { assert } from 'tsafe'; - -import { areTypesEqual } from './areTypesEqual'; -import { getCollectItemType } from './getCollectItemType'; -import { getHasCycles } from './getHasCycles'; /** - * Finds the first valid field for a pending connection between two nodes. - * @param templates The invocation templates + * + * @param source The source (node id) + * @param sourceHandle The source handle (field name), if any + * @param target The target (node id) + * @param targetHandle The target handle (field name), if any * @param nodes The current nodes * @param edges The current edges - * @param pendingConnection The pending connection - * @param candidateNode The candidate node to which the connection is being made - * @param candidateTemplate The candidate template for the candidate node - * @returns The first valid connection, or null if no valid connection is found + * @param templates The current templates + * @param edgePendingUpdate The edge pending update, if any + * @returns */ - export const getFirstValidConnection = ( - templates: Templates, + source: string, + sourceHandle: string | null, + target: string, + targetHandle: string | null, nodes: AnyNode[], edges: InvocationNodeEdge[], - pendingConnection: PendingConnection, - candidateNode: InvocationNode, - candidateTemplate: InvocationTemplate, + templates: Templates, edgePendingUpdate: Edge | null ): Connection | null => { - if (pendingConnection.node.id === candidateNode.id) { - // Cannot connect to self + if (source === target) { return null; } - const pendingFieldKind = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; + if (sourceHandle && targetHandle) { + return { source, sourceHandle, target, targetHandle }; + } - if (pendingFieldKind === 'source') { - // Connecting from a source to a target - if (getHasCycles(pendingConnection.node.id, candidateNode.id, nodes, edges)) { - return null; - } - if (candidateNode.data.type === 'collect') { - // Special handling for collect node - the `item` field takes any number of connections - return { - source: pendingConnection.node.id, - sourceHandle: pendingConnection.fieldTemplate.name, - target: candidateNode.id, - targetHandle: 'item', - }; - } - // Only one connection per target field is allowed - look for an unconnected target field - const candidateFields = map(candidateTemplate.inputs); - const candidateConnectedFields = edges - .filter((edge) => edge.target === candidateNode.id || edge.id === edgePendingUpdate?.id) - .map((edge) => { - // Edges must always have a targetHandle, safe to assert here - assert(edge.targetHandle); - return edge.targetHandle; - }); - const candidateUnconnectedFields = differenceWith( - candidateFields, - candidateConnectedFields, - (field, connectedFieldName) => field.name === connectedFieldName + if (sourceHandle && !targetHandle) { + const candidates = getTargetCandidateFields( + source, + sourceHandle, + target, + nodes, + edges, + templates, + edgePendingUpdate ); - const candidateField = candidateUnconnectedFields.find((field) => - validateConnectionTypes(pendingConnection.fieldTemplate.type, field.type) - ); - if (candidateField) { - return { - source: pendingConnection.node.id, - sourceHandle: pendingConnection.fieldTemplate.name, - target: candidateNode.id, - targetHandle: candidateField.name, - }; - } - } else { - // Connecting from a target to a source - // Ensure we there is not already an edge to the target, except for collect nodes - const isCollect = pendingConnection.node.data.type === 'collect'; - const isTargetAlreadyConnected = edges.some( - (e) => e.target === pendingConnection.node.id && e.targetHandle === pendingConnection.fieldTemplate.name - ); - if (!isCollect && isTargetAlreadyConnected) { + + const firstCandidate = candidates[0]; + if (!firstCandidate) { return null; } - if (getHasCycles(candidateNode.id, pendingConnection.node.id, nodes, edges)) { + return { source, sourceHandle, target, targetHandle: firstCandidate.name }; + } + + if (!sourceHandle && targetHandle) { + const candidates = getSourceCandidateFields( + target, + targetHandle, + source, + nodes, + edges, + templates, + edgePendingUpdate + ); + + const firstCandidate = candidates[0]; + if (!firstCandidate) { return null; } - // Sources/outputs can have any number of edges, we can take the first matching output field - let candidateFields = map(candidateTemplate.outputs); - if (isCollect) { - // Narrow candidates to same field type as already is connected to the collect node - const collectItemType = getCollectItemType(templates, nodes, edges, pendingConnection.node.id); - if (collectItemType) { - candidateFields = candidateFields.filter((field) => areTypesEqual(field.type, collectItemType)); - } - } - const candidateField = candidateFields.find((field) => { - const isValid = validateConnectionTypes(field.type, pendingConnection.fieldTemplate.type); - const isAlreadyConnected = edges.some((e) => e.source === candidateNode.id && e.sourceHandle === field.name); - return isValid && !isAlreadyConnected; - }); - if (candidateField) { - return { - source: candidateNode.id, - sourceHandle: candidateField.name, - target: pendingConnection.node.id, - targetHandle: pendingConnection.fieldTemplate.name, - }; - } + return { source, sourceHandle: firstCandidate.name, target, targetHandle }; } return null; From b1e28c2f2c3cb1e638c2c3d76941c92678233f87 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 09:59:59 +1000 Subject: [PATCH 220/442] tests(ui): coverage for getFirstValidConnection --- .../util/getFirstValidConnection.test.ts | 119 +++++++++++++----- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.test.ts index 59d5bbadf6..7d04ea8a58 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getFirstValidConnection.test.ts @@ -1,5 +1,4 @@ import { deepClone } from 'common/util/deepClone'; -import type { PendingConnection } from 'features/nodes/store/types'; import { getFirstValidConnection, getSourceCandidateFields, @@ -11,60 +10,116 @@ import { describe, expect, it } from 'vitest'; describe('getFirstValidConnection', () => { it('should return null if the pending and candidate nodes are the same node', () => { - const pc: PendingConnection = { node: buildNode(add), template: add, fieldTemplate: add.inputs['a']! }; - const candidateNode = pc.node; - expect(getFirstValidConnection(templates, [pc.node], [], pc, candidateNode, add, null)).toBe(null); + const n = buildNode(add); + expect(getFirstValidConnection(n.id, 'value', n.id, null, [n], [], templates, null)).toBe(null); + }); + + it('should return null if the sourceHandle and targetHandle are null', () => { + const n1 = buildNode(add); + const n2 = buildNode(add); + expect(getFirstValidConnection(n1.id, null, n2.id, null, [n1, n2], [], templates, null)).toBe(null); + }); + + it('should return itself if both sourceHandle and targetHandle are provided', () => { + const n1 = buildNode(add); + const n2 = buildNode(add); + expect(getFirstValidConnection(n1.id, 'value', n2.id, 'a', [n1, n2], [], templates, null)).toEqual({ + source: n1.id, + sourceHandle: 'value', + target: n2.id, + targetHandle: 'a', + }); }); describe('connecting from a source to a target', () => { - const pc: PendingConnection = { - node: buildNode(img_resize), - template: img_resize, - fieldTemplate: img_resize.outputs['width']!, - }; - const candidateNode = buildNode(img_resize); + const n1 = buildNode(img_resize); + const n2 = buildNode(img_resize); it('should return the first valid connection if there are no connected fields', () => { - const r = getFirstValidConnection(templates, [pc.node, candidateNode], [], pc, candidateNode, img_resize, null); + const r = getFirstValidConnection(n1.id, 'width', n2.id, null, [n1, n2], [], templates, null); const c = { - source: pc.node.id, - sourceHandle: pc.fieldTemplate.name, - target: candidateNode.id, + source: n1.id, + sourceHandle: 'width', + target: n2.id, targetHandle: 'width', }; expect(r).toEqual(c); }); it('should return the first valid connection if there is a connected field', () => { - const r = getFirstValidConnection( - templates, - [pc.node, candidateNode], - [buildEdge(pc.node.id, 'width', candidateNode.id, 'width')], - pc, - candidateNode, - img_resize, - null - ); + const e = buildEdge(n1.id, 'height', n2.id, 'width'); + const r = getFirstValidConnection(n1.id, 'width', n2.id, null, [n1, n2], [e], templates, null); const c = { - source: pc.node.id, - sourceHandle: pc.fieldTemplate.name, - target: candidateNode.id, + source: n1.id, + sourceHandle: 'width', + target: n2.id, targetHandle: 'height', }; expect(r).toEqual(c); }); it('should return the first valid connection if there is an edgePendingUpdate', () => { - const e = buildEdge(pc.node.id, 'width', candidateNode.id, 'width'); - const r = getFirstValidConnection(templates, [pc.node, candidateNode], [e], pc, candidateNode, img_resize, e); + const e = buildEdge(n1.id, 'width', n2.id, 'width'); + const r = getFirstValidConnection(n1.id, 'width', n2.id, null, [n1, n2], [e], templates, e); const c = { - source: pc.node.id, - sourceHandle: pc.fieldTemplate.name, - target: candidateNode.id, + source: n1.id, + sourceHandle: 'width', + target: n2.id, targetHandle: 'width', }; expect(r).toEqual(c); }); + it('should return null if the target has no valid fields', () => { + const e1 = buildEdge(n1.id, 'width', n2.id, 'width'); + const e2 = buildEdge(n1.id, 'height', n2.id, 'height'); + const n3 = buildNode(add); + const r = getFirstValidConnection(n3.id, 'value', n2.id, null, [n1, n2, n3], [e1, e2], templates, null); + expect(r).toEqual(null); + }); + }); + + describe('connecting from a target to a source', () => { + const n1 = buildNode(img_resize); + const n2 = buildNode(img_resize); + + it('should return the first valid connection if there are no connected fields', () => { + const r = getFirstValidConnection(n1.id, null, n2.id, 'width', [n1, n2], [], templates, null); + const c = { + source: n1.id, + sourceHandle: 'width', + target: n2.id, + targetHandle: 'width', + }; + expect(r).toEqual(c); + }); + it('should return the first valid connection if there is a connected field', () => { + const e = buildEdge(n1.id, 'height', n2.id, 'width'); + const r = getFirstValidConnection(n1.id, null, n2.id, 'height', [n1, n2], [e], templates, null); + const c = { + source: n1.id, + sourceHandle: 'width', + target: n2.id, + targetHandle: 'height', + }; + expect(r).toEqual(c); + }); + it('should return the first valid connection if there is an edgePendingUpdate', () => { + const e = buildEdge(n1.id, 'width', n2.id, 'width'); + const r = getFirstValidConnection(n1.id, null, n2.id, 'width', [n1, n2], [e], templates, e); + const c = { + source: n1.id, + sourceHandle: 'width', + target: n2.id, + targetHandle: 'width', + }; + expect(r).toEqual(c); + }); + it('should return null if the target has no valid fields', () => { + const e1 = buildEdge(n1.id, 'width', n2.id, 'width'); + const e2 = buildEdge(n1.id, 'height', n2.id, 'height'); + const n3 = buildNode(add); + const r = getFirstValidConnection(n3.id, null, n2.id, 'a', [n1, n2, n3], [e1, e2], templates, null); + expect(r).toEqual(null); + }); }); - describe('connecting from a target to a source', () => {}); }); describe('getTargetCandidateFields', () => { From 4bda174eb99c5c9ed63690c4811ee187a1035a5b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 11:11:31 +1000 Subject: [PATCH 221/442] tests(ui): coverage for getCollectItemType --- .../store/util/getCollectItemType.test.ts | 33 ++++++++++++++++--- .../nodes/store/util/getCollectItemType.ts | 7 ++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts index 2fed41c14c..935250b697 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts @@ -1,21 +1,44 @@ +import { deepClone } from 'common/util/deepClone'; import { getCollectItemType } from 'features/nodes/store/util/getCollectItemType'; import { add, buildEdge, buildNode, collect, templates } from 'features/nodes/store/util/testUtils'; import type { FieldType } from 'features/nodes/types/field'; +import { unset } from 'lodash-es'; import { describe, expect, it } from 'vitest'; describe(getCollectItemType.name, () => { it('should return the type of the items the collect node collects', () => { const n1 = buildNode(add); const n2 = buildNode(collect); - const nodes = [n1, n2]; - const edges = [buildEdge(n1.id, 'value', n2.id, 'item')]; - const result = getCollectItemType(templates, nodes, edges, n2.id); + const e1 = buildEdge(n1.id, 'value', n2.id, 'item'); + const result = getCollectItemType(templates, [n1, n2], [e1], n2.id); expect(result).toEqual({ name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }); }); it('should return null if the collect node does not have any connections', () => { const n1 = buildNode(collect); - const nodes = [n1]; - const result = getCollectItemType(templates, nodes, [], n1.id); + const result = getCollectItemType(templates, [n1], [], n1.id); + expect(result).toBeNull(); + }); + it("should return null if the first edge to collect's node doesn't exist", () => { + const n1 = buildNode(collect); + const n2 = buildNode(add); + const e1 = buildEdge(n2.id, 'value', n1.id, 'item'); + const result = getCollectItemType(templates, [n1], [e1], n1.id); + expect(result).toBeNull(); + }); + it("should return null if the first edge to collect's node template doesn't exist", () => { + const n1 = buildNode(collect); + const n2 = buildNode(add); + const e1 = buildEdge(n2.id, 'value', n1.id, 'item'); + const result = getCollectItemType({ collect }, [n1, n2], [e1], n1.id); + expect(result).toBeNull(); + }); + it("should return null if the first edge to the collect's field template doesn't exist", () => { + const n1 = buildNode(collect); + const n2 = buildNode(add); + const addWithoutOutputValue = deepClone(add); + unset(addWithoutOutputValue, 'outputs.value'); + const e1 = buildEdge(n2.id, 'value', n1.id, 'item'); + const result = getCollectItemType({ add: addWithoutOutputValue, collect }, [n2, n1], [e1], n1.id); expect(result).toBeNull(); }); }); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts index 9e0ce0fbee..e6c117d91e 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts @@ -30,6 +30,9 @@ export const getCollectItemType = ( if (!template) { return null; } - const fieldType = template.outputs[firstEdgeToCollect.sourceHandle]?.type ?? null; - return fieldType; + const fieldTemplate = template.outputs[firstEdgeToCollect.sourceHandle]; + if (!fieldTemplate) { + return null; + } + return fieldTemplate.type; }; From a80e3448f57f90e8e1993d09493498d0179468df Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 11:49:40 +1000 Subject: [PATCH 222/442] feat(ui): rework pendingConnection --- .../flow/AddNodePopover/AddNodePopover.tsx | 37 ++++++++---- .../src/features/nodes/hooks/useConnection.ts | 56 ++++++++++--------- .../nodes/hooks/useConnectionState.ts | 4 +- .../web/src/features/nodes/store/types.ts | 7 ++- .../store/util/makeConnectionErrorSelector.ts | 12 ++-- .../features/nodes/store/util/testUtils.ts | 6 +- .../nodes/store/util/validateConnection.ts | 20 +------ 7 files changed, 73 insertions(+), 69 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 14d69b4720..561890245e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -9,6 +9,7 @@ import type { SelectInstance } from 'chakra-react-select'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; import { $cursorPos, + $edgePendingUpdate, $isAddNodePopoverOpen, $pendingConnection, $templates, @@ -28,7 +29,6 @@ import { useHotkeys } from 'react-hotkeys-hook'; import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types'; import { useTranslation } from 'react-i18next'; import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; -import { assert } from 'tsafe'; const createRegex = memoize( (inputValue: string) => @@ -68,16 +68,18 @@ const AddNodePopover = () => { const filteredTemplates = useMemo(() => { // If we have a connection in progress, we need to filter the node choices + const templatesArray = map(templates); if (!pendingConnection) { - return map(templates); + return templatesArray; } return filter(templates, (template) => { - const pendingFieldKind = pendingConnection.fieldTemplate.fieldKind; - const fields = pendingFieldKind === 'input' ? template.outputs : template.inputs; - return some(fields, (field) => { - const sourceType = pendingFieldKind === 'input' ? field.type : pendingConnection.fieldTemplate.type; - const targetType = pendingFieldKind === 'output' ? field.type : pendingConnection.fieldTemplate.type; + const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs; + return some(candidateFields, (field) => { + const sourceType = + pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type; + const targetType = + pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type; return validateConnectionTypes(sourceType, targetType); }); }); @@ -144,10 +146,25 @@ const AddNodePopover = () => { // Auto-connect an edge if we just added a node and have a pending connection if (pendingConnection && isInvocationNode(node)) { - const template = templates[node.data.type]; - assert(template, 'Template not found'); + const edgePendingUpdate = $edgePendingUpdate.get(); + const { handleType } = pendingConnection; + + const source = handleType === 'source' ? pendingConnection.nodeId : node.id; + const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null; + const target = handleType === 'target' ? pendingConnection.nodeId : node.id; + const targetHandle = handleType === 'target' ? pendingConnection.handleId : null; + const { nodes, edges } = store.getState().nodes.present; - const connection = getFirstValidConnection(templates, nodes, edges, pendingConnection, node, template); + const connection = getFirstValidConnection( + source, + sourceHandle, + target, + targetHandle, + nodes, + edges, + templates, + edgePendingUpdate + ); if (connection) { dispatch(connectionMade(connection)); } diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index d81a9e5807..f7bf1b8740 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -9,10 +9,10 @@ import { connectionMade, } from 'features/nodes/store/nodesSlice'; import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; -import { isInvocationNode } from 'features/nodes/types/invocation'; import { isString } from 'lodash-es'; import { useCallback, useMemo } from 'react'; -import { type OnConnect, type OnConnectEnd, type OnConnectStart, useUpdateNodeInternals } from 'reactflow'; +import type { OnConnect, OnConnectEnd, OnConnectStart } from 'reactflow'; +import { useUpdateNodeInternals } from 'reactflow'; import { assert } from 'tsafe'; export const useConnection = () => { @@ -21,21 +21,27 @@ export const useConnection = () => { const updateNodeInternals = useUpdateNodeInternals(); const onConnectStart = useCallback( - (event, params) => { + (event, { nodeId, handleId, handleType }) => { + assert(nodeId && handleId && handleType, 'Invalid connection start event'); const nodes = store.getState().nodes.present.nodes; - const { nodeId, handleId, handleType } = params; - assert(nodeId && handleId && handleType, `Invalid connection start params: ${JSON.stringify(params)}`); + const node = nodes.find((n) => n.id === nodeId); - assert(isInvocationNode(node), `Invalid node during connection: ${JSON.stringify(node)}`); + if (!node) { + return; + } + const template = templates[node.data.type]; - assert(template, `Template not found for node type: ${node.data.type}`); - const fieldTemplate = handleType === 'source' ? template.outputs[handleId] : template.inputs[handleId]; - assert(fieldTemplate, `Field template not found for field: ${node.data.type}.${handleId}`); - $pendingConnection.set({ - node, - template, - fieldTemplate, - }); + if (!template) { + return; + } + + const fieldTemplates = template[handleType === 'source' ? 'outputs' : 'inputs']; + const fieldTemplate = fieldTemplates[handleId]; + if (!fieldTemplate) { + return; + } + + $pendingConnection.set({ nodeId, handleId, handleType, fieldTemplate }); }, [store, templates] ); @@ -67,20 +73,20 @@ export const useConnection = () => { } const { nodes, edges } = store.getState().nodes.present; if (mouseOverNodeId) { - const candidateNode = nodes.filter(isInvocationNode).find((n) => n.id === mouseOverNodeId); - if (!candidateNode) { - // The mouse is over a non-invocation node - bail - return; - } - const candidateTemplate = templates[candidateNode.data.type]; - assert(candidateTemplate, `Template not found for node type: ${candidateNode.data.type}`); + const { handleType } = pendingConnection; + const source = handleType === 'source' ? pendingConnection.nodeId : mouseOverNodeId; + const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null; + const target = handleType === 'target' ? pendingConnection.nodeId : mouseOverNodeId; + const targetHandle = handleType === 'target' ? pendingConnection.handleId : null; + const connection = getFirstValidConnection( - templates, + source, + sourceHandle, + target, + targetHandle, nodes, edges, - pendingConnection, - candidateNode, - candidateTemplate, + templates, edgePendingUpdate ); if (connection) { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index 7649209863..d218734fff 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -43,8 +43,8 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta return false; } return ( - pendingConnection.node.id === nodeId && - pendingConnection.fieldTemplate.name === fieldName && + pendingConnection.nodeId === nodeId && + pendingConnection.handleId === fieldName && pendingConnection.fieldTemplate.fieldKind === { inputs: 'input', outputs: 'output' }[kind] ); }, [fieldName, kind, nodeId, pendingConnection]); diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index 2f514bdb5b..6dcf70cfad 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -6,19 +6,20 @@ import type { } from 'features/nodes/types/field'; import type { AnyNode, - InvocationNode, InvocationNodeEdge, InvocationTemplate, NodeExecutionState, } from 'features/nodes/types/invocation'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; +import type { HandleType } from 'reactflow'; export type Templates = Record; export type NodeExecutionStates = Record; export type PendingConnection = { - node: InvocationNode; - template: InvocationTemplate; + nodeId: string; + handleId: string; + handleType: HandleType; fieldTemplate: FieldInputTemplate | FieldOutputTemplate; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts index e1a443a60e..c6d05d2c7c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts @@ -36,9 +36,7 @@ export const makeConnectionErrorSelector = ( return i18n.t('nodes.noConnectionInProgress'); } - const connectionHandleType = pendingConnection.fieldTemplate.fieldKind === 'input' ? 'target' : 'source'; - - if (handleType === connectionHandleType) { + if (handleType === pendingConnection.handleType) { if (handleType === 'source') { return i18n.t('nodes.cannotConnectOutputToOutput'); } @@ -46,10 +44,10 @@ export const makeConnectionErrorSelector = ( } // we have to figure out which is the target and which is the source - const source = handleType === 'source' ? nodeId : pendingConnection.node.id; - const sourceHandle = handleType === 'source' ? fieldName : pendingConnection.fieldTemplate.name; - const target = handleType === 'target' ? nodeId : pendingConnection.node.id; - const targetHandle = handleType === 'target' ? fieldName : pendingConnection.fieldTemplate.name; + const source = handleType === 'source' ? nodeId : pendingConnection.nodeId; + const sourceHandle = handleType === 'source' ? fieldName : pendingConnection.handleId; + const target = handleType === 'target' ? nodeId : pendingConnection.nodeId; + const targetHandle = handleType === 'target' ? fieldName : pendingConnection.handleId; const validationResult = validateConnection( { diff --git a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts index f351083bc5..5155bb14ea 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts @@ -2,7 +2,7 @@ import type { Templates } from 'features/nodes/store/types'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode'; import type { OpenAPIV3_1 } from 'openapi-types'; -import type { Edge, XYPosition } from 'reactflow'; +import type { Edge } from 'reactflow'; export const buildEdge = (source: string, sourceHandle: string, target: string, targetHandle: string): Edge => ({ source, @@ -13,8 +13,6 @@ export const buildEdge = (source: string, sourceHandle: string, target: string, id: `reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`, }); -export const position: XYPosition = { x: 0, y: 0 }; - export const buildNode = (template: InvocationTemplate) => buildInvocationNode({ x: 0, y: 0 }, template); export const add: InvocationTemplate = { @@ -176,7 +174,7 @@ export const collect: InvocationTemplate = { classification: 'stable', }; -export const scheduler: InvocationTemplate = { +const scheduler: InvocationTemplate = { title: 'Scheduler', type: 'scheduler', version: '1.0.0', diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts index edb8ac5ecb..56e45c0d80 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -6,11 +6,10 @@ import { validateConnectionTypes } from 'features/nodes/store/util/validateConne import type { AnyNode } from 'features/nodes/types/invocation'; import type { Connection as NullableConnection, Edge } from 'reactflow'; import type { O } from 'ts-toolbelt'; -import { assert } from 'tsafe'; type Connection = O.NonNullable; -export type ValidateConnectionResult = +type ValidateConnectionResult = | { isValid: true; messageTKey?: string; @@ -20,7 +19,7 @@ export type ValidateConnectionResult = messageTKey: string; }; -export type ValidateConnectionFunc = ( +type ValidateConnectionFunc = ( connection: Connection, nodes: AnyNode[], edges: Edge[], @@ -29,21 +28,6 @@ export type ValidateConnectionFunc = ( strict?: boolean ) => ValidateConnectionResult; -export const buildResult = (isValid: boolean, messageTKey?: string): ValidateConnectionResult => { - if (isValid) { - return { - isValid, - messageTKey, - }; - } else { - assert(messageTKey !== undefined); - return { - isValid, - messageTKey, - }; - } -}; - const getEqualityPredicate = (c: Connection) => (e: Edge): boolean => { From 6b11740ddafaa75ef80a3a018a931db6075dfc4d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 11:53:05 +1000 Subject: [PATCH 223/442] chore(ui): knip --- .../web/src/features/nodes/hooks/useFieldType.ts.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts deleted file mode 100644 index 90c08a94aa..0000000000 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate'; -import type { FieldType } from 'features/nodes/types/field'; -import { useMemo } from 'react'; - -export const useFieldType = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): FieldType => { - const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); - const fieldType = useMemo(() => fieldTemplate.type, [fieldTemplate]); - return fieldType; -}; From 504ac82077fce730651798d27057a8c003810f58 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 12:15:57 +1000 Subject: [PATCH 224/442] fix(ui): duplicated edges when updating edge with lazy connect --- .../frontend/web/src/features/nodes/hooks/useConnection.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index f7bf1b8740..de01c79b30 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -2,11 +2,13 @@ import { useStore } from '@nanostores/react'; import { useAppStore } from 'app/store/storeHooks'; import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { + $didUpdateEdge, $edgePendingUpdate, $isAddNodePopoverOpen, $pendingConnection, $templates, connectionMade, + edgeDeleted, } from 'features/nodes/store/nodesSlice'; import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; import { isString } from 'lodash-es'; @@ -93,6 +95,10 @@ export const useConnection = () => { dispatch(connectionMade(connection)); const nodesToUpdate = [connection.source, connection.target].filter(isString); updateNodeInternals(nodesToUpdate); + if (edgePendingUpdate) { + dispatch(edgeDeleted(edgePendingUpdate.id)); + $didUpdateEdge.set(true); + } } $pendingConnection.set(null); } else { From 26029108f7150a1bf870b26b91205d6da2f9d0c0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 13:54:32 +1000 Subject: [PATCH 225/442] feat(ui): rework node and edge mutation logic Remove our DIY'd reducers, consolidating all node and edge mutations to use `edgesChanged` and `nodesChanged`, which are called by reactflow. This makes the API for manipulating nodes and edges less tangly and error-prone. --- .../flow/AddNodePopover/AddNodePopover.tsx | 6 +- .../features/nodes/components/flow/Flow.tsx | 50 ++++------- .../flow/nodes/Invocation/MissingFallback.tsx | 20 +++++ .../Invocation/fields/LinearViewField.tsx | 11 ++- .../sidePanel/viewMode/WorkflowField.tsx | 11 ++- .../sidePanel/workflow/WorkflowLinearTab.tsx | 10 ++- .../src/features/nodes/hooks/useConnection.ts | 25 +++--- .../features/nodes/hooks/useDoesFieldExist.ts | 20 +++++ .../src/features/nodes/store/nodesSlice.ts | 87 ++++++++++--------- .../nodes/store/util/reactFlowUtil.ts | 32 +++++++ .../src/features/nodes/store/workflowSlice.ts | 14 +-- 11 files changed, 186 insertions(+), 100 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useDoesFieldExist.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/reactFlowUtil.ts diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 561890245e..12592c86da 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -14,11 +14,12 @@ import { $pendingConnection, $templates, closeAddNodePopover, - connectionMade, + edgesChanged, nodeAdded, openAddNodePopover, } from 'features/nodes/store/nodesSlice'; import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; +import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; import type { AnyNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; @@ -166,7 +167,8 @@ const AddNodePopover = () => { edgePendingUpdate ); if (connection) { - dispatch(connectionMade(connection)); + const newEdge = connectionToEdge(connection); + dispatch(edgesChanged([{ type: 'add', item: newEdge }])); } } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 18bbac0b44..5327d72478 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -14,29 +14,24 @@ import { $lastEdgeUpdateMouseEvent, $pendingConnection, $viewport, - connectionMade, - edgeDeleted, edgesChanged, - edgesDeleted, nodesChanged, - nodesDeleted, redo, selectedAll, + selectionDeleted, undo, } from 'features/nodes/store/nodesSlice'; import { $flow } from 'features/nodes/store/reactFlowInstance'; -import { isString } from 'lodash-es'; +import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; import type { CSSProperties, MouseEvent } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import type { OnEdgesChange, - OnEdgesDelete, OnEdgeUpdateFunc, OnInit, OnMoveEnd, OnNodesChange, - OnNodesDelete, ProOptions, ReactFlowProps, ReactFlowState, @@ -50,8 +45,6 @@ import CurrentImageNode from './nodes/CurrentImage/CurrentImageNode'; import InvocationNodeWrapper from './nodes/Invocation/InvocationNodeWrapper'; import NotesNode from './nodes/Notes/NotesNode'; -const DELETE_KEYS = ['Delete', 'Backspace']; - const edgeTypes = { collapsed: InvocationCollapsedEdge, default: InvocationDefaultEdge, @@ -109,20 +102,6 @@ export const Flow = memo(() => { [dispatch] ); - const onEdgesDelete: OnEdgesDelete = useCallback( - (edges) => { - dispatch(edgesDeleted(edges)); - }, - [dispatch] - ); - - const onNodesDelete: OnNodesDelete = useCallback( - (nodes) => { - dispatch(nodesDeleted(nodes)); - }, - [dispatch] - ); - const handleMoveEnd: OnMoveEnd = useCallback((e, viewport) => { $viewport.set(viewport); }, []); @@ -167,16 +146,20 @@ export const Flow = memo(() => { }, []); const onEdgeUpdate: OnEdgeUpdateFunc = useCallback( - (edge, newConnection) => { + (oldEdge, newConnection) => { // This event is fired when an edge update is successful $didUpdateEdge.set(true); // When an edge update is successful, we need to delete the old edge and create a new one - dispatch(edgeDeleted(edge.id)); - dispatch(connectionMade(newConnection)); + const newEdge = connectionToEdge(newConnection); + dispatch( + edgesChanged([ + { type: 'remove', id: oldEdge.id }, + { type: 'add', item: newEdge }, + ]) + ); // Because we shift the position of handles depending on whether a field is connected or not, we must use // updateNodeInternals to tell reactflow to recalculate the positions of the handles - const nodesToUpdate = [edge.source, edge.target, newConnection.source, newConnection.target].filter(isString); - updateNodeInternals(nodesToUpdate); + updateNodeInternals([oldEdge.source, oldEdge.target, newEdge.source, newEdge.target]); }, [dispatch, updateNodeInternals] ); @@ -193,7 +176,7 @@ export const Flow = memo(() => { // If we got this far and did not successfully update an edge, and the mouse moved away from the handle, // the user probably intended to delete the edge if (!didUpdateEdge && didMouseMove) { - dispatch(edgeDeleted(edge.id)); + dispatch(edgesChanged([{ type: 'remove', id: edge.id }])); } $edgePendingUpdate.set(null); @@ -267,6 +250,11 @@ export const Flow = memo(() => { }, [cancelConnection]); useHotkeys('esc', onEscapeHotkey); + const onDeleteHotkey = useCallback(() => { + dispatch(selectionDeleted()); + }, [dispatch]); + useHotkeys(['delete', 'backspace'], onDeleteHotkey); + return ( { onMouseMove={onMouseMove} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onEdgesDelete={onEdgesDelete} onEdgeUpdate={onEdgeUpdate} onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} - onNodesDelete={onNodesDelete} onConnectStart={onConnectStart} onConnect={onConnect} onConnectEnd={onConnectEnd} @@ -298,7 +284,7 @@ export const Flow = memo(() => { proOptions={proOptions} style={flowStyles} onPaneClick={handlePaneClick} - deleteKeyCode={DELETE_KEYS} + deleteKeyCode={null} selectionMode={selectionMode} elevateEdgesOnSelect > diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx new file mode 100644 index 0000000000..ca5b74b7ff --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx @@ -0,0 +1,20 @@ +import { useDoesFieldExist } from 'features/nodes/hooks/useDoesFieldExist'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +type Props = PropsWithChildren<{ + nodeId: string; + fieldName?: string; +}>; + +export const MissingFallback = memo((props: Props) => { + // We must be careful here to avoid race conditions where a deleted node is still referenced as an exposed field + const exists = useDoesFieldExist(props.nodeId, props.fieldName); + if (!exists) { + return null; + } + + return props.children; +}); + +MissingFallback.displayName = 'MissingFallback'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index 0cd199f7a4..f7ff85f479 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -3,6 +3,7 @@ import { CSS } from '@dnd-kit/utilities'; import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; +import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback'; import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue'; import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice'; @@ -20,7 +21,7 @@ type Props = { fieldName: string; }; -const LinearViewField = ({ nodeId, fieldName }: Props) => { +const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName); const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId); @@ -99,4 +100,12 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { ); }; +const LinearViewField = ({ nodeId, fieldName }: Props) => { + return ( + + + + ); +}; + export default memo(LinearViewField); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx index e707dd4f54..a30bda354d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx @@ -1,6 +1,7 @@ import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent'; import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer'; +import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback'; import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel'; import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue'; import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle'; @@ -14,7 +15,7 @@ type Props = { fieldName: string; }; -const WorkflowField = ({ nodeId, fieldName }: Props) => { +const WorkflowFieldInternal = ({ nodeId, fieldName }: Props) => { const label = useFieldLabel(nodeId, fieldName); const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, 'inputs'); const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName); @@ -50,4 +51,12 @@ const WorkflowField = ({ nodeId, fieldName }: Props) => { ); }; +const WorkflowField = ({ nodeId, fieldName }: Props) => { + return ( + + + + ); +}; + export default memo(WorkflowField); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx index fa1767138e..9b0e5bb9d6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx @@ -6,10 +6,10 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import DndSortable from 'features/dnd/components/DndSortable'; import type { DragEndEvent } from 'features/dnd/types'; -import LinearViewField from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField'; +import LinearViewFieldInternal from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField'; import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice'; import type { FieldIdentifier } from 'features/nodes/types/field'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; @@ -40,16 +40,18 @@ const WorkflowLinearTab = () => { [dispatch, fields] ); + const items = useMemo(() => fields.map((field) => `${field.nodeId}.${field.fieldName}`), [fields]); + return ( - `${field.nodeId}.${field.fieldName}`)}> + {isLoading ? ( ) : fields.length ? ( fields.map(({ nodeId, fieldName }) => ( - + )) ) : ( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index de01c79b30..36491e80bc 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -7,13 +7,12 @@ import { $isAddNodePopoverOpen, $pendingConnection, $templates, - connectionMade, - edgeDeleted, + edgesChanged, } from 'features/nodes/store/nodesSlice'; import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; -import { isString } from 'lodash-es'; +import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; import { useCallback, useMemo } from 'react'; -import type { OnConnect, OnConnectEnd, OnConnectStart } from 'reactflow'; +import type { EdgeChange, OnConnect, OnConnectEnd, OnConnectStart } from 'reactflow'; import { useUpdateNodeInternals } from 'reactflow'; import { assert } from 'tsafe'; @@ -50,9 +49,9 @@ export const useConnection = () => { const onConnect = useCallback( (connection) => { const { dispatch } = store; - dispatch(connectionMade(connection)); - const nodesToUpdate = [connection.source, connection.target].filter(isString); - updateNodeInternals(nodesToUpdate); + const newEdge = connectionToEdge(connection); + dispatch(edgesChanged([{ type: 'add', item: newEdge }])); + updateNodeInternals([newEdge.source, newEdge.target]); $pendingConnection.set(null); }, [store, updateNodeInternals] @@ -92,13 +91,17 @@ export const useConnection = () => { edgePendingUpdate ); if (connection) { - dispatch(connectionMade(connection)); - const nodesToUpdate = [connection.source, connection.target].filter(isString); - updateNodeInternals(nodesToUpdate); + const newEdge = connectionToEdge(connection); + const changes: EdgeChange[] = [{ type: 'add', item: newEdge }]; + + const nodesToUpdate = [newEdge.source, newEdge.target]; if (edgePendingUpdate) { - dispatch(edgeDeleted(edgePendingUpdate.id)); $didUpdateEdge.set(true); + changes.push({ type: 'remove', id: edgePendingUpdate.id }); + nodesToUpdate.push(edgePendingUpdate.source, edgePendingUpdate.target); } + dispatch(edgesChanged(changes)); + updateNodeInternals(nodesToUpdate); } $pendingConnection.set(null); } else { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useDoesFieldExist.ts b/invokeai/frontend/web/src/features/nodes/hooks/useDoesFieldExist.ts new file mode 100644 index 0000000000..4e97b1689c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useDoesFieldExist.ts @@ -0,0 +1,20 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { isInvocationNode } from 'features/nodes/types/invocation'; + +export const useDoesFieldExist = (nodeId: string, fieldName?: string) => { + const doesFieldExist = useAppSelector((s) => { + const node = s.nodes.present.nodes.find((n) => n.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + if (fieldName === undefined) { + return true; + } + if (!node.data.inputs[fieldName]) { + return false; + } + return true; + }); + + return doesFieldExist; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 7915d3608c..a1e32a72fe 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; import { workflowLoaded } from 'features/nodes/store/actions'; import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; import type { @@ -48,8 +49,8 @@ import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocatio import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; import { atom } from 'nanostores'; import type { MouseEvent } from 'react'; -import type { Connection, Edge, EdgeChange, EdgeRemoveChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow'; -import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow'; +import type { Edge, EdgeChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow'; +import { applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow'; import type { UndoableOptions } from 'redux-undo'; import type { z } from 'zod'; @@ -124,10 +125,27 @@ export const nodesSlice = createSlice({ state.nodes.push(node); }, edgesChanged: (state, action: PayloadAction) => { - state.edges = applyEdgeChanges(action.payload, state.edges); - }, - connectionMade: (state, action: PayloadAction) => { - state.edges = addEdge({ ...action.payload, type: 'default' }, state.edges); + const changes = deepClone(action.payload); + action.payload.forEach((change) => { + if (change.type === 'remove' || change.type === 'select') { + const edge = state.edges.find((e) => e.id === change.id); + // If we deleted or selected a collapsed edge, we need to find its "hidden" edges and do the same to them + if (edge && edge.type === 'collapsed') { + const hiddenEdges = state.edges.filter((e) => e.source === edge.source && e.target === edge.target); + if (change.type === 'remove') { + hiddenEdges.forEach((e) => { + changes.push({ type: 'remove', id: e.id }); + }); + } + if (change.type === 'select') { + hiddenEdges.forEach((e) => { + changes.push({ type: 'select', id: e.id, selected: change.selected }); + }); + } + } + } + }); + state.edges = applyEdgeChanges(changes, state.edges); }, fieldLabelChanged: ( state, @@ -264,33 +282,6 @@ export const nodesSlice = createSlice({ } } }, - edgeDeleted: (state, action: PayloadAction) => { - state.edges = state.edges.filter((e) => e.id !== action.payload); - }, - edgesDeleted: (state, action: PayloadAction) => { - const edges = action.payload; - const collapsedEdges = edges.filter((e) => e.type === 'collapsed'); - - // if we delete a collapsed edge, we need to delete all collapsed edges between the same nodes - if (collapsedEdges.length) { - const edgeChanges: EdgeRemoveChange[] = []; - collapsedEdges.forEach((collapsedEdge) => { - state.edges.forEach((edge) => { - if (edge.source === collapsedEdge.source && edge.target === collapsedEdge.target) { - edgeChanges.push({ id: edge.id, type: 'remove' }); - } - }); - }); - state.edges = applyEdgeChanges(edgeChanges, state.edges); - } - }, - nodesDeleted: (state, action: PayloadAction) => { - action.payload.forEach((node) => { - if (!isInvocationNode(node)) { - return; - } - }); - }, nodeLabelChanged: (state, action: PayloadAction<{ nodeId: string; label: string }>) => { const { nodeId, label } = action.payload; const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId); @@ -435,6 +426,23 @@ export const nodesSlice = createSlice({ state.nodes = applyNodeChanges(nodeChanges, state.nodes); state.edges = applyEdgeChanges(edgeChanges, state.edges); }, + selectionDeleted: (state) => { + const selectedNodes = state.nodes.filter((n) => n.selected); + const selectedEdges = state.edges.filter((e) => e.selected); + + const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ + id: n.id, + type: 'remove', + })); + + const edgeChanges: EdgeChange[] = selectedEdges.map((e) => ({ + id: e.id, + type: 'remove', + })); + + state.nodes = applyNodeChanges(nodeChanges, state.nodes); + state.edges = applyEdgeChanges(edgeChanges, state.edges); + }, undo: (state) => state, redo: (state) => state, }, @@ -457,10 +465,7 @@ export const nodesSlice = createSlice({ }); export const { - connectionMade, - edgeDeleted, edgesChanged, - edgesDeleted, fieldValueReset, fieldBoardValueChanged, fieldBooleanValueChanged, @@ -488,11 +493,11 @@ export const { nodeLabelChanged, nodeNotesChanged, nodesChanged, - nodesDeleted, nodeUseCacheChanged, notesNodeValueChanged, selectedAll, selectionPasted, + selectionDeleted, undo, redo, } = nodesSlice.actions; @@ -580,10 +585,7 @@ export const nodesUndoableConfig: UndoableOptions = { // This is used for tracking `state.workflow.isTouched` export const isAnyNodeOrEdgeMutation = isAnyOf( - connectionMade, - edgeDeleted, edgesChanged, - edgesDeleted, fieldBoardValueChanged, fieldBooleanValueChanged, fieldColorValueChanged, @@ -601,13 +603,14 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( fieldStringValueChanged, fieldVaeModelValueChanged, nodeAdded, + nodesChanged, nodeReplaced, nodeIsIntermediateChanged, nodeIsOpenChanged, nodeLabelChanged, nodeNotesChanged, - nodesDeleted, nodeUseCacheChanged, notesNodeValueChanged, - selectionPasted + selectionPasted, + selectionDeleted ); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/reactFlowUtil.ts b/invokeai/frontend/web/src/features/nodes/store/util/reactFlowUtil.ts new file mode 100644 index 0000000000..89be7951a2 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/reactFlowUtil.ts @@ -0,0 +1,32 @@ +import type { Connection, Edge } from 'reactflow'; +import { assert } from 'tsafe'; + +/** + * Gets the edge id for a connection + * Copied from: https://github.com/xyflow/xyflow/blob/v11/packages/core/src/utils/graph.ts#L44-L45 + * Requested for this to be exported in: https://github.com/xyflow/xyflow/issues/4290 + * @param connection The connection to get the id for + * @returns The edge id + */ +const getEdgeId = (connection: Connection): string => { + const { source, sourceHandle, target, targetHandle } = connection; + return `reactflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`; +}; + +/** + * Converts a connection to an edge + * @param connection The connection to convert to an edge + * @returns The edge + * @throws If the connection is invalid (e.g. missing source, sourcehandle, target, or targetHandle) + */ +export const connectionToEdge = (connection: Connection): Edge => { + const { source, sourceHandle, target, targetHandle } = connection; + assert(source && sourceHandle && target && targetHandle, 'Invalid connection'); + return { + source, + sourceHandle, + target, + targetHandle, + id: getEdgeId({ source, sourceHandle, target, targetHandle }), + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index 6293d3cce5..b3ec4f0614 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -3,7 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { workflowLoaded } from 'features/nodes/store/actions'; -import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged, nodesDeleted } from 'features/nodes/store/nodesSlice'; +import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged } from 'features/nodes/store/nodesSlice'; import type { FieldIdentifierWithValue, WorkflowMode, @@ -139,16 +139,16 @@ export const workflowSlice = createSlice({ }; }); - builder.addCase(nodesDeleted, (state, action) => { - action.payload.forEach((node) => { - state.exposedFields = state.exposedFields.filter((f) => f.nodeId !== node.id); - }); - }); - builder.addCase(nodeEditorReset, () => deepClone(initialWorkflowState)); builder.addCase(nodesChanged, (state, action) => { // Not all changes to nodes should result in the workflow being marked touched + action.payload.forEach((change) => { + if (change.type === 'remove') { + state.exposedFields = state.exposedFields.filter((f) => f.nodeId !== change.id); + } + }); + const filteredChanges = action.payload.filter((change) => { // We always want to mark the workflow as touched if a node is added, removed, or reset if (['add', 'remove', 'reset'].includes(change.type)) { From e4808440429b2b35342e058c7083623688909068 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:06:02 +1000 Subject: [PATCH 226/442] fix(ui): edge styling --- .../flow/edges/InvocationCollapsedEdge.tsx | 30 ++++++++--------- .../flow/edges/InvocationDefaultEdge.tsx | 28 +++++++--------- .../flow/edges/util/getEdgeColor.ts | 14 ++++++++ .../flow/edges/util/makeEdgeSelector.ts | 33 ++++++++++--------- 4 files changed, 56 insertions(+), 49 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx index 2e2fb31154..0d7e7b7d5e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx @@ -2,13 +2,13 @@ import { Badge, Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; +import { getEdgeStyles } from 'features/nodes/components/flow/edges/util/getEdgeColor'; +import { makeEdgeSelector } from 'features/nodes/components/flow/edges/util/makeEdgeSelector'; import { $templates } from 'features/nodes/store/nodesSlice'; import { memo, useMemo } from 'react'; import type { EdgeProps } from 'reactflow'; import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow'; -import { makeEdgeSelector } from './util/makeEdgeSelector'; - const InvocationCollapsedEdge = ({ sourceX, sourceY, @@ -18,19 +18,19 @@ const InvocationCollapsedEdge = ({ targetPosition, markerEnd, data, - selected, + selected = false, source, - target, sourceHandleId, + target, targetHandleId, }: EdgeProps<{ count: number }>) => { const templates = useStore($templates); const selector = useMemo( - () => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId, selected), - [templates, selected, source, sourceHandleId, target, targetHandleId] + () => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId), + [templates, source, sourceHandleId, target, targetHandleId] ); - const { isSelected, shouldAnimate } = useAppSelector(selector); + const { shouldAnimateEdges, areConnectedNodesSelected } = useAppSelector(selector); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, @@ -44,14 +44,8 @@ const InvocationCollapsedEdge = ({ const { base500 } = useChakraThemeTokens(); const edgeStyles = useMemo( - () => ({ - strokeWidth: isSelected ? 3 : 2, - stroke: base500, - opacity: isSelected ? 0.8 : 0.5, - animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined, - strokeDasharray: shouldAnimate ? 5 : 'none', - }), - [base500, isSelected, shouldAnimate] + () => getEdgeStyles(base500, selected, shouldAnimateEdges, areConnectedNodesSelected), + [areConnectedNodesSelected, base500, selected, shouldAnimateEdges] ); return ( @@ -60,11 +54,15 @@ const InvocationCollapsedEdge = ({ {data?.count && data.count > 1 && ( - + {data.count} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx index 2e4340975b..5a27e974e5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx @@ -1,8 +1,8 @@ import { Flex, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; +import { getEdgeStyles } from 'features/nodes/components/flow/edges/util/getEdgeColor'; import { $templates } from 'features/nodes/store/nodesSlice'; -import type { CSSProperties } from 'react'; import { memo, useMemo } from 'react'; import type { EdgeProps } from 'reactflow'; import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow'; @@ -17,7 +17,7 @@ const InvocationDefaultEdge = ({ sourcePosition, targetPosition, markerEnd, - selected, + selected = false, source, target, sourceHandleId, @@ -25,11 +25,11 @@ const InvocationDefaultEdge = ({ }: EdgeProps) => { const templates = useStore($templates); const selector = useMemo( - () => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId, selected), - [templates, source, sourceHandleId, target, targetHandleId, selected] + () => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId), + [templates, source, sourceHandleId, target, targetHandleId] ); - const { isSelected, shouldAnimate, stroke, label } = useAppSelector(selector); + const { shouldAnimateEdges, areConnectedNodesSelected, stroke, label } = useAppSelector(selector); const shouldShowEdgeLabels = useAppSelector((s) => s.workflowSettings.shouldShowEdgeLabels); const [edgePath, labelX, labelY] = getBezierPath({ @@ -41,15 +41,9 @@ const InvocationDefaultEdge = ({ targetPosition, }); - const edgeStyles = useMemo( - () => ({ - strokeWidth: isSelected ? 3 : 2, - stroke, - opacity: isSelected ? 0.8 : 0.5, - animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined, - strokeDasharray: shouldAnimate ? 5 : 'none', - }), - [isSelected, shouldAnimate, stroke] + const edgeStyles = useMemo( + () => getEdgeStyles(stroke, selected, shouldAnimateEdges, areConnectedNodesSelected), + [areConnectedNodesSelected, stroke, selected, shouldAnimateEdges] ); return ( @@ -65,13 +59,13 @@ const InvocationDefaultEdge = ({ bg="base.800" borderRadius="base" borderWidth={1} - borderColor={isSelected ? 'undefined' : 'transparent'} - opacity={isSelected ? 1 : 0.5} + borderColor={selected ? 'undefined' : 'transparent'} + opacity={selected ? 1 : 0.5} py={1} px={3} shadow="md" > - + {label} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts index e7fa43015b..91c834011c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts @@ -1,6 +1,7 @@ import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { FIELD_COLORS } from 'features/nodes/types/constants'; import type { FieldType } from 'features/nodes/types/field'; +import type { CSSProperties } from 'react'; export const getFieldColor = (fieldType: FieldType | null): string => { if (!fieldType) { @@ -10,3 +11,16 @@ export const getFieldColor = (fieldType: FieldType | null): string => { return color ? colorTokenToCssVar(color) : colorTokenToCssVar('base.500'); }; + +export const getEdgeStyles = ( + stroke: string, + selected: boolean, + shouldAnimateEdges: boolean, + areConnectedNodesSelected: boolean +): CSSProperties => ({ + strokeWidth: selected ? 3 : areConnectedNodesSelected ? 2 : 1, + stroke, + opacity: selected ? 1 : 0.5, + animation: shouldAnimateEdges ? 'dashdraw 0.5s linear infinite' : undefined, + strokeDasharray: selected || areConnectedNodesSelected ? 5 : 'none', +}); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts index 87ef8eb629..9c67728722 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts @@ -1,5 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; +import { deepClone } from 'common/util/deepClone'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { Templates } from 'features/nodes/store/types'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; @@ -8,8 +9,8 @@ import { isInvocationNode } from 'features/nodes/types/invocation'; import { getFieldColor } from './getEdgeColor'; const defaultReturnValue = { - isSelected: false, - shouldAnimate: false, + areConnectedNodesSelected: false, + shouldAnimateEdges: false, stroke: colorTokenToCssVar('base.500'), label: '', }; @@ -19,21 +20,27 @@ export const makeEdgeSelector = ( source: string, sourceHandleId: string | null | undefined, target: string, - targetHandleId: string | null | undefined, - selected?: boolean + targetHandleId: string | null | undefined ) => createMemoizedSelector( selectNodesSlice, selectWorkflowSettingsSlice, - (nodes, workflowSettings): { isSelected: boolean; shouldAnimate: boolean; stroke: string; label: string } => { + ( + nodes, + workflowSettings + ): { areConnectedNodesSelected: boolean; shouldAnimateEdges: boolean; stroke: string; label: string } => { + const { shouldAnimateEdges, shouldColorEdges } = workflowSettings; const sourceNode = nodes.nodes.find((node) => node.id === source); const targetNode = nodes.nodes.find((node) => node.id === target); + const returnValue = deepClone(defaultReturnValue); + returnValue.shouldAnimateEdges = shouldAnimateEdges; + const isInvocationToInvocationEdge = isInvocationNode(sourceNode) && isInvocationNode(targetNode); - const isSelected = Boolean(sourceNode?.selected || targetNode?.selected || selected); + returnValue.areConnectedNodesSelected = Boolean(sourceNode?.selected || targetNode?.selected); if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) { - return defaultReturnValue; + return returnValue; } const sourceNodeTemplate = templates[sourceNode.data.type]; @@ -42,16 +49,10 @@ export const makeEdgeSelector = ( const outputFieldTemplate = sourceNodeTemplate?.outputs[sourceHandleId]; const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined; - const stroke = - sourceType && workflowSettings.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); + returnValue.stroke = sourceType && shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); - const label = `${sourceNodeTemplate?.title || sourceNode.data?.label} -> ${targetNodeTemplate?.title || targetNode.data?.label}`; + returnValue.label = `${sourceNodeTemplate?.title || sourceNode.data?.label} -> ${targetNodeTemplate?.title || targetNode.data?.label}`; - return { - isSelected, - shouldAnimate: workflowSettings.shouldAnimateEdges && isSelected, - stroke, - label, - }; + return returnValue; } ); From b3429553bb7280556ff111160c85f585c94c2329 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:25:54 +1000 Subject: [PATCH 227/442] fix(ui): collapsed edges selected state --- invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index a1e32a72fe..05b53c518d 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -250,6 +250,7 @@ export const nodesSlice = createSlice({ type: 'collapsed', data: { count: 1 }, updatable: false, + selected: edge.selected, }); } } @@ -270,6 +271,7 @@ export const nodesSlice = createSlice({ type: 'collapsed', data: { count: 1 }, updatable: false, + selected: edge.selected, }); } } From 21fab9785ac3ae0a9723b587762e977d102a9a96 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:28:39 +1000 Subject: [PATCH 228/442] feat(ui): tweak edge styling --- .../features/nodes/components/flow/edges/util/getEdgeColor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts index 91c834011c..b5801c45ed 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts @@ -18,7 +18,7 @@ export const getEdgeStyles = ( shouldAnimateEdges: boolean, areConnectedNodesSelected: boolean ): CSSProperties => ({ - strokeWidth: selected ? 3 : areConnectedNodesSelected ? 2 : 1, + strokeWidth: 3, stroke, opacity: selected ? 1 : 0.5, animation: shouldAnimateEdges ? 'dashdraw 0.5s linear infinite' : undefined, From e38d75c3dc7df3ecad9c897cabbc31d6644312a0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:36:49 +1000 Subject: [PATCH 229/442] feat(ui): get rid of nodeAdded --- .../flow/AddNodePopover/AddNodePopover.tsx | 26 ++++++++++++++++--- .../src/features/nodes/store/nodesSlice.ts | 25 ------------------ 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 12592c86da..6e695561a2 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -15,9 +15,10 @@ import { $templates, closeAddNodePopover, edgesChanged, - nodeAdded, + nodesChanged, openAddNodePopover, } from 'features/nodes/store/nodesSlice'; +import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; @@ -30,6 +31,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types'; import { useTranslation } from 'react-i18next'; import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; +import type { EdgeChange, NodeChange } from 'reactflow'; const createRegex = memoize( (inputValue: string) => @@ -131,11 +133,29 @@ const AddNodePopover = () => { }); return null; } + + // Find a cozy spot for the node const cursorPos = $cursorPos.get(); - dispatch(nodeAdded({ node, cursorPos })); + const { nodes, edges } = store.getState().nodes.present; + node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y); + node.selected = true; + + // Deselect all other nodes and edges + const nodeChanges: NodeChange[] = [{ type: 'add', item: node }]; + const edgeChanges: EdgeChange[] = []; + nodes.forEach((n) => { + nodeChanges.push({ id: n.id, type: 'select', selected: false }); + }); + edges.forEach((e) => { + edgeChanges.push({ id: e.id, type: 'select', selected: false }); + }); + + // Onwards! + dispatch(nodesChanged(nodeChanges)); + dispatch(edgesChanged(edgeChanges)); return node; }, - [dispatch, buildInvocation, toaster, t] + [buildInvocation, store, dispatch, t, toaster] ); const onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 05b53c518d..5f0dbb2b14 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -55,7 +55,6 @@ import type { UndoableOptions } from 'redux-undo'; import type { z } from 'zod'; import type { NodesState, PendingConnection, Templates } from './types'; -import { findUnoccupiedPosition } from './util/findUnoccupiedPosition'; const initialNodesState: NodesState = { _version: 1, @@ -102,28 +101,6 @@ export const nodesSlice = createSlice({ } state.nodes[nodeIndex] = action.payload.node; }, - nodeAdded: (state, action: PayloadAction<{ node: AnyNode; cursorPos: XYPosition | null }>) => { - const { node, cursorPos } = action.payload; - const position = findUnoccupiedPosition( - state.nodes, - cursorPos?.x ?? node.position.x, - cursorPos?.y ?? node.position.y - ); - node.position = position; - node.selected = true; - - state.nodes = applyNodeChanges( - state.nodes.map((n) => ({ id: n.id, type: 'select', selected: false })), - state.nodes - ); - - state.edges = applyEdgeChanges( - state.edges.map((e) => ({ id: e.id, type: 'select', selected: false })), - state.edges - ); - - state.nodes.push(node); - }, edgesChanged: (state, action: PayloadAction) => { const changes = deepClone(action.payload); action.payload.forEach((change) => { @@ -486,7 +463,6 @@ export const { fieldSchedulerValueChanged, fieldStringValueChanged, fieldVaeModelValueChanged, - nodeAdded, nodeReplaced, nodeEditorReset, nodeExclusivelySelected, @@ -604,7 +580,6 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( fieldSchedulerValueChanged, fieldStringValueChanged, fieldVaeModelValueChanged, - nodeAdded, nodesChanged, nodeReplaced, nodeIsIntermediateChanged, From 1d7671298f768dad7ebbb037fd3a0e3b7cde6132 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:43:44 +1000 Subject: [PATCH 230/442] fix(ui): group edge selection actions --- invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 5f0dbb2b14..28a5e2edb2 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -528,6 +528,11 @@ const isSelectionAction = (action: UnknownAction) => { return true; } } + if (edgesChanged.match(action)) { + if (action.payload.every((change) => change.type === 'select')) { + return true; + } + } return false; }; From 9a8e0842bbe5da5c820b17e36789494fe1c8ae42 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:44:27 +1000 Subject: [PATCH 231/442] feat(ui): remove nodeReplaced action --- .../listeners/updateAllNodesRequested.ts | 9 +++++++-- .../web/src/features/nodes/store/nodesSlice.ts | 11 +---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts index 63d960b406..05cc2f8e83 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts @@ -1,7 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { updateAllNodesRequested } from 'features/nodes/store/actions'; -import { $templates, nodeReplaced } from 'features/nodes/store/nodesSlice'; +import { $templates, nodesChanged } from 'features/nodes/store/nodesSlice'; import { NodeUpdateError } from 'features/nodes/types/error'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate'; @@ -31,7 +31,12 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi } try { const updatedNode = updateNode(node, template); - dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode })); + dispatch( + nodesChanged([ + { type: 'remove', id: updatedNode.id }, + { type: 'add', item: updatedNode }, + ]) + ); } catch (e) { if (e instanceof NodeUpdateError) { unableToUpdateCount++; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 28a5e2edb2..4ef03ee658 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -49,7 +49,7 @@ import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocatio import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; import { atom } from 'nanostores'; import type { MouseEvent } from 'react'; -import type { Edge, EdgeChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow'; +import type { Edge, EdgeChange, NodeChange, Viewport, XYPosition } from 'reactflow'; import { applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow'; import type { UndoableOptions } from 'redux-undo'; import type { z } from 'zod'; @@ -94,13 +94,6 @@ export const nodesSlice = createSlice({ nodesChanged: (state, action: PayloadAction) => { state.nodes = applyNodeChanges(action.payload, state.nodes); }, - nodeReplaced: (state, action: PayloadAction<{ nodeId: string; node: Node }>) => { - const nodeIndex = state.nodes.findIndex((n) => n.id === action.payload.nodeId); - if (nodeIndex < 0) { - return; - } - state.nodes[nodeIndex] = action.payload.node; - }, edgesChanged: (state, action: PayloadAction) => { const changes = deepClone(action.payload); action.payload.forEach((change) => { @@ -463,7 +456,6 @@ export const { fieldSchedulerValueChanged, fieldStringValueChanged, fieldVaeModelValueChanged, - nodeReplaced, nodeEditorReset, nodeExclusivelySelected, nodeIsIntermediateChanged, @@ -586,7 +578,6 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( fieldStringValueChanged, fieldVaeModelValueChanged, nodesChanged, - nodeReplaced, nodeIsIntermediateChanged, nodeIsOpenChanged, nodeLabelChanged, From cbe32b647a3d9829d0bc1b8054209b585f2e382b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:47:24 +1000 Subject: [PATCH 232/442] feat(ui): remove selectedAll action --- .../features/nodes/components/flow/Flow.tsx | 20 +++++++++++++++---- .../src/features/nodes/store/nodesSlice.ts | 13 +----------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 5327d72478..df233f4a18 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -1,6 +1,6 @@ import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; import { useConnection } from 'features/nodes/hooks/useConnection'; import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste'; import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState'; @@ -17,7 +17,6 @@ import { edgesChanged, nodesChanged, redo, - selectedAll, selectionDeleted, undo, } from 'features/nodes/store/nodesSlice'; @@ -27,6 +26,8 @@ import type { CSSProperties, MouseEvent } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import type { + EdgeChange, + NodeChange, OnEdgesChange, OnEdgeUpdateFunc, OnInit, @@ -77,6 +78,7 @@ export const Flow = memo(() => { const isValidConnection = useIsValidConnection(); const cancelConnection = useReactFlowStore(selectCancelConnection); const updateNodeInternals = useUpdateNodeInternals(); + const store = useAppStore(); useWorkflowWatcher(); useSyncExecutionState(); const [borderRadius] = useToken('radii', ['base']); @@ -203,9 +205,19 @@ export const Flow = memo(() => { const onSelectAllHotkey = useCallback( (e: KeyboardEvent) => { e.preventDefault(); - dispatch(selectedAll()); + const { nodes, edges } = store.getState().nodes.present; + const nodeChanges: NodeChange[] = []; + const edgeChanges: EdgeChange[] = []; + nodes.forEach((n) => { + nodeChanges.push({ id: n.id, type: 'select', selected: true }); + }); + edges.forEach((e) => { + edgeChanges.push({ id: e.id, type: 'select', selected: true }); + }); + dispatch(nodesChanged(nodeChanges)); + dispatch(edgesChanged(edgeChanges)); }, - [dispatch] + [dispatch, store] ); useHotkeys(['Ctrl+a', 'Meta+a'], onSelectAllHotkey); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 4ef03ee658..0838778454 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -347,16 +347,6 @@ export const nodesSlice = createSlice({ state.nodes = []; state.edges = []; }, - selectedAll: (state) => { - state.nodes = applyNodeChanges( - state.nodes.map((n) => ({ id: n.id, type: 'select', selected: true })), - state.nodes - ); - state.edges = applyEdgeChanges( - state.edges.map((e) => ({ id: e.id, type: 'select', selected: true })), - state.edges - ); - }, selectionPasted: (state, action: PayloadAction<{ nodes: AnyNode[]; edges: InvocationNodeEdge[] }>) => { const { nodes, edges } = action.payload; @@ -465,7 +455,6 @@ export const { nodesChanged, nodeUseCacheChanged, notesNodeValueChanged, - selectedAll, selectionPasted, selectionDeleted, undo, @@ -509,7 +498,7 @@ export const nodesPersistConfig: PersistConfig = { persistDenylist: [], }; -const selectionMatcher = isAnyOf(selectedAll, selectionPasted, nodeExclusivelySelected); +const selectionMatcher = isAnyOf(selectionPasted, nodeExclusivelySelected); const isSelectionAction = (action: UnknownAction) => { if (selectionMatcher(action)) { From 7cceafe0dd49eeb67ad8373faf5b16cf5ac7ef5d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:52:11 +1000 Subject: [PATCH 233/442] feat(ui): remove selectionPasted action --- .../src/features/nodes/hooks/useCopyPaste.ts | 43 ++++++++++++++++-- .../src/features/nodes/store/nodesSlice.ts | 45 +------------------ 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts index 08def1514c..8be972363f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts @@ -5,11 +5,13 @@ import { $copiedNodes, $cursorPos, $edgesToCopiedNodes, - selectionPasted, + edgesChanged, + nodesChanged, selectNodesSlice, } from 'features/nodes/store/nodesSlice'; import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; import { isEqual, uniqWith } from 'lodash-es'; +import type { EdgeChange, NodeChange } from 'reactflow'; import { v4 as uuidv4 } from 'uuid'; const copySelection = () => { @@ -26,7 +28,7 @@ const copySelection = () => { const pasteSelection = (withEdgesToCopiedNodes?: boolean) => { const { getState, dispatch } = getStore(); - const currentNodes = selectNodesSlice(getState()).nodes; + const { nodes, edges } = selectNodesSlice(getState()); const cursorPos = $cursorPos.get(); const copiedNodes = deepClone($copiedNodes.get()); @@ -46,7 +48,7 @@ const pasteSelection = (withEdgesToCopiedNodes?: boolean) => { const offsetY = cursorPos ? cursorPos.y - minY : 50; copiedNodes.forEach((node) => { - const { x, y } = findUnoccupiedPosition(currentNodes, node.position.x + offsetX, node.position.y + offsetY); + const { x, y } = findUnoccupiedPosition(nodes, node.position.x + offsetX, node.position.y + offsetY); node.position.x = x; node.position.y = y; // Pasted nodes are selected @@ -68,7 +70,40 @@ const pasteSelection = (withEdgesToCopiedNodes?: boolean) => { node.data.id = id; }); - dispatch(selectionPasted({ nodes: copiedNodes, edges: copiedEdges })); + const nodeChanges: NodeChange[] = []; + const edgeChanges: EdgeChange[] = []; + // Deselect existing nodes + nodes.forEach((n) => { + nodeChanges.push({ + id: n.data.id, + type: 'select', + selected: false, + }); + }); + // Add new nodes + copiedNodes.forEach((n) => { + nodeChanges.push({ + item: n, + type: 'add', + }); + }); + // Deselect existing edges + edges.forEach((e) => { + edgeChanges.push({ + id: e.id, + type: 'select', + selected: false, + }); + }); + // Add new edges + copiedEdges.forEach((e) => { + edgeChanges.push({ + item: e, + type: 'add', + }); + }); + dispatch(nodesChanged(nodeChanges)); + dispatch(edgesChanged(edgeChanges)); }; const api = { copySelection, pasteSelection }; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 0838778454..416d7065bb 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -347,47 +347,6 @@ export const nodesSlice = createSlice({ state.nodes = []; state.edges = []; }, - selectionPasted: (state, action: PayloadAction<{ nodes: AnyNode[]; edges: InvocationNodeEdge[] }>) => { - const { nodes, edges } = action.payload; - - const nodeChanges: NodeChange[] = []; - - // Deselect existing nodes - state.nodes.forEach((n) => { - nodeChanges.push({ - id: n.data.id, - type: 'select', - selected: false, - }); - }); - // Add new nodes - nodes.forEach((n) => { - nodeChanges.push({ - item: n, - type: 'add', - }); - }); - - const edgeChanges: EdgeChange[] = []; - // Deselect existing edges - state.edges.forEach((e) => { - edgeChanges.push({ - id: e.id, - type: 'select', - selected: false, - }); - }); - // Add new edges - edges.forEach((e) => { - edgeChanges.push({ - item: e, - type: 'add', - }); - }); - - state.nodes = applyNodeChanges(nodeChanges, state.nodes); - state.edges = applyEdgeChanges(edgeChanges, state.edges); - }, selectionDeleted: (state) => { const selectedNodes = state.nodes.filter((n) => n.selected); const selectedEdges = state.edges.filter((e) => e.selected); @@ -455,7 +414,6 @@ export const { nodesChanged, nodeUseCacheChanged, notesNodeValueChanged, - selectionPasted, selectionDeleted, undo, redo, @@ -498,7 +456,7 @@ export const nodesPersistConfig: PersistConfig = { persistDenylist: [], }; -const selectionMatcher = isAnyOf(selectionPasted, nodeExclusivelySelected); +const selectionMatcher = isAnyOf(nodeExclusivelySelected); const isSelectionAction = (action: UnknownAction) => { if (selectionMatcher(action)) { @@ -573,6 +531,5 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( nodeNotesChanged, nodeUseCacheChanged, notesNodeValueChanged, - selectionPasted, selectionDeleted ); From b8b671c0db0f18935d5370d35ccbe830c4615348 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:54:35 +1000 Subject: [PATCH 234/442] feat(ui): remove selectionDeleted action --- .../features/nodes/components/flow/Flow.tsx | 19 ++++++++++++++--- .../src/features/nodes/store/nodesSlice.ts | 21 +------------------ 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index df233f4a18..75983b1617 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -17,7 +17,6 @@ import { edgesChanged, nodesChanged, redo, - selectionDeleted, undo, } from 'features/nodes/store/nodesSlice'; import { $flow } from 'features/nodes/store/reactFlowInstance'; @@ -263,8 +262,22 @@ export const Flow = memo(() => { useHotkeys('esc', onEscapeHotkey); const onDeleteHotkey = useCallback(() => { - dispatch(selectionDeleted()); - }, [dispatch]); + const { nodes, edges } = store.getState().nodes.present; + const nodeChanges: NodeChange[] = []; + const edgeChanges: EdgeChange[] = []; + nodes + .filter((n) => n.selected) + .forEach(({ id }) => { + nodeChanges.push({ type: 'remove', id }); + }); + edges + .filter((e) => e.selected) + .forEach(({ id }) => { + edgeChanges.push({ type: 'remove', id }); + }); + dispatch(nodesChanged(nodeChanges)); + dispatch(edgesChanged(edgeChanges)); + }, [dispatch, store]); useHotkeys(['delete', 'backspace'], onDeleteHotkey); return ( diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 416d7065bb..70ac801009 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -347,23 +347,6 @@ export const nodesSlice = createSlice({ state.nodes = []; state.edges = []; }, - selectionDeleted: (state) => { - const selectedNodes = state.nodes.filter((n) => n.selected); - const selectedEdges = state.edges.filter((e) => e.selected); - - const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ - id: n.id, - type: 'remove', - })); - - const edgeChanges: EdgeChange[] = selectedEdges.map((e) => ({ - id: e.id, - type: 'remove', - })); - - state.nodes = applyNodeChanges(nodeChanges, state.nodes); - state.edges = applyEdgeChanges(edgeChanges, state.edges); - }, undo: (state) => state, redo: (state) => state, }, @@ -414,7 +397,6 @@ export const { nodesChanged, nodeUseCacheChanged, notesNodeValueChanged, - selectionDeleted, undo, redo, } = nodesSlice.actions; @@ -530,6 +512,5 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( nodeLabelChanged, nodeNotesChanged, nodeUseCacheChanged, - notesNodeValueChanged, - selectionDeleted + notesNodeValueChanged ); From a51142674a3b1a435aceca9a6e435dce92a76cf5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 14:57:54 +1000 Subject: [PATCH 235/442] tidy(ui): more succinct syntax for edge and node updates --- .../flow/AddNodePopover/AddNodePopover.tsx | 8 ++++---- .../features/nodes/components/flow/Flow.tsx | 8 ++++---- .../src/features/nodes/hooks/useCopyPaste.ts | 12 ++++++------ .../web/src/features/nodes/store/nodesSlice.ts | 18 +++++++++--------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 6e695561a2..357514f380 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -143,11 +143,11 @@ const AddNodePopover = () => { // Deselect all other nodes and edges const nodeChanges: NodeChange[] = [{ type: 'add', item: node }]; const edgeChanges: EdgeChange[] = []; - nodes.forEach((n) => { - nodeChanges.push({ id: n.id, type: 'select', selected: false }); + nodes.forEach(({ id }) => { + nodeChanges.push({ type: 'select', id, selected: false }); }); - edges.forEach((e) => { - edgeChanges.push({ id: e.id, type: 'select', selected: false }); + edges.forEach(({ id }) => { + edgeChanges.push({ type: 'select', id, selected: false }); }); // Onwards! diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 75983b1617..8e67758d62 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -207,11 +207,11 @@ export const Flow = memo(() => { const { nodes, edges } = store.getState().nodes.present; const nodeChanges: NodeChange[] = []; const edgeChanges: EdgeChange[] = []; - nodes.forEach((n) => { - nodeChanges.push({ id: n.id, type: 'select', selected: true }); + nodes.forEach(({ id }) => { + nodeChanges.push({ type: 'select', id, selected: true }); }); - edges.forEach((e) => { - edgeChanges.push({ id: e.id, type: 'select', selected: true }); + edges.forEach(({ id }) => { + edgeChanges.push({ type: 'select', id, selected: true }); }); dispatch(nodesChanged(nodeChanges)); dispatch(edgesChanged(edgeChanges)); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts index 8be972363f..4ca331d61b 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts @@ -73,33 +73,33 @@ const pasteSelection = (withEdgesToCopiedNodes?: boolean) => { const nodeChanges: NodeChange[] = []; const edgeChanges: EdgeChange[] = []; // Deselect existing nodes - nodes.forEach((n) => { + nodes.forEach(({ id }) => { nodeChanges.push({ - id: n.data.id, type: 'select', + id, selected: false, }); }); // Add new nodes copiedNodes.forEach((n) => { nodeChanges.push({ - item: n, type: 'add', + item: n, }); }); // Deselect existing edges - edges.forEach((e) => { + edges.forEach(({ id }) => { edgeChanges.push({ - id: e.id, type: 'select', + id, selected: false, }); }); // Add new edges copiedEdges.forEach((e) => { edgeChanges.push({ - item: e, type: 'add', + item: e, }); }); dispatch(nodesChanged(nodeChanges)); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 70ac801009..3f8e76825c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -103,13 +103,13 @@ export const nodesSlice = createSlice({ if (edge && edge.type === 'collapsed') { const hiddenEdges = state.edges.filter((e) => e.source === edge.source && e.target === edge.target); if (change.type === 'remove') { - hiddenEdges.forEach((e) => { - changes.push({ type: 'remove', id: e.id }); + hiddenEdges.forEach(({ id }) => { + changes.push({ type: 'remove', id }); }); } if (change.type === 'select') { - hiddenEdges.forEach((e) => { - changes.push({ type: 'select', id: e.id, selected: change.selected }); + hiddenEdges.forEach(({ id }) => { + changes.push({ type: 'select', id, selected: change.selected }); }); } } @@ -275,10 +275,10 @@ export const nodesSlice = createSlice({ nodeExclusivelySelected: (state, action: PayloadAction) => { const nodeId = action.payload; state.nodes = applyNodeChanges( - state.nodes.map((n) => ({ - id: n.id, + state.nodes.map(({ id }) => ({ type: 'select', - selected: n.id === nodeId ? true : false, + id, + selected: id === nodeId ? true : false, })), state.nodes ); @@ -355,13 +355,13 @@ export const nodesSlice = createSlice({ const { nodes, edges } = action.payload; state.nodes = applyNodeChanges( nodes.map((node) => ({ - item: { ...node, ...SHARED_NODE_PROPERTIES }, type: 'add', + item: { ...node, ...SHARED_NODE_PROPERTIES }, })), [] ); state.edges = applyEdgeChanges( - edges.map((edge) => ({ item: edge, type: 'add' })), + edges.map((edge) => ({ type: 'add', item: edge })), [] ); }); From 0b5696c5d4ce5df220f28cdc258e4febe5317d55 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 15:01:30 +1000 Subject: [PATCH 236/442] feat(ui): remove nodeExclusivelySelected action --- .../flow/nodes/common/NodeWrapper.tsx | 12 ++++++++---- .../web/src/features/nodes/store/nodesSlice.ts | 17 ----------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx index 57426982ef..a0260c7301 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx @@ -1,14 +1,15 @@ import type { ChakraProps } from '@invoke-ai/ui-library'; import { Box, useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; import { useExecutionState } from 'features/nodes/hooks/useExecutionState'; import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; -import { nodeExclusivelySelected } from 'features/nodes/store/nodesSlice'; +import { nodesChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from 'features/nodes/types/constants'; import { zNodeStatus } from 'features/nodes/types/invocation'; import type { MouseEvent, PropsWithChildren } from 'react'; import { memo, useCallback } from 'react'; +import type { NodeChange } from 'reactflow'; type NodeWrapperProps = PropsWithChildren & { nodeId: string; @@ -18,6 +19,7 @@ type NodeWrapperProps = PropsWithChildren & { const NodeWrapper = (props: NodeWrapperProps) => { const { nodeId, width, children, selected } = props; + const store = useAppStore(); const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId); const executionState = useExecutionState(nodeId); @@ -37,11 +39,13 @@ const NodeWrapper = (props: NodeWrapperProps) => { const handleClick = useCallback( (e: MouseEvent) => { if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) { - dispatch(nodeExclusivelySelected(nodeId)); + const { nodes } = store.getState().nodes.present; + const nodeChanges: NodeChange[] = nodes.map(({ id }) => ({ type: 'select', id, selected: id === nodeId })); + dispatch(nodesChanged(nodeChanges)); } onCloseGlobal(); }, - [dispatch, onCloseGlobal, nodeId] + [onCloseGlobal, store, dispatch, nodeId] ); return ( diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 3f8e76825c..9cc641769c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -272,17 +272,6 @@ export const nodesSlice = createSlice({ } node.data.notes = notes; }, - nodeExclusivelySelected: (state, action: PayloadAction) => { - const nodeId = action.payload; - state.nodes = applyNodeChanges( - state.nodes.map(({ id }) => ({ - type: 'select', - id, - selected: id === nodeId ? true : false, - })), - state.nodes - ); - }, fieldValueReset: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zStatefulFieldValue); }, @@ -389,7 +378,6 @@ export const { fieldStringValueChanged, fieldVaeModelValueChanged, nodeEditorReset, - nodeExclusivelySelected, nodeIsIntermediateChanged, nodeIsOpenChanged, nodeLabelChanged, @@ -438,12 +426,7 @@ export const nodesPersistConfig: PersistConfig = { persistDenylist: [], }; -const selectionMatcher = isAnyOf(nodeExclusivelySelected); - const isSelectionAction = (action: UnknownAction) => { - if (selectionMatcher(action)) { - return true; - } if (nodesChanged.match(action)) { if (action.payload.every((change) => change.type === 'select')) { return true; From 9ed5698aa8e7bbb3afc312c891a51f06ff4bbc64 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 15:11:51 +1000 Subject: [PATCH 237/442] fix(ui): do not remove exposed fields when updating workflows --- .../src/features/nodes/store/workflowSlice.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index b3ec4f0614..0d358f56e4 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -142,13 +142,29 @@ export const workflowSlice = createSlice({ builder.addCase(nodeEditorReset, () => deepClone(initialWorkflowState)); builder.addCase(nodesChanged, (state, action) => { - // Not all changes to nodes should result in the workflow being marked touched - action.payload.forEach((change) => { - if (change.type === 'remove') { - state.exposedFields = state.exposedFields.filter((f) => f.nodeId !== change.id); + // If a node was removed, we should remove any exposed fields that were associated with it. However, node changes + // may remove and then add the same node back. For example, when updating a workflow, we replace old nodes with + // updated nodes. In this case, we should not remove the exposed fields. To handle this, we find the last remove + // and add changes for each exposed field. If the remove change comes after the add change, we remove the exposed + // field. + const exposedFieldsToRemove: FieldIdentifier[] = []; + state.exposedFields.forEach((field) => { + const removeIndex = action.payload.findLastIndex( + (change) => change.type === 'remove' && change.id === field.nodeId + ); + const addIndex = action.payload.findLastIndex( + (change) => change.type === 'add' && change.item.id === field.nodeId + ); + if (removeIndex > addIndex) { + exposedFieldsToRemove.push({ nodeId: field.nodeId, fieldName: field.fieldName }); } }); + state.exposedFields = state.exposedFields.filter( + (field) => !exposedFieldsToRemove.some((f) => isEqual(f, field)) + ); + + // Not all changes to nodes should result in the workflow being marked touched const filteredChanges = action.payload.filter((change) => { // We always want to mark the workflow as touched if a node is added, removed, or reset if (['add', 'remove', 'reset'].includes(change.type)) { @@ -165,7 +181,7 @@ export const workflowSlice = createSlice({ return false; }); - if (filteredChanges.length > 0) { + if (filteredChanges.length > 0 || exposedFieldsToRemove.length > 0) { state.isTouched = true; } }); From 059c5586a48628fb37ce654392e656087f6b8a6d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 15:43:14 +1000 Subject: [PATCH 238/442] perf(ui): ignore all no-op node and edge changes --- .../flow/AddNodePopover/AddNodePopover.tsx | 20 +++++++---- .../features/nodes/components/flow/Flow.tsx | 36 +++++++++++++------ .../flow/nodes/common/NodeWrapper.tsx | 11 ++++-- .../src/features/nodes/hooks/useConnection.ts | 6 ++-- .../src/features/nodes/hooks/useCopyPaste.ts | 36 +++++++++++-------- 5 files changed, 73 insertions(+), 36 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 357514f380..226a8f006d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -143,16 +143,24 @@ const AddNodePopover = () => { // Deselect all other nodes and edges const nodeChanges: NodeChange[] = [{ type: 'add', item: node }]; const edgeChanges: EdgeChange[] = []; - nodes.forEach(({ id }) => { - nodeChanges.push({ type: 'select', id, selected: false }); + nodes.forEach(({ id, selected }) => { + if (selected) { + nodeChanges.push({ type: 'select', id, selected: false }); + } }); - edges.forEach(({ id }) => { - edgeChanges.push({ type: 'select', id, selected: false }); + edges.forEach(({ id, selected }) => { + if (selected) { + edgeChanges.push({ type: 'select', id, selected: false }); + } }); // Onwards! - dispatch(nodesChanged(nodeChanges)); - dispatch(edgesChanged(edgeChanges)); + if (nodeChanges.length > 0) { + dispatch(nodesChanged(nodeChanges)); + } + if (edgeChanges.length > 0) { + dispatch(edgesChanged(edgeChanges)); + } return node; }, [buildInvocation, store, dispatch, t, toaster] diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 8e67758d62..19f56b7747 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -90,15 +90,17 @@ export const Flow = memo(() => { ); const onNodesChange: OnNodesChange = useCallback( - (changes) => { - dispatch(nodesChanged(changes)); + (nodeChanges) => { + dispatch(nodesChanged(nodeChanges)); }, [dispatch] ); const onEdgesChange: OnEdgesChange = useCallback( (changes) => { - dispatch(edgesChanged(changes)); + if (changes.length > 0) { + dispatch(edgesChanged(changes)); + } }, [dispatch] ); @@ -207,14 +209,22 @@ export const Flow = memo(() => { const { nodes, edges } = store.getState().nodes.present; const nodeChanges: NodeChange[] = []; const edgeChanges: EdgeChange[] = []; - nodes.forEach(({ id }) => { - nodeChanges.push({ type: 'select', id, selected: true }); + nodes.forEach(({ id, selected }) => { + if (!selected) { + nodeChanges.push({ type: 'select', id, selected: true }); + } }); - edges.forEach(({ id }) => { - edgeChanges.push({ type: 'select', id, selected: true }); + edges.forEach(({ id, selected }) => { + if (!selected) { + edgeChanges.push({ type: 'select', id, selected: true }); + } }); - dispatch(nodesChanged(nodeChanges)); - dispatch(edgesChanged(edgeChanges)); + if (nodeChanges.length > 0) { + dispatch(nodesChanged(nodeChanges)); + } + if (edgeChanges.length > 0) { + dispatch(edgesChanged(edgeChanges)); + } }, [dispatch, store] ); @@ -275,8 +285,12 @@ export const Flow = memo(() => { .forEach(({ id }) => { edgeChanges.push({ type: 'remove', id }); }); - dispatch(nodesChanged(nodeChanges)); - dispatch(edgesChanged(edgeChanges)); + if (nodeChanges.length > 0) { + dispatch(nodesChanged(nodeChanges)); + } + if (edgeChanges.length > 0) { + dispatch(edgesChanged(edgeChanges)); + } }, [dispatch, store]); useHotkeys(['delete', 'backspace'], onDeleteHotkey); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx index a0260c7301..983aee1d48 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx @@ -40,8 +40,15 @@ const NodeWrapper = (props: NodeWrapperProps) => { (e: MouseEvent) => { if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) { const { nodes } = store.getState().nodes.present; - const nodeChanges: NodeChange[] = nodes.map(({ id }) => ({ type: 'select', id, selected: id === nodeId })); - dispatch(nodesChanged(nodeChanges)); + const nodeChanges: NodeChange[] = []; + nodes.forEach(({ id, selected }) => { + if (selected !== (id === nodeId)) { + nodeChanges.push({ type: 'select', id, selected: id === nodeId }); + } + }); + if (nodeChanges.length > 0) { + dispatch(nodesChanged(nodeChanges)); + } } onCloseGlobal(); }, diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index 36491e80bc..0bca73731e 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -92,15 +92,15 @@ export const useConnection = () => { ); if (connection) { const newEdge = connectionToEdge(connection); - const changes: EdgeChange[] = [{ type: 'add', item: newEdge }]; + const edgeChanges: EdgeChange[] = [{ type: 'add', item: newEdge }]; const nodesToUpdate = [newEdge.source, newEdge.target]; if (edgePendingUpdate) { $didUpdateEdge.set(true); - changes.push({ type: 'remove', id: edgePendingUpdate.id }); + edgeChanges.push({ type: 'remove', id: edgePendingUpdate.id }); nodesToUpdate.push(edgePendingUpdate.source, edgePendingUpdate.target); } - dispatch(edgesChanged(changes)); + dispatch(edgesChanged(edgeChanges)); updateNodeInternals(nodesToUpdate); } $pendingConnection.set(null); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts index 4ca331d61b..32db806cde 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts @@ -73,12 +73,14 @@ const pasteSelection = (withEdgesToCopiedNodes?: boolean) => { const nodeChanges: NodeChange[] = []; const edgeChanges: EdgeChange[] = []; // Deselect existing nodes - nodes.forEach(({ id }) => { - nodeChanges.push({ - type: 'select', - id, - selected: false, - }); + nodes.forEach(({ id, selected }) => { + if (selected) { + nodeChanges.push({ + type: 'select', + id, + selected: false, + }); + } }); // Add new nodes copiedNodes.forEach((n) => { @@ -88,12 +90,14 @@ const pasteSelection = (withEdgesToCopiedNodes?: boolean) => { }); }); // Deselect existing edges - edges.forEach(({ id }) => { - edgeChanges.push({ - type: 'select', - id, - selected: false, - }); + edges.forEach(({ id, selected }) => { + if (selected) { + edgeChanges.push({ + type: 'select', + id, + selected: false, + }); + } }); // Add new edges copiedEdges.forEach((e) => { @@ -102,8 +106,12 @@ const pasteSelection = (withEdgesToCopiedNodes?: boolean) => { item: e, }); }); - dispatch(nodesChanged(nodeChanges)); - dispatch(edgesChanged(edgeChanges)); + if (nodeChanges.length > 0) { + dispatch(nodesChanged(nodeChanges)); + } + if (edgeChanges.length > 0) { + dispatch(edgesChanged(edgeChanges)); + } }; const api = { copySelection, pasteSelection }; From 26d0d55d9729f9734b0c0920d2c9466f8d0e6661 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 15:51:23 +1000 Subject: [PATCH 239/442] fix(ui): set nodeDragThreshold to prevent spurious position change events --- .../frontend/web/src/features/nodes/components/flow/Flow.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 19f56b7747..1748989394 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -326,6 +326,7 @@ export const Flow = memo(() => { deleteKeyCode={null} selectionMode={selectionMode} elevateEdgesOnSelect + nodeDragThreshold={1} > From 89b0e9e4de57c1217f4bafcf88a38bb8fec6f377 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 17:07:55 +1000 Subject: [PATCH 240/442] feat(ui): use connection validationResults directly in components --- .../nodes/Invocation/fields/FieldHandle.tsx | 19 +++++++++++-------- .../nodes/Invocation/fields/InputField.tsx | 6 +++--- .../nodes/Invocation/fields/OutputField.tsx | 4 ++-- .../nodes/hooks/useConnectionState.ts | 10 +++++----- .../store/util/makeConnectionErrorSelector.ts | 13 +++++-------- .../nodes/store/util/validateConnection.ts | 8 ++++---- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx index 959b13c2d0..033aa61bdf 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx @@ -2,10 +2,12 @@ import { Tooltip } from '@invoke-ai/ui-library'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor'; import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType'; +import type { ValidationResult } from 'features/nodes/store/util/validateConnection'; import { HANDLE_TOOLTIP_OPEN_DELAY, MODEL_TYPES } from 'features/nodes/types/constants'; import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; import type { CSSProperties } from 'react'; import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import type { HandleType } from 'reactflow'; import { Handle, Position } from 'reactflow'; @@ -14,11 +16,12 @@ type FieldHandleProps = { handleType: HandleType; isConnectionInProgress: boolean; isConnectionStartField: boolean; - connectionError?: string; + validationResult: ValidationResult; }; const FieldHandle = (props: FieldHandleProps) => { - const { fieldTemplate, handleType, isConnectionInProgress, isConnectionStartField, connectionError } = props; + const { fieldTemplate, handleType, isConnectionInProgress, isConnectionStartField, validationResult } = props; + const { t } = useTranslation(); const { name } = fieldTemplate; const type = fieldTemplate.type; const fieldTypeName = useFieldTypeName(type); @@ -43,11 +46,11 @@ const FieldHandle = (props: FieldHandleProps) => { s.insetInlineEnd = '-1rem'; } - if (isConnectionInProgress && !isConnectionStartField && connectionError) { + if (isConnectionInProgress && !isConnectionStartField && !validationResult.isValid) { s.filter = 'opacity(0.4) grayscale(0.7)'; } - if (isConnectionInProgress && connectionError) { + if (isConnectionInProgress && !validationResult.isValid) { if (isConnectionStartField) { s.cursor = 'grab'; } else { @@ -58,14 +61,14 @@ const FieldHandle = (props: FieldHandleProps) => { } return s; - }, [connectionError, handleType, isConnectionInProgress, isConnectionStartField, type]); + }, [handleType, isConnectionInProgress, isConnectionStartField, type, validationResult.isValid]); const tooltip = useMemo(() => { - if (isConnectionInProgress && connectionError) { - return connectionError; + if (isConnectionInProgress && validationResult.messageTKey) { + return t(validationResult.messageTKey); } return fieldTypeName; - }, [connectionError, fieldTypeName, isConnectionInProgress]); + }, [fieldTypeName, isConnectionInProgress, t, validationResult.messageTKey]); return ( { const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName); const [isHovered, setIsHovered] = useState(false); - const { isConnected, isConnectionInProgress, isConnectionStartField, connectionError, shouldDim } = + const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } = useConnectionState({ nodeId, fieldName, kind: 'inputs' }); const isMissingInput = useMemo(() => { @@ -88,7 +88,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { handleType="target" isConnectionInProgress={isConnectionInProgress} isConnectionStartField={isConnectionStartField} - connectionError={connectionError} + validationResult={validationResult} /> ); @@ -126,7 +126,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { handleType="target" isConnectionInProgress={isConnectionInProgress} isConnectionStartField={isConnectionStartField} - connectionError={connectionError} + validationResult={validationResult} /> )} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx index f2d776a2da..94e8b62744 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx @@ -18,7 +18,7 @@ const OutputField = ({ nodeId, fieldName }: Props) => { const { t } = useTranslation(); const fieldTemplate = useFieldOutputTemplate(nodeId, fieldName); - const { isConnected, isConnectionInProgress, isConnectionStartField, connectionError, shouldDim } = + const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } = useConnectionState({ nodeId, fieldName, kind: 'outputs' }); if (!fieldTemplate) { @@ -52,7 +52,7 @@ const OutputField = ({ nodeId, fieldName }: Props) => { handleType="source" isConnectionInProgress={isConnectionInProgress} isConnectionStartField={isConnectionStartField} - connectionError={connectionError} + validationResult={validationResult} /> ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index d218734fff..64bb72c54e 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -31,7 +31,7 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta [fieldName, kind, nodeId] ); - const selectConnectionError = useMemo( + const selectValidationResult = useMemo( () => makeConnectionErrorSelector(templates, nodeId, fieldName, kind === 'inputs' ? 'target' : 'source'), [templates, nodeId, fieldName, kind] ); @@ -48,18 +48,18 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta pendingConnection.fieldTemplate.fieldKind === { inputs: 'input', outputs: 'output' }[kind] ); }, [fieldName, kind, nodeId, pendingConnection]); - const connectionError = useAppSelector((s) => selectConnectionError(s, pendingConnection, edgePendingUpdate)); + const validationResult = useAppSelector((s) => selectValidationResult(s, pendingConnection, edgePendingUpdate)); const shouldDim = useMemo( - () => Boolean(isConnectionInProgress && connectionError && !isConnectionStartField), - [connectionError, isConnectionInProgress, isConnectionStartField] + () => Boolean(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField), + [validationResult, isConnectionInProgress, isConnectionStartField] ); return { isConnected, isConnectionInProgress, isConnectionStartField, - connectionError, + validationResult, shouldDim, }; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts index c6d05d2c7c..ec607c60c5 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts @@ -2,8 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import type { RootState } from 'app/store/store'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { NodesState, PendingConnection, Templates } from 'features/nodes/store/types'; -import { validateConnection } from 'features/nodes/store/util/validateConnection'; -import i18n from 'i18next'; +import { buildRejectResult, validateConnection } from 'features/nodes/store/util/validateConnection'; import type { Edge, HandleType } from 'reactflow'; /** @@ -33,14 +32,14 @@ export const makeConnectionErrorSelector = ( const { nodes, edges } = nodesSlice; if (!pendingConnection) { - return i18n.t('nodes.noConnectionInProgress'); + return buildRejectResult('nodes.noConnectionInProgress'); } if (handleType === pendingConnection.handleType) { if (handleType === 'source') { - return i18n.t('nodes.cannotConnectOutputToOutput'); + return buildRejectResult('nodes.cannotConnectOutputToOutput'); } - return i18n.t('nodes.cannotConnectInputToInput'); + return buildRejectResult('nodes.cannotConnectInputToInput'); } // we have to figure out which is the target and which is the source @@ -62,9 +61,7 @@ export const makeConnectionErrorSelector = ( edgePendingUpdate ); - if (!validationResult.isValid) { - return i18n.t(validationResult.messageTKey); - } + return validationResult; } ); }; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts index 56e45c0d80..8ece852b07 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -9,7 +9,7 @@ import type { O } from 'ts-toolbelt'; type Connection = O.NonNullable; -type ValidateConnectionResult = +export type ValidationResult = | { isValid: true; messageTKey?: string; @@ -26,7 +26,7 @@ type ValidateConnectionFunc = ( templates: Templates, ignoreEdge: Edge | null, strict?: boolean -) => ValidateConnectionResult; +) => ValidationResult; const getEqualityPredicate = (c: Connection) => @@ -45,8 +45,8 @@ const getTargetEqualityPredicate = return e.target === c.target && e.targetHandle === c.targetHandle; }; -export const buildAcceptResult = (): ValidateConnectionResult => ({ isValid: true }); -export const buildRejectResult = (messageTKey: string): ValidateConnectionResult => ({ isValid: false, messageTKey }); +export const buildAcceptResult = (): ValidationResult => ({ isValid: true }); +export const buildRejectResult = (messageTKey: string): ValidationResult => ({ isValid: false, messageTKey }); export const validateConnection: ValidateConnectionFunc = (c, nodes, edges, templates, ignoreEdge, strict = true) => { if (c.source === c.target) { From cea1874e009361383a766327262185f7c83c4ed3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 17:13:21 +1000 Subject: [PATCH 241/442] perf(ui): memoize WorkflowName selectors --- .../src/features/nodes/components/sidePanel/WorkflowName.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx index 14852945ab..b983e12e11 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx @@ -7,8 +7,10 @@ import WorkflowInfoTooltipContent from './viewMode/WorkflowInfoTooltipContent'; import { WorkflowWarning } from './viewMode/WorkflowWarning'; export const WorkflowName = () => { - const { name, isTouched, mode } = useAppSelector((s) => s.workflow); const { t } = useTranslation(); + const name = useAppSelector((s) => s.workflow.name); + const isTouched = useAppSelector((s) => s.workflow.isTouched); + const mode = useAppSelector((s) => s.workflow.mode); return ( From 281bd31db2b1b9bd10c79c17433fc007c513facc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 19:26:17 +1000 Subject: [PATCH 242/442] feat(nodes): make ModelIdentifierInvocation a prototype --- invokeai/app/invocations/model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index 6f78cf43bf..94a6136fcb 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -11,6 +11,7 @@ from invokeai.backend.model_manager.config import AnyModelConfig, BaseModelType, from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, + Classification, invocation, invocation_output, ) @@ -106,9 +107,12 @@ class ModelIdentifierOutput(BaseInvocationOutput): tags=["model"], category="model", version="1.0.0", + classification=Classification.Prototype, ) class ModelIdentifierInvocation(BaseInvocation): - """Selects any model, outputting it.""" + """Selects any model, outputting it its identifier. Be careful with this one! The identifier will be accepted as + input for any model, even if the model types don't match. If you connect this to a mismatched input, you'll get an + error.""" model: ModelIdentifierField = InputField(description="The model to select", title="Model") From e2f109807c4ca29ae729a4e52b014fcb1ab4b652 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 19:37:26 +1000 Subject: [PATCH 243/442] fix(ui): delete edges when their source or target no longer exists --- .../web/src/features/nodes/store/nodesSlice.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 9cc641769c..c63734c871 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -93,6 +93,16 @@ export const nodesSlice = createSlice({ reducers: { nodesChanged: (state, action: PayloadAction) => { state.nodes = applyNodeChanges(action.payload, state.nodes); + // Remove edges that are no longer valid, due to a removed or otherwise changed node + const edgeChanges: EdgeChange[] = []; + state.edges.forEach((e) => { + const sourceExists = state.nodes.some((n) => n.id === e.source); + const targetExists = state.nodes.some((n) => n.id === e.target); + if (!(sourceExists && targetExists)) { + edgeChanges.push({ type: 'remove', id: e.id }); + } + }); + state.edges = applyEdgeChanges(edgeChanges, state.edges); }, edgesChanged: (state, action: PayloadAction) => { const changes = deepClone(action.payload); From ca186bca614eff10f256fb3ac706229fda744700 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 19:44:48 +1000 Subject: [PATCH 244/442] fix(ui): missed node execution state for progress images --- .../listeners/socketio/socketGeneratorProgress.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts index 2dd598396a..e0c6d4f33d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts @@ -1,7 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { deepClone } from 'common/util/deepClone'; -import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState'; +import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; import { socketGeneratorProgress } from 'services/events/actions'; @@ -18,6 +18,7 @@ export const addGeneratorProgressEventListener = (startAppListening: AppStartLis nes.status = zNodeStatus.enum.IN_PROGRESS; nes.progress = (step + 1) / total_steps; nes.progressImage = progress_image ?? null; + upsertExecutionState(nes.nodeId, nes); } }, }); From ba8bed68708b99b65184bfcb7dea149228318136 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 23:44:07 +1000 Subject: [PATCH 245/442] fix(ui): edge case resulting in no node templates when loading workflow, causing failure Depending on the user behaviour and network conditions, it's possible that we could try to load a workflow before the invocation templates are available. Fix is simple: - Use the RTKQ query hook for openAPI schema in App.tsx - Disable the load workflow buttons until w have templates parsed --- invokeai/frontend/web/src/app/components/App.tsx | 2 ++ .../ImageContextMenu/SingleSelectionMenuItems.tsx | 5 ++++- .../gallery/components/ImageViewer/CurrentImageButtons.tsx | 7 +++++-- .../WorkflowLibraryMenu/LoadWorkflowFromGraphMenuItem.tsx | 6 +++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 30d8f41200..1ff093f348 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -21,6 +21,7 @@ import i18n from 'i18n'; import { size } from 'lodash-es'; import { memo, useCallback, useEffect } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; +import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; import PreselectedImage from './PreselectedImage'; @@ -46,6 +47,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => { useSocketIO(); useGlobalModifiersInit(); useGlobalHotkeys(); + useGetOpenAPISchemaQuery(); const { dropzone, isHandlingUpload, setIsHandlingUpload } = useFullscreenDropzone(); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index a25f6d8c0e..f5063ea717 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -11,10 +11,12 @@ import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; +import { size } from 'lodash-es'; import { memo, useCallback } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; @@ -48,6 +50,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const isCanvasEnabled = useFeatureStatus('canvas'); const customStarUi = useStore($customStarUI); const { downloadImage } = useDownloadImage(); + const templates = useStore($templates); const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } = useImageActions(imageDTO?.image_name); @@ -133,7 +136,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { : } onClickCapture={handleLoadWorkflow} - isDisabled={!imageDTO.has_workflow} + isDisabled={!imageDTO.has_workflow || !size(templates)} > {t('nodes.loadWorkflow')} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index ada9c35d28..d500d692fe 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -1,4 +1,5 @@ import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/query'; import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested'; @@ -12,12 +13,14 @@ import { sentImageToImg2Img } from 'features/gallery/store/actions'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers'; +import { $templates } from 'features/nodes/store/nodesSlice'; import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings'; import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { selectSystemSlice } from 'features/system/store/systemSlice'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; +import { size } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -48,7 +51,7 @@ const CurrentImageButtons = () => { const lastSelectedImage = useAppSelector(selectLastSelectedImage); const selection = useAppSelector((s) => s.gallery.selection); const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons); - + const templates = useStore($templates); const isUpscalingEnabled = useFeatureStatus('upscaling'); const isQueueMutationInProgress = useIsQueueMutationInProgress(); const { t } = useTranslation(); @@ -143,7 +146,7 @@ const CurrentImageButtons = () => { icon={} tooltip={`${t('nodes.loadWorkflow')} (W)`} aria-label={`${t('nodes.loadWorkflow')} (W)`} - isDisabled={!imageDTO?.has_workflow} + isDisabled={!imageDTO?.has_workflow || !size(templates)} onClick={handleLoadWorkflow} isLoading={getAndLoadEmbeddedWorkflowResult.isLoading} /> diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/LoadWorkflowFromGraphMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/LoadWorkflowFromGraphMenuItem.tsx index 8f3cb0c6f6..8006ca937f 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/LoadWorkflowFromGraphMenuItem.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/LoadWorkflowFromGraphMenuItem.tsx @@ -1,15 +1,19 @@ import { MenuItem } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { useLoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal'; +import { size } from 'lodash-es'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiFlaskBold } from 'react-icons/pi'; const LoadWorkflowFromGraphMenuItem = () => { const { t } = useTranslation(); + const templates = useStore($templates); const { onOpen } = useLoadWorkflowFromGraphModal(); return ( - } onClick={onOpen}> + } onClick={onOpen} isDisabled={!size(templates)}> {t('workflows.loadFromGraph')} ); From ecfff6cb1e1cf672c44bfccfe37e6458290d0260 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 20 May 2024 09:33:50 +1000 Subject: [PATCH 246/442] feat(api): add metadata to upload route Canvas images are saved by uploading a blob generated from the HTML canvas element. This means the existing metadata handling, inside the graph execution engine, is not available. To save metadata to canvas images, we need to provide it when uploading that blob. The upload route now has a `metadata` body param. If this is provided, we use it over any metadata embedded in the image. --- invokeai/app/api/routers/images.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 9c55ff6531..84d4a5d27f 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -6,7 +6,7 @@ from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, from fastapi.responses import FileResponse from fastapi.routing import APIRouter from PIL import Image -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, JsonValue from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin @@ -41,14 +41,17 @@ async def upload_image( board_id: Optional[str] = Query(default=None, description="The board to add this image to, if any"), session_id: Optional[str] = Query(default=None, description="The session ID associated with this upload, if any"), crop_visible: Optional[bool] = Query(default=False, description="Whether to crop the image"), + metadata: Optional[JsonValue] = Body( + default=None, description="The metadata to associate with the image", embed=True + ), ) -> ImageDTO: """Uploads an image""" if not file.content_type or not file.content_type.startswith("image"): raise HTTPException(status_code=415, detail="Not an image") - metadata = None - workflow = None - graph = None + _metadata = None + _workflow = None + _graph = None contents = await file.read() try: @@ -62,9 +65,9 @@ async def upload_image( # TODO: retain non-invokeai metadata on upload? # attempt to parse metadata from image - metadata_raw = pil_image.info.get("invokeai_metadata", None) + metadata_raw = metadata if isinstance(metadata, str) else pil_image.info.get("invokeai_metadata", None) if isinstance(metadata_raw, str): - metadata = metadata_raw + _metadata = metadata_raw else: ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") pass @@ -72,7 +75,7 @@ async def upload_image( # attempt to parse workflow from image workflow_raw = pil_image.info.get("invokeai_workflow", None) if isinstance(workflow_raw, str): - workflow = workflow_raw + _workflow = workflow_raw else: ApiDependencies.invoker.services.logger.warn("Failed to parse workflow for uploaded image") pass @@ -80,7 +83,7 @@ async def upload_image( # attempt to extract graph from image graph_raw = pil_image.info.get("invokeai_graph", None) if isinstance(graph_raw, str): - graph = graph_raw + _graph = graph_raw else: ApiDependencies.invoker.services.logger.warn("Failed to parse graph for uploaded image") pass @@ -92,9 +95,9 @@ async def upload_image( image_category=image_category, session_id=session_id, board_id=board_id, - metadata=metadata, - workflow=workflow, - graph=graph, + metadata=_metadata, + workflow=_workflow, + graph=_graph, is_intermediate=is_intermediate, ) From a34faf0bd8d036e595e07de0df71d976af5c0088 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 20 May 2024 09:34:01 +1000 Subject: [PATCH 247/442] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 335 +++++++++++------- 1 file changed, 198 insertions(+), 137 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index c1f9486bc7..cb3d11c06b 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -1175,6 +1175,11 @@ export type components = { * Format: binary */ file: Blob; + /** + * Metadata + * @description The metadata to associate with the image + */ + metadata?: Record | null; }; /** * Boolean Collection Primitive @@ -2542,7 +2547,7 @@ export type components = { /** @description The control image */ image?: components["schemas"]["ImageField"]; /** @description ControlNet model to load */ - control_model: components["schemas"]["ModelIdentifierField"]; + control_model?: components["schemas"]["ModelIdentifierField"]; /** * Control Weight * @description The weight given to the ControlNet @@ -4256,7 +4261,7 @@ export type components = { * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["IdealSizeInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["ImageScaleInvocation"]; + [key: string]: components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"]; }; /** * Edges @@ -4293,7 +4298,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["LoRALoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["String2Output"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["CLIPSkipInvocationOutput"]; + [key: string]: components["schemas"]["NoiseOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["String2Output"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["MetadataOutput"]; }; /** * Errors @@ -4635,7 +4640,7 @@ export type components = { * IP-Adapter Model * @description The IP-Adapter model. */ - ip_adapter_model: components["schemas"]["ModelIdentifierField"]; + ip_adapter_model?: components["schemas"]["ModelIdentifierField"]; /** * Clip Vision Model * @description CLIP Vision model to use. Overrides model settings. Mandatory for checkpoint models. @@ -6926,7 +6931,7 @@ export type components = { * LoRA * @description LoRA model to load */ - lora: components["schemas"]["ModelIdentifierField"]; + lora?: components["schemas"]["ModelIdentifierField"]; /** * Weight * @description The weight at which the LoRA is applied to each model @@ -7084,7 +7089,7 @@ export type components = { * LoRA * @description LoRA model to load */ - lora: components["schemas"]["ModelIdentifierField"]; + lora?: components["schemas"]["ModelIdentifierField"]; /** * Weight * @description The weight at which the LoRA is applied to each model @@ -7373,7 +7378,7 @@ export type components = { */ use_cache?: boolean; /** @description Main model (UNet, VAE, CLIP) to load */ - model: components["schemas"]["ModelIdentifierField"]; + model?: components["schemas"]["ModelIdentifierField"]; /** * type * @default main_model_loader @@ -8014,6 +8019,61 @@ export type components = { */ submodel_type?: components["schemas"]["SubModelType"] | null; }; + /** + * Model identifier + * @description Selects any model, outputting it its identifier. Be careful with this one! The identifier will be accepted as + * input for any model, even if the model types don't match. If you connect this to a mismatched input, you'll get an + * error. + */ + ModelIdentifierInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * Model + * @description The model to select + */ + model?: components["schemas"]["ModelIdentifierField"]; + /** + * type + * @default model_identifier + * @constant + * @enum {string} + */ + type: "model_identifier"; + }; + /** + * ModelIdentifierOutput + * @description Model identifier output + */ + ModelIdentifierOutput: { + /** + * Model + * @description Model identifier + */ + model: components["schemas"]["ModelIdentifierField"]; + /** + * type + * @default model_identifier_output + * @constant + * @enum {string} + */ + type: "model_identifier_output"; + }; /** * ModelInstallJob * @description Object that tracks the current status of an install request. @@ -9241,7 +9301,7 @@ export type components = { * LoRA * @description LoRA model to load */ - lora: components["schemas"]["ModelIdentifierField"]; + lora?: components["schemas"]["ModelIdentifierField"]; /** * Weight * @description The weight at which the LoRA is applied to each model @@ -9325,7 +9385,7 @@ export type components = { */ use_cache?: boolean; /** @description SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load */ - model: components["schemas"]["ModelIdentifierField"]; + model?: components["schemas"]["ModelIdentifierField"]; /** * type * @default sdxl_model_loader @@ -9454,7 +9514,7 @@ export type components = { */ use_cache?: boolean; /** @description SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load */ - model: components["schemas"]["ModelIdentifierField"]; + model?: components["schemas"]["ModelIdentifierField"]; /** * type * @default sdxl_refiner_model_loader @@ -10682,7 +10742,7 @@ export type components = { * T2I-Adapter Model * @description The T2I-Adapter model. */ - t2i_adapter_model: components["schemas"]["ModelIdentifierField"]; + t2i_adapter_model?: components["schemas"]["ModelIdentifierField"]; /** * Weight * @description The weight given to the T2I-Adapter @@ -11356,7 +11416,7 @@ export type components = { * VAE * @description VAE model to load */ - vae_model: components["schemas"]["ModelIdentifierField"]; + vae_model?: components["schemas"]["ModelIdentifierField"]; /** * type * @default vae_loader @@ -11841,143 +11901,144 @@ export type components = { */ UIType: "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "T2IAdapterModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict"; InvocationOutputMap: { - ideal_size: components["schemas"]["IdealSizeOutput"]; - lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; - color_map_image_processor: components["schemas"]["ImageOutput"]; - img_resize: components["schemas"]["ImageOutput"]; - calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; - lineart_image_processor: components["schemas"]["ImageOutput"]; - boolean_collection: components["schemas"]["BooleanCollectionOutput"]; - ip_adapter: components["schemas"]["IPAdapterOutput"]; - face_mask_detection: components["schemas"]["FaceMaskOutput"]; - string_replace: components["schemas"]["StringOutput"]; - infill_lama: components["schemas"]["ImageOutput"]; - calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; - tile_image_processor: components["schemas"]["ImageOutput"]; calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; - img_blur: components["schemas"]["ImageOutput"]; - scheduler: components["schemas"]["SchedulerOutput"]; - range: components["schemas"]["IntegerCollectionOutput"]; - lora_selector: components["schemas"]["LoRASelectorOutput"]; - metadata: components["schemas"]["MetadataOutput"]; clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; - rand_float: components["schemas"]["FloatOutput"]; - float_collection: components["schemas"]["FloatCollectionOutput"]; - zoe_depth_image_processor: components["schemas"]["ImageOutput"]; - create_gradient_mask: components["schemas"]["GradientMaskOutput"]; - i2l: components["schemas"]["LatentsOutput"]; - dynamic_prompt: components["schemas"]["StringCollectionOutput"]; - create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; - img_ilerp: components["schemas"]["ImageOutput"]; - tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; - infill_cv2: components["schemas"]["ImageOutput"]; - string_join_three: components["schemas"]["StringOutput"]; - denoise_latents: components["schemas"]["LatentsOutput"]; - iterate: components["schemas"]["IterateInvocationOutput"]; - step_param_easing: components["schemas"]["FloatCollectionOutput"]; - img_nsfw: components["schemas"]["ImageOutput"]; - infill_patchmatch: components["schemas"]["ImageOutput"]; - pair_tile_image: components["schemas"]["PairTileImageOutput"]; - alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; - lora_loader: components["schemas"]["LoRALoaderOutput"]; - normalbae_image_processor: components["schemas"]["ImageOutput"]; - img_hue_adjust: components["schemas"]["ImageOutput"]; - conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; - image_mask_to_tensor: components["schemas"]["MaskOutput"]; - t2i_adapter: components["schemas"]["T2IAdapterOutput"]; - infill_rgba: components["schemas"]["ImageOutput"]; - vae_loader: components["schemas"]["VAEOutput"]; - blank_image: components["schemas"]["ImageOutput"]; - latents: components["schemas"]["LatentsOutput"]; - sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - boolean: components["schemas"]["BooleanOutput"]; - float_range: components["schemas"]["FloatCollectionOutput"]; - integer: components["schemas"]["IntegerOutput"]; - mul: components["schemas"]["IntegerOutput"]; - img_crop: components["schemas"]["ImageOutput"]; - face_identifier: components["schemas"]["ImageOutput"]; - main_model_loader: components["schemas"]["ModelLoaderOutput"]; - mlsd_image_processor: components["schemas"]["ImageOutput"]; - esrgan: components["schemas"]["ImageOutput"]; - integer_math: components["schemas"]["IntegerOutput"]; - sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - img_chan: components["schemas"]["ImageOutput"]; - round_float: components["schemas"]["FloatOutput"]; - random_range: components["schemas"]["IntegerCollectionOutput"]; - image_collection: components["schemas"]["ImageCollectionOutput"]; - sub: components["schemas"]["IntegerOutput"]; - lblend: components["schemas"]["LatentsOutput"]; - sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; - cv_inpaint: components["schemas"]["ImageOutput"]; - sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; - invert_tensor_mask: components["schemas"]["MaskOutput"]; - image: components["schemas"]["ImageOutput"]; - img_mul: components["schemas"]["ImageOutput"]; - l2i: components["schemas"]["ImageOutput"]; - canny_image_processor: components["schemas"]["ImageOutput"]; - save_image: components["schemas"]["ImageOutput"]; - string_split: components["schemas"]["String2Output"]; - segment_anything_processor: components["schemas"]["ImageOutput"]; - heuristic_resize: components["schemas"]["ImageOutput"]; - face_off: components["schemas"]["FaceOffOutput"]; - img_channel_offset: components["schemas"]["ImageOutput"]; - img_conv: components["schemas"]["ImageOutput"]; - add: components["schemas"]["IntegerOutput"]; - infill_tile: components["schemas"]["ImageOutput"]; - color: components["schemas"]["ColorOutput"]; - mediapipe_face_processor: components["schemas"]["ImageOutput"]; - freeu: components["schemas"]["UNetOutput"]; - pidi_image_processor: components["schemas"]["ImageOutput"]; - depth_anything_image_processor: components["schemas"]["ImageOutput"]; - noise: components["schemas"]["NoiseOutput"]; - collect: components["schemas"]["CollectInvocationOutput"]; - content_shuffle_image_processor: components["schemas"]["ImageOutput"]; - string_split_neg: components["schemas"]["StringPosNegOutput"]; - img_lerp: components["schemas"]["ImageOutput"]; - leres_image_processor: components["schemas"]["ImageOutput"]; - div: components["schemas"]["IntegerOutput"]; - lscale: components["schemas"]["LatentsOutput"]; - metadata_item: components["schemas"]["MetadataItemOutput"]; - seamless: components["schemas"]["SeamlessModeOutput"]; - img_paste: components["schemas"]["ImageOutput"]; - string: components["schemas"]["StringOutput"]; - mask_combine: components["schemas"]["ImageOutput"]; - float_math: components["schemas"]["FloatOutput"]; - tomask: components["schemas"]["ImageOutput"]; - img_channel_multiply: components["schemas"]["ImageOutput"]; - sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; - mask_edge: components["schemas"]["ImageOutput"]; - merge_tiles_to_image: components["schemas"]["ImageOutput"]; range_of_size: components["schemas"]["IntegerCollectionOutput"]; - sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; - canvas_paste_back: components["schemas"]["ImageOutput"]; + lora_loader: components["schemas"]["LoRALoaderOutput"]; + img_mul: components["schemas"]["ImageOutput"]; + div: components["schemas"]["IntegerOutput"]; + crop_latents: components["schemas"]["LatentsOutput"]; + scheduler: components["schemas"]["SchedulerOutput"]; + blank_image: components["schemas"]["ImageOutput"]; + invert_tensor_mask: components["schemas"]["MaskOutput"]; controlnet: components["schemas"]["ControlOutput"]; - dw_openpose_image_processor: components["schemas"]["ImageOutput"]; - string_collection: components["schemas"]["StringCollectionOutput"]; - float_to_int: components["schemas"]["IntegerOutput"]; - color_correct: components["schemas"]["ImageOutput"]; - unsharp_mask: components["schemas"]["ImageOutput"]; - float: components["schemas"]["FloatOutput"]; + create_gradient_mask: components["schemas"]["GradientMaskOutput"]; + img_crop: components["schemas"]["ImageOutput"]; + img_chan: components["schemas"]["ImageOutput"]; + iterate: components["schemas"]["IterateInvocationOutput"]; + img_hue_adjust: components["schemas"]["ImageOutput"]; + merge_tiles_to_image: components["schemas"]["ImageOutput"]; + denoise_latents: components["schemas"]["LatentsOutput"]; + string_split_neg: components["schemas"]["StringPosNegOutput"]; + metadata_item: components["schemas"]["MetadataItemOutput"]; + face_off: components["schemas"]["FaceOffOutput"]; + zoe_depth_image_processor: components["schemas"]["ImageOutput"]; + prompt_from_file: components["schemas"]["StringCollectionOutput"]; + img_nsfw: components["schemas"]["ImageOutput"]; + infill_lama: components["schemas"]["ImageOutput"]; + vae_loader: components["schemas"]["VAEOutput"]; + noise: components["schemas"]["NoiseOutput"]; + midas_depth_image_processor: components["schemas"]["ImageOutput"]; + string: components["schemas"]["StringOutput"]; + img_conv: components["schemas"]["ImageOutput"]; + mlsd_image_processor: components["schemas"]["ImageOutput"]; + core_metadata: components["schemas"]["MetadataOutput"]; + float_math: components["schemas"]["FloatOutput"]; + hed_image_processor: components["schemas"]["ImageOutput"]; + lineart_anime_image_processor: components["schemas"]["ImageOutput"]; + main_model_loader: components["schemas"]["ModelLoaderOutput"]; + infill_cv2: components["schemas"]["ImageOutput"]; + image: components["schemas"]["ImageOutput"]; + normalbae_image_processor: components["schemas"]["ImageOutput"]; rand_int: components["schemas"]["IntegerOutput"]; - mask_from_id: components["schemas"]["ImageOutput"]; - latents_collection: components["schemas"]["LatentsCollectionOutput"]; - conditioning: components["schemas"]["ConditioningOutput"]; + image_collection: components["schemas"]["ImageCollectionOutput"]; + step_param_easing: components["schemas"]["FloatCollectionOutput"]; + infill_patchmatch: components["schemas"]["ImageOutput"]; + sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; + string_collection: components["schemas"]["StringCollectionOutput"]; + img_paste: components["schemas"]["ImageOutput"]; + infill_rgba: components["schemas"]["ImageOutput"]; integer_collection: components["schemas"]["IntegerCollectionOutput"]; + float_to_int: components["schemas"]["IntegerOutput"]; + tile_image_processor: components["schemas"]["ImageOutput"]; + mask_combine: components["schemas"]["ImageOutput"]; + merge_metadata: components["schemas"]["MetadataOutput"]; + rectangle_mask: components["schemas"]["MaskOutput"]; + color_map_image_processor: components["schemas"]["ImageOutput"]; + img_lerp: components["schemas"]["ImageOutput"]; + mask_edge: components["schemas"]["ImageOutput"]; + ip_adapter: components["schemas"]["IPAdapterOutput"]; + lineart_image_processor: components["schemas"]["ImageOutput"]; + seamless: components["schemas"]["SeamlessModeOutput"]; + img_channel_offset: components["schemas"]["ImageOutput"]; + sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; + range: components["schemas"]["IntegerCollectionOutput"]; + lresize: components["schemas"]["LatentsOutput"]; + freeu: components["schemas"]["UNetOutput"]; string_join: components["schemas"]["StringOutput"]; compel: components["schemas"]["ConditioningOutput"]; - crop_latents: components["schemas"]["LatentsOutput"]; + collect: components["schemas"]["CollectInvocationOutput"]; img_watermark: components["schemas"]["ImageOutput"]; - rectangle_mask: components["schemas"]["MaskOutput"]; - prompt_from_file: components["schemas"]["StringCollectionOutput"]; - merge_metadata: components["schemas"]["MetadataOutput"]; + float_range: components["schemas"]["FloatCollectionOutput"]; + sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; + i2l: components["schemas"]["LatentsOutput"]; + sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; + float: components["schemas"]["FloatOutput"]; + dynamic_prompt: components["schemas"]["StringCollectionOutput"]; + save_image: components["schemas"]["ImageOutput"]; + heuristic_resize: components["schemas"]["ImageOutput"]; + lblend: components["schemas"]["LatentsOutput"]; + tomask: components["schemas"]["ImageOutput"]; + leres_image_processor: components["schemas"]["ImageOutput"]; + lscale: components["schemas"]["LatentsOutput"]; + conditioning: components["schemas"]["ConditioningOutput"]; + mediapipe_face_processor: components["schemas"]["ImageOutput"]; + esrgan: components["schemas"]["ImageOutput"]; img_pad_crop: components["schemas"]["ImageOutput"]; - midas_depth_image_processor: components["schemas"]["ImageOutput"]; - core_metadata: components["schemas"]["MetadataOutput"]; + content_shuffle_image_processor: components["schemas"]["ImageOutput"]; + color_correct: components["schemas"]["ImageOutput"]; + unsharp_mask: components["schemas"]["ImageOutput"]; + infill_tile: components["schemas"]["ImageOutput"]; + canny_image_processor: components["schemas"]["ImageOutput"]; show_image: components["schemas"]["ImageOutput"]; - hed_image_processor: components["schemas"]["ImageOutput"]; - lresize: components["schemas"]["LatentsOutput"]; - lineart_anime_image_processor: components["schemas"]["ImageOutput"]; + pidi_image_processor: components["schemas"]["ImageOutput"]; + pair_tile_image: components["schemas"]["PairTileImageOutput"]; + segment_anything_processor: components["schemas"]["ImageOutput"]; + rand_float: components["schemas"]["FloatOutput"]; + canvas_paste_back: components["schemas"]["ImageOutput"]; + depth_anything_image_processor: components["schemas"]["ImageOutput"]; + img_channel_multiply: components["schemas"]["ImageOutput"]; + metadata: components["schemas"]["MetadataOutput"]; + string_replace: components["schemas"]["StringOutput"]; + image_mask_to_tensor: components["schemas"]["MaskOutput"]; + mul: components["schemas"]["IntegerOutput"]; img_scale: components["schemas"]["ImageOutput"]; + model_identifier: components["schemas"]["ModelIdentifierOutput"]; + alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; + latents: components["schemas"]["LatentsOutput"]; + dw_openpose_image_processor: components["schemas"]["ImageOutput"]; + mask_from_id: components["schemas"]["ImageOutput"]; + conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; + round_float: components["schemas"]["FloatOutput"]; + face_mask_detection: components["schemas"]["FaceMaskOutput"]; + calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; + img_resize: components["schemas"]["ImageOutput"]; + l2i: components["schemas"]["ImageOutput"]; + color: components["schemas"]["ColorOutput"]; + lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; + sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + string_join_three: components["schemas"]["StringOutput"]; + sub: components["schemas"]["IntegerOutput"]; + img_blur: components["schemas"]["ImageOutput"]; + float_collection: components["schemas"]["FloatCollectionOutput"]; + integer: components["schemas"]["IntegerOutput"]; + face_identifier: components["schemas"]["ImageOutput"]; + latents_collection: components["schemas"]["LatentsCollectionOutput"]; + cv_inpaint: components["schemas"]["ImageOutput"]; + t2i_adapter: components["schemas"]["T2IAdapterOutput"]; + create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; + random_range: components["schemas"]["IntegerCollectionOutput"]; + sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + ideal_size: components["schemas"]["IdealSizeOutput"]; + tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; + img_ilerp: components["schemas"]["ImageOutput"]; + integer_math: components["schemas"]["IntegerOutput"]; + add: components["schemas"]["IntegerOutput"]; + boolean: components["schemas"]["BooleanOutput"]; + string_split: components["schemas"]["String2Output"]; + lora_selector: components["schemas"]["LoRASelectorOutput"]; + boolean_collection: components["schemas"]["BooleanCollectionOutput"]; + calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; }; }; responses: never; From c94742bde678a5993f908fb7d61c8fa4528e5a67 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 20 May 2024 09:35:34 +1000 Subject: [PATCH 248/442] feat(ui): add canvas objects to metadata when saving canvas to gallery --- .../listenerMiddleware/listeners/canvasSavedToGallery.ts | 4 ++++ invokeai/frontend/web/src/services/api/endpoints/images.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts index e3ba988886..7f456e9a68 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { parseify } from 'common/util/serialize'; import { canvasSavedToGallery } from 'features/canvas/store/actions'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { addToast } from 'features/system/store/systemSlice'; @@ -43,6 +44,9 @@ export const addCanvasSavedToGalleryListener = (startAppListening: AppStartListe type: 'TOAST', toastOptions: { title: t('toast.canvasSavedGallery') }, }, + metadata: { + _canvas_objects: parseify(state.canvas.layerState.objects), + }, }) ); }, diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 98c253b479..14edf6fb87 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -571,11 +571,13 @@ export const imagesApi = api.injectEndpoints({ session_id?: string; board_id?: string; crop_visible?: boolean; + metadata?: JSONObject; } >({ - query: ({ file, image_category, is_intermediate, session_id, board_id, crop_visible }) => { + query: ({ file, image_category, is_intermediate, session_id, board_id, crop_visible, metadata }) => { const formData = new FormData(); formData.append('file', file); + formData.append('metadata', JSON.stringify(metadata)); return { url: buildImagesUrl('upload'), method: 'POST', From f4625c2671c94c1d9bda79678aca143e412bfa24 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 20 May 2024 09:35:47 +1000 Subject: [PATCH 249/442] feat(ui): add canvas objects to metadat a for all canvas graphs --- .../util/graph/canvas/buildCanvasImageToImageGraph.ts | 1 + .../nodes/util/graph/canvas/buildCanvasInpaintGraph.ts | 10 ++++++++++ .../util/graph/canvas/buildCanvasOutpaintGraph.ts | 10 ++++++++++ .../graph/canvas/buildCanvasSDXLImageToImageGraph.ts | 1 + .../util/graph/canvas/buildCanvasSDXLInpaintGraph.ts | 10 ++++++++++ .../util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts | 10 ++++++++++ .../graph/canvas/buildCanvasSDXLTextToImageGraph.ts | 1 + .../util/graph/canvas/buildCanvasTextToImageGraph.ts | 1 + 8 files changed, 44 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts index 8f5fe9f2b8..5c89dcbf29 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts @@ -330,6 +330,7 @@ export const buildCanvasImageToImageGraph = async ( clip_skip: clipSkip, strength, init_image: initialImage.image_name, + _canvas_objects: state.canvas.layerState.objects, }, CANVAS_OUTPUT ); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts index c995c38a3c..20304b8830 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { addCoreMetadataNode } from 'features/nodes/util/graph/canvas/metadata'; import { CANVAS_INPAINT_GRAPH, CANVAS_OUTPUT, @@ -421,6 +422,15 @@ export const buildCanvasInpaintGraph = async ( }); } + addCoreMetadataNode( + graph, + { + generation_mode: 'inpaint', + _canvas_objects: state.canvas.layerState.objects, + }, + CANVAS_OUTPUT + ); + // Add Seamless To Graph if (seamlessXAxis || seamlessYAxis) { addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts index e4a9b11b96..2c85b20222 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { addCoreMetadataNode } from 'features/nodes/util/graph/canvas/metadata'; import { CANVAS_OUTPAINT_GRAPH, CANVAS_OUTPUT, @@ -579,6 +580,15 @@ export const buildCanvasOutpaintGraph = async ( ); } + addCoreMetadataNode( + graph, + { + generation_mode: 'outpaint', + _canvas_objects: state.canvas.layerState.objects, + }, + CANVAS_OUTPUT + ); + // Add Seamless To Graph if (seamlessXAxis || seamlessYAxis) { addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts index 186dfa53b3..b4549ff582 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts @@ -332,6 +332,7 @@ export const buildCanvasSDXLImageToImageGraph = async ( init_image: initialImage.image_name, positive_style_prompt: positiveStylePrompt, negative_style_prompt: negativeStylePrompt, + _canvas_objects: state.canvas.layerState.objects, }, CANVAS_OUTPUT ); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts index 277b713079..dfbe2436d2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { addCoreMetadataNode } from 'features/nodes/util/graph/canvas/metadata'; import { CANVAS_OUTPUT, INPAINT_CREATE_MASK, @@ -432,6 +433,15 @@ export const buildCanvasSDXLInpaintGraph = async ( }); } + addCoreMetadataNode( + graph, + { + generation_mode: 'sdxl_inpaint', + _canvas_objects: state.canvas.layerState.objects, + }, + CANVAS_OUTPUT + ); + // Add Seamless To Graph if (seamlessXAxis || seamlessYAxis) { addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts index b09d7d8b90..d58796575c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { addCoreMetadataNode } from 'features/nodes/util/graph/canvas/metadata'; import { CANVAS_OUTPUT, INPAINT_CREATE_MASK, @@ -588,6 +589,15 @@ export const buildCanvasSDXLOutpaintGraph = async ( ); } + addCoreMetadataNode( + graph, + { + generation_mode: 'sdxl_outpaint', + _canvas_objects: state.canvas.layerState.objects, + }, + CANVAS_OUTPUT + ); + // Add Seamless To Graph if (seamlessXAxis || seamlessYAxis) { addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts index b2a8aa6ada..b9e8e011b3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts @@ -291,6 +291,7 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise steps, rand_device: use_cpu ? 'cpu' : 'cuda', scheduler, + _canvas_objects: state.canvas.layerState.objects, }, CANVAS_OUTPUT ); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts index 8ce5134480..fe33ab5cf3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts @@ -280,6 +280,7 @@ export const buildCanvasTextToImageGraph = async (state: RootState): Promise Date: Sat, 18 May 2024 09:12:10 +1000 Subject: [PATCH 250/442] fix(ui): fix t2i adapter dimensions error message It now indicates the correct dimension of 64 (SD1.5) or 32 (SDXL) - before was hardcoded to 64. --- invokeai/frontend/web/public/locales/en.json | 2 +- invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 1f44e641fc..5dd411c544 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -951,7 +951,7 @@ "controlAdapterIncompatibleBaseModel": "incompatible Control Adapter base model", "controlAdapterNoImageSelected": "no Control Adapter image selected", "controlAdapterImageNotProcessed": "Control Adapter image not processed", - "t2iAdapterIncompatibleDimensions": "T2I Adapter requires image dimension to be multiples of 64", + "t2iAdapterIncompatibleDimensions": "T2I Adapter requires image dimension to be multiples of {{multiple}}", "ipAdapterNoModelSelected": "no IP adapter selected", "ipAdapterIncompatibleBaseModel": "incompatible IP Adapter base model", "ipAdapterNoImageSelected": "no IP Adapter image selected", diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 41d6f4607e..dbf3c41480 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -137,7 +137,7 @@ const createSelector = (templates: Templates) => if (l.controlAdapter.type === 't2i_adapter') { const multiple = model?.base === 'sdxl' ? 32 : 64; if (size.width % multiple !== 0 || size.height % multiple !== 0) { - problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions')); + problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); } } } From dba8c43ecbbde65708da82baa3eae5a8a1a96f27 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 22:54:11 +1000 Subject: [PATCH 251/442] feat(ui): explicit field type cardinality Replace the `isCollection` and `isCollectionOrScalar` flags with a single enum value `cardinality`. Valid values are `SINGLE`, `COLLECTION` and `SINGLE_OR_COLLECTION`. Why: - The two flags were mutually exclusive, but this wasn't enforce. You could create a field type that had both `isCollection` and `isCollectionOrScalar` set to true, whuch makes no sense. - There was no explicit declaration for scalar/single types. - Checking if a type had only a single flag was tedious. Thanks to a change a couple months back in which the workflows schema was revised, field types are internal implementation details. Changes to them are non-breaking. --- .../frontend/web/src/features/nodes/types/field.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts index 8a1a0b5039..e2a84e3390 100644 --- a/invokeai/frontend/web/src/features/nodes/types/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/field.ts @@ -54,9 +54,10 @@ const zFieldOutputTemplateBase = zFieldTemplateBase.extend({ fieldKind: z.literal('output'), }); +const zCardinality = z.enum(['SINGLE', 'COLLECTION', 'SINGLE_OR_COLLECTION']); + const zFieldTypeBase = z.object({ - isCollection: z.boolean(), - isCollectionOrScalar: z.boolean(), + cardinality: zCardinality, }); export const zFieldIdentifier = z.object({ @@ -168,6 +169,11 @@ export const isStatefulFieldType = (fieldType: FieldType): fieldType is Stateful (statefulFieldTypeNames as string[]).includes(fieldType.name); const zFieldType = z.union([zStatefulFieldType, zStatelessFieldType]); export type FieldType = z.infer; + +export const isSingle = (fieldType: FieldType): boolean => fieldType.cardinality === zCardinality.enum.SINGLE; +export const isCollection = (fieldType: FieldType): boolean => fieldType.cardinality === zCardinality.enum.COLLECTION; +export const isSingleOrCollection = (fieldType: FieldType): boolean => + fieldType.cardinality === zCardinality.enum.SINGLE_OR_COLLECTION; // #endregion // #region IntegerField From 8062a47d16c713afd3374e9199a72e93f85cb634 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 22:57:12 +1000 Subject: [PATCH 252/442] fix(ui): use new field type cardinality throughout app Update business logic and tests. --- .../nodes/Invocation/fields/FieldHandle.tsx | 6 +- .../hooks/useAnyOrDirectInputFieldNames.ts | 3 +- .../hooks/useConnectionInputFieldNames.ts | 3 +- .../nodes/hooks/usePrettyFieldType.ts | 6 +- .../nodes/store/util/areTypesEqual.test.ts | 73 +++----- .../store/util/getCollectItemType.test.ts | 2 +- .../features/nodes/store/util/testUtils.ts | 93 ++++----- .../util/validateConnectionTypes.test.ts | 176 +++++++++--------- .../store/util/validateConnectionTypes.ts | 47 +++-- .../nodes/util/schema/parseFieldType.test.ts | 83 +++++---- .../nodes/util/schema/parseFieldType.ts | 27 +-- .../features/nodes/util/schema/parseSchema.ts | 10 +- 12 files changed, 239 insertions(+), 290 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx index 033aa61bdf..143dee983f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx @@ -4,7 +4,7 @@ import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdge import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType'; import type { ValidationResult } from 'features/nodes/store/util/validateConnection'; import { HANDLE_TOOLTIP_OPEN_DELAY, MODEL_TYPES } from 'features/nodes/types/constants'; -import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; +import { type FieldInputTemplate, type FieldOutputTemplate, isSingle } from 'features/nodes/types/field'; import type { CSSProperties } from 'react'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -29,11 +29,11 @@ const FieldHandle = (props: FieldHandleProps) => { const isModelType = MODEL_TYPES.some((t) => t === type.name); const color = getFieldColor(type); const s: CSSProperties = { - backgroundColor: type.isCollection || type.isCollectionOrScalar ? colorTokenToCssVar('base.900') : color, + backgroundColor: !isSingle(type) ? colorTokenToCssVar('base.900') : color, position: 'absolute', width: '1rem', height: '1rem', - borderWidth: type.isCollection || type.isCollectionOrScalar ? 4 : 0, + borderWidth: !isSingle(type) ? 4 : 0, borderStyle: 'solid', borderColor: color, borderRadius: isModelType ? 4 : '100%', diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts index 3b7a1b74c1..7fae0de16e 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts @@ -1,5 +1,6 @@ import { EMPTY_ARRAY } from 'app/store/constants'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; +import { isSingleOrCollection } from 'features/nodes/types/field'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; @@ -11,7 +12,7 @@ export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => { const fieldNames = useMemo(() => { const fields = map(template.inputs).filter((field) => { return ( - (['any', 'direct'].includes(field.input) || field.type.isCollectionOrScalar) && + (['any', 'direct'].includes(field.input) || isSingleOrCollection(field.type)) && keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) ); }); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts index d071ac76d2..16ace597c1 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts @@ -1,5 +1,6 @@ import { EMPTY_ARRAY } from 'app/store/constants'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; +import { isSingleOrCollection } from 'features/nodes/types/field'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; @@ -11,7 +12,7 @@ export const useConnectionInputFieldNames = (nodeId: string): string[] => { // get the visible fields const fields = map(template.inputs).filter( (field) => - (field.input === 'connection' && !field.type.isCollectionOrScalar) || + (field.input === 'connection' && !isSingleOrCollection(field.type)) || !keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts b/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts index df4b742842..2600eae078 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts @@ -1,4 +1,4 @@ -import type { FieldType } from 'features/nodes/types/field'; +import { type FieldType, isCollection, isSingleOrCollection } from 'features/nodes/types/field'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,10 +10,10 @@ export const useFieldTypeName = (fieldType?: FieldType): string => { return ''; } const { name } = fieldType; - if (fieldType.isCollection) { + if (isCollection(fieldType)) { return t('nodes.collectionFieldType', { name }); } - if (fieldType.isCollectionOrScalar) { + if (isSingleOrCollection(fieldType)) { return t('nodes.collectionOrScalarFieldType', { name }); } return name; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.test.ts index 7be307d07e..ae9d4f6742 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/areTypesEqual.test.ts @@ -1,99 +1,84 @@ +import type { FieldType } from 'features/nodes/types/field'; import { describe, expect, it } from 'vitest'; import { areTypesEqual } from './areTypesEqual'; describe(areTypesEqual.name, () => { it('should handle equal source and target type', () => { - const sourceType = { + const sourceType: FieldType = { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', originalType: { name: 'Foo', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }; - const targetType = { + const targetType: FieldType = { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', originalType: { name: 'Bar', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }; expect(areTypesEqual(sourceType, targetType)).toBe(true); }); it('should handle equal source type and original target type', () => { - const sourceType = { + const sourceType: FieldType = { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', originalType: { name: 'Foo', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }; - const targetType = { - name: 'Bar', - isCollection: false, - isCollectionOrScalar: false, + const targetType: FieldType = { + name: 'MainModelField', + cardinality: 'SINGLE', originalType: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }; expect(areTypesEqual(sourceType, targetType)).toBe(true); }); it('should handle equal original source type and target type', () => { - const sourceType = { - name: 'Foo', - isCollection: false, - isCollectionOrScalar: false, + const sourceType: FieldType = { + name: 'MainModelField', + cardinality: 'SINGLE', originalType: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }; - const targetType = { + const targetType: FieldType = { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', originalType: { name: 'Bar', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }; expect(areTypesEqual(sourceType, targetType)).toBe(true); }); it('should handle equal original source type and original target type', () => { - const sourceType = { - name: 'Foo', - isCollection: false, - isCollectionOrScalar: false, + const sourceType: FieldType = { + name: 'MainModelField', + cardinality: 'SINGLE', originalType: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }; - const targetType = { - name: 'Bar', - isCollection: false, - isCollectionOrScalar: false, + const targetType: FieldType = { + name: 'LoRAModelField', + cardinality: 'SINGLE', originalType: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }; expect(areTypesEqual(sourceType, targetType)).toBe(true); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts index 935250b697..be0b553d8b 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts @@ -11,7 +11,7 @@ describe(getCollectItemType.name, () => { const n2 = buildNode(collect); const e1 = buildEdge(n1.id, 'value', n2.id, 'item'); const result = getCollectItemType(templates, [n1, n2], [e1], n2.id); - expect(result).toEqual({ name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }); + expect(result).toEqual({ name: 'IntegerField', cardinality: 'SINGLE' }); }); it('should return null if the collect node does not have any connections', () => { const n1 = buildNode(collect); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts index 5155bb14ea..83988d55ea 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts @@ -33,8 +33,7 @@ export const add: InvocationTemplate = { ui_hidden: false, type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, default: 0, }, @@ -48,8 +47,7 @@ export const add: InvocationTemplate = { ui_hidden: false, type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, default: 0, }, @@ -62,8 +60,7 @@ export const add: InvocationTemplate = { description: 'The output integer', type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, @@ -91,8 +88,7 @@ export const sub: InvocationTemplate = { ui_hidden: false, type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, default: 0, }, @@ -106,8 +102,7 @@ export const sub: InvocationTemplate = { ui_hidden: false, type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, default: 0, }, @@ -120,8 +115,7 @@ export const sub: InvocationTemplate = { description: 'The output integer', type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, @@ -150,8 +144,7 @@ export const collect: InvocationTemplate = { ui_type: 'CollectionItemField', type: { name: 'CollectionItemField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }, }, @@ -163,8 +156,7 @@ export const collect: InvocationTemplate = { description: 'The collection of input items', type: { name: 'CollectionField', - isCollection: true, - isCollectionOrScalar: false, + cardinality: 'COLLECTION', }, ui_hidden: false, ui_type: 'CollectionField', @@ -193,12 +185,11 @@ const scheduler: InvocationTemplate = { ui_type: 'SchedulerField', type: { name: 'SchedulerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', + originalType: { name: 'EnumField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }, default: 'euler', @@ -212,12 +203,11 @@ const scheduler: InvocationTemplate = { description: 'Scheduler to use during inference', type: { name: 'SchedulerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', + originalType: { name: 'EnumField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }, ui_hidden: false, @@ -248,12 +238,11 @@ export const main_model_loader: InvocationTemplate = { ui_type: 'MainModelField', type: { name: 'MainModelField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', + originalType: { name: 'ModelIdentifierField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }, }, @@ -266,8 +255,7 @@ export const main_model_loader: InvocationTemplate = { description: 'VAE', type: { name: 'VAEField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, @@ -278,8 +266,7 @@ export const main_model_loader: InvocationTemplate = { description: 'CLIP (tokenizer, text encoder, LoRAs) and skipped layer count', type: { name: 'CLIPField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, @@ -290,8 +277,7 @@ export const main_model_loader: InvocationTemplate = { description: 'UNet (scheduler, LoRAs)', type: { name: 'UNetField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, @@ -319,8 +305,7 @@ export const img_resize: InvocationTemplate = { ui_hidden: false, type: { name: 'BoardField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }, metadata: { @@ -333,8 +318,7 @@ export const img_resize: InvocationTemplate = { ui_hidden: false, type: { name: 'MetadataField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }, image: { @@ -347,8 +331,7 @@ export const img_resize: InvocationTemplate = { ui_hidden: false, type: { name: 'ImageField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, }, width: { @@ -361,8 +344,7 @@ export const img_resize: InvocationTemplate = { ui_hidden: false, type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, default: 512, exclusiveMinimum: 0, @@ -377,8 +359,7 @@ export const img_resize: InvocationTemplate = { ui_hidden: false, type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, default: 512, exclusiveMinimum: 0, @@ -393,8 +374,7 @@ export const img_resize: InvocationTemplate = { ui_hidden: false, type: { name: 'EnumField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, options: ['nearest', 'box', 'bilinear', 'hamming', 'bicubic', 'lanczos'], default: 'bicubic', @@ -408,8 +388,7 @@ export const img_resize: InvocationTemplate = { description: 'The output image', type: { name: 'ImageField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, @@ -420,8 +399,7 @@ export const img_resize: InvocationTemplate = { description: 'The width of the image in pixels', type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, @@ -432,8 +410,7 @@ export const img_resize: InvocationTemplate = { description: 'The height of the image in pixels', type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, @@ -462,8 +439,7 @@ const iterate: InvocationTemplate = { ui_type: 'CollectionField', type: { name: 'CollectionField', - isCollection: true, - isCollectionOrScalar: false, + cardinality: 'COLLECTION', }, }, }, @@ -475,8 +451,7 @@ const iterate: InvocationTemplate = { description: 'The item being iterated over', type: { name: 'CollectionItemField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, ui_type: 'CollectionItemField', @@ -488,8 +463,7 @@ const iterate: InvocationTemplate = { description: 'The index of the item', type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, @@ -500,8 +474,7 @@ const iterate: InvocationTemplate = { description: 'The total number of items', type: { name: 'IntegerField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }, ui_hidden: false, }, diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts index 10344dd349..56d4cfe70a 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts @@ -4,148 +4,148 @@ import { validateConnectionTypes } from './validateConnectionTypes'; describe(validateConnectionTypes.name, () => { describe('generic cases', () => { - it('should accept Scalar to Scalar of same type', () => { + it('should accept SINGLE to SINGLE of same type', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, - { name: 'FooField', isCollection: false, isCollectionOrScalar: false } + { name: 'FooField', cardinality: 'SINGLE' }, + { name: 'FooField', cardinality: 'SINGLE' } ); expect(r).toBe(true); }); - it('should accept Collection to Collection of same type', () => { + it('should accept COLLECTION to COLLECTION of same type', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: true, isCollectionOrScalar: false }, - { name: 'FooField', isCollection: true, isCollectionOrScalar: false } + { name: 'FooField', cardinality: 'COLLECTION' }, + { name: 'FooField', cardinality: 'COLLECTION' } ); expect(r).toBe(true); }); - it('should accept Scalar to CollectionOrScalar of same type', () => { + it('should accept SINGLE to SINGLE_OR_COLLECTION of same type', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, - { name: 'FooField', isCollection: false, isCollectionOrScalar: true } + { name: 'FooField', cardinality: 'SINGLE' }, + { name: 'FooField', cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); - it('should accept Collection to CollectionOrScalar of same type', () => { + it('should accept COLLECTION to SINGLE_OR_COLLECTION of same type', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: true, isCollectionOrScalar: false }, - { name: 'FooField', isCollection: false, isCollectionOrScalar: true } + { name: 'FooField', cardinality: 'COLLECTION' }, + { name: 'FooField', cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); - it('should reject Collection to Scalar of same type', () => { + it('should reject COLLECTION to SINGLE of same type', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: true, isCollectionOrScalar: false }, - { name: 'FooField', isCollection: false, isCollectionOrScalar: false } + { name: 'FooField', cardinality: 'COLLECTION' }, + { name: 'FooField', cardinality: 'SINGLE' } ); expect(r).toBe(false); }); - it('should reject CollectionOrScalar to Scalar of same type', () => { + it('should reject SINGLE_OR_COLLECTION to SINGLE of same type', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: false, isCollectionOrScalar: true }, - { name: 'FooField', isCollection: false, isCollectionOrScalar: false } + { name: 'FooField', cardinality: 'SINGLE_OR_COLLECTION' }, + { name: 'FooField', cardinality: 'SINGLE' } ); expect(r).toBe(false); }); it('should reject mismatched types', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, - { name: 'BarField', isCollection: false, isCollectionOrScalar: false } + { name: 'FooField', cardinality: 'SINGLE' }, + { name: 'BarField', cardinality: 'SINGLE' } ); expect(r).toBe(false); }); }); describe('special cases', () => { - it('should reject a collection input to a collection input', () => { + it('should reject a COLLECTION input to a COLLECTION input', () => { const r = validateConnectionTypes( - { name: 'CollectionField', isCollection: true, isCollectionOrScalar: false }, - { name: 'CollectionField', isCollection: true, isCollectionOrScalar: false } + { name: 'CollectionField', cardinality: 'COLLECTION' }, + { name: 'CollectionField', cardinality: 'COLLECTION' } ); expect(r).toBe(false); }); it('should accept equal types', () => { const r = validateConnectionTypes( - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }, - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false } + { name: 'IntegerField', cardinality: 'SINGLE' }, + { name: 'IntegerField', cardinality: 'SINGLE' } ); expect(r).toBe(true); }); describe('CollectionItemField', () => { - it('should accept CollectionItemField to any Scalar target', () => { + it('should accept CollectionItemField to any SINGLE target', () => { const r = validateConnectionTypes( - { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false }, - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false } + { name: 'CollectionItemField', cardinality: 'SINGLE' }, + { name: 'IntegerField', cardinality: 'SINGLE' } ); expect(r).toBe(true); }); - it('should accept CollectionItemField to any CollectionOrScalar target', () => { + it('should accept CollectionItemField to any SINGLE_OR_COLLECTION target', () => { const r = validateConnectionTypes( - { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false }, - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + { name: 'CollectionItemField', cardinality: 'SINGLE' }, + { name: 'IntegerField', cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); - it('should accept any non-Collection to CollectionItemField', () => { + it('should accept any SINGLE to CollectionItemField', () => { const r = validateConnectionTypes( - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }, - { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false } + { name: 'IntegerField', cardinality: 'SINGLE' }, + { name: 'CollectionItemField', cardinality: 'SINGLE' } ); expect(r).toBe(true); }); - it('should reject any Collection to CollectionItemField', () => { + it('should reject any COLLECTION to CollectionItemField', () => { const r = validateConnectionTypes( - { name: 'IntegerField', isCollection: true, isCollectionOrScalar: false }, - { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false } + { name: 'IntegerField', cardinality: 'COLLECTION' }, + { name: 'CollectionItemField', cardinality: 'SINGLE' } ); expect(r).toBe(false); }); - it('should reject any CollectionOrScalar to CollectionItemField', () => { + it('should reject any SINGLE_OR_COLLECTION to CollectionItemField', () => { const r = validateConnectionTypes( - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true }, - { name: 'CollectionItemField', isCollection: false, isCollectionOrScalar: false } + { name: 'IntegerField', cardinality: 'SINGLE_OR_COLLECTION' }, + { name: 'CollectionItemField', cardinality: 'SINGLE' } ); expect(r).toBe(false); }); }); - describe('CollectionOrScalar', () => { - it('should accept any Scalar of same type to CollectionOrScalar', () => { + describe('SINGLE_OR_COLLECTION', () => { + it('should accept any SINGLE of same type to SINGLE_OR_COLLECTION', () => { const r = validateConnectionTypes( - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }, - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + { name: 'IntegerField', cardinality: 'SINGLE' }, + { name: 'IntegerField', cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); - it('should accept any Collection of same type to CollectionOrScalar', () => { + it('should accept any COLLECTION of same type to SINGLE_OR_COLLECTION', () => { const r = validateConnectionTypes( - { name: 'IntegerField', isCollection: true, isCollectionOrScalar: false }, - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + { name: 'IntegerField', cardinality: 'COLLECTION' }, + { name: 'IntegerField', cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); - it('should accept any CollectionOrScalar of same type to CollectionOrScalar', () => { + it('should accept any SINGLE_OR_COLLECTION of same type to SINGLE_OR_COLLECTION', () => { const r = validateConnectionTypes( - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true }, - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + { name: 'IntegerField', cardinality: 'SINGLE_OR_COLLECTION' }, + { name: 'IntegerField', cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); }); describe('CollectionField', () => { - it('should accept any CollectionField to any Collection type', () => { + it('should accept any CollectionField to any COLLECTION type', () => { const r = validateConnectionTypes( - { name: 'CollectionField', isCollection: false, isCollectionOrScalar: false }, - { name: 'IntegerField', isCollection: true, isCollectionOrScalar: false } + { name: 'CollectionField', cardinality: 'SINGLE' }, + { name: 'IntegerField', cardinality: 'COLLECTION' } ); expect(r).toBe(true); }); - it('should accept any CollectionField to any CollectionOrScalar type', () => { + it('should accept any CollectionField to any SINGLE_OR_COLLECTION type', () => { const r = validateConnectionTypes( - { name: 'CollectionField', isCollection: false, isCollectionOrScalar: false }, - { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true } + { name: 'CollectionField', cardinality: 'SINGLE' }, + { name: 'IntegerField', cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); @@ -158,62 +158,62 @@ describe(validateConnectionTypes.name, () => { { t1: 'IntegerField', t2: 'StringField' }, { t1: 'FloatField', t2: 'StringField' }, ]; - it.each(typePairs)('should accept Scalar $t1 to Scalar $t2', ({ t1, t2 }: TypePair) => { + it.each(typePairs)('should accept SINGLE $t1 to SINGLE $t2', ({ t1, t2 }: TypePair) => { + const r = validateConnectionTypes({ name: t1, cardinality: 'SINGLE' }, { name: t2, cardinality: 'SINGLE' }); + expect(r).toBe(true); + }); + it.each(typePairs)('should accept SINGLE $t1 to SINGLE_OR_COLLECTION $t2', ({ t1, t2 }: TypePair) => { const r = validateConnectionTypes( - { name: t1, isCollection: false, isCollectionOrScalar: false }, - { name: t2, isCollection: false, isCollectionOrScalar: false } + { name: t1, cardinality: 'SINGLE' }, + { name: t2, cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); - it.each(typePairs)('should accept Scalar $t1 to CollectionOrScalar $t2', ({ t1, t2 }: TypePair) => { + it.each(typePairs)('should accept COLLECTION $t1 to COLLECTION $t2', ({ t1, t2 }: TypePair) => { const r = validateConnectionTypes( - { name: t1, isCollection: false, isCollectionOrScalar: false }, - { name: t2, isCollection: false, isCollectionOrScalar: true } + { name: t1, cardinality: 'COLLECTION' }, + { name: t2, cardinality: 'COLLECTION' } ); expect(r).toBe(true); }); - it.each(typePairs)('should accept Collection $t1 to Collection $t2', ({ t1, t2 }: TypePair) => { + it.each(typePairs)('should accept COLLECTION $t1 to SINGLE_OR_COLLECTION $t2', ({ t1, t2 }: TypePair) => { const r = validateConnectionTypes( - { name: t1, isCollection: true, isCollectionOrScalar: false }, - { name: t2, isCollection: true, isCollectionOrScalar: false } - ); - expect(r).toBe(true); - }); - it.each(typePairs)('should accept Collection $t1 to CollectionOrScalar $t2', ({ t1, t2 }: TypePair) => { - const r = validateConnectionTypes( - { name: t1, isCollection: true, isCollectionOrScalar: false }, - { name: t2, isCollection: false, isCollectionOrScalar: true } - ); - expect(r).toBe(true); - }); - it.each(typePairs)('should accept CollectionOrScalar $t1 to CollectionOrScalar $t2', ({ t1, t2 }: TypePair) => { - const r = validateConnectionTypes( - { name: t1, isCollection: false, isCollectionOrScalar: true }, - { name: t2, isCollection: false, isCollectionOrScalar: true } + { name: t1, cardinality: 'COLLECTION' }, + { name: t2, cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); + it.each(typePairs)( + 'should accept SINGLE_OR_COLLECTION $t1 to SINGLE_OR_COLLECTION $t2', + ({ t1, t2 }: TypePair) => { + const r = validateConnectionTypes( + { name: t1, cardinality: 'SINGLE_OR_COLLECTION' }, + { name: t2, cardinality: 'SINGLE_OR_COLLECTION' } + ); + expect(r).toBe(true); + } + ); }); describe('AnyField', () => { - it('should accept any Scalar type to AnyField', () => { + it('should accept any SINGLE type to AnyField', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, - { name: 'AnyField', isCollection: false, isCollectionOrScalar: false } + { name: 'FooField', cardinality: 'SINGLE' }, + { name: 'AnyField', cardinality: 'SINGLE' } ); expect(r).toBe(true); }); - it('should accept any Collection type to AnyField', () => { + it('should accept any COLLECTION type to AnyField', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, - { name: 'AnyField', isCollection: true, isCollectionOrScalar: false } + { name: 'FooField', cardinality: 'SINGLE' }, + { name: 'AnyField', cardinality: 'COLLECTION' } ); expect(r).toBe(true); }); - it('should accept any CollectionOrScalar type to AnyField', () => { + it('should accept any SINGLE_OR_COLLECTION type to AnyField', () => { const r = validateConnectionTypes( - { name: 'FooField', isCollection: false, isCollectionOrScalar: false }, - { name: 'AnyField', isCollection: false, isCollectionOrScalar: true } + { name: 'FooField', cardinality: 'SINGLE' }, + { name: 'AnyField', cardinality: 'SINGLE_OR_COLLECTION' } ); expect(r).toBe(true); }); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts index 778b33a7b1..a71ff513aa 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts @@ -1,5 +1,5 @@ import { areTypesEqual } from 'features/nodes/store/util/areTypesEqual'; -import type { FieldType } from 'features/nodes/types/field'; +import { type FieldType, isCollection, isSingle, isSingleOrCollection } from 'features/nodes/types/field'; /** * Validates that the source and target types are compatible for a connection. @@ -27,38 +27,37 @@ export const validateConnectionTypes = (sourceType: FieldType, targetType: Field * - Generic Collection can connect to any other Collection or CollectionOrScalar * - Any Collection can connect to a Generic Collection */ - const isCollectionItemToNonCollection = sourceType.name === 'CollectionItemField' && !targetType.isCollection; + const isCollectionItemToNonCollection = sourceType.name === 'CollectionItemField' && !isCollection(targetType); - const isNonCollectionToCollectionItem = - targetType.name === 'CollectionItemField' && !sourceType.isCollection && !sourceType.isCollectionOrScalar; + const isNonCollectionToCollectionItem = isSingle(sourceType) && targetType.name === 'CollectionItemField'; - const isAnythingToCollectionOrScalarOfSameBaseType = - targetType.isCollectionOrScalar && sourceType.name === targetType.name; + const isAnythingToSingleOrCollectionOfSameBaseType = + isSingleOrCollection(targetType) && sourceType.name === targetType.name; - const isGenericCollectionToAnyCollectionOrCollectionOrScalar = - sourceType.name === 'CollectionField' && (targetType.isCollection || targetType.isCollectionOrScalar); + const isGenericCollectionToAnyCollectionOrSingleOrCollection = + sourceType.name === 'CollectionField' && !isSingle(targetType); - const isCollectionToGenericCollection = targetType.name === 'CollectionField' && sourceType.isCollection; + const isCollectionToGenericCollection = targetType.name === 'CollectionField' && isCollection(sourceType); - const isSourceScalar = !sourceType.isCollection && !sourceType.isCollectionOrScalar; - const isTargetScalar = !targetType.isCollection && !targetType.isCollectionOrScalar; - const isScalarToScalar = isSourceScalar && isTargetScalar; - const isScalarToCollectionOrScalar = isSourceScalar && targetType.isCollectionOrScalar; - const isCollectionToCollection = sourceType.isCollection && targetType.isCollection; - const isCollectionToCollectionOrScalar = sourceType.isCollection && targetType.isCollectionOrScalar; - const isCollectionOrScalarToCollectionOrScalar = sourceType.isCollectionOrScalar && targetType.isCollectionOrScalar; - const isPluralityMatch = - isScalarToScalar || + const isSourceSingle = isSingle(sourceType); + const isTargetSingle = isSingle(targetType); + const isSingleToSingle = isSourceSingle && isTargetSingle; + const isSingleToSingleOrCollection = isSourceSingle && isSingleOrCollection(targetType); + const isCollectionToCollection = isCollection(sourceType) && isCollection(targetType); + const isCollectionToSingleOrCollection = isCollection(sourceType) && isSingleOrCollection(targetType); + const isSingleOrCollectionToSingleOrCollection = isSingleOrCollection(sourceType) && isSingleOrCollection(targetType); + const doesCardinalityMatch = + isSingleToSingle || isCollectionToCollection || - isCollectionToCollectionOrScalar || - isCollectionOrScalarToCollectionOrScalar || - isScalarToCollectionOrScalar; + isCollectionToSingleOrCollection || + isSingleOrCollectionToSingleOrCollection || + isSingleToSingleOrCollection; const isIntToFloat = sourceType.name === 'IntegerField' && targetType.name === 'FloatField'; const isIntToString = sourceType.name === 'IntegerField' && targetType.name === 'StringField'; const isFloatToString = sourceType.name === 'FloatField' && targetType.name === 'StringField'; - const isSubTypeMatch = isPluralityMatch && (isIntToFloat || isIntToString || isFloatToString); + const isSubTypeMatch = doesCardinalityMatch && (isIntToFloat || isIntToString || isFloatToString); const isTargetAnyType = targetType.name === 'AnyField'; @@ -66,8 +65,8 @@ export const validateConnectionTypes = (sourceType: FieldType, targetType: Field return ( isCollectionItemToNonCollection || isNonCollectionToCollectionItem || - isAnythingToCollectionOrScalarOfSameBaseType || - isGenericCollectionToAnyCollectionOrCollectionOrScalar || + isAnythingToSingleOrCollectionOfSameBaseType || + isGenericCollectionToAnyCollectionOrSingleOrCollection || isCollectionToGenericCollection || isSubTypeMatch || isTargetAnyType diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts index cc12b45aa6..3d3aff3cd6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts @@ -4,6 +4,7 @@ import { UnsupportedPrimitiveTypeError, UnsupportedUnionError, } from 'features/nodes/types/error'; +import type { FieldType } from 'features/nodes/types/field'; import type { InvocationFieldSchema, OpenAPIV3_1SchemaOrRef } from 'features/nodes/types/openapi'; import { parseFieldType, refObjectToSchemaName } from 'features/nodes/util/schema/parseFieldType'; import { describe, expect, it } from 'vitest'; @@ -11,52 +12,52 @@ import { describe, expect, it } from 'vitest'; type ParseFieldTypeTestCase = { name: string; schema: OpenAPIV3_1SchemaOrRef | InvocationFieldSchema; - expected: { name: string; isCollection: boolean; isCollectionOrScalar: boolean }; + expected: FieldType; }; const primitiveTypes: ParseFieldTypeTestCase[] = [ { - name: 'Scalar IntegerField', + name: 'SINGLE IntegerField', schema: { type: 'integer' }, - expected: { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'IntegerField', cardinality: 'SINGLE' }, }, { - name: 'Scalar FloatField', + name: 'SINGLE FloatField', schema: { type: 'number' }, - expected: { name: 'FloatField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'FloatField', cardinality: 'SINGLE' }, }, { - name: 'Scalar StringField', + name: 'SINGLE StringField', schema: { type: 'string' }, - expected: { name: 'StringField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'StringField', cardinality: 'SINGLE' }, }, { - name: 'Scalar BooleanField', + name: 'SINGLE BooleanField', schema: { type: 'boolean' }, - expected: { name: 'BooleanField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'BooleanField', cardinality: 'SINGLE' }, }, { - name: 'Collection IntegerField', + name: 'COLLECTION IntegerField', schema: { items: { type: 'integer' }, type: 'array' }, - expected: { name: 'IntegerField', isCollection: true, isCollectionOrScalar: false }, + expected: { name: 'IntegerField', cardinality: 'COLLECTION' }, }, { - name: 'Collection FloatField', + name: 'COLLECTION FloatField', schema: { items: { type: 'number' }, type: 'array' }, - expected: { name: 'FloatField', isCollection: true, isCollectionOrScalar: false }, + expected: { name: 'FloatField', cardinality: 'COLLECTION' }, }, { - name: 'Collection StringField', + name: 'COLLECTION StringField', schema: { items: { type: 'string' }, type: 'array' }, - expected: { name: 'StringField', isCollection: true, isCollectionOrScalar: false }, + expected: { name: 'StringField', cardinality: 'COLLECTION' }, }, { - name: 'Collection BooleanField', + name: 'COLLECTION BooleanField', schema: { items: { type: 'boolean' }, type: 'array' }, - expected: { name: 'BooleanField', isCollection: true, isCollectionOrScalar: false }, + expected: { name: 'BooleanField', cardinality: 'COLLECTION' }, }, { - name: 'CollectionOrScalar IntegerField', + name: 'SINGLE_OR_COLLECTION IntegerField', schema: { anyOf: [ { @@ -70,10 +71,10 @@ const primitiveTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true }, + expected: { name: 'IntegerField', cardinality: 'SINGLE_OR_COLLECTION' }, }, { - name: 'CollectionOrScalar FloatField', + name: 'SINGLE_OR_COLLECTION FloatField', schema: { anyOf: [ { @@ -87,10 +88,10 @@ const primitiveTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'FloatField', isCollection: false, isCollectionOrScalar: true }, + expected: { name: 'FloatField', cardinality: 'SINGLE_OR_COLLECTION' }, }, { - name: 'CollectionOrScalar StringField', + name: 'SINGLE_OR_COLLECTION StringField', schema: { anyOf: [ { @@ -104,10 +105,10 @@ const primitiveTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'StringField', isCollection: false, isCollectionOrScalar: true }, + expected: { name: 'StringField', cardinality: 'SINGLE_OR_COLLECTION' }, }, { - name: 'CollectionOrScalar BooleanField', + name: 'SINGLE_OR_COLLECTION BooleanField', schema: { anyOf: [ { @@ -121,13 +122,13 @@ const primitiveTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'BooleanField', isCollection: false, isCollectionOrScalar: true }, + expected: { name: 'BooleanField', cardinality: 'SINGLE_OR_COLLECTION' }, }, ]; const complexTypes: ParseFieldTypeTestCase[] = [ { - name: 'Scalar ConditioningField', + name: 'SINGLE ConditioningField', schema: { allOf: [ { @@ -135,10 +136,10 @@ const complexTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'ConditioningField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'ConditioningField', cardinality: 'SINGLE' }, }, { - name: 'Nullable Scalar ConditioningField', + name: 'Nullable SINGLE ConditioningField', schema: { anyOf: [ { @@ -149,10 +150,10 @@ const complexTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'ConditioningField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'ConditioningField', cardinality: 'SINGLE' }, }, { - name: 'Collection ConditioningField', + name: 'COLLECTION ConditioningField', schema: { anyOf: [ { @@ -163,7 +164,7 @@ const complexTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'ConditioningField', isCollection: true, isCollectionOrScalar: false }, + expected: { name: 'ConditioningField', cardinality: 'COLLECTION' }, }, { name: 'Nullable Collection ConditioningField', @@ -180,10 +181,10 @@ const complexTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'ConditioningField', isCollection: true, isCollectionOrScalar: false }, + expected: { name: 'ConditioningField', cardinality: 'COLLECTION' }, }, { - name: 'CollectionOrScalar ConditioningField', + name: 'SINGLE_OR_COLLECTION ConditioningField', schema: { anyOf: [ { @@ -197,10 +198,10 @@ const complexTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'ConditioningField', isCollection: false, isCollectionOrScalar: true }, + expected: { name: 'ConditioningField', cardinality: 'SINGLE_OR_COLLECTION' }, }, { - name: 'Nullable CollectionOrScalar ConditioningField', + name: 'Nullable SINGLE_OR_COLLECTION ConditioningField', schema: { anyOf: [ { @@ -217,7 +218,7 @@ const complexTypes: ParseFieldTypeTestCase[] = [ }, ], }, - expected: { name: 'ConditioningField', isCollection: false, isCollectionOrScalar: true }, + expected: { name: 'ConditioningField', cardinality: 'SINGLE_OR_COLLECTION' }, }, ]; @@ -228,14 +229,14 @@ const specialCases: ParseFieldTypeTestCase[] = [ type: 'string', enum: ['large', 'base', 'small'], }, - expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'EnumField', cardinality: 'SINGLE' }, }, { name: 'String EnumField with one value', schema: { const: 'Some Value', }, - expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'EnumField', cardinality: 'SINGLE' }, }, { name: 'Explicit ui_type (SchedulerField)', @@ -244,7 +245,7 @@ const specialCases: ParseFieldTypeTestCase[] = [ enum: ['ddim', 'ddpm', 'deis'], ui_type: 'SchedulerField', }, - expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'EnumField', cardinality: 'SINGLE' }, }, { name: 'Explicit ui_type (AnyField)', @@ -253,7 +254,7 @@ const specialCases: ParseFieldTypeTestCase[] = [ enum: ['ddim', 'ddpm', 'deis'], ui_type: 'AnyField', }, - expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'EnumField', cardinality: 'SINGLE' }, }, { name: 'Explicit ui_type (CollectionField)', @@ -262,7 +263,7 @@ const specialCases: ParseFieldTypeTestCase[] = [ enum: ['ddim', 'ddpm', 'deis'], ui_type: 'CollectionField', }, - expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, + expected: { name: 'EnumField', cardinality: 'SINGLE' }, }, ]; diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts index 6f6ecaa5bb..ea9bf5bce4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts @@ -48,8 +48,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType // Fields with a single const value are defined as `Literal["value"]` in the pydantic schema - it's actually an enum return { name: 'EnumField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }; } if (!schemaObject.type) { @@ -65,8 +64,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType } return { name, - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }; } } else if (schemaObject.anyOf) { @@ -89,8 +87,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType return { name, - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }; } else if (isSchemaObject(filteredAnyOf[0])) { return parseFieldType(filteredAnyOf[0]); @@ -143,8 +140,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType if (firstType && firstType === secondType) { return { name: OPENAPI_TO_FIELD_TYPE_MAP[firstType] ?? firstType, - isCollection: false, - isCollectionOrScalar: true, // <-- don't forget, CollectionOrScalar type! + cardinality: 'SINGLE_OR_COLLECTION', }; } @@ -158,8 +154,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType } else if (schemaObject.enum) { return { name: 'EnumField', - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }; } else if (schemaObject.type) { if (schemaObject.type === 'array') { @@ -185,8 +180,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType } return { name, - isCollection: true, // <-- don't forget, collection! - isCollectionOrScalar: false, + cardinality: 'COLLECTION', }; } @@ -197,8 +191,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType } return { name, - isCollection: true, // <-- don't forget, collection! - isCollectionOrScalar: false, + cardinality: 'COLLECTION', }; } else if (!isArray(schemaObject.type)) { // This is an OpenAPI primitive - 'null', 'object', 'array', 'integer', 'number', 'string', 'boolean' @@ -213,8 +206,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType } return { name, - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }; } } @@ -225,8 +217,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType } return { name, - isCollection: false, - isCollectionOrScalar: false, + cardinality: 'SINGLE', }; } throw new FieldParseError(t('nodes.unableToParseFieldType')); diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts index f9b93382f9..3981b759db 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts @@ -100,11 +100,10 @@ export const parseSchema = ( return inputsAccumulator; } - const fieldTypeOverride = property.ui_type + const fieldTypeOverride: FieldType | null = property.ui_type ? { name: property.ui_type, - isCollection: isCollectionFieldType(property.ui_type), - isCollectionOrScalar: false, + cardinality: isCollectionFieldType(property.ui_type) ? 'COLLECTION' : 'SINGLE', } : null; @@ -178,11 +177,10 @@ export const parseSchema = ( return outputsAccumulator; } - const fieldTypeOverride = property.ui_type + const fieldTypeOverride: FieldType | null = property.ui_type ? { name: property.ui_type, - isCollection: isCollectionFieldType(property.ui_type), - isCollectionOrScalar: false, + cardinality: isCollectionFieldType(property.ui_type) ? 'COLLECTION' : 'SINGLE', } : null; From 9e55ef3d4bbd8da2c787f3cb5af9162580a53a2c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 22:56:35 +1000 Subject: [PATCH 253/442] fix(ui): workflow migration field type At some point, I made a mistake and imported the wrong types to some files for the old v1 and v2 workflow schema migration data. The relevant zod schemas and inferred types have been restored. This change doesn't alter runtime behaviour. Only type annotations. --- .../features/nodes/types/v1/fieldTypeMap.ts | 4 ++-- .../web/src/features/nodes/types/v2/field.ts | 22 +++++++++++++++++++ .../nodes/util/workflow/migrations.ts | 6 ++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/types/v1/fieldTypeMap.ts b/invokeai/frontend/web/src/features/nodes/types/v1/fieldTypeMap.ts index 79946cd8d5..f1d4e61300 100644 --- a/invokeai/frontend/web/src/features/nodes/types/v1/fieldTypeMap.ts +++ b/invokeai/frontend/web/src/features/nodes/types/v1/fieldTypeMap.ts @@ -1,4 +1,4 @@ -import type { FieldType, StatefulFieldType } from 'features/nodes/types/field'; +import type { StatefulFieldType, StatelessFieldType } from 'features/nodes/types/v2/field'; import type { FieldTypeV1 } from './workflowV1'; @@ -165,7 +165,7 @@ const FIELD_TYPE_V1_TO_STATEFUL_FIELD_TYPE_V2: { * Thus, this object was manually edited to ensure it is correct. */ const FIELD_TYPE_V1_TO_STATELESS_FIELD_TYPE_V2: { - [key in FieldTypeV1]?: FieldType; + [key in FieldTypeV1]?: StatelessFieldType; } = { Any: { name: 'AnyField', isCollection: false, isCollectionOrScalar: false }, ClipField: { diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/field.ts b/invokeai/frontend/web/src/features/nodes/types/v2/field.ts index 1e464fa76d..4b680d1de3 100644 --- a/invokeai/frontend/web/src/features/nodes/types/v2/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/v2/field.ts @@ -316,6 +316,7 @@ const zSchedulerFieldOutputInstance = zFieldOutputInstanceBase.extend({ const zStatelessFieldType = zFieldTypeBase.extend({ name: z.string().min(1), // stateless --> we accept the field's name as the type }); +export type StatelessFieldType = z.infer; const zStatelessFieldValue = z.undefined().catch(undefined); // stateless --> no value, but making this z.never() introduces a lot of extra TS fanagling const zStatelessFieldInputInstance = zFieldInputInstanceBase.extend({ type: zStatelessFieldType, @@ -327,6 +328,27 @@ const zStatelessFieldOutputInstance = zFieldOutputInstanceBase.extend({ // #endregion +const zStatefulFieldType = z.union([ + zIntegerFieldType, + zFloatFieldType, + zStringFieldType, + zBooleanFieldType, + zEnumFieldType, + zImageFieldType, + zBoardFieldType, + zMainModelFieldType, + zSDXLMainModelFieldType, + zSDXLRefinerModelFieldType, + zVAEModelFieldType, + zLoRAModelFieldType, + zControlNetModelFieldType, + zIPAdapterModelFieldType, + zT2IAdapterModelFieldType, + zColorFieldType, + zSchedulerFieldType, +]); +export type StatefulFieldType = z.infer; + /** * Here we define the main field unions: * - FieldType diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts index 32369b88c9..c7bcbf0953 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts @@ -1,12 +1,12 @@ import { deepClone } from 'common/util/deepClone'; import { $templates } from 'features/nodes/store/nodesSlice'; import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error'; -import type { FieldType } from 'features/nodes/types/field'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { zSemVer } from 'features/nodes/types/semver'; import { FIELD_TYPE_V1_TO_FIELD_TYPE_V2_MAPPING } from 'features/nodes/types/v1/fieldTypeMap'; import type { WorkflowV1 } from 'features/nodes/types/v1/workflowV1'; import { zWorkflowV1 } from 'features/nodes/types/v1/workflowV1'; +import type { StatelessFieldType } from 'features/nodes/types/v2/field'; import type { WorkflowV2 } from 'features/nodes/types/v2/workflow'; import { zWorkflowV2 } from 'features/nodes/types/v2/workflow'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; @@ -43,14 +43,14 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => { if (!newFieldType) { throw new WorkflowMigrationError(t('nodes.unknownFieldType', { type: input.type })); } - (input.type as unknown as FieldType) = newFieldType; + (input.type as unknown as StatelessFieldType) = newFieldType; }); forEach(node.data.outputs, (output) => { const newFieldType = FIELD_TYPE_V1_TO_FIELD_TYPE_V2_MAPPING[output.type]; if (!newFieldType) { throw new WorkflowMigrationError(t('nodes.unknownFieldType', { type: output.type })); } - (output.type as unknown as FieldType) = newFieldType; + (output.type as unknown as StatelessFieldType) = newFieldType; }); // Add node pack const invocationTemplate = templates[node.data.type]; From e88b807a13ab6f664c56f97a10d0c97467e2a103 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 23:22:07 +1000 Subject: [PATCH 254/442] docs(ui): update field type docs & comments --- docs/contributing/frontend/WORKFLOWS.md | 21 +++++++++---------- .../store/util/validateConnectionTypes.ts | 10 ++++----- .../canvas/addControlNetToLinearGraph.ts | 2 +- .../graph/canvas/addIPAdapterToLinearGraph.ts | 2 +- .../canvas/addT2IAdapterToLinearGraph.ts | 2 +- .../nodes/util/schema/parseFieldType.ts | 2 +- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/contributing/frontend/WORKFLOWS.md b/docs/contributing/frontend/WORKFLOWS.md index e71d797b8a..533419e070 100644 --- a/docs/contributing/frontend/WORKFLOWS.md +++ b/docs/contributing/frontend/WORKFLOWS.md @@ -117,13 +117,13 @@ Stateless fields do not store their value in the node, so their field instances "Custom" fields will always be treated as stateless fields. -##### Collection and Scalar Fields +##### Single and Collection Fields -Field types have a name and two flags which may identify it as a **collection** or **collection or scalar** field. +Field types have a name and cardinality property which may identify it as a **SINGLE**, **COLLECTION** or **SINGLE_OR_COLLECTION** field. -If a field is annotated in python as a list, its field type is parsed and flagged as a **collection** type (e.g. `list[int]`). - -If it is annotated as a union of a type and list, the type will be flagged as a **collection or scalar** type (e.g. `Union[int, list[int]]`). Fields may not be unions of different types (e.g. `Union[int, list[str]]` and `Union[int, str]` are not allowed). +- If a field is annotated in python as a singular value or class, its field type is parsed as a **SINGLE** type (e.g. `int`, `ImageField`, `str`). +- If a field is annotated in python as a list, its field type is parsed as a **COLLECTION** type (e.g. `list[int]`). +- If it is annotated as a union of a type and list, the type will be parsed as a **SINGLE_OR_COLLECTION** type (e.g. `Union[int, list[int]]`). Fields may not be unions of different types (e.g. `Union[int, list[str]]` and `Union[int, str]` are not allowed). ## Implementation @@ -173,8 +173,7 @@ Field types are represented as structured objects: ```ts type FieldType = { name: string; - isCollection: boolean; - isCollectionOrScalar: boolean; + cardinality: 'SINGLE' | 'COLLECTION' | 'SINGLE_OR_COLLECTION'; }; ``` @@ -186,7 +185,7 @@ There are 4 general cases for field type parsing. When a field is annotated as a primitive values (e.g. `int`, `str`, `float`), the field type parsing is fairly straightforward. The field is represented by a simple OpenAPI **schema object**, which has a `type` property. -We create a field type name from this `type` string (e.g. `string` -> `StringField`). +We create a field type name from this `type` string (e.g. `string` -> `StringField`). The cardinality is `"SINGLE"`. ##### Complex Types @@ -200,13 +199,13 @@ We need to **dereference** the schema to pull these out. Dereferencing may requi When a field is annotated as a list of a single type, the schema object has an `items` property. They may be a schema object or reference object and must be parsed to determine the item type. -We use the item type for field type name, adding `isCollection: true` to the field type. +We use the item type for field type name. The cardinality is `"COLLECTION"`. -##### Collection or Scalar Types +##### Single or Collection Types When a field is annotated as a union of a type and list of that type, the schema object has an `anyOf` property, which holds a list of valid types for the union. -After verifying that the union has two members (a type and list of the same type), we use the type for field type name, adding `isCollectionOrScalar: true` to the field type. +After verifying that the union has two members (a type and list of the same type), we use the type for field type name, with cardinality `"SINGLE_OR_COLLECTION"`. ##### Optional Fields diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts index a71ff513aa..d5dee6dbaf 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts @@ -21,11 +21,11 @@ export const validateConnectionTypes = (sourceType: FieldType, targetType: Field /** * Connection types must be the same for a connection, with exceptions: - * - CollectionItem can connect to any non-Collection - * - Non-Collections can connect to CollectionItem - * - Anything (non-Collections, Collections, CollectionOrScalar) can connect to CollectionOrScalar of the same base type - * - Generic Collection can connect to any other Collection or CollectionOrScalar - * - Any Collection can connect to a Generic Collection + * - CollectionItem can connect to any non-COLLECTION (e.g. SINGLE or SINGLE_OR_COLLECTION) + * - SINGLE can connect to CollectionItem + * - Anything (SINGLE, COLLECTION, SINGLE_OR_COLLECTION) can connect to SINGLE_OR_COLLECTION of the same base type + * - Generic CollectionField can connect to any other COLLECTION or SINGLE_OR_COLLECTION + * - Any COLLECTION can connect to a Generic Collection */ const isCollectionItemToNonCollection = sourceType.name === 'CollectionItemField' && !isCollection(targetType); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts index 2feba262c2..110a20e5a7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts @@ -29,7 +29,7 @@ export const addControlNetToLinearGraph = async ( assert(activeTabName !== 'generation', 'Tried to use addControlNetToLinearGraph on generation tab'); if (controlNets.length) { - // Even though denoise_latents' control input is collection or scalar, keep it simple and always use a collect + // Even though denoise_latents' control input is SINGLE_OR_COLLECTION, keep it simple and always use a collect const controlNetIterateNode: Invocation<'collect'> = { id: CONTROL_NET_COLLECT, type: 'collect', diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts index e9d9bd4663..1f24463419 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts @@ -25,7 +25,7 @@ export const addIPAdapterToLinearGraph = async ( }); if (ipAdapters.length) { - // Even though denoise_latents' ip adapter input is collection or scalar, keep it simple and always use a collect + // Even though denoise_latents' ip adapter input is SINGLE_OR_COLLECTION, keep it simple and always use a collect const ipAdapterCollectNode: Invocation<'collect'> = { id: IP_ADAPTER_COLLECT, type: 'collect', diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts index 7c51d9488f..72cf9ca0f8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts @@ -28,7 +28,7 @@ export const addT2IAdaptersToLinearGraph = async ( ); if (t2iAdapters.length) { - // Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect + // Even though denoise_latents' t2i adapter input is SINGLE_OR_COLLECTION, keep it simple and always use a collect const t2iAdapterCollectNode: Invocation<'collect'> = { id: T2I_ADAPTER_COLLECT, type: 'collect', diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts index ea9bf5bce4..18dcd8fb21 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts @@ -94,7 +94,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType } } /** - * Handle CollectionOrScalar inputs, eg string | string[]. In OpenAPI, this is: + * Handle SINGLE_OR_COLLECTION inputs, eg string | string[]. In OpenAPI, this is: * - an `anyOf` with two items * - one is an `ArraySchemaObject` with a single `SchemaObject or ReferenceObject` of type T in its `items` * - the other is a `SchemaObject` or `ReferenceObject` of type T From 1c29b3bd8573913deb915e4d5c67b86ec1daff8b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 19 May 2024 23:24:16 +1000 Subject: [PATCH 255/442] feat(ui): updated field type translations --- invokeai/frontend/web/public/locales/en.json | 5 +++-- .../web/src/features/nodes/hooks/usePrettyFieldType.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 5dd411c544..1d41a1de63 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -780,8 +780,9 @@ "missingFieldTemplate": "Missing field template", "nodePack": "Node pack", "collection": "Collection", - "collectionFieldType": "{{name}} Collection", - "collectionOrScalarFieldType": "{{name}} Collection|Scalar", + "singleFieldType": "{{name}} (Single)", + "collectionFieldType": "{{name}} (Collection)", + "collectionOrScalarFieldType": "{{name}} (Single or Collection)", "colorCodeEdges": "Color-Code Edges", "colorCodeEdgesHelp": "Color-code edges according to their connected fields", "connectionWouldCreateCycle": "Connection would create a cycle", diff --git a/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts b/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts index 2600eae078..7f531c3dba 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/usePrettyFieldType.ts @@ -16,7 +16,7 @@ export const useFieldTypeName = (fieldType?: FieldType): string => { if (isSingleOrCollection(fieldType)) { return t('nodes.collectionOrScalarFieldType', { name }); } - return name; + return t('nodes.singleFieldType', { name }); }, [fieldType, t]); return name; From 55535881473c3dd9aa9672ff0f575802f87f08f2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 20 May 2024 08:38:29 +1000 Subject: [PATCH 256/442] fix(ui): ensure invocation edges have a type --- .../web/src/features/nodes/store/nodesSlice.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index c63734c871..e7c1877647 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -1,7 +1,6 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; import { workflowLoaded } from 'features/nodes/store/actions'; import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; import type { @@ -105,7 +104,8 @@ export const nodesSlice = createSlice({ state.edges = applyEdgeChanges(edgeChanges, state.edges); }, edgesChanged: (state, action: PayloadAction) => { - const changes = deepClone(action.payload); + const changes: EdgeChange[] = []; + // We may need to massage the edge changes or otherwise handle them action.payload.forEach((change) => { if (change.type === 'remove' || change.type === 'select') { const edge = state.edges.find((e) => e.id === change.id); @@ -124,6 +124,13 @@ export const nodesSlice = createSlice({ } } } + if (change.type === 'add') { + if (!change.item.type) { + // We must add the edge type! + change.item.type = 'default'; + } + } + changes.push(change); }); state.edges = applyEdgeChanges(changes, state.edges); }, From 620ee2875ef52f796bb7ac4eb3146f32d9e9fa8f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 20 May 2024 08:41:05 +1000 Subject: [PATCH 257/442] fix(ui): store `hidden` state of edges in workflows This prevents a minor visual bug where collapsed edges between collapsed nodes didn't display correctly on first load of a workflow. --- invokeai/frontend/web/src/features/nodes/types/workflow.ts | 1 + .../web/src/features/nodes/util/workflow/buildWorkflow.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/types/workflow.ts b/invokeai/frontend/web/src/features/nodes/types/workflow.ts index a424bf8d4b..9805edfaf2 100644 --- a/invokeai/frontend/web/src/features/nodes/types/workflow.ts +++ b/invokeai/frontend/web/src/features/nodes/types/workflow.ts @@ -47,6 +47,7 @@ const zWorkflowEdgeDefault = zWorkflowEdgeBase.extend({ type: z.literal('default'), sourceHandle: z.string().trim().min(1), targetHandle: z.string().trim().min(1), + hidden: z.boolean().optional(), }); const zWorkflowEdgeCollapsed = zWorkflowEdgeBase.extend({ type: z.literal('collapsed'), diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts index b164dde90e..cec8b0a2b7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts @@ -66,6 +66,7 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo target: edge.target, sourceHandle: edge.sourceHandle, targetHandle: edge.targetHandle, + hidden: edge.hidden, }); } else if (edge.type === 'collapsed') { newWorkflow.edges.push({ From 32277193b6768dc1ad5b11b6ceaf7b17ddfc3dbd Mon Sep 17 00:00:00 2001 From: steffylo Date: Mon, 20 May 2024 15:49:18 +0800 Subject: [PATCH 258/442] fix(ui): retain denoise strength and opacity when changing image --- .../controlLayers/store/controlLayersSlice.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 32e29918ae..dbd99c2450 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -616,12 +616,24 @@ export const controlLayersSlice = createSlice({ iiLayerAdded: { reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; + + // Retain opacity and denoising strength of existing initial image layer if exists + let opacity = 1; + let denoisingStrength = 0.75; + const iiLayer = state.layers.find((l) => l.id === layerId); + if (iiLayer) { + assert(isInitialImageLayer(iiLayer)); + opacity = iiLayer.opacity; + denoisingStrength = iiLayer.denoisingStrength; + } + // Highlander! There can be only one! state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); + const layer: InitialImageLayer = { id: layerId, type: 'initial_image_layer', - opacity: 1, + opacity, x: 0, y: 0, bbox: null, @@ -629,7 +641,7 @@ export const controlLayersSlice = createSlice({ isEnabled: true, image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, isSelected: true, - denoisingStrength: 0.75, + denoisingStrength, }; state.layers.push(layer); exclusivelySelectLayer(state, layer.id); From 66c9f4708d14682d98ef0dc326c08d6a249de07c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 21 May 2024 06:59:56 +1000 Subject: [PATCH 259/442] Update invokeai_version.py --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index aef46acb47..2e905e44da 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.1" +__version__ = "4.2.2" From 1249d4a6e3a9237272aef59835035cc683dc6e22 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 21 May 2024 10:06:09 +1000 Subject: [PATCH 260/442] fix(ui): crash when using a notes node --- .../src/features/nodes/hooks/useNodeLabel.ts | 6 ++--- .../nodes/hooks/useNodeTemplateTitle.ts | 22 ++++++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts index 31dcb9c466..56e77a39e8 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts @@ -1,14 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeData } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; export const useNodeLabel = (nodeId: string) => { const selector = useMemo( () => - createSelector(selectNodesSlice, (nodes) => { - return selectNodeData(nodes, nodeId)?.label ?? null; + createSelector(selectNodesSlice, (nodesSlice) => { + const node = nodesSlice.nodes.find((node) => node.id === nodeId); + return node?.data.label; }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts index a63e0433aa..39ae617460 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts @@ -1,8 +1,24 @@ -import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; +import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { isInvocationNode } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; export const useNodeTemplateTitle = (nodeId: string): string | null => { - const template = useNodeTemplate(nodeId); - const title = useMemo(() => template.title, [template.title]); + const templates = useStore($templates); + const selector = useMemo( + () => + createSelector(selectNodesSlice, (nodesSlice) => { + const node = nodesSlice.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return null; + } + const template = templates[node.data.type]; + return template?.title ?? null; + }), + [nodeId, templates] + ); + const title = useAppSelector(selector); return title; }; From e75f98317f2333909cd9e180c211876711d9cccb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 21 May 2024 10:06:25 +1000 Subject: [PATCH 261/442] fix(ui): notes node text not selectable --- .../features/nodes/components/flow/nodes/Notes/NotesNode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx index 966809cb0e..76666af396 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx @@ -48,7 +48,7 @@ const NotesNode = (props: NodeProps) => { gap={1} > -