From 5ae14bffbab38a9da77647d01cdd95373026b54a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Aug 2023 14:17:22 +1000 Subject: [PATCH 01/45] fix(ui): clear exposedFields when resetting graph --- invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 437980d436..5ee2eefeb2 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -528,6 +528,7 @@ const nodesSlice = createSlice({ nodeEditorReset: (state) => { state.nodes = []; state.edges = []; + state.workflow.exposedFields = []; }, shouldValidateGraphChanged: (state, action: PayloadAction) => { state.shouldValidateGraph = action.payload; From 64a6aa0293e128fb3f23ce896e28750bfe3d029b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Aug 2023 15:40:56 +1000 Subject: [PATCH 02/45] fix(ui): move `BoardContextMenu` to use `IAIContextMenu` --- invokeai/frontend/web/package.json | 1 - .../gallery/components/Boards/BoardContextMenu.tsx | 13 ++++++++----- invokeai/frontend/web/yarn.lock | 5 ----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 6c9db74bbc..1f86d2585c 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -73,7 +73,6 @@ "@nanostores/react": "^0.7.1", "@reduxjs/toolkit": "^1.9.5", "@roarr/browser-log-writer": "^1.1.5", - "chakra-ui-contextmenu": "^1.0.5", "dateformat": "^5.0.3", "downshift": "^7.6.0", "formik": "^2.4.2", diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx index 0667c05435..c27897ce57 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx @@ -2,8 +2,12 @@ import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; +import { + IAIContextMenu, + IAIContextMenuProps, +} from 'common/components/IAIContextMenu'; import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice'; +import { BoardId } from 'features/gallery/store/types'; import { MouseEvent, memo, useCallback, useMemo } from 'react'; import { FaPlus } from 'react-icons/fa'; import { useBoardName } from 'services/api/hooks/useBoardName'; @@ -11,12 +15,11 @@ import { BoardDTO } from 'services/api/types'; import { menuListMotionProps } from 'theme/components/menu'; import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems'; import NoBoardContextMenuItems from './NoBoardContextMenuItems'; -import { BoardId } from 'features/gallery/store/types'; type Props = { board?: BoardDTO; board_id: BoardId; - children: ContextMenuProps['children']; + children: IAIContextMenuProps['children']; setBoardToDelete?: (board?: BoardDTO) => void; }; @@ -48,7 +51,7 @@ const BoardContextMenu = memo( }, []); return ( - + menuProps={{ size: 'sm', isLazy: true }} menuButtonProps={{ bg: 'transparent', @@ -80,7 +83,7 @@ const BoardContextMenu = memo( )} > {children} - + ); } ); diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 834e6368c4..7511efd0fb 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -2680,11 +2680,6 @@ camelcase@^6.3.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -chakra-ui-contextmenu@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/chakra-ui-contextmenu/-/chakra-ui-contextmenu-1.0.5.tgz#de54ad83c413a62040a06fefd3d73264a580a987" - integrity sha512-0pvi2RmNFpaoXPBT8mRDBZ1q6Ic8lE7YIyHBMgx4AubgN7dySww4SlN9g3mKWN3egkBL/ORCmxRfW6AlDeR+Nw== - chalk@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" From 84cf8bdc08aa94e608ffa6715910c883cca991cb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:44:34 +1000 Subject: [PATCH 03/45] feat(ui): field context menu, add/remove from linear ui --- .../common/components/SelectionOverlay.tsx | 1 + .../nodes/components/Invocation/NodeTitle.tsx | 1 + .../components/fields/FieldContextMenu.tsx | 135 +++++++++++++++--- .../nodes/components/fields/FieldTitle.tsx | 93 +++++------- .../components/fields/FieldTooltipContent.tsx | 3 +- .../nodes/components/fields/InputField.tsx | 111 ++++++++------ .../components/fields/LinearViewField.tsx | 74 +++++++--- .../fields/fieldTypes/ImageInputField.tsx | 16 ++- .../components/panel/workflow/LinearTab.tsx | 11 +- .../src/features/nodes/hooks/useNodeData.ts | 105 +++++++++++++- .../src/features/nodes/store/nodesSlice.ts | 8 ++ .../web/src/features/nodes/store/types.ts | 2 + 12 files changed, 400 insertions(+), 160 deletions(-) diff --git a/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx b/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx index 9ff6cd341b..eb04c7c56d 100644 --- a/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx @@ -18,6 +18,7 @@ const SelectionOverlay = ({ isSelected, isHovered }: Props) => { opacity: isSelected ? 1 : 0.7, transitionProperty: 'common', transitionDuration: '0.1s', + pointerEvents: 'none', shadow: isSelected ? isHovered ? 'hoverSelected.light' diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx index d816f3cea1..6b14d4e952 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx @@ -79,6 +79,7 @@ const NodeTitle = ({ nodeId, title }: Props) => { fontSize="sm" sx={{ p: 0, + fontWeight: 'initial', _focusVisible: { p: 0, boxShadow: 'none', diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldContextMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldContextMenu.tsx index d9f8f951bc..d93f9a4241 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldContextMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldContextMenu.tsx @@ -1,26 +1,111 @@ -import { MenuItem, MenuList } from '@chakra-ui/react'; -import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; +import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { - InputFieldTemplate, - InputFieldValue, -} from 'features/nodes/types/types'; -import { MouseEvent, useCallback } from 'react'; + IAIContextMenu, + IAIContextMenuProps, +} from 'common/components/IAIContextMenu'; +import { + useFieldInputKind, + useFieldLabel, + useFieldTemplateTitle, +} from 'features/nodes/hooks/useNodeData'; +import { + workflowExposedFieldAdded, + workflowExposedFieldRemoved, +} from 'features/nodes/store/nodesSlice'; +import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react'; +import { FaMinus, FaPlus } from 'react-icons/fa'; import { menuListMotionProps } from 'theme/components/menu'; type Props = { nodeId: string; - field: InputFieldValue; - fieldTemplate: InputFieldTemplate; - children: ContextMenuProps['children']; + fieldName: string; + kind: 'input' | 'output'; + children: IAIContextMenuProps['children']; }; -const FieldContextMenu = (props: Props) => { +const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => { + const dispatch = useAppDispatch(); + const label = useFieldLabel(nodeId, fieldName); + const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind); + const skipEvent = useCallback((e: MouseEvent) => { e.preventDefault(); }, []); + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const isExposed = Boolean( + nodes.workflow.exposedFields.find( + (f) => f.nodeId === nodeId && f.fieldName === fieldName + ) + ); + + return { isExposed }; + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const input = useFieldInputKind(nodeId, fieldName); + const mayExpose = useMemo( + () => ['any', 'direct'].includes(input ?? '__UNKNOWN_INPUT__'), + [input] + ); + + const { isExposed } = useAppSelector(selector); + + const handleExposeField = useCallback(() => { + dispatch(workflowExposedFieldAdded({ nodeId, fieldName })); + }, [dispatch, fieldName, nodeId]); + + const handleUnexposeField = useCallback(() => { + dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); + }, [dispatch, fieldName, nodeId]); + + const menuItems = useMemo(() => { + const menuItems: ReactNode[] = []; + if (mayExpose && !isExposed) { + menuItems.push( + } + onClick={handleExposeField} + > + Add to Linear View + + ); + } + if (mayExpose && isExposed) { + menuItems.push( + } + onClick={handleUnexposeField} + > + Remove from Linear View + + ); + } + return menuItems; + }, [ + fieldName, + handleExposeField, + handleUnexposeField, + isExposed, + mayExpose, + nodeId, + ]); + return ( - + menuProps={{ size: 'sm', isLazy: true, @@ -29,19 +114,23 @@ const FieldContextMenu = (props: Props) => { bg: 'transparent', _hover: { bg: 'transparent' }, }} - renderMenu={() => ( - - Test - - )} + renderMenu={() => + !menuItems.length ? null : ( + + + {menuItems} + + + ) + } > - {props.children} - + {children} + ); }; -export default FieldContextMenu; +export default memo(FieldContextMenu); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx index e9a49989f6..a84358bf78 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx @@ -3,63 +3,42 @@ import { EditableInput, EditablePreview, Flex, + forwardRef, useEditableControls, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import IAIDraggable from 'common/components/IAIDraggable'; -import { NodeFieldDraggableData } from 'features/dnd/types'; import { - useFieldData, - useFieldTemplate, + useFieldLabel, + useFieldTemplateTitle, } from 'features/nodes/hooks/useNodeData'; import { fieldLabelChanged } from 'features/nodes/store/nodesSlice'; -import { - MouseEvent, - memo, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import { MouseEvent, memo, useCallback, useEffect, useState } from 'react'; interface Props { nodeId: string; fieldName: string; - isDraggable?: boolean; kind: 'input' | 'output'; } -const FieldTitle = (props: Props) => { - const { nodeId, fieldName, isDraggable = false, kind } = props; - const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); - const field = useFieldData(nodeId, fieldName); +const FieldTitle = forwardRef((props: Props, ref) => { + const { nodeId, fieldName, kind } = props; + const label = useFieldLabel(nodeId, fieldName); + const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind); const dispatch = useAppDispatch(); const [localTitle, setLocalTitle] = useState( - field?.label || fieldTemplate?.title || 'Unknown Field' - ); - - const draggableData: NodeFieldDraggableData | undefined = useMemo( - () => - field && - fieldTemplate?.fieldKind === 'input' && - fieldTemplate?.input !== 'connection' && - isDraggable - ? { - id: `${nodeId}-${fieldName}`, - payloadType: 'NODE_FIELD', - payload: { nodeId, field, fieldTemplate }, - } - : undefined, - [field, fieldName, fieldTemplate, isDraggable, nodeId] + label || fieldTemplateTitle || 'Unknown Field' ); const handleSubmit = useCallback( async (newTitle: string) => { + if (newTitle === label || newTitle === fieldTemplateTitle) { + return; + } + setLocalTitle(newTitle || fieldTemplateTitle || 'Unknown Field'); dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle })); - setLocalTitle(newTitle || fieldTemplate?.title || 'Unknown Field'); }, - [dispatch, nodeId, fieldName, fieldTemplate?.title] + [label, fieldTemplateTitle, dispatch, nodeId, fieldName] ); const handleChange = useCallback((newTitle: string) => { @@ -68,27 +47,33 @@ const FieldTitle = (props: Props) => { useEffect(() => { // Another component may change the title; sync local title with global state - setLocalTitle(field?.label || fieldTemplate?.title || 'Unknown Field'); - }, [field?.label, fieldTemplate?.title]); + setLocalTitle(label || fieldTemplateTitle || 'Unknown Field'); + }, [label, fieldTemplateTitle]); return ( { { }, }} /> - + ); -}; +}); export default memo(FieldTitle); -type EditableControlsProps = { - draggableData?: NodeFieldDraggableData; -}; - -const EditableControls = memo((props: EditableControlsProps) => { +const EditableControls = memo(() => { const { isEditing, getEditButtonProps } = useEditableControls(); - const handleDoubleClick = useCallback( + const handleClick = useCallback( (e: MouseEvent) => { const { onClick } = getEditButtonProps(); if (!onClick) { return; } onClick(e); + e.preventDefault(); }, [getEditButtonProps] ); @@ -137,19 +124,9 @@ const EditableControls = memo((props: EditableControlsProps) => { return null; } - if (props.draggableData) { - return ( - - ); - } - return ( { const isInputTemplate = isInputFieldTemplate(fieldTemplate); const fieldTitle = useMemo(() => { if (isInputFieldValue(field)) { - if (field.label && fieldTemplate) { + console.log(field, fieldTemplate); + if (field.label && fieldTemplate?.title) { return `${field.label} (${fieldTemplate.title})`; } diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx index 47033baa7b..e099180a7f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx @@ -3,13 +3,16 @@ import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; import { useDoesInputHaveValue, useFieldTemplate, + useIsMouseOverField, } from 'features/nodes/hooks/useNodeData'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { PropsWithChildren, memo, useMemo } from 'react'; +import FieldContextMenu from './FieldContextMenu'; import FieldHandle from './FieldHandle'; import FieldTitle from './FieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; import InputFieldRenderer from './InputFieldRenderer'; +import SelectionOverlay from 'common/components/SelectionOverlay'; interface Props { nodeId: string; @@ -48,7 +51,11 @@ const InputField = ({ nodeId, fieldName }: Props) => { if (fieldTemplate?.fieldKind !== 'input') { return ( - + @@ -59,40 +66,48 @@ const InputField = ({ nodeId, fieldName }: Props) => { } return ( - + - - } - openDelay={HANDLE_TOOLTIP_OPEN_DELAY} - placement="top" - shouldWrapChildren - hasArrow - > - - - - + + {(ref) => ( + + } + openDelay={HANDLE_TOOLTIP_OPEN_DELAY} + placement="top" + hasArrow + > + + + + + )} + @@ -113,27 +128,37 @@ export default InputField; type InputFieldWrapperProps = PropsWithChildren<{ shouldDim: boolean; + nodeId: string; + fieldName: string; }>; const InputFieldWrapper = memo( - ({ shouldDim, children }: InputFieldWrapperProps) => ( - - {children} - - ) + ({ shouldDim, nodeId, fieldName, children }: InputFieldWrapperProps) => { + const { isMouseOverField, handleMouseOver, handleMouseOut } = + useIsMouseOverField(nodeId, fieldName); + + return ( + + {children} + + + ); + } ); InputFieldWrapper.displayName = 'InputFieldWrapper'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx index ea4bb76d62..2f4dc84827 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx @@ -1,6 +1,12 @@ -import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; +import { Flex, FormControl, FormLabel, Icon, Tooltip } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/store/storeHooks'; +import IAIIconButton from 'common/components/IAIIconButton'; +import SelectionOverlay from 'common/components/SelectionOverlay'; +import { useIsMouseOverField } from 'features/nodes/hooks/useNodeData'; +import { workflowExposedFieldRemoved } from 'features/nodes/store/nodesSlice'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; -import { memo } from 'react'; +import { memo, useCallback } from 'react'; +import { FaInfoCircle, FaTrash } from 'react-icons/fa'; import FieldTitle from './FieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; import InputFieldRenderer from './InputFieldRenderer'; @@ -11,8 +17,18 @@ type Props = { }; const LinearViewField = ({ nodeId, fieldName }: Props) => { + const dispatch = useAppDispatch(); + const { isMouseOverField, handleMouseOut, handleMouseOver } = + useIsMouseOverField(nodeId, fieldName); + + const handleRemoveField = useCallback(() => { + dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); + }, [dispatch, fieldName, nodeId]); + return ( { }} > - - } - openDelay={HANDLE_TOOLTIP_OPEN_DELAY} - placement="top" - shouldWrapChildren - hasArrow + - + + } + openDelay={HANDLE_TOOLTIP_OPEN_DELAY} + placement="top" + hasArrow > - - - + + + + + } + /> + + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx index 0391136dba..21b89c2231 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx @@ -1,4 +1,4 @@ -import { Flex } from '@chakra-ui/react'; +import { Flex, Text } from '@chakra-ui/react'; import { skipToken } from '@reduxjs/toolkit/dist/query'; import { useAppDispatch } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; @@ -81,6 +81,9 @@ const ImageInputFieldComponent = ( draggableData={draggableData} postUploadAction={postUploadAction} useThumbailFallback + uploadElement={} + dropLabel={} + minSize={8} > ( + + Drop or Upload + +); +const DropLabel = () => ( + + Drop + +); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx index cc7428a8ec..b77453b749 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx @@ -3,9 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import IAIDroppable from 'common/components/IAIDroppable'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import { AddFieldToLinearViewDropData } from 'features/dnd/types'; import { memo } from 'react'; import LinearViewField from '../../fields/LinearViewField'; import ScrollableContent from '../ScrollableContent'; @@ -20,11 +18,6 @@ const selector = createSelector( defaultSelectorOptions ); -const droppableData: AddFieldToLinearViewDropData = { - id: 'add-field-to-linear-view', - actionType: 'ADD_FIELD_TO_LINEAR', -}; - const LinearTabContent = () => { const { fields } = useAppSelector(selector); @@ -42,6 +35,7 @@ const LinearTabContent = () => { position: 'relative', flexDir: 'column', alignItems: 'flex-start', + p: 1, gap: 2, h: 'full', w: 'full', @@ -50,7 +44,7 @@ const LinearTabContent = () => { {fields.length ? ( fields.map(({ nodeId, fieldName }) => ( @@ -63,7 +57,6 @@ const LinearTabContent = () => { )} - ); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts index 231c7678ef..e0fddb9f8c 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -1,9 +1,10 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { map, some } from 'lodash-es'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; +import { mouseOverFieldChanged } from '../store/nodesSlice'; import { FOOTER_FIELDS, IMAGE_FIELDS } from '../types/constants'; import { isInvocationNode } from '../types/types'; @@ -51,6 +52,28 @@ export const useNodeData = (nodeId: string) => { return nodeData; }; +export const useFieldLabel = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return node?.data.inputs[fieldName]?.label; + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const label = useAppSelector(selector); + + return label; +}; + export const useFieldData = (nodeId: string, fieldName: string) => { const selector = useMemo( () => @@ -73,6 +96,30 @@ export const useFieldData = (nodeId: string, fieldName: string) => { return fieldData; }; +export const useFieldInputKind = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + const fieldTemplate = nodeTemplate?.inputs[fieldName]; + return fieldTemplate?.input; + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const fieldType = useAppSelector(selector); + + return fieldType; +}; + export const useFieldType = ( nodeId: string, fieldName: string, @@ -236,6 +283,33 @@ export const useNodeTemplateTitle = (nodeId: string) => { return title; }; +export const useFieldTemplateTitle = ( + nodeId: string, + fieldName: string, + kind: 'input' | 'output' +) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + return nodeTemplate?.[KIND_MAP[kind]][fieldName]?.title; + }, + defaultSelectorOptions + ), + [fieldName, kind, nodeId] + ); + + const fieldTemplate = useAppSelector(selector); + + return fieldTemplate; +}; + export const useFieldTemplate = ( nodeId: string, fieldName: string, @@ -284,3 +358,30 @@ export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => { return doesFieldHaveValue; }; + +export const useIsMouseOverField = (nodeId: string, fieldName: string) => { + const dispatch = useAppDispatch(); + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => + nodes.mouseOverField?.nodeId === nodeId && + nodes.mouseOverField?.fieldName === fieldName, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const isMouseOverField = useAppSelector(selector); + + const handleMouseOver = useCallback(() => { + dispatch(mouseOverFieldChanged({ nodeId, fieldName })); + }, [dispatch, fieldName, nodeId]); + + const handleMouseOut = useCallback(() => { + dispatch(mouseOverFieldChanged(null)); + }, [dispatch]); + + return { isMouseOverField, handleMouseOver, handleMouseOut }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 5ee2eefeb2..048894b166 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -81,6 +81,7 @@ export const initialNodesState: NodesState = { }, nodeExecutionStates: {}, viewport: { x: 0, y: 0, zoom: 1 }, + mouseOverField: null, }; type FieldValueAction = PayloadAction<{ @@ -594,6 +595,12 @@ const nodesSlice = createSlice({ viewportChanged: (state, action: PayloadAction) => { state.viewport = action.payload; }, + mouseOverFieldChanged: ( + state, + action: PayloadAction + ) => { + state.mouseOverField = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(receivedOpenAPISchema.pending, (state) => { @@ -701,6 +708,7 @@ export const { workflowExposedFieldRemoved, fieldLabelChanged, viewportChanged, + mouseOverFieldChanged, } = nodesSlice.actions; export default nodesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index 27e25b8731..160336cef5 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -1,6 +1,7 @@ import { OpenAPIV3 } from 'openapi-types'; import { Edge, Node, OnConnectStartParams, Viewport } from 'reactflow'; import { + FieldIdentifier, FieldType, InvocationEdgeExtra, InvocationTemplate, @@ -29,4 +30,5 @@ export type NodesState = { nodeExecutionStates: Record; viewport: Viewport; isReady: boolean; + mouseOverField: FieldIdentifier | null; }; From 9332ce639ca3d8db7c243ce79311785901288f52 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:15:45 +1000 Subject: [PATCH 04/45] fix(ui): fix node mouse interactions Add "nodrag", "nowheel" and "nopan" class names in interactable elements, as neeeded. This fixes the mouse interactions and also makes the node draggable from anywhere without needing shift. Also fixes ctrl/cmd multi-select to support deselecting. --- .../components/Invocation/InvocationNode.tsx | 7 +- .../Invocation/NodeCollapseButton.tsx | 2 +- .../components/Invocation/NodeNotesEdit.tsx | 3 +- .../components/Invocation/NodeSettings.tsx | 69 ------------------- .../nodes/components/Invocation/NodeTitle.tsx | 2 +- .../components/Invocation/NodeWrapper.tsx | 41 +++-------- .../nodes/components/fields/FieldTitle.tsx | 2 +- .../nodes/components/fields/InputField.tsx | 1 - .../fields/fieldTypes/BooleanInputField.tsx | 6 +- .../fieldTypes/ControlNetModelInputField.tsx | 2 +- .../fields/fieldTypes/EnumInputField.tsx | 2 +- .../fields/fieldTypes/ImageInputField.tsx | 1 + .../fields/fieldTypes/LoRAModelInputField.tsx | 2 +- .../fields/fieldTypes/MainModelInputField.tsx | 4 +- .../fields/fieldTypes/NumberInputField.tsx | 2 +- .../fieldTypes/RefinerModelInputField.tsx | 4 +- .../fieldTypes/SDXLMainModelInputField.tsx | 4 +- .../fields/fieldTypes/StringInputField.tsx | 1 + .../fields/fieldTypes/VaeModelInputField.tsx | 1 + .../src/features/nodes/store/nodesSlice.ts | 14 ---- .../SyncModelsButton.tsx | 9 ++- 21 files changed, 40 insertions(+), 139 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/components/Invocation/NodeSettings.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx index 6c610d7f34..03b69faf78 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx @@ -33,9 +33,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { <> { borderBottomRadius: withFooter ? 0 : 'base', }} > - + {outputFieldNames.map((fieldName) => ( { return ( { shouldWrapChildren > { - const { data } = props; - const dispatch = useAppDispatch(); - - const handleChangeIsIntermediate = useCallback( - (e: ChangeEvent) => { - dispatch( - fieldBooleanValueChanged({ - nodeId: data.id, - fieldName: 'is_intermediate', - value: e.target.checked, - }) - ); - }, - [data.id, dispatch] - ); - - return ( - } - /> - } - > - - - - - ); -}; - -export default memo(NodeSettings); diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx index 6b14d4e952..ee85c38ecc 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx @@ -45,7 +45,6 @@ const NodeTitle = ({ nodeId, title }: Props) => { return ( { noOfLines={1} /> { - const dispatch = useAppDispatch(); - - const selectNode = useCallback( - (e: MouseEvent) => { - dispatch(nodeClicked({ nodeId, ctrlOrMeta: e.ctrlKey || e.metaKey })); - }, - [dispatch, nodeId] - ); - - return selectNode; -}; - type NodeWrapperProps = PropsWithChildren & { nodeId: string; selected: boolean; @@ -35,7 +16,7 @@ type NodeWrapperProps = PropsWithChildren & { }; const NodeWrapper = (props: NodeWrapperProps) => { - const { width, children, nodeId, selected } = props; + const { width, children, selected } = props; const [ nodeSelectedOutlineLight, @@ -49,24 +30,23 @@ const NodeWrapper = (props: NodeWrapperProps) => { 'shadows.base', ]); - const selectNode = useNodeSelect(nodeId); + const dispatch = useAppDispatch(); const shadow = useColorModeValue( nodeSelectedOutlineLight, nodeSelectedOutlineDark ); - const shift = useAppSelector((state) => state.hotkeys.shift); const opacity = useAppSelector((state) => state.nodes.nodeOpacity); - const className = useMemo( - () => (shift ? DRAG_HANDLE_CLASSNAME : 'nopan'), - [shift] - ); + + const handleClick = useCallback(() => { + dispatch(contextMenusClosed()); + }, [dispatch]); return ( { transitionProperty: 'common', transitionDuration: '0.1s', shadow: selected ? shadow : undefined, + cursor: 'grab', opacity, }} > diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx index a84358bf78..42e42fcf8a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx @@ -53,7 +53,6 @@ const FieldTitle = forwardRef((props: Props, ref) => { return ( { noOfLines={1} /> + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx index 492ec51d20..3192e7583b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx @@ -85,7 +85,7 @@ const ControlNetModelInputFieldComponent = ( return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx index 21b89c2231..7db48f5248 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx @@ -68,6 +68,7 @@ const ImageInputFieldComponent = ( return ( 0 ? 'Select a LoRA' : 'No LoRAs available'} data={data} diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx index 681a597235..3847e14ae2 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx @@ -123,7 +123,7 @@ const MainModelInputFieldComponent = ( Loading... ) : ( )} - {isSyncModelEnabled && } + {isSyncModelEnabled && } ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx index df5c3f763e..bd670bd394 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx @@ -64,7 +64,7 @@ const NumberInputFieldComponent = ( step={isIntegerField ? 1 : 0.1} precision={isIntegerField ? 0 : 3} > - + diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx index 0eec884de0..5ba3ad529d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx @@ -96,7 +96,7 @@ const RefinerModelInputFieldComponent = ( ) : ( 0 ? 'Select a model' : 'No models available'} @@ -107,7 +107,7 @@ const RefinerModelInputFieldComponent = ( /> {isSyncModelEnabled && ( - + )} diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx index e904aad246..1240cb95a0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx @@ -123,7 +123,7 @@ const ModelInputFieldComponent = ( ) : ( 0 ? 'Select a model' : 'No models available'} @@ -132,7 +132,7 @@ const ModelInputFieldComponent = ( disabled={data.length === 0} onChange={handleChangeModel} /> - {isSyncModelEnabled && } + {isSyncModelEnabled && } ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx index c172e928d0..4561f9cc32 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx @@ -31,6 +31,7 @@ const StringInputFieldComponent = ( if (fieldTemplate.ui_component === 'textarea') { return ( - ) => { - const { nodeId, ctrlOrMeta } = action.payload; - state.nodes.forEach((node) => { - if (node.id === nodeId) { - node.selected = true; - } else if (!ctrlOrMeta) { - node.selected = false; - } - }); - }, notesNodeValueChanged: ( state, action: PayloadAction<{ nodeId: string; value: string }> @@ -665,7 +652,6 @@ export const { connectionMade, connectionStarted, connectionEnded, - nodeClicked, shouldShowFieldTypeLegendChanged, shouldShowMinimapPanelChanged, nodeTemplatesBuilt, diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton.tsx index 6405fba1a7..33ee345ef7 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton.tsx @@ -1,18 +1,19 @@ -import { makeToast } from 'features/system/util/makeToast'; +import { ButtonProps } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton'; import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/util/makeToast'; import { useTranslation } from 'react-i18next'; import { FaSync } from 'react-icons/fa'; import { useSyncModelsMutation } from 'services/api/endpoints/models'; -type SyncModelsButtonProps = { +type SyncModelsButtonProps = ButtonProps & { iconMode?: boolean; }; export default function SyncModelsButton(props: SyncModelsButtonProps) { - const { iconMode = false } = props; + const { iconMode = false, ...rest } = props; const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -50,6 +51,7 @@ export default function SyncModelsButton(props: SyncModelsButtonProps) { isLoading={isLoading} onClick={syncModelsHandler} minW="max-content" + {...rest} > Sync Models @@ -61,6 +63,7 @@ export default function SyncModelsButton(props: SyncModelsButtonProps) { isLoading={isLoading} onClick={syncModelsHandler} size="sm" + {...rest} /> ); } From 210a3f9aa79947101c41bbb0401194e41ef7fa40 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:56:12 +1000 Subject: [PATCH 05/45] feat(ui): make mantine single selects *exactly* the same size as chakra ones --- .../hooks/useMantineSelectStyles.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/mantine-theme/hooks/useMantineSelectStyles.ts b/invokeai/frontend/web/src/mantine-theme/hooks/useMantineSelectStyles.ts index f8aa16558f..d56b6cc70d 100644 --- a/invokeai/frontend/web/src/mantine-theme/hooks/useMantineSelectStyles.ts +++ b/invokeai/frontend/web/src/mantine-theme/hooks/useMantineSelectStyles.ts @@ -24,6 +24,9 @@ export const useMantineSelectStyles = () => { const { colorMode } = useColorMode(); const [boxShadow] = useToken('shadows', ['dark-lg']); + const [space1, space2, space6] = useToken('space', [1, 2, 6]); + const [radiiBase] = useToken('radii', ['base']); + const [lineHeightsBase] = useToken('lineHeights', ['base']); const styles = useCallback( () => ({ @@ -35,11 +38,22 @@ export const useMantineSelectStyles = () => { '::after': { borderTopColor: mode(base300, base700)(colorMode) }, }, input: { + border: 'unset', backgroundColor: mode(base50, base900)(colorMode), + borderRadius: radiiBase, + borderStyle: 'solid', borderWidth: '2px', borderColor: mode(base200, base800)(colorMode), color: mode(base900, base100)(colorMode), - paddingRight: 24, + minHeight: 'unset', + lineHeight: lineHeightsBase, + height: 'auto', + paddingRight: 0, + paddingLeft: 0, + paddingInlineStart: space2, + paddingInlineEnd: space6, + paddingTop: space1, + paddingBottom: space1, fontWeight: 600, '&:hover': { borderColor: mode(base300, base600)(colorMode) }, '&:focus': { @@ -127,6 +141,11 @@ export const useMantineSelectStyles = () => { base900, boxShadow, colorMode, + lineHeightsBase, + radiiBase, + space1, + space2, + space6, ] ); From 98431b3de4b3477319af7f8e59515e65cbe67a81 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:58:01 +1000 Subject: [PATCH 06/45] feat: add `Scheduler` as field type - update node schemas - add `UIType.Scheduler` - add field type to schema parser, input components --- invokeai/app/invocations/baseinvocation.py | 1 + invokeai/app/invocations/latent.py | 4 +- invokeai/app/invocations/onnx.py | 2 +- .../components/fields/InputFieldRenderer.tsx | 28 ++++++- .../fields/fieldTypes/SchedulerInputField.tsx | 75 +++++++++++++++++++ .../src/features/nodes/store/nodesSlice.ts | 8 ++ .../web/src/features/nodes/types/constants.ts | 5 ++ .../web/src/features/nodes/types/types.ts | 18 ++++- .../nodes/util/fieldTemplateBuilders.ts | 20 +++++ .../features/nodes/util/fieldValueBuilders.ts | 4 + 10 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 11d9d40047..1a57225a34 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -143,6 +143,7 @@ class UIType(str, Enum): # region Misc FilePath = "FilePath" Enum = "enum" + Scheduler = "Scheduler" # endregion diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index f65a95999d..8f44d43546 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -119,7 +119,9 @@ class DenoiseLatentsInvocation(BaseInvocation): ) denoising_start: float = InputField(default=0.0, ge=0, le=1, description=FieldDescriptions.denoising_start) denoising_end: float = InputField(default=1.0, ge=0, le=1, description=FieldDescriptions.denoising_end) - scheduler: SAMPLER_NAME_VALUES = InputField(default="euler", description=FieldDescriptions.scheduler) + scheduler: SAMPLER_NAME_VALUES = InputField( + default="euler", description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler + ) unet: UNetField = InputField(description=FieldDescriptions.unet, input=Input.Connection) control: Union[ControlField, list[ControlField]] = InputField( default=None, description=FieldDescriptions.control, input=Input.Connection diff --git a/invokeai/app/invocations/onnx.py b/invokeai/app/invocations/onnx.py index 3e65c1e55d..a2a5e436f9 100644 --- a/invokeai/app/invocations/onnx.py +++ b/invokeai/app/invocations/onnx.py @@ -169,7 +169,7 @@ class ONNXTextToLatentsInvocation(BaseInvocation): ui_type=UIType.Float, ) scheduler: SAMPLER_NAME_VALUES = InputField( - default="euler", description=FieldDescriptions.scheduler, input=Input.Direct + default="euler", description=FieldDescriptions.scheduler, input=Input.Direct, ui_type=UIType.Scheduler ) precision: PRECISION_VALUES = InputField(default="tensor(float16)", description=FieldDescriptions.precision) unet: UNetField = InputField( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx index acec921d8e..7eda868447 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx @@ -1,4 +1,4 @@ -import { Box } from '@chakra-ui/react'; +import { Box, Text } from '@chakra-ui/react'; import { useFieldData, useFieldTemplate, @@ -21,6 +21,7 @@ import MainModelInputField from './fieldTypes/MainModelInputField'; import NumberInputField from './fieldTypes/NumberInputField'; import RefinerModelInputField from './fieldTypes/RefinerModelInputField'; import SDXLMainModelInputField from './fieldTypes/SDXLMainModelInputField'; +import SchedulerInputField from './fieldTypes/SchedulerInputField'; import StringInputField from './fieldTypes/StringInputField'; import UnetInputField from './fieldTypes/UnetInputField'; import VaeInputField from './fieldTypes/VaeInputField'; @@ -286,7 +287,30 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => { ); } - return Unknown field type: {field?.type}; + if (field?.type === 'Scheduler' && fieldTemplate?.type === 'Scheduler') { + return ( + + ); + } + + return ( + + + Unknown field type: {field?.type} + + + ); }; export default memo(InputFieldRenderer); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx new file mode 100644 index 0000000000..93a7d7e131 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx @@ -0,0 +1,75 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect'; +import { fieldSchedulerValueChanged } from 'features/nodes/store/nodesSlice'; +import { + SchedulerInputFieldTemplate, + SchedulerInputFieldValue, +} from 'features/nodes/types/types'; +import { + SCHEDULER_LABEL_MAP, + SchedulerParam, +} from 'features/parameters/types/parameterSchemas'; +import { map } from 'lodash-es'; +import { memo, useCallback } from 'react'; +import { FieldComponentProps } from './types'; + +const selector = createSelector( + [stateSelector], + ({ ui }) => { + const { favoriteSchedulers: enabledSchedulers } = ui; + + const data = map(SCHEDULER_LABEL_MAP, (label, name) => ({ + value: name, + label: label, + group: enabledSchedulers.includes(name as SchedulerParam) + ? 'Favorites' + : undefined, + })).sort((a, b) => a.label.localeCompare(b.label)); + + return { + data, + }; + }, + defaultSelectorOptions +); + +const SchedulerInputField = ( + props: FieldComponentProps< + SchedulerInputFieldValue, + SchedulerInputFieldTemplate + > +) => { + const { nodeId, field } = props; + const dispatch = useAppDispatch(); + const { data } = useAppSelector(selector); + + const handleChange = useCallback( + (value: string | null) => { + if (!value) { + return; + } + dispatch( + fieldSchedulerValueChanged({ + nodeId, + fieldName: field.name, + value: value as SchedulerParam, + }) + ); + }, + [dispatch, field.name, nodeId] + ); + + return ( + + ); +}; + +export default memo(SchedulerInputField); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 0efb675149..90c7859995 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -45,6 +45,7 @@ import { MainModelInputFieldValue, NodeStatus, NotesNodeData, + SchedulerInputFieldValue, SDXLRefinerModelInputFieldValue, StringInputFieldValue, VaeModelInputFieldValue, @@ -447,6 +448,12 @@ const nodesSlice = createSlice({ ) => { fieldValueReducer(state, action); }, + fieldSchedulerValueChanged: ( + state, + action: FieldValueAction + ) => { + fieldValueReducer(state, action); + }, imageCollectionFieldValueChanged: ( state, action: PayloadAction<{ @@ -670,6 +677,7 @@ export const { fieldEnumModelValueChanged, fieldControlNetModelValueChanged, fieldRefinerModelValueChanged, + fieldSchedulerValueChanged, nodeIsOpenChanged, nodeLabelChanged, nodeNotesChanged, diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index 1c5c89ff2d..07d5543da4 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -127,6 +127,11 @@ export const FIELDS: Record = { title: 'ControlNet', description: 'TODO', }, + Scheduler: { + color: 'base.500', + title: 'Scheduler', + description: 'TODO', + }, Collection: { color: 'base.500', title: 'Collection', diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 60e4877fd8..3183b31c56 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -3,6 +3,7 @@ import { LoRAModelParam, MainModelParam, OnnxModelParam, + SchedulerParam, VaeModelParam, } from 'features/parameters/types/parameterSchemas'; import { OpenAPIV3 } from 'openapi-types'; @@ -98,6 +99,7 @@ export const zFieldType = z.enum([ // region Misc 'FilePath', 'enum', + 'Scheduler', // endregion ]); @@ -137,7 +139,8 @@ export type InputFieldValue = | CollectionInputFieldValue | CollectionItemInputFieldValue | ColorInputFieldValue - | ImageCollectionInputFieldValue; + | ImageCollectionInputFieldValue + | SchedulerInputFieldValue; /** * An input field template is generated on each page load from the OpenAPI schema. @@ -167,7 +170,8 @@ export type InputFieldTemplate = | CollectionInputFieldTemplate | CollectionItemInputFieldTemplate | ColorInputFieldTemplate - | ImageCollectionInputFieldTemplate; + | ImageCollectionInputFieldTemplate + | SchedulerInputFieldTemplate; /** * An output field is persisted across as part of the user's local state. @@ -322,6 +326,11 @@ export type ColorInputFieldValue = InputFieldValueBase & { value?: RgbaColor; }; +export type SchedulerInputFieldValue = InputFieldValueBase & { + type: 'Scheduler'; + value?: SchedulerParam; +}; + export type InputFieldTemplateBase = { name: string; title: string; @@ -456,6 +465,11 @@ export type ColorInputFieldTemplate = InputFieldTemplateBase & { type: 'ColorField'; }; +export type SchedulerInputFieldTemplate = InputFieldTemplateBase & { + default: SchedulerParam; + type: 'Scheduler'; +}; + export const isInputFieldValue = ( field?: InputFieldValue | OutputFieldValue ): field is InputFieldValue => Boolean(field && field.fieldKind === 'input'); diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts index 37aaab59b6..63a0d51f05 100644 --- a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts +++ b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts @@ -27,6 +27,7 @@ import { OutputFieldTemplate, SDXLMainModelInputFieldTemplate, SDXLRefinerModelInputFieldTemplate, + SchedulerInputFieldTemplate, StringInputFieldTemplate, UNetInputFieldTemplate, VaeInputFieldTemplate, @@ -400,6 +401,19 @@ const buildColorInputFieldTemplate = ({ return template; }; +const buildSchedulerInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): SchedulerInputFieldTemplate => { + const template: SchedulerInputFieldTemplate = { + ...baseField, + type: 'Scheduler', + default: schemaObject.default ?? 'euler', + }; + + return template; +}; + export const getFieldType = ( schemaObject: InvocationFieldSchema ): FieldType => { @@ -606,6 +620,12 @@ export const buildInputFieldTemplate = ( baseField, }); } + if (fieldType === 'Scheduler') { + return buildSchedulerInputFieldTemplate({ + schemaObject: fieldSchema, + baseField, + }); + } return; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts index 473dc83bb6..91e7ca522d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts +++ b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts @@ -93,5 +93,9 @@ export const buildInputFieldValue = ( fieldValue.value = undefined; } + if (template.type === 'Scheduler') { + fieldValue.value = undefined; + } + return fieldValue; }; From 3d84e7756a7ecbbddb50076a08246fbb9fe7c2d5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Aug 2023 21:37:28 +1000 Subject: [PATCH 07/45] fix(nodes): fix field names --- invokeai/app/invocations/latent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 8f44d43546..2a23931c3d 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -115,14 +115,14 @@ class DenoiseLatentsInvocation(BaseInvocation): noise: Optional[LatentsField] = InputField(description=FieldDescriptions.noise, input=Input.Connection) steps: int = InputField(default=10, gt=0, description=FieldDescriptions.steps) cfg_scale: Union[float, List[float]] = InputField( - default=7.5, ge=1, description=FieldDescriptions.cfg_scale, ui_type=UIType.Float + default=7.5, ge=1, description=FieldDescriptions.cfg_scale, ui_type=UIType.Float, title="CFG Scale" ) denoising_start: float = InputField(default=0.0, ge=0, le=1, description=FieldDescriptions.denoising_start) denoising_end: float = InputField(default=1.0, ge=0, le=1, description=FieldDescriptions.denoising_end) scheduler: SAMPLER_NAME_VALUES = InputField( default="euler", description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler ) - unet: UNetField = InputField(description=FieldDescriptions.unet, input=Input.Connection) + unet: UNetField = InputField(description=FieldDescriptions.unet, input=Input.Connection, title="UNet") control: Union[ControlField, list[ControlField]] = InputField( default=None, description=FieldDescriptions.control, input=Input.Connection ) From ae6db67068fb9c6ea22d16498b4e71d4c553c357 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Aug 2023 21:38:12 +1000 Subject: [PATCH 08/45] feat(ui): add width to mantine selects --- .../components/fields/fieldTypes/LoRAModelInputField.tsx | 5 +++++ .../components/fields/fieldTypes/MainModelInputField.tsx | 5 +++++ .../components/fields/fieldTypes/RefinerModelInputField.tsx | 5 +++++ .../components/fields/fieldTypes/SDXLMainModelInputField.tsx | 5 +++++ .../components/fields/fieldTypes/SchedulerInputField.tsx | 5 +++++ .../components/fields/fieldTypes/VaeModelInputField.tsx | 5 +++++ 6 files changed, 30 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx index f911bf8746..8e4d04a55d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx @@ -102,6 +102,11 @@ const LoRAModelInputFieldComponent = ( item.value.toLowerCase().includes(value.toLowerCase().trim()) } onChange={handleChange} + sx={{ + '.mantine-Select-dropdown': { + width: '16rem !important', + }, + }} /> ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx index 3847e14ae2..f5d2393532 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx @@ -133,6 +133,11 @@ const MainModelInputFieldComponent = ( error={!selectedModel} disabled={data.length === 0} onChange={handleChangeModel} + sx={{ + '.mantine-Select-dropdown': { + width: '16rem !important', + }, + }} /> )} {isSyncModelEnabled && } diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx index 5ba3ad529d..d42334c7b2 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx @@ -104,6 +104,11 @@ const RefinerModelInputFieldComponent = ( error={data.length === 0} disabled={data.length === 0} onChange={handleChangeModel} + sx={{ + '.mantine-Select-dropdown': { + width: '16rem !important', + }, + }} /> {isSyncModelEnabled && ( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx index 1240cb95a0..2193835228 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx @@ -131,6 +131,11 @@ const ModelInputFieldComponent = ( error={data.length === 0} disabled={data.length === 0} onChange={handleChangeModel} + sx={{ + '.mantine-Select-dropdown': { + width: '16rem !important', + }, + }} /> {isSyncModelEnabled && } diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx index 93a7d7e131..87f2f5b5e4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx @@ -65,6 +65,11 @@ const SchedulerInputField = ( return ( ); }; From a495c8c1562da13cbe0259b1b4d16ca0377b69b4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Aug 2023 21:38:34 +1000 Subject: [PATCH 09/45] feat(ui): misc cleanups --- .../components/Invocation/NodeHeader.tsx | 2 +- .../nodes/components/Invocation/NodeTitle.tsx | 2 +- .../nodes/components/fields/FieldTitle.tsx | 7 +++- .../components/fields/FieldTooltipContent.tsx | 1 - .../nodes/components/fields/InputField.tsx | 36 ++++++++++++++++--- .../fields/fieldTypes/ImageInputField.tsx | 13 ++++--- .../src/features/nodes/hooks/useNodeData.ts | 4 +-- 7 files changed, 50 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx index ea503a8f27..cff15812fd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx @@ -25,7 +25,7 @@ const NodeHeader = ({ nodeId, isOpen }: Props) => { justifyContent: 'space-between', h: 8, textAlign: 'center', - fontWeight: 600, + fontWeight: 500, color: 'base.700', _dark: { color: 'base.200' }, }} diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx index ee85c38ecc..eeb5147c74 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx @@ -79,7 +79,7 @@ const NodeTitle = ({ nodeId, title }: Props) => { fontSize="sm" sx={{ p: 0, - fontWeight: 'initial', + fontWeight: 700, _focusVisible: { p: 0, boxShadow: 'none', diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx index 42e42fcf8a..8fb22422cd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx @@ -18,10 +18,11 @@ interface Props { nodeId: string; fieldName: string; kind: 'input' | 'output'; + isMissingInput?: boolean; } const FieldTitle = forwardRef((props: Props, ref) => { - const { nodeId, fieldName, kind } = props; + const { nodeId, fieldName, kind, isMissingInput = false } = props; const label = useFieldLabel(nodeId, fieldName); const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind); @@ -78,7 +79,11 @@ const FieldTitle = forwardRef((props: Props, ref) => { diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx index c45cd880ed..03ca843780 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx @@ -23,7 +23,6 @@ const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => { const isInputTemplate = isInputFieldTemplate(fieldTemplate); const fieldTitle = useMemo(() => { if (isInputFieldValue(field)) { - console.log(field, fieldTemplate); if (field.label && fieldTemplate?.title) { return `${field.label} (${fieldTemplate.title})`; } diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx index 8bf3296944..c64b8c8dfe 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx @@ -1,18 +1,19 @@ -import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; +import { Box, Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; +import SelectionOverlay from 'common/components/SelectionOverlay'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; import { useDoesInputHaveValue, + useFieldInputKind, useFieldTemplate, useIsMouseOverField, } from 'features/nodes/hooks/useNodeData'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; -import { PropsWithChildren, memo, useMemo } from 'react'; +import { PropsWithChildren, memo, useCallback, useMemo, useState } from 'react'; import FieldContextMenu from './FieldContextMenu'; import FieldHandle from './FieldHandle'; import FieldTitle from './FieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; import InputFieldRenderer from './InputFieldRenderer'; -import SelectionOverlay from 'common/components/SelectionOverlay'; interface Props { nodeId: string; @@ -22,6 +23,16 @@ interface Props { const InputField = ({ nodeId, fieldName }: Props) => { const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input'); const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName); + const input = useFieldInputKind(nodeId, fieldName); + const [isHovered, setIsHovered] = useState(false); + + const handleMouseOver = useCallback(() => { + setIsHovered(true); + }, []); + + const handleMouseOut = useCallback(() => { + setIsHovered(false); + }, []); const { isConnected, @@ -81,6 +92,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { ps: 2, gap: 2, h: 'full', + w: 'full', }} > @@ -97,18 +109,32 @@ const InputField = ({ nodeId, fieldName }: Props) => { placement="top" hasArrow > - + )} - + + + {fieldTemplate.input !== 'direct' && ( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx index 7db48f5248..cb2dfcac66 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx @@ -98,13 +98,18 @@ const ImageInputFieldComponent = ( export default memo(ImageInputFieldComponent); -const UploadElement = () => ( +const UploadElement = memo(() => ( Drop or Upload -); -const DropLabel = () => ( +)); + +UploadElement.displayName = 'UploadElement'; + +const DropLabel = memo(() => ( Drop -); +)); + +DropLabel.displayName = 'DropLabel'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts index e0fddb9f8c..daca2aad45 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -115,9 +115,9 @@ export const useFieldInputKind = (nodeId: string, fieldName: string) => { [fieldName, nodeId] ); - const fieldType = useAppSelector(selector); + const inputKind = useAppSelector(selector); - return fieldType; + return inputKind; }; export const useFieldType = ( From 030802295ba4d3fdb0432cdd15b2ef695d920f71 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 16:37:09 +1000 Subject: [PATCH 10/45] feat(ui): reset only specific nodes/cnet that use images Previously if an image was used in nodes and you deleted it, it would reset all of node editor. Same for controlnet. Now it only resets the specific nodes or controlnets that used that image. --- .../listeners/imageDeleted.ts | 128 +++++++++++++++--- 1 file changed, 106 insertions(+), 22 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 b419e98782..770c9fc11b 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 @@ -1,13 +1,17 @@ import { logger } from 'app/logging/logger'; import { resetCanvas } from 'features/canvas/store/canvasSlice'; -import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; +import { + controlNetImageChanged, + controlNetProcessedImageChanged, +} from 'features/controlNet/store/controlNetSlice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors'; import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; +import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; +import { isInvocationNode } from 'features/nodes/types/types'; import { clearInitialImage } from 'features/parameters/store/generationSlice'; -import { clamp } from 'lodash-es'; +import { clamp, forEach } from 'lodash-es'; import { api } from 'services/api'; import { imagesApi } from 'services/api/endpoints/images'; import { imagesAdapter } from 'services/api/util'; @@ -73,22 +77,61 @@ export const addRequestedSingleImageDeletionListener = () => { } // We need to reset the features where the image is in use - none of these work if their image(s) don't exist - if (imageUsage.isCanvasImage) { dispatch(resetCanvas()); } - if (imageUsage.isControlNetImage) { - dispatch(controlNetReset()); - } + imageDTOs.forEach((imageDTO) => { + // reset init image if we deleted it + if ( + getState().generation.initialImage?.imageName === imageDTO.image_name + ) { + dispatch(clearInitialImage()); + } - if (imageUsage.isInitialImage) { - dispatch(clearInitialImage()); - } + // reset controlNets that use the deleted images + forEach(getState().controlNet.controlNets, (controlNet) => { + if ( + controlNet.controlImage === imageDTO.image_name || + controlNet.processedControlImage === imageDTO.image_name + ) { + dispatch( + controlNetImageChanged({ + controlNetId: controlNet.controlNetId, + controlImage: null, + }) + ); + dispatch( + controlNetProcessedImageChanged({ + controlNetId: controlNet.controlNetId, + processedControlImage: null, + }) + ); + } + }); - if (imageUsage.isNodesImage) { - dispatch(nodeEditorReset()); - } + // reset nodes that use the deleted images + getState().nodes.nodes.forEach((node) => { + if (!isInvocationNode(node)) { + return; + } + + forEach(node.data.inputs, (input) => { + if ( + input.type === 'ImageField' && + input.value?.image_name === imageDTO.image_name + ) { + dispatch( + fieldImageValueChanged({ + nodeId: node.data.id, + fieldName: input.name, + value: undefined, + }) + ); + } + }); + }); + }); // Delete from server const { requestId } = dispatch( @@ -154,17 +197,58 @@ export const addRequestedMultipleImageDeletionListener = () => { dispatch(resetCanvas()); } - if (imagesUsage.some((i) => i.isControlNetImage)) { - dispatch(controlNetReset()); - } + imageDTOs.forEach((imageDTO) => { + // reset init image if we deleted it + if ( + getState().generation.initialImage?.imageName === + imageDTO.image_name + ) { + dispatch(clearInitialImage()); + } - if (imagesUsage.some((i) => i.isInitialImage)) { - dispatch(clearInitialImage()); - } + // reset controlNets that use the deleted images + forEach(getState().controlNet.controlNets, (controlNet) => { + if ( + controlNet.controlImage === imageDTO.image_name || + controlNet.processedControlImage === imageDTO.image_name + ) { + dispatch( + controlNetImageChanged({ + controlNetId: controlNet.controlNetId, + controlImage: null, + }) + ); + dispatch( + controlNetProcessedImageChanged({ + controlNetId: controlNet.controlNetId, + processedControlImage: null, + }) + ); + } + }); - if (imagesUsage.some((i) => i.isNodesImage)) { - dispatch(nodeEditorReset()); - } + // reset nodes that use the deleted images + getState().nodes.nodes.forEach((node) => { + if (!isInvocationNode(node)) { + return; + } + + forEach(node.data.inputs, (input) => { + if ( + input.type === 'ImageField' && + input.value?.image_name === imageDTO.image_name + ) { + dispatch( + fieldImageValueChanged({ + nodeId: node.data.id, + fieldName: input.name, + value: undefined, + }) + ); + } + }); + }); + }); } catch { // no-op } From 567d46b646495fdbf3fd8c2850b80bee165ebdcb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 16:38:40 +1000 Subject: [PATCH 11/45] feat(ui): delete key works on workflow editor --- invokeai/frontend/web/src/features/nodes/components/Flow.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index 3290a65054..f419f5fc48 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -38,6 +38,8 @@ import TopCenterPanel from './editorPanels/TopCenterPanel'; import TopLeftPanel from './editorPanels/TopLeftPanel'; import TopRightPanel from './editorPanels/TopRightPanel'; +const DELETE_KEYS = ['Delete', 'Backspace']; + // TODO: can we support reactflow? if not, we could style the attribution so it matches the app const proOptions: ProOptions = { hideAttribution: true }; @@ -144,6 +146,7 @@ export const Flow = () => { proOptions={proOptions} style={{ borderRadius }} onPaneClick={handlePaneClick} + deleteKeyCode={DELETE_KEYS} > From 519bcb38c1addf9d14f95c2eb9e9ff4925904152 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 18:14:21 +1000 Subject: [PATCH 12/45] feat(ui): node delete, copy, paste --- .../src/features/nodes/components/Flow.tsx | 12 +++ .../features/nodes/hooks/useBuildNodeData.ts | 20 +++- .../nodes/store/nodesPersistDenylist.ts | 2 + .../src/features/nodes/store/nodesSlice.ts | 91 +++++++++++++++++++ .../web/src/features/nodes/store/types.ts | 2 + .../store/util/findUnoccupiedPosition.ts | 11 +++ 6 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index f419f5fc48..462977c477 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -2,6 +2,7 @@ import { useToken } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { contextMenusClosed } from 'features/ui/store/uiSlice'; import { useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import { Background, OnConnect, @@ -27,6 +28,8 @@ import { nodesDeleted, selectedEdgesChanged, selectedNodesChanged, + selectionCopied, + selectionPasted, viewportChanged, } from '../store/nodesSlice'; import { CustomConnectionLine } from './CustomConnectionLine'; @@ -121,8 +124,17 @@ export const Flow = () => { dispatch(contextMenusClosed()); }, [dispatch]); + useHotkeys(['Ctrl+c', 'Meta+c'], () => { + dispatch(selectionCopied()); + }); + + useHotkeys(['Ctrl+v', 'Meta+v'], () => { + dispatch(selectionPasted()); + }); + return ( state.nodes], @@ -34,10 +34,24 @@ export const useBuildNodeData = () => { (type: AnyInvocationType | 'current_image' | 'notes') => { const nodeId = uuidv4(); + let _x = window.innerWidth / 2; + let _y = window.innerHeight / 2; + + // attempt to center the node in the middle of the flow + const rect = document + .querySelector('#workflow-editor') + ?.getBoundingClientRect(); + + if (rect) { + _x = rect.width / 2 - NODE_WIDTH / 2; + _y = rect.height / 2 - NODE_WIDTH / 2; + } + const { x, y } = flow.project({ - x: window.innerWidth / 2.5, - y: window.innerHeight / 8, + x: _x, + y: _y, }); + if (type === 'current_image') { const node: Node = { ...SHARED_NODE_PROPERTIES, diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts b/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts index cf3ee3918c..fe52b63bb2 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts @@ -11,4 +11,6 @@ export const nodesPersistDenylist: (keyof NodesState)[] = [ 'selectedNodes', 'selectedEdges', 'isReady', + 'nodesToCopy', + 'edgesToCopy', ]; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 90c7859995..23056d50a6 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -25,6 +25,7 @@ import { appSocketInvocationError, appSocketInvocationStarted, } from 'services/events/actions'; +import { v4 as uuidv4 } from 'uuid'; import { DRAG_HANDLE_CLASSNAME } from '../types/constants'; import { BooleanInputFieldValue, @@ -52,6 +53,7 @@ import { Workflow, } from '../types/types'; import { NodesState } from './types'; +import { findUnoccupiedPosition } from './util/findUnoccupiedPosition'; export const initialNodesState: NodesState = { nodes: [], @@ -83,6 +85,8 @@ export const initialNodesState: NodesState = { nodeExecutionStates: {}, viewport: { x: 0, y: 0, zoom: 1 }, mouseOverField: null, + nodesToCopy: [], + edgesToCopy: [], }; type FieldValueAction = PayloadAction<{ @@ -124,6 +128,12 @@ const nodesSlice = createSlice({ > ) => { const node = action.payload; + const position = findUnoccupiedPosition( + state.nodes, + node.position.x, + node.position.y + ); + node.position = position; state.nodes.push(node); if (!isInvocationNode(node)) { @@ -595,6 +605,85 @@ const nodesSlice = createSlice({ ) => { state.mouseOverField = action.payload; }, + selectionCopied: (state) => { + state.nodesToCopy = state.nodes.filter((n) => n.selected).map(cloneDeep); + state.edgesToCopy = state.edges.filter((e) => e.selected).map(cloneDeep); + }, + selectionPasted: (state) => { + const newNodes = state.nodesToCopy.map(cloneDeep); + const oldNodeIds = newNodes.map((n) => n.data.id); + const newEdges = state.edgesToCopy + .filter( + (e) => oldNodeIds.includes(e.source) && oldNodeIds.includes(e.target) + ) + .map(cloneDeep); + + 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, + node.position.y + ); + + 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 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( + nodeAdditions.concat(nodeSelectionChanges), + state.nodes + ); + + state.edges = applyEdgeChanges( + edgeAdditions.concat(edgeSelectionChanges), + state.edges + ); + + newNodes.forEach((node) => { + state.nodeExecutionStates[node.id] = { + status: NodeStatus.PENDING, + error: null, + progress: null, + progressImage: null, + }; + }); + }, }, extraReducers: (builder) => { builder.addCase(receivedOpenAPISchema.pending, (state) => { @@ -703,6 +792,8 @@ export const { fieldLabelChanged, viewportChanged, mouseOverFieldChanged, + selectionCopied, + selectionPasted, } = nodesSlice.actions; export default nodesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index 160336cef5..1a26f959fd 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -31,4 +31,6 @@ export type NodesState = { viewport: Viewport; isReady: boolean; mouseOverField: FieldIdentifier | null; + nodesToCopy: Node[]; + edgesToCopy: Edge[]; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts b/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts new file mode 100644 index 0000000000..57f36f4d5e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/findUnoccupiedPosition.ts @@ -0,0 +1,11 @@ +import { Node } from 'reactflow'; + +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; + } + return { x: newX, y: newY }; +}; From 81385d7d35d046d2762925c550a9f4d4a6b23432 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:06:09 +1000 Subject: [PATCH 13/45] fix(stats): fix fail case when previous graph is invalid When retrieving a graph, it is parsed through pydantic. It is possible that this graph is invalid, and an error is thrown. Handle this by deleting the failed graph from the stats if this occurs. --- invokeai/app/services/invocation_stats.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/invocation_stats.py b/invokeai/app/services/invocation_stats.py index e8557c40f7..93f797e998 100644 --- a/invokeai/app/services/invocation_stats.py +++ b/invokeai/app/services/invocation_stats.py @@ -34,6 +34,7 @@ from abc import ABC, abstractmethod from contextlib import AbstractContextManager from dataclasses import dataclass, field from typing import Dict +from pydantic import ValidationError import torch @@ -269,7 +270,13 @@ class InvocationStatsService(InvocationStatsServiceBase): """ completed = set() for graph_id, node_log in self._stats.items(): - current_graph_state = self.graph_execution_manager.get(graph_id) + try: + current_graph_state = self.graph_execution_manager.get(graph_id) + except ValidationError: + del self._stats[graph_id] + del self._cache_stats[graph_id] + continue + if not current_graph_state.is_complete(): continue From cd9baf809200d3405727b1a958538c730db2055f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:16:45 +1000 Subject: [PATCH 14/45] fix(stats): fix `InvocationStatsService` types - move docstrings to ABC - `start_time: int` -> `start_time: float` - remove class attribute assignments in `StatsContext` - add `update_mem_stats()` to ABC - add class attributes to ABC, because they are referenced in instances of the class. if they should not be on the ABC, then maybe there needs to be some restructuring --- invokeai/app/services/invocation_stats.py | 104 ++++++++++------------ 1 file changed, 47 insertions(+), 57 deletions(-) diff --git a/invokeai/app/services/invocation_stats.py b/invokeai/app/services/invocation_stats.py index 93f797e998..75c36d0e16 100644 --- a/invokeai/app/services/invocation_stats.py +++ b/invokeai/app/services/invocation_stats.py @@ -50,9 +50,36 @@ from invokeai.backend.model_management.model_cache import CacheStats GIG = 1073741824 +@dataclass +class NodeStats: + """Class for tracking execution stats of an invocation node""" + + calls: int = 0 + time_used: float = 0.0 # seconds + max_vram: float = 0.0 # GB + cache_hits: int = 0 + cache_misses: int = 0 + cache_high_watermark: int = 0 + + +@dataclass +class NodeLog: + """Class for tracking node usage""" + + # {node_type => NodeStats} + nodes: Dict[str, NodeStats] = field(default_factory=dict) + + class InvocationStatsServiceBase(ABC): "Abstract base class for recording node memory/time performance statistics" + graph_execution_manager: ItemStorageABC["GraphExecutionState"] + # {graph_id => NodeLog} + _stats: Dict[str, NodeLog] + _cache_stats: Dict[str, CacheStats] + ram_used: float + ram_changed: float + @abstractmethod def __init__(self, graph_execution_manager: ItemStorageABC["GraphExecutionState"]): """ @@ -95,8 +122,6 @@ class InvocationStatsServiceBase(ABC): invocation_type: str, time_used: float, vram_used: float, - ram_used: float, - ram_changed: float, ): """ Add timing information on execution of a node. Usually @@ -105,8 +130,6 @@ class InvocationStatsServiceBase(ABC): :param invocation_type: String literal type of the node :param time_used: Time used by node's exection (sec) :param vram_used: Maximum VRAM used during exection (GB) - :param ram_used: Current RAM available (GB) - :param ram_changed: Change in RAM usage over course of the run (GB) """ pass @@ -117,25 +140,19 @@ class InvocationStatsServiceBase(ABC): """ pass + @abstractmethod + def update_mem_stats( + self, + ram_used: float, + ram_changed: float, + ): + """ + Update the collector with RAM memory usage info. -@dataclass -class NodeStats: - """Class for tracking execution stats of an invocation node""" - - calls: int = 0 - time_used: float = 0.0 # seconds - max_vram: float = 0.0 # GB - cache_hits: int = 0 - cache_misses: int = 0 - cache_high_watermark: int = 0 - - -@dataclass -class NodeLog: - """Class for tracking node usage""" - - # {node_type => NodeStats} - nodes: Dict[str, NodeStats] = field(default_factory=dict) + :param ram_used: How much RAM is currently in use. + :param ram_changed: How much RAM changed since last generation. + """ + pass class InvocationStatsService(InvocationStatsServiceBase): @@ -153,12 +170,12 @@ class InvocationStatsService(InvocationStatsServiceBase): class StatsContext: """Context manager for collecting statistics.""" - invocation: BaseInvocation = None - collector: "InvocationStatsServiceBase" = None - graph_id: str = None - start_time: int = 0 - ram_used: int = 0 - model_manager: ModelManagerService = None + invocation: BaseInvocation + collector: "InvocationStatsServiceBase" + graph_id: str + start_time: float + ram_used: int + model_manager: ModelManagerService def __init__( self, @@ -171,7 +188,7 @@ class InvocationStatsService(InvocationStatsServiceBase): self.invocation = invocation self.collector = collector self.graph_id = graph_id - self.start_time = 0 + self.start_time = 0.0 self.ram_used = 0 self.model_manager = model_manager @@ -192,7 +209,7 @@ class InvocationStatsService(InvocationStatsServiceBase): ) self.collector.update_invocation_stats( graph_id=self.graph_id, - invocation_type=self.invocation.type, + invocation_type=self.invocation.type, # type: ignore - `type` is not on the `BaseInvocation` model, but *is* on all invocations time_used=time.time() - self.start_time, vram_used=torch.cuda.max_memory_allocated() / GIG if torch.cuda.is_available() else 0.0, ) @@ -203,11 +220,6 @@ class InvocationStatsService(InvocationStatsServiceBase): graph_execution_state_id: str, model_manager: ModelManagerService, ) -> StatsContext: - """ - Return a context object that will capture the statistics. - :param invocation: BaseInvocation object from the current graph. - :param graph_execution_state: GraphExecutionState object from the current session. - """ if not self._stats.get(graph_execution_state_id): # first time we're seeing this self._stats[graph_execution_state_id] = NodeLog() self._cache_stats[graph_execution_state_id] = CacheStats() @@ -218,7 +230,6 @@ class InvocationStatsService(InvocationStatsServiceBase): self._stats = {} def reset_stats(self, graph_execution_id: str): - """Zero the statistics for the indicated graph.""" try: self._stats.pop(graph_execution_id) except KeyError: @@ -229,12 +240,6 @@ class InvocationStatsService(InvocationStatsServiceBase): ram_used: float, ram_changed: float, ): - """ - Update the collector with RAM memory usage info. - - :param ram_used: How much RAM is currently in use. - :param ram_changed: How much RAM changed since last generation. - """ self.ram_used = ram_used self.ram_changed = ram_changed @@ -245,16 +250,6 @@ class InvocationStatsService(InvocationStatsServiceBase): time_used: float, vram_used: float, ): - """ - Add timing information on execution of a node. Usually - used internally. - :param graph_id: ID of the graph that is currently executing - :param invocation_type: String literal type of the node - :param time_used: Time used by node's exection (sec) - :param vram_used: Maximum VRAM used during exection (GB) - :param ram_used: Current RAM available (GB) - :param ram_changed: Change in RAM usage over course of the run (GB) - """ if not self._stats[graph_id].nodes.get(invocation_type): self._stats[graph_id].nodes[invocation_type] = NodeStats() stats = self._stats[graph_id].nodes[invocation_type] @@ -263,11 +258,6 @@ class InvocationStatsService(InvocationStatsServiceBase): stats.max_vram = max(stats.max_vram, vram_used) def log_stats(self): - """ - Send the statistics to the system logger at the info level. - Stats will only be printed when the execution of the graph - is complete. - """ completed = set() for graph_id, node_log in self._stats.items(): try: From 484b572023b7e9d8b6a626a97fc1cf2bbdcec0c2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:17:21 +1000 Subject: [PATCH 15/45] feat(nodes): primitives have `value` instead of `a` as field names --- invokeai/app/invocations/math.py | 10 +++++----- invokeai/app/invocations/primitives.py | 26 ++++++++++++------------- invokeai/app/services/default_graphs.py | 18 ++++++++--------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/invokeai/app/invocations/math.py b/invokeai/app/invocations/math.py index 13e3d92f52..80cdc09221 100644 --- a/invokeai/app/invocations/math.py +++ b/invokeai/app/invocations/math.py @@ -21,7 +21,7 @@ class AddInvocation(BaseInvocation): b: int = InputField(default=0, description=FieldDescriptions.num_2) def invoke(self, context: InvocationContext) -> IntegerOutput: - return IntegerOutput(a=self.a + self.b) + return IntegerOutput(value=self.a + self.b) @title("Subtract Integers") @@ -36,7 +36,7 @@ class SubtractInvocation(BaseInvocation): b: int = InputField(default=0, description=FieldDescriptions.num_2) def invoke(self, context: InvocationContext) -> IntegerOutput: - return IntegerOutput(a=self.a - self.b) + return IntegerOutput(value=self.a - self.b) @title("Multiply Integers") @@ -51,7 +51,7 @@ class MultiplyInvocation(BaseInvocation): b: int = InputField(default=0, description=FieldDescriptions.num_2) def invoke(self, context: InvocationContext) -> IntegerOutput: - return IntegerOutput(a=self.a * self.b) + return IntegerOutput(value=self.a * self.b) @title("Divide Integers") @@ -66,7 +66,7 @@ class DivideInvocation(BaseInvocation): b: int = InputField(default=0, description=FieldDescriptions.num_2) def invoke(self, context: InvocationContext) -> IntegerOutput: - return IntegerOutput(a=int(self.a / self.b)) + return IntegerOutput(value=int(self.a / self.b)) @title("Random Integer") @@ -81,4 +81,4 @@ class RandomIntInvocation(BaseInvocation): high: int = InputField(default=np.iinfo(np.int32).max, description="The exclusive high value") def invoke(self, context: InvocationContext) -> IntegerOutput: - return IntegerOutput(a=np.random.randint(self.low, self.high)) + return IntegerOutput(value=np.random.randint(self.low, self.high)) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index f32cb14f3a..fcf6adef87 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -2,8 +2,8 @@ from typing import Literal, Optional, Tuple -from pydantic import BaseModel, Field import torch +from pydantic import BaseModel, Field from .baseinvocation import ( BaseInvocation, @@ -33,7 +33,7 @@ class BooleanOutput(BaseInvocationOutput): """Base class for nodes that output a single boolean""" type: Literal["boolean_output"] = "boolean_output" - a: bool = OutputField(description="The output boolean") + value: bool = OutputField(description="The output boolean") class BooleanCollectionOutput(BaseInvocationOutput): @@ -55,10 +55,10 @@ class BooleanInvocation(BaseInvocation): type: Literal["boolean"] = "boolean" # Inputs - a: bool = InputField(default=False, description="The boolean value") + value: bool = InputField(default=False, description="The boolean value") def invoke(self, context: InvocationContext) -> BooleanOutput: - return BooleanOutput(a=self.a) + return BooleanOutput(value=self.value) @title("Boolean Primitive Collection") @@ -86,7 +86,7 @@ class IntegerOutput(BaseInvocationOutput): """Base class for nodes that output a single integer""" type: Literal["integer_output"] = "integer_output" - a: int = OutputField(description="The output integer") + value: int = OutputField(description="The output integer") class IntegerCollectionOutput(BaseInvocationOutput): @@ -108,10 +108,10 @@ class IntegerInvocation(BaseInvocation): type: Literal["integer"] = "integer" # Inputs - a: int = InputField(default=0, description="The integer value") + value: int = InputField(default=0, description="The integer value") def invoke(self, context: InvocationContext) -> IntegerOutput: - return IntegerOutput(a=self.a) + return IntegerOutput(value=self.value) @title("Integer Primitive Collection") @@ -139,7 +139,7 @@ class FloatOutput(BaseInvocationOutput): """Base class for nodes that output a single float""" type: Literal["float_output"] = "float_output" - a: float = OutputField(description="The output float") + value: float = OutputField(description="The output float") class FloatCollectionOutput(BaseInvocationOutput): @@ -161,10 +161,10 @@ class FloatInvocation(BaseInvocation): type: Literal["float"] = "float" # Inputs - param: float = InputField(default=0.0, description="The float value") + value: float = InputField(default=0.0, description="The float value") def invoke(self, context: InvocationContext) -> FloatOutput: - return FloatOutput(a=self.param) + return FloatOutput(value=self.value) @title("Float Primitive Collection") @@ -192,7 +192,7 @@ class StringOutput(BaseInvocationOutput): """Base class for nodes that output a single string""" type: Literal["string_output"] = "string_output" - text: str = OutputField(description="The output string") + value: str = OutputField(description="The output string") class StringCollectionOutput(BaseInvocationOutput): @@ -214,10 +214,10 @@ class StringInvocation(BaseInvocation): type: Literal["string"] = "string" # Inputs - text: str = InputField(default="", description="The string value", ui_component=UIComponent.Textarea) + value: str = InputField(default="", description="The string value", ui_component=UIComponent.Textarea) def invoke(self, context: InvocationContext) -> StringOutput: - return StringOutput(text=self.text) + return StringOutput(value=self.value) @title("String Primitive Collection") diff --git a/invokeai/app/services/default_graphs.py b/invokeai/app/services/default_graphs.py index 7135e031b0..5e1a594b91 100644 --- a/invokeai/app/services/default_graphs.py +++ b/invokeai/app/services/default_graphs.py @@ -17,9 +17,9 @@ def create_text_to_image() -> LibraryGraph: description="Converts text to an image", graph=Graph( nodes={ - "width": IntegerInvocation(id="width", a=512), - "height": IntegerInvocation(id="height", a=512), - "seed": IntegerInvocation(id="seed", a=-1), + "width": IntegerInvocation(id="width", value=512), + "height": IntegerInvocation(id="height", value=512), + "seed": IntegerInvocation(id="seed", value=-1), "3": NoiseInvocation(id="3"), "4": CompelInvocation(id="4"), "5": CompelInvocation(id="5"), @@ -29,15 +29,15 @@ def create_text_to_image() -> LibraryGraph: }, edges=[ Edge( - source=EdgeConnection(node_id="width", field="a"), + source=EdgeConnection(node_id="width", field="value"), destination=EdgeConnection(node_id="3", field="width"), ), Edge( - source=EdgeConnection(node_id="height", field="a"), + source=EdgeConnection(node_id="height", field="value"), destination=EdgeConnection(node_id="3", field="height"), ), Edge( - source=EdgeConnection(node_id="seed", field="a"), + source=EdgeConnection(node_id="seed", field="value"), destination=EdgeConnection(node_id="3", field="seed"), ), Edge( @@ -65,9 +65,9 @@ def create_text_to_image() -> LibraryGraph: exposed_inputs=[ ExposedNodeInput(node_path="4", field="prompt", alias="positive_prompt"), ExposedNodeInput(node_path="5", field="prompt", alias="negative_prompt"), - ExposedNodeInput(node_path="width", field="a", alias="width"), - ExposedNodeInput(node_path="height", field="a", alias="height"), - ExposedNodeInput(node_path="seed", field="a", alias="seed"), + ExposedNodeInput(node_path="width", field="value", alias="width"), + ExposedNodeInput(node_path="height", field="value", alias="height"), + ExposedNodeInput(node_path="seed", field="value", alias="seed"), ], exposed_outputs=[ExposedNodeOutput(node_path="8", field="image", alias="image")], ) From f952f8f6858658a701b60e07ddec093ee28cec4a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:19:36 +1000 Subject: [PATCH 16/45] feat(ui): add typegen customisation for invocation outputs The `type` property is required on all of them, but because this is defined in pydantic as a Literal, it is not required in the OpenAPI schema. Easier to fix this by changing the generated types than fiddling around with pydantic. --- invokeai/frontend/web/scripts/typegen.js | 27 +++-- .../frontend/web/src/services/api/schema.d.ts | 108 +++++++++--------- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/invokeai/frontend/web/scripts/typegen.js b/invokeai/frontend/web/scripts/typegen.js index d105917e66..80302a9c99 100644 --- a/invokeai/frontend/web/scripts/typegen.js +++ b/invokeai/frontend/web/scripts/typegen.js @@ -42,7 +42,6 @@ async function main() { // We only want to make fields optional if they are required if (!Array.isArray(schemaObject?.required)) { schemaObject.required = ['id', 'type']; - return; } schemaObject.required.forEach((prop) => { @@ -68,12 +67,26 @@ async function main() { return; } - // if ( - // 'input' in schemaObject && - // (schemaObject.input === 'any' || schemaObject.input === 'connection') - // ) { - // schemaObject.required = false; - // } + + // Check if we are generating types for an invocation output + const isInvocationOutputPath = metadata.path.match( + /^#\/components\/schemas\/\w*Output$/ + ); + + const hasOutputProperties = + schemaObject.properties && 'type' in schemaObject.properties; + + if (isInvocationOutputPath && hasOutputProperties) { + if (!Array.isArray(schemaObject?.required)) { + schemaObject.required = ['type']; + } + schemaObject.required = [ + ...new Set(schemaObject.required.concat(['type'])), + ]; + console.log( + `Making output's "type" required: ${COLORS.fg.yellow}${schemaObject.title}${COLORS.reset}` + ); + } }, }); fs.writeFileSync(OUTPUT_FILE, types); diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index 316ee0c085..320ef9d77c 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -611,7 +611,7 @@ export type components = { * @default boolean_collection_output * @enum {string} */ - type?: "boolean_collection_output"; + type: "boolean_collection_output"; /** * Collection * @description The output boolean collection @@ -641,11 +641,11 @@ export type components = { */ type: "boolean"; /** - * A + * Value * @description The boolean value * @default false */ - a?: boolean; + value?: boolean; }; /** * BooleanOutput @@ -657,12 +657,12 @@ export type components = { * @default boolean_output * @enum {string} */ - type?: "boolean_output"; + type: "boolean_output"; /** - * A + * Value * @description The output boolean */ - a: boolean; + value: boolean; }; /** * Canny Processor @@ -771,7 +771,7 @@ export type components = { * @default clip_skip_output * @enum {string} */ - type?: "clip_skip_output"; + type: "clip_skip_output"; /** * CLIP * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count @@ -821,7 +821,7 @@ export type components = { * @default collect_output * @enum {string} */ - type?: "collect_output"; + type: "collect_output"; /** * Collection * @description The collection of input items @@ -838,7 +838,7 @@ export type components = { * @default color_collection_output * @enum {string} */ - type?: "color_collection_output"; + type: "color_collection_output"; /** * Collection * @description The output colors @@ -960,7 +960,7 @@ export type components = { * @default color_output * @enum {string} */ - type?: "color_output"; + type: "color_output"; /** * Color * @description The output color @@ -1040,7 +1040,7 @@ export type components = { * @default conditioning_collection_output * @enum {string} */ - type?: "conditioning_collection_output"; + type: "conditioning_collection_output"; /** * Collection * @description The output conditioning tensors @@ -1096,7 +1096,7 @@ export type components = { * @default conditioning_output * @enum {string} */ - type?: "conditioning_output"; + type: "conditioning_output"; /** * Conditioning * @description Conditioning tensor @@ -1339,7 +1339,7 @@ export type components = { * @default control_output * @enum {string} */ - type?: "control_output"; + type: "control_output"; /** * Control * @description ControlNet(s) to apply @@ -1578,7 +1578,7 @@ export type components = { */ steps?: number; /** - * Cfg Scale + * CFG Scale * @description Classifier-Free Guidance scale * @default 7.5 */ @@ -1628,7 +1628,7 @@ export type components = { */ negative_conditioning?: components["schemas"]["ConditioningField"]; /** - * Unet + * UNet * @description UNet (scheduler, LoRAs) */ unet?: components["schemas"]["UNetField"]; @@ -1808,7 +1808,7 @@ export type components = { * @default float_collection_output * @enum {string} */ - type?: "float_collection_output"; + type: "float_collection_output"; /** * Collection * @description The float collection @@ -1838,11 +1838,11 @@ export type components = { */ type: "float"; /** - * Param + * Value * @description The float value * @default 0 */ - param?: number; + value?: number; }; /** * Float Range @@ -1895,12 +1895,12 @@ export type components = { * @default float_output * @enum {string} */ - type?: "float_output"; + type: "float_output"; /** - * A + * Value * @description The output float */ - a: number; + value: number; }; /** Graph */ Graph: { @@ -2199,7 +2199,7 @@ export type components = { * @default image_collection_output * @enum {string} */ - type?: "image_collection_output"; + type: "image_collection_output"; /** * Collection * @description The output images @@ -2647,7 +2647,7 @@ export type components = { * @default image_output * @enum {string} */ - type?: "image_output"; + type: "image_output"; /** * Image * @description The output image @@ -3151,7 +3151,7 @@ export type components = { * @default integer_collection_output * @enum {string} */ - type?: "integer_collection_output"; + type: "integer_collection_output"; /** * Collection * @description The int collection @@ -3181,11 +3181,11 @@ export type components = { */ type: "integer"; /** - * A + * Value * @description The integer value * @default 0 */ - a?: number; + value?: number; }; /** * IntegerOutput @@ -3197,12 +3197,12 @@ export type components = { * @default integer_output * @enum {string} */ - type?: "integer_output"; + type: "integer_output"; /** - * A + * Value * @description The output integer */ - a: number; + value: number; }; /** * IterateInvocation @@ -3248,7 +3248,7 @@ export type components = { * @default iterate_output * @enum {string} */ - type?: "iterate_output"; + type: "iterate_output"; /** * Collection Item * @description The item being iterated over @@ -3294,7 +3294,7 @@ export type components = { * @default latents_collection_output * @enum {string} */ - type?: "latents_collection_output"; + type: "latents_collection_output"; /** * Collection * @description Latents tensor @@ -3355,7 +3355,7 @@ export type components = { * @default latents_output * @enum {string} */ - type?: "latents_output"; + type: "latents_output"; /** * Latents * @description Latents tensor @@ -3697,7 +3697,7 @@ export type components = { * @default lora_loader_output * @enum {string} */ - type?: "lora_loader_output"; + type: "lora_loader_output"; /** * UNet * @description UNet (scheduler, LoRAs) @@ -4076,7 +4076,7 @@ export type components = { * @default metadata_accumulator_output * @enum {string} */ - type?: "metadata_accumulator_output"; + type: "metadata_accumulator_output"; /** * Metadata * @description The core metadata for the image @@ -4205,7 +4205,7 @@ export type components = { * @default model_loader_output * @enum {string} */ - type?: "model_loader_output"; + type: "model_loader_output"; /** * UNet * @description UNet (scheduler, LoRAs) @@ -4330,7 +4330,7 @@ export type components = { * @default noise_output * @enum {string} */ - type?: "noise_output"; + type: "noise_output"; /** * Noise * @description Noise tensor @@ -4435,7 +4435,7 @@ export type components = { * @default model_loader_output_onnx * @enum {string} */ - type?: "model_loader_output_onnx"; + type: "model_loader_output_onnx"; /** * UNet * @description UNet (scheduler, LoRAs) @@ -5253,7 +5253,7 @@ export type components = { * @default sdxl_lora_loader_output * @enum {string} */ - type?: "sdxl_lora_loader_output"; + type: "sdxl_lora_loader_output"; /** * UNet * @description UNet (scheduler, LoRAs) @@ -5308,7 +5308,7 @@ export type components = { * @default sdxl_model_loader_output * @enum {string} */ - type?: "sdxl_model_loader_output"; + type: "sdxl_model_loader_output"; /** * UNet * @description UNet (scheduler, LoRAs) @@ -5428,7 +5428,7 @@ export type components = { * @default sdxl_refiner_model_loader_output * @enum {string} */ - type?: "sdxl_refiner_model_loader_output"; + type: "sdxl_refiner_model_loader_output"; /** * UNet * @description UNet (scheduler, LoRAs) @@ -5824,7 +5824,7 @@ export type components = { * @default string_collection_output * @enum {string} */ - type?: "string_collection_output"; + type: "string_collection_output"; /** * Collection * @description The output strings @@ -5854,11 +5854,11 @@ export type components = { */ type: "string"; /** - * Text + * Value * @description The string value * @default */ - text?: string; + value?: string; }; /** * StringOutput @@ -5870,12 +5870,12 @@ export type components = { * @default string_output * @enum {string} */ - type?: "string_output"; + type: "string_output"; /** - * Text + * Value * @description The output string */ - text: string; + value: string; }; /** * SubModelType @@ -6060,7 +6060,7 @@ export type components = { * @default vae_loader_output * @enum {string} */ - type?: "vae_loader_output"; + type: "vae_loader_output"; /** * VAE * @description VAE @@ -6160,7 +6160,7 @@ export type components = { * If a field should be provided a data type that does not exactly match the python type of the field, use this to provide the type that should be used instead. See the node development docs for detail on adding a new field type, which involves client-side changes. * @enum {string} */ - UIType: "integer" | "float" | "boolean" | "string" | "array" | "ImageField" | "LatentsField" | "ConditioningField" | "ControlField" | "ColorField" | "ImageCollection" | "ConditioningCollection" | "ColorCollection" | "LatentsCollection" | "IntegerCollection" | "FloatCollection" | "StringCollection" | "BooleanCollection" | "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VaeModelField" | "LoRAModelField" | "ControlNetModelField" | "UNetField" | "VaeField" | "ClipField" | "Collection" | "CollectionItem" | "FilePath" | "enum"; + UIType: "integer" | "float" | "boolean" | "string" | "array" | "ImageField" | "LatentsField" | "ConditioningField" | "ControlField" | "ColorField" | "ImageCollection" | "ConditioningCollection" | "ColorCollection" | "LatentsCollection" | "IntegerCollection" | "FloatCollection" | "StringCollection" | "BooleanCollection" | "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VaeModelField" | "LoRAModelField" | "ControlNetModelField" | "UNetField" | "VaeField" | "ClipField" | "Collection" | "CollectionItem" | "FilePath" | "enum" | "Scheduler"; /** * UIComponent * @description The type of UI component to use for a field, used to override the default components, which are inferred from the field type. @@ -6211,18 +6211,18 @@ export type components = { * @enum {string} */ ControlNetModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusion2ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionXLModelFormat * @description An enumeration. * @enum {string} */ StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; From 2514af79a043728145fda0d7771b4fe924b0e3e7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:27:40 +1000 Subject: [PATCH 17/45] feat(ui): crude node outputs display Resets on invoke. Nothing fancy for the UI yet, just simple text (for numbers and strings) or image. For other output types, the output in JSON. --- .../components/panel/ImageOutputPreview.tsx | 17 +++ .../nodes/components/panel/InspectorPanel.tsx | 11 +- .../components/panel/NodeResultsInspector.tsx | 101 ++++++++++++++++++ .../components/panel/NumberOutputPreview.tsx | 13 +++ .../components/panel/StringOutputPreview.tsx | 13 +++ .../src/features/nodes/store/nodesSlice.ts | 37 ++++--- .../web/src/features/nodes/types/types.ts | 29 ++++- .../frontend/web/src/services/api/types.ts | 3 + 8 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/panel/ImageOutputPreview.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/panel/NodeResultsInspector.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/panel/NumberOutputPreview.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/panel/StringOutputPreview.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/ImageOutputPreview.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/ImageOutputPreview.tsx new file mode 100644 index 0000000000..d35e80fa62 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panel/ImageOutputPreview.tsx @@ -0,0 +1,17 @@ +import IAIDndImage from 'common/components/IAIDndImage'; +import { memo } from 'react'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import { ImageOutput } from 'services/api/types'; + +type Props = { + output: ImageOutput; +}; + +const ImageOutputPreview = ({ output }: Props) => { + const { image, width, height } = output; + const { data: imageDTO } = useGetImageDTOQuery(image.image_name); + + return ; +}; + +export default memo(ImageOutputPreview); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx index 587bea19ec..b67f749027 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx @@ -8,6 +8,7 @@ import { } from '@chakra-ui/react'; import { memo } from 'react'; import NodeDataInspector from './NodeDataInspector'; +import NodeResultsInspector from './NodeResultsInspector'; import NodeTemplateInspector from './NodeTemplateInspector'; const InspectorPanel = () => { @@ -15,10 +16,12 @@ const InspectorPanel = () => { { sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }} > - Node Template + Node Outputs Node Data + Node Template - + + + + diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NodeResultsInspector.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/NodeResultsInspector.tsx new file mode 100644 index 0000000000..6e2a2a3dbd --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panel/NodeResultsInspector.tsx @@ -0,0 +1,101 @@ +import { Box, Flex } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { memo } from 'react'; +import ImageOutputPreview from './ImageOutputPreview'; +import NumberOutputPreview from './NumberOutputPreview'; +import ScrollableContent from './ScrollableContent'; +import StringOutputPreview from './StringOutputPreview'; +import { AnyResult } from 'services/events/types'; + +const selector = createSelector( + stateSelector, + ({ nodes }) => { + const lastSelectedNodeId = + nodes.selectedNodes[nodes.selectedNodes.length - 1]; + + const lastSelectedNode = nodes.nodes.find( + (node) => node.id === lastSelectedNodeId + ); + + const nes = + nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__']; + + return { + node: lastSelectedNode, + nes, + }; + }, + defaultSelectorOptions +); + +const NodeResultsInspector = () => { + const { node, nes } = useAppSelector(selector); + + if (!node || !nes) { + return ; + } + + if (nes.outputs.length === 0) { + return ; + } + + return ( + + + + {nes.outputs.map((result, i) => { + if (result.type === 'string_output') { + return ( + + ); + } + if (result.type === 'float_output') { + return ( + + ); + } + if (result.type === 'integer_output') { + return ( + + ); + } + if (result.type === 'image_output') { + return ( + + ); + } + return ( +
+                {JSON.stringify(result, null, 2)}
+              
+ ); + })} +
+
+
+ ); +}; + +export default memo(NodeResultsInspector); + +const getKey = (result: AnyResult, i: number) => `${result.type}-${i}`; diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NumberOutputPreview.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/NumberOutputPreview.tsx new file mode 100644 index 0000000000..ebe03740b3 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panel/NumberOutputPreview.tsx @@ -0,0 +1,13 @@ +import { Text } from '@chakra-ui/react'; +import { memo } from 'react'; +import { FloatOutput, IntegerOutput } from 'services/api/types'; + +type Props = { + output: IntegerOutput | FloatOutput; +}; + +const NumberOutputPreview = ({ output }: Props) => { + return {output.value}; +}; + +export default memo(NumberOutputPreview); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/StringOutputPreview.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/StringOutputPreview.tsx new file mode 100644 index 0000000000..1dce0530dd --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panel/StringOutputPreview.tsx @@ -0,0 +1,13 @@ +import { Text } from '@chakra-ui/react'; +import { memo } from 'react'; +import { StringOutput } from 'services/api/types'; + +type Props = { + output: StringOutput; +}; + +const StringOutputPreview = ({ output }: Props) => { + return {output.value}; +}; + +export default memo(StringOutputPreview); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 23056d50a6..b384060d4b 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -44,6 +44,7 @@ import { isNotesNode, LoRAModelInputFieldValue, MainModelInputFieldValue, + NodeExecutionState, NodeStatus, NotesNodeData, SchedulerInputFieldValue, @@ -55,6 +56,14 @@ import { import { NodesState } from './types'; import { findUnoccupiedPosition } from './util/findUnoccupiedPosition'; +const initialNodeExecutionState: Omit = { + status: NodeStatus.PENDING, + error: null, + progress: null, + progressImage: null, + outputs: [], +}; + export const initialNodesState: NodesState = { nodes: [], edges: [], @@ -67,7 +76,7 @@ export const initialNodesState: NodesState = { shouldShowMinimapPanel: true, shouldValidateGraph: true, shouldAnimateEdges: true, - shouldSnapToGrid: true, + shouldSnapToGrid: false, shouldColorEdges: true, nodeOpacity: 1, selectedNodes: [], @@ -141,10 +150,8 @@ const nodesSlice = createSlice({ } state.nodeExecutionStates[node.id] = { - status: NodeStatus.PENDING, - error: null, - progress: null, - progressImage: null, + nodeId: node.id, + ...initialNodeExecutionState, }; }, edgesChanged: (state, action: PayloadAction) => { @@ -677,10 +684,8 @@ const nodesSlice = createSlice({ newNodes.forEach((node) => { state.nodeExecutionStates[node.id] = { - status: NodeStatus.PENDING, - error: null, - progress: null, - progressImage: null, + nodeId: node.id, + ...initialNodeExecutionState, }; }); }, @@ -700,13 +705,14 @@ const nodesSlice = createSlice({ } }); builder.addCase(appSocketInvocationComplete, (state, action) => { - const { source_node_id } = action.payload.data; - const node = state.nodeExecutionStates[source_node_id]; - if (node) { - node.status = NodeStatus.COMPLETED; - if (node.progress !== null) { - node.progress = 1; + const { source_node_id, result } = action.payload.data; + const nes = state.nodeExecutionStates[source_node_id]; + if (nes) { + nes.status = NodeStatus.COMPLETED; + if (nes.progress !== null) { + nes.progress = 1; } + nes.outputs.push(result); } }); builder.addCase(appSocketInvocationError, (state, action) => { @@ -735,6 +741,7 @@ const nodesSlice = createSlice({ nes.error = null; nes.progress = null; nes.progressImage = null; + nes.outputs = []; }); }); }, diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 3183b31c56..fe0593c5dc 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -9,14 +9,20 @@ import { import { OpenAPIV3 } from 'openapi-types'; import { RgbaColor } from 'react-colorful'; import { Edge, Node } from 'reactflow'; +import { components } from 'services/api/schema'; import { Graph, + GraphExecutionState, ImageDTO, ImageField, _InputField, _OutputField, } from 'services/api/types'; -import { AnyInvocationType, ProgressImage } from 'services/events/types'; +import { + AnyInvocationType, + AnyResult, + ProgressImage, +} from 'services/events/types'; import { O } from 'ts-toolbelt'; import { z } from 'zod'; @@ -671,11 +677,32 @@ export enum NodeStatus { FAILED, } +type SavedOutput = + | components['schemas']['StringOutput'] + | components['schemas']['IntegerOutput'] + | components['schemas']['FloatOutput'] + | components['schemas']['ImageOutput']; + +export const isSavedOutput = ( + output: GraphExecutionState['results'][string] +): output is SavedOutput => + Boolean( + output && + [ + 'string_output', + 'integer_output', + 'float_output', + 'image_output', + ].includes(output?.type) + ); + export type NodeExecutionState = { + nodeId: string; status: NodeStatus; progress: number | null; progressImage: ProgressImage | null; error: string | null; + outputs: AnyResult[]; }; export type FieldIdentifier = { diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index f230354312..4e30794a51 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -155,6 +155,9 @@ export type ZoeDepthImageProcessorInvocation = // Node Outputs export type ImageOutput = s['ImageOutput']; +export type StringOutput = s['StringOutput']; +export type FloatOutput = s['FloatOutput']; +export type IntegerOutput = s['IntegerOutput']; export type IterateInvocationOutput = s['IterateInvocationOutput']; export type CollectInvocationOutput = s['CollectInvocationOutput']; export type LatentsOutput = s['LatentsOutput']; From 165c57c001dd21dffcd0252620af01e8033fec36 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:38:09 +1000 Subject: [PATCH 18/45] feat(ui): add select all to workflow editor --- .../web/src/features/nodes/components/Flow.tsx | 12 ++++++++++-- .../web/src/features/nodes/store/nodesSlice.ts | 11 +++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index 462977c477..b7e528b314 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -26,6 +26,7 @@ import { edgesDeleted, nodesChanged, nodesDeleted, + selectedAll, selectedEdgesChanged, selectedNodesChanged, selectionCopied, @@ -124,11 +125,18 @@ export const Flow = () => { dispatch(contextMenusClosed()); }, [dispatch]); - useHotkeys(['Ctrl+c', 'Meta+c'], () => { + useHotkeys(['Ctrl+c', 'Meta+c'], (e) => { + e.preventDefault(); dispatch(selectionCopied()); }); - useHotkeys(['Ctrl+v', 'Meta+v'], () => { + useHotkeys(['Ctrl+a', 'Meta+a'], (e) => { + e.preventDefault(); + dispatch(selectedAll()); + }); + + useHotkeys(['Ctrl+v', 'Meta+v'], (e) => { + e.preventDefault(); dispatch(selectionPasted()); }); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index b384060d4b..2e39b7cfc1 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -612,6 +612,16 @@ const nodesSlice = createSlice({ ) => { state.mouseOverField = action.payload; }, + 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 + ); + }, selectionCopied: (state) => { state.nodesToCopy = state.nodes.filter((n) => n.selected).map(cloneDeep); state.edgesToCopy = state.edges.filter((e) => e.selected).map(cloneDeep); @@ -801,6 +811,7 @@ export const { mouseOverFieldChanged, selectionCopied, selectionPasted, + selectedAll, } = nodesSlice.actions; export default nodesSlice.reducer; From 0b9ae7419286d537370bda1b58add9f0e6a2d4c5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Aug 2023 21:04:44 +1000 Subject: [PATCH 19/45] fix(stats): RuntimeError: dictionary changed size during iteration --- invokeai/app/services/invocation_stats.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/invokeai/app/services/invocation_stats.py b/invokeai/app/services/invocation_stats.py index 75c36d0e16..6c0b90626e 100644 --- a/invokeai/app/services/invocation_stats.py +++ b/invokeai/app/services/invocation_stats.py @@ -259,12 +259,12 @@ class InvocationStatsService(InvocationStatsServiceBase): def log_stats(self): completed = set() + errored = set() for graph_id, node_log in self._stats.items(): try: current_graph_state = self.graph_execution_manager.get(graph_id) - except ValidationError: - del self._stats[graph_id] - del self._cache_stats[graph_id] + except Exception: + errored.add(graph_id) continue if not current_graph_state.is_complete(): @@ -299,3 +299,7 @@ class InvocationStatsService(InvocationStatsServiceBase): for graph_id in completed: del self._stats[graph_id] del self._cache_stats[graph_id] + + for graph_id in errored: + del self._stats[graph_id] + del self._cache_stats[graph_id] From 211e8203f8205c01c9e4d6d3a4af86c1916e4375 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 19 Aug 2023 14:19:32 +1000 Subject: [PATCH 20/45] feat(ui): organise nodes files - also remove old `.gitignore` of `inputs/` which wasn't used and was ignoring a frontend folder --- .gitignore | 4 - .../features/nodes/components/CustomEdges.tsx | 199 ---------- .../features/nodes/components/CustomNodes.tsx | 9 - .../features/nodes/components/NodeEditor.tsx | 4 +- .../nodes/components/NodeGraphOverlay.tsx | 26 -- .../components/fields/fieldTypes/types.ts | 13 - .../nodes/components/{ => flow}/Flow.tsx | 34 +- .../connectionLines}/CustomConnectionLine.tsx | 7 +- .../flow/edges/InvocationCollapsedEdge.tsx | 94 +++++ .../flow/edges/InvocationDefaultEdge.tsx | 58 +++ .../flow/edges/util/makeEdgeSelector.ts | 41 ++ .../nodes/CurrentImage}/CurrentImageNode.tsx | 2 +- .../nodes}/Invocation/InvocationNode.tsx | 17 +- .../InvocationNodeCollapsedHandles.tsx} | 4 +- .../Invocation/InvocationNodeFooter.tsx} | 10 +- .../Invocation/InvocationNodeHeader.tsx} | 20 +- .../nodes/Invocation/InvocationNodeNotes.tsx} | 14 +- .../InvocationNodeStatusIndicator.tsx} | 4 +- .../InvocationNodeUnknownFallback.tsx} | 8 +- .../Invocation}/InvocationNodeWrapper.tsx | 4 +- .../Invocation}/fields/FieldContextMenu.tsx | 10 +- .../nodes/Invocation}/fields/FieldHandle.tsx | 7 +- .../nodes/Invocation}/fields/FieldTitle.tsx | 6 +- .../fields/FieldTooltipContent.tsx | 6 +- .../nodes/Invocation}/fields/InputField.tsx | 21 +- .../Invocation}/fields/InputFieldRenderer.tsx | 50 ++- .../Invocation}/fields/LinearViewField.tsx | 2 +- .../nodes/Invocation}/fields/OutputField.tsx | 2 +- .../fields/inputs}/BooleanInputField.tsx | 2 +- .../fields/inputs}/ClipInputField.tsx | 2 +- .../fields/inputs}/CollectionInputField.tsx | 2 +- .../inputs}/CollectionItemInputField.tsx | 2 +- .../fields/inputs}/ColorInputField.tsx | 2 +- .../fields/inputs}/ConditioningInputField.tsx | 2 +- .../fields/inputs}/ControlInputField.tsx | 2 +- .../inputs}/ControlNetModelInputField.tsx | 2 +- .../fields/inputs}/EnumInputField.tsx | 2 +- .../inputs}/ImageCollectionInputField.tsx | 2 +- .../fields/inputs}/ImageInputField.tsx | 2 +- .../fields/inputs}/LatentsInputField.tsx | 2 +- .../fields/inputs}/LoRAModelInputField.tsx | 2 +- .../fields/inputs}/MainModelInputField.tsx | 2 +- .../fields/inputs}/NumberInputField.tsx | 2 +- .../fields/inputs}/RefinerModelInputField.tsx | 2 +- .../inputs}/SDXLMainModelInputField.tsx | 2 +- .../fields/inputs}/SchedulerInputField.tsx | 2 +- .../fields/inputs}/StringInputField.tsx | 2 +- .../fields/inputs}/UnetInputField.tsx | 2 +- .../fields/inputs}/VaeInputField.tsx | 2 +- .../fields/inputs}/VaeModelInputField.tsx | 2 +- .../{nodes => flow/nodes/Notes}/NotesNode.tsx | 6 +- .../nodes/common}/NodeCollapseButton.tsx | 0 .../nodes/common}/NodeResizer.tsx | 0 .../nodes/common}/NodeTitle.tsx | 6 +- .../nodes/common}/NodeWrapper.tsx | 5 +- .../BottomLeftPanel}/BottomLeftPanel.tsx | 4 +- .../BottomLeftPanel}/NodeOpacitySlider.tsx | 2 +- .../BottomLeftPanel}/ViewportControls.tsx | 2 +- .../panels/MinimapPanel}/MinimapPanel.tsx | 0 .../TopCenterPanel}/ClearGraphButton.tsx | 0 .../TopCenterPanel}/NodeEditorSettings.tsx | 2 +- .../TopCenterPanel}/NodeInvokeButton.tsx | 0 .../TopCenterPanel}/ReloadSchemaButton.tsx | 0 .../panels/TopCenterPanel}/TopCenterPanel.tsx | 8 +- .../panels/TopLeftPanel}/AddNodeMenu.tsx | 4 +- .../panels/TopLeftPanel}/TopLeftPanel.tsx | 2 +- .../panels/TopRightPanel}/FieldTypeLegend.tsx | 2 +- .../panels/TopRightPanel}/TopRightPanel.tsx | 2 +- .../components/panel/ImageOutputPreview.tsx | 17 - .../components/panel/NumberOutputPreview.tsx | 13 - .../components/panel/StringOutputPreview.tsx | 13 - .../NodeEditorPanelGroup.tsx | 4 +- .../ScrollableContent.tsx | 0 .../inspector/InspectorDataTab.tsx} | 4 +- .../inspector/InspectorOutputsTab.tsx} | 12 +- .../inspector}/InspectorPanel.tsx | 18 +- .../inspector/InspectorTemplateTab.tsx} | 0 .../workflow/WorkflowGeneralTab.tsx} | 4 +- .../workflow/WorkflowJSONTab.tsx} | 4 +- .../workflow/WorkflowLinearTab.tsx} | 6 +- .../workflow/WorkflowNotesTab.tsx} | 4 +- .../workflow}/WorkflowPanel.tsx | 12 +- .../nodes/hooks/useConnectionState.ts | 2 +- .../nodes/hooks/useDoesInputHaveValue.ts | 28 ++ .../src/features/nodes/hooks/useFieldData.ts | 28 ++ .../features/nodes/hooks/useFieldInputKind.ts | 30 ++ .../src/features/nodes/hooks/useFieldLabel.ts | 28 ++ .../src/features/nodes/hooks/useFieldNames.ts | 31 ++ .../features/nodes/hooks/useFieldTemplate.ts | 34 ++ .../nodes/hooks/useFieldTemplateTitle.ts | 34 ++ .../features/nodes/hooks/useFieldType.ts.ts | 33 ++ .../features/nodes/hooks/useHasImageOutput.ts | 31 ++ .../features/nodes/hooks/useIsIntermediate.ts | 27 ++ .../nodes/hooks/useIsMouseOverField.ts | 33 ++ .../src/features/nodes/hooks/useNodeData.ts | 367 +----------------- .../src/features/nodes/hooks/useNodeLabel.ts | 28 ++ .../features/nodes/hooks/useNodeTemplate.ts | 25 ++ .../nodes/hooks/useNodeTemplateTitle.ts | 31 ++ .../src/features/nodes/hooks/useWithFooter.ts | 31 ++ .../web/src/features/nodes/types/constants.ts | 5 + .../web/src/features/nodes/types/types.ts | 9 + 101 files changed, 856 insertions(+), 855 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/CustomNodes.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts rename invokeai/frontend/web/src/features/nodes/components/{ => flow}/Flow.tsx (79%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/connectionLines}/CustomConnectionLine.tsx (88%) create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts rename invokeai/frontend/web/src/features/nodes/components/{nodes => flow/nodes/CurrentImage}/CurrentImageNode.tsx (97%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/nodes}/Invocation/InvocationNode.tsx (76%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation/NodeCollapsedHandles.tsx => flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx} (94%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation/NodeFooter.tsx => flow/nodes/Invocation/InvocationNodeFooter.tsx} (86%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation/NodeHeader.tsx => flow/nodes/Invocation/InvocationNodeHeader.tsx} (54%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation/NodeNotesEdit.tsx => flow/nodes/Invocation/InvocationNodeNotes.tsx} (88%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation/NodeStatusIndicator.tsx => flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx} (97%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation/UnknownNodeFallback.tsx => flow/nodes/Invocation/InvocationNodeUnknownFallback.tsx} (88%) rename invokeai/frontend/web/src/features/nodes/components/{nodes => flow/nodes/Invocation}/InvocationNodeWrapper.tsx (90%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/nodes/Invocation}/fields/FieldContextMenu.tsx (94%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/nodes/Invocation}/fields/FieldHandle.tsx (95%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/nodes/Invocation}/fields/FieldTitle.tsx (95%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/nodes/Invocation}/fields/FieldTooltipContent.tsx (91%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/nodes/Invocation}/fields/InputField.tsx (91%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/nodes/Invocation}/fields/InputFieldRenderer.tsx (79%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/nodes/Invocation}/fields/LinearViewField.tsx (97%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/nodes/Invocation}/fields/OutputField.tsx (97%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/BooleanInputField.tsx (95%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/ClipInputField.tsx (86%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/CollectionInputField.tsx (88%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/CollectionItemInputField.tsx (88%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/ColorInputField.tsx (95%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/ConditioningInputField.tsx (88%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/ControlInputField.tsx (87%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/ControlNetModelInputField.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/EnumInputField.tsx (95%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/ImageCollectionInputField.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/ImageInputField.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/LatentsInputField.tsx (87%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/LoRAModelInputField.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/MainModelInputField.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/NumberInputField.tsx (97%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/RefinerModelInputField.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/SDXLMainModelInputField.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/SchedulerInputField.tsx (97%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/StringInputField.tsx (96%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/UnetInputField.tsx (86%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/VaeInputField.tsx (86%) rename invokeai/frontend/web/src/features/nodes/components/{fields/fieldTypes => flow/nodes/Invocation/fields/inputs}/VaeModelInputField.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{nodes => flow/nodes/Notes}/NotesNode.tsx (92%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation => flow/nodes/common}/NodeCollapseButton.tsx (100%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation => flow/nodes/common}/NodeResizer.tsx (100%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation => flow/nodes/common}/NodeTitle.tsx (95%) rename invokeai/frontend/web/src/features/nodes/components/{Invocation => flow/nodes/common}/NodeWrapper.tsx (95%) rename invokeai/frontend/web/src/features/nodes/components/{editorPanels => flow/panels/BottomLeftPanel}/BottomLeftPanel.tsx (75%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/panels/BottomLeftPanel}/NodeOpacitySlider.tsx (92%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/panels/BottomLeftPanel}/ViewportControls.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{editorPanels => flow/panels/MinimapPanel}/MinimapPanel.tsx (100%) rename invokeai/frontend/web/src/features/nodes/components/{ui => flow/panels/TopCenterPanel}/ClearGraphButton.tsx (100%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/panels/TopCenterPanel}/NodeEditorSettings.tsx (98%) rename invokeai/frontend/web/src/features/nodes/components/{ui => flow/panels/TopCenterPanel}/NodeInvokeButton.tsx (100%) rename invokeai/frontend/web/src/features/nodes/components/{ui => flow/panels/TopCenterPanel}/ReloadSchemaButton.tsx (100%) rename invokeai/frontend/web/src/features/nodes/components/{editorPanels => flow/panels/TopCenterPanel}/TopCenterPanel.tsx (69%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/panels/TopLeftPanel}/AddNodeMenu.tsx (96%) rename invokeai/frontend/web/src/features/nodes/components/{editorPanels => flow/panels/TopLeftPanel}/TopLeftPanel.tsx (82%) rename invokeai/frontend/web/src/features/nodes/components/{ => flow/panels/TopRightPanel}/FieldTypeLegend.tsx (93%) rename invokeai/frontend/web/src/features/nodes/components/{editorPanels => flow/panels/TopRightPanel}/TopRightPanel.tsx (89%) delete mode 100644 invokeai/frontend/web/src/features/nodes/components/panel/ImageOutputPreview.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/panel/NumberOutputPreview.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/panel/StringOutputPreview.tsx rename invokeai/frontend/web/src/features/nodes/components/{panel => sidePanel}/NodeEditorPanelGroup.tsx (91%) rename invokeai/frontend/web/src/features/nodes/components/{panel => sidePanel}/ScrollableContent.tsx (100%) rename invokeai/frontend/web/src/features/nodes/components/{panel/NodeDataInspector.tsx => sidePanel/inspector/InspectorDataTab.tsx} (93%) rename invokeai/frontend/web/src/features/nodes/components/{panel/NodeResultsInspector.tsx => sidePanel/inspector/InspectorOutputsTab.tsx} (88%) rename invokeai/frontend/web/src/features/nodes/components/{panel => sidePanel/inspector}/InspectorPanel.tsx (66%) rename invokeai/frontend/web/src/features/nodes/components/{panel/NodeTemplateInspector.tsx => sidePanel/inspector/InspectorTemplateTab.tsx} (100%) rename invokeai/frontend/web/src/features/nodes/components/{panel/workflow/GeneralTab.tsx => sidePanel/workflow/WorkflowGeneralTab.tsx} (98%) rename invokeai/frontend/web/src/features/nodes/components/{panel/workflow/WorkflowTab.tsx => sidePanel/workflow/WorkflowJSONTab.tsx} (92%) rename invokeai/frontend/web/src/features/nodes/components/{panel/workflow/LinearTab.tsx => sidePanel/workflow/WorkflowLinearTab.tsx} (90%) rename invokeai/frontend/web/src/features/nodes/components/{panel/workflow/NotesTab.tsx => sidePanel/workflow/WorkflowNotesTab.tsx} (94%) rename invokeai/frontend/web/src/features/nodes/components/{panel => sidePanel/workflow}/WorkflowPanel.tsx (76%) create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldData.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldInputKind.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useIsMouseOverField.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts diff --git a/.gitignore b/.gitignore index cc000de20e..ca136a17e5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,6 @@ invokeai.init # ignore the Anaconda/Miniconda installer used while building Docker image anaconda.sh -# ignore a directory which serves as a place for initial images -inputs/ - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -213,7 +210,6 @@ invokeai/frontend/node_modules gfpgan/ models/ldm/stable-diffusion-v1/*.sha256 - # GFPGAN model files gfpgan/ diff --git a/invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx b/invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx deleted file mode 100644 index f80f0451e4..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { Badge, Flex } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { stateSelector } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; -import { memo, useMemo } from 'react'; -import { - BaseEdge, - EdgeLabelRenderer, - EdgeProps, - getBezierPath, -} from 'reactflow'; -import { FIELDS, colorTokenToCssVar } from '../types/constants'; -import { isInvocationNode } from '../types/types'; - -const makeEdgeSelector = ( - source: string, - sourceHandleId: string | null | undefined, - target: string, - targetHandleId: string | null | undefined, - selected?: boolean -) => - createSelector( - stateSelector, - ({ nodes }) => { - const sourceNode = nodes.nodes.find((node) => node.id === source); - const targetNode = nodes.nodes.find((node) => node.id === target); - - const isInvocationToInvocationEdge = - isInvocationNode(sourceNode) && isInvocationNode(targetNode); - - const isSelected = - sourceNode?.selected || targetNode?.selected || selected; - const sourceType = isInvocationToInvocationEdge - ? sourceNode?.data?.outputs[sourceHandleId || '']?.type - : undefined; - - const stroke = - sourceType && nodes.shouldColorEdges - ? colorTokenToCssVar(FIELDS[sourceType].color) - : colorTokenToCssVar('base.500'); - - return { - isSelected, - shouldAnimate: nodes.shouldAnimateEdges && isSelected, - stroke, - }; - }, - defaultSelectorOptions - ); - -const CollapsedEdge = memo( - ({ - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - markerEnd, - data, - selected, - source, - target, - sourceHandleId, - targetHandleId, - }: EdgeProps<{ count: number }>) => { - const selector = useMemo( - () => - makeEdgeSelector( - source, - sourceHandleId, - target, - targetHandleId, - selected - ), - [selected, source, sourceHandleId, target, targetHandleId] - ); - - const { isSelected, shouldAnimate } = useAppSelector(selector); - - const [edgePath, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - - const { base500 } = useChakraThemeTokens(); - - return ( - <> - - {data?.count && data.count > 1 && ( - - - - {data.count} - - - - )} - - ); - } -); - -CollapsedEdge.displayName = 'CollapsedEdge'; - -const DefaultEdge = memo( - ({ - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - markerEnd, - selected, - source, - target, - sourceHandleId, - targetHandleId, - }: EdgeProps) => { - const selector = useMemo( - () => - makeEdgeSelector( - source, - sourceHandleId, - target, - targetHandleId, - selected - ), - [source, sourceHandleId, target, targetHandleId, selected] - ); - - const { isSelected, shouldAnimate, stroke } = useAppSelector(selector); - - const [edgePath] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - - return ( - - ); - } -); - -DefaultEdge.displayName = 'DefaultEdge'; - -export const edgeTypes = { - collapsed: CollapsedEdge, - default: DefaultEdge, -}; diff --git a/invokeai/frontend/web/src/features/nodes/components/CustomNodes.tsx b/invokeai/frontend/web/src/features/nodes/components/CustomNodes.tsx deleted file mode 100644 index be845df435..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/CustomNodes.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import CurrentImageNode from './nodes/CurrentImageNode'; -import InvocationNodeWrapper from './nodes/InvocationNodeWrapper'; -import NotesNode from './nodes/NotesNode'; - -export const nodeTypes = { - invocation: InvocationNodeWrapper, - current_image: CurrentImageNode, - notes: NotesNode, -}; diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 5e610cfc39..ffda25c2a6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -6,8 +6,8 @@ import { memo, useState } from 'react'; import { MdDeviceHub } from 'react-icons/md'; import { Panel, PanelGroup } from 'react-resizable-panels'; import 'reactflow/dist/style.css'; -import NodeEditorPanelGroup from './panel/NodeEditorPanelGroup'; -import { Flow } from './Flow'; +import NodeEditorPanelGroup from './sidePanel/NodeEditorPanelGroup'; +import { Flow } from './flow/Flow'; import { AnimatePresence, motion } from 'framer-motion'; const NodeEditor = () => { diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx deleted file mode 100644 index 4525dc5f6b..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { RootState } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON'; -import { omit } from 'lodash-es'; -import { useMemo } from 'react'; -import { useDebounce } from 'use-debounce'; -import { buildNodesGraph } from '../util/graphBuilders/buildNodesGraph'; - -const useNodesGraph = () => { - const nodes = useAppSelector((state: RootState) => state.nodes); - const [debouncedNodes] = useDebounce(nodes, 300); - const graph = useMemo( - () => omit(buildNodesGraph(debouncedNodes), 'id'), - [debouncedNodes] - ); - - return graph; -}; - -const NodeGraph = () => { - const graph = useNodesGraph(); - - return ; -}; - -export default NodeGraph; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts deleted file mode 100644 index 5a5e3a9dcf..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - InputFieldTemplate, - InputFieldValue, -} from 'features/nodes/types/types'; - -export type FieldComponentProps< - V extends InputFieldValue, - T extends InputFieldTemplate -> = { - nodeId: string; - field: V; - fieldTemplate: T; -}; diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx similarity index 79% rename from invokeai/frontend/web/src/features/nodes/components/Flow.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index b7e528b314..817ef4ff3d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -17,7 +17,7 @@ import { ProOptions, ReactFlow, } from 'reactflow'; -import { useIsValidConnection } from '../hooks/useIsValidConnection'; +import { useIsValidConnection } from '../../hooks/useIsValidConnection'; import { connectionEnded, connectionMade, @@ -32,18 +32,32 @@ import { selectionCopied, selectionPasted, viewportChanged, -} from '../store/nodesSlice'; -import { CustomConnectionLine } from './CustomConnectionLine'; -import { edgeTypes } from './CustomEdges'; -import { nodeTypes } from './CustomNodes'; -import BottomLeftPanel from './editorPanels/BottomLeftPanel'; -import MinimapPanel from './editorPanels/MinimapPanel'; -import TopCenterPanel from './editorPanels/TopCenterPanel'; -import TopLeftPanel from './editorPanels/TopLeftPanel'; -import TopRightPanel from './editorPanels/TopRightPanel'; +} from '../../store/nodesSlice'; +import CustomConnectionLine from './connectionLines/CustomConnectionLine'; +import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge'; +import InvocationDefaultEdge from './edges/InvocationDefaultEdge'; +import CurrentImageNode from './nodes/CurrentImage/CurrentImageNode'; +import InvocationNodeWrapper from './nodes/Invocation/InvocationNodeWrapper'; +import NotesNode from './nodes/Notes/NotesNode'; +import BottomLeftPanel from './panels/BottomLeftPanel/BottomLeftPanel'; +import MinimapPanel from './panels/MinimapPanel/MinimapPanel'; +import TopCenterPanel from './panels/TopCenterPanel/TopCenterPanel'; +import TopLeftPanel from './panels/TopLeftPanel/TopLeftPanel'; +import TopRightPanel from './panels/TopRightPanel/TopRightPanel'; const DELETE_KEYS = ['Delete', 'Backspace']; +const edgeTypes = { + collapsed: InvocationCollapsedEdge, + default: InvocationDefaultEdge, +}; + +const nodeTypes = { + invocation: InvocationNodeWrapper, + current_image: CurrentImageNode, + notes: NotesNode, +}; + // TODO: can we support reactflow? if not, we could style the attribution so it matches the app const proOptions: ProOptions = { hideAttribution: true }; diff --git a/invokeai/frontend/web/src/features/nodes/components/CustomConnectionLine.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx similarity index 88% rename from invokeai/frontend/web/src/features/nodes/components/CustomConnectionLine.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx index 678d8e3d1d..ad8ba3dc62 100644 --- a/invokeai/frontend/web/src/features/nodes/components/CustomConnectionLine.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx @@ -2,7 +2,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { ConnectionLineComponentProps, getBezierPath } from 'reactflow'; -import { FIELDS, colorTokenToCssVar } from '../types/constants'; +import { FIELDS, colorTokenToCssVar } from '../../../types/constants'; +import { memo } from 'react'; const selector = createSelector(stateSelector, ({ nodes }) => { const { shouldAnimateEdges, currentConnectionFieldType, shouldColorEdges } = @@ -25,7 +26,7 @@ const selector = createSelector(stateSelector, ({ nodes }) => { }; }); -export const CustomConnectionLine = ({ +const CustomConnectionLine = ({ fromX, fromY, fromPosition, @@ -59,3 +60,5 @@ export const CustomConnectionLine = ({ ); }; + +export default memo(CustomConnectionLine); 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 new file mode 100644 index 0000000000..fca38def34 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx @@ -0,0 +1,94 @@ +import { Badge, Flex } from '@chakra-ui/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; +import { memo, useMemo } from 'react'; +import { + BaseEdge, + EdgeLabelRenderer, + EdgeProps, + getBezierPath, +} from 'reactflow'; +import { makeEdgeSelector } from './util/makeEdgeSelector'; + +const InvocationCollapsedEdge = ({ + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + markerEnd, + data, + selected, + source, + target, + sourceHandleId, + targetHandleId, +}: EdgeProps<{ count: number }>) => { + const selector = useMemo( + () => + makeEdgeSelector( + source, + sourceHandleId, + target, + targetHandleId, + selected + ), + [selected, source, sourceHandleId, target, targetHandleId] + ); + + const { isSelected, shouldAnimate } = useAppSelector(selector); + + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + const { base500 } = useChakraThemeTokens(); + + return ( + <> + + {data?.count && data.count > 1 && ( + + + + {data.count} + + + + )} + + ); +}; + +export default memo(InvocationCollapsedEdge); 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 new file mode 100644 index 0000000000..effefb12ab --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx @@ -0,0 +1,58 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { memo, useMemo } from 'react'; +import { BaseEdge, EdgeProps, getBezierPath } from 'reactflow'; +import { makeEdgeSelector } from './util/makeEdgeSelector'; + +const InvocationDefaultEdge = ({ + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + markerEnd, + selected, + source, + target, + sourceHandleId, + targetHandleId, +}: EdgeProps) => { + const selector = useMemo( + () => + makeEdgeSelector( + source, + sourceHandleId, + target, + targetHandleId, + selected + ), + [source, sourceHandleId, target, targetHandleId, selected] + ); + + const { isSelected, shouldAnimate, stroke } = useAppSelector(selector); + + const [edgePath] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + return ( + + ); +}; + +export default memo(InvocationDefaultEdge); 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 new file mode 100644 index 0000000000..ed692042c6 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts @@ -0,0 +1,41 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { FIELDS, colorTokenToCssVar } from 'features/nodes/types/constants'; +import { isInvocationNode } from 'features/nodes/types/types'; + +export const makeEdgeSelector = ( + source: string, + sourceHandleId: string | null | undefined, + target: string, + targetHandleId: string | null | undefined, + selected?: boolean +) => + createSelector( + stateSelector, + ({ nodes }) => { + const sourceNode = nodes.nodes.find((node) => node.id === source); + const targetNode = nodes.nodes.find((node) => node.id === target); + + const isInvocationToInvocationEdge = + isInvocationNode(sourceNode) && isInvocationNode(targetNode); + + const isSelected = + sourceNode?.selected || targetNode?.selected || selected; + const sourceType = isInvocationToInvocationEdge + ? sourceNode?.data?.outputs[sourceHandleId || '']?.type + : undefined; + + const stroke = + sourceType && nodes.shouldColorEdges + ? colorTokenToCssVar(FIELDS[sourceType].color) + : colorTokenToCssVar('base.500'); + + return { + isSelected, + shouldAnimate: nodes.shouldAnimateEdges && isSelected, + stroke, + }; + }, + defaultSelectorOptions + ); diff --git a/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx similarity index 97% rename from invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx index 985978f72d..6a8a2a3552 100644 --- a/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx @@ -7,7 +7,7 @@ import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import { PropsWithChildren, memo } from 'react'; import { useSelector } from 'react-redux'; import { NodeProps } from 'reactflow'; -import NodeWrapper from '../Invocation/NodeWrapper'; +import NodeWrapper from '../common/NodeWrapper'; const selector = createSelector(stateSelector, ({ system, gallery }) => { const imageDTO = gallery.selection[gallery.selection.length - 1]; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx similarity index 76% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx index 03b69faf78..624578003e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx @@ -1,11 +1,12 @@ import { Flex } from '@chakra-ui/react'; -import { useFieldNames, useWithFooter } from 'features/nodes/hooks/useNodeData'; import { memo } from 'react'; -import InputField from '../fields/InputField'; -import OutputField from '../fields/OutputField'; -import NodeFooter from './NodeFooter'; -import NodeHeader from './NodeHeader'; -import NodeWrapper from './NodeWrapper'; +import InvocationNodeFooter from './InvocationNodeFooter'; +import InvocationNodeHeader from './InvocationNodeHeader'; +import NodeWrapper from '../common/NodeWrapper'; +import OutputField from './fields/OutputField'; +import InputField from './fields/InputField'; +import { useFieldNames } from 'features/nodes/hooks/useFieldNames'; +import { useWithFooter } from 'features/nodes/hooks/useWithFooter'; type Props = { nodeId: string; @@ -22,7 +23,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { return ( - { ))}
- {withFooter && } + {withFooter && } )} diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx similarity index 94% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx index 32dd554ef4..30e02bfd84 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx @@ -10,7 +10,7 @@ interface Props { nodeId: string; } -const NodeCollapsedHandles = ({ nodeId }: Props) => { +const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => { const data = useNodeData(nodeId); const { base400, base600 } = useChakraThemeTokens(); const backgroundColor = useColorModeValue(base400, base600); @@ -71,4 +71,4 @@ const NodeCollapsedHandles = ({ nodeId }: Props) => { ); }; -export default memo(NodeCollapsedHandles); +export default memo(InvocationNodeCollapsedHandles); diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx similarity index 86% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx index c858872b57..ffcdd13fef 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx @@ -6,10 +6,8 @@ import { Spacer, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import { - useHasImageOutput, - useIsIntermediate, -} from 'features/nodes/hooks/useNodeData'; +import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput'; +import { useIsIntermediate } from 'features/nodes/hooks/useIsIntermediate'; import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import { ChangeEvent, memo, useCallback } from 'react'; @@ -18,7 +16,7 @@ type Props = { nodeId: string; }; -const NodeFooter = ({ nodeId }: Props) => { +const InvocationNodeFooter = ({ nodeId }: Props) => { return ( { ); }; -export default memo(NodeFooter); +export default memo(InvocationNodeFooter); const SaveImageCheckbox = memo(({ nodeId }: { nodeId: string }) => { const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeHeader.tsx similarity index 54% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeHeader.tsx index cff15812fd..cd6c5215d1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeHeader.tsx @@ -1,10 +1,10 @@ import { Flex } from '@chakra-ui/react'; import { memo } from 'react'; -import NodeCollapseButton from '../Invocation/NodeCollapseButton'; -import NodeCollapsedHandles from '../Invocation/NodeCollapsedHandles'; -import NodeNotesEdit from '../Invocation/NodeNotesEdit'; -import NodeStatusIndicator from '../Invocation/NodeStatusIndicator'; -import NodeTitle from '../Invocation/NodeTitle'; +import NodeCollapseButton from '../common/NodeCollapseButton'; +import NodeTitle from '../common/NodeTitle'; +import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles'; +import InvocationNodeNotes from './InvocationNodeNotes'; +import InvocationNodeStatusIndicator from './InvocationNodeStatusIndicator'; type Props = { nodeId: string; @@ -14,7 +14,7 @@ type Props = { selected: boolean; }; -const NodeHeader = ({ nodeId, isOpen }: Props) => { +const InvocationNodeHeader = ({ nodeId, isOpen }: Props) => { return ( { - - + + - {!isOpen && } + {!isOpen && } ); }; -export default memo(NodeHeader); +export default memo(InvocationNodeHeader); diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotes.tsx similarity index 88% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotes.tsx index 9330ef3b09..aca5f75224 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotes.tsx @@ -16,12 +16,10 @@ import { } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import IAITextarea from 'common/components/IAITextarea'; -import { - useNodeData, - useNodeLabel, - useNodeTemplate, - useNodeTemplateTitle, -} from 'features/nodes/hooks/useNodeData'; +import { useNodeData } from 'features/nodes/hooks/useNodeData'; +import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; +import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle'; import { nodeNotesChanged } from 'features/nodes/store/nodesSlice'; import { isInvocationNodeData } from 'features/nodes/types/types'; import { ChangeEvent, memo, useCallback } from 'react'; @@ -31,7 +29,7 @@ interface Props { nodeId: string; } -const NodeNotesEdit = ({ nodeId }: Props) => { +const InvocationNodeNotes = ({ nodeId }: Props) => { const { isOpen, onOpen, onClose } = useDisclosure(); const label = useNodeLabel(nodeId); const title = useNodeTemplateTitle(nodeId); @@ -76,7 +74,7 @@ const NodeNotesEdit = ({ nodeId }: Props) => { ); }; -export default memo(NodeNotesEdit); +export default memo(InvocationNodeNotes); const TooltipContent = memo(({ nodeId }: { nodeId: string }) => { const data = useNodeData(nodeId); diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx similarity index 97% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx index d53fec4b42..6e1da90ad8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx @@ -28,7 +28,7 @@ const circleStyles = { '.chakra-progress__track': { stroke: 'transparent' }, }; -const NodeStatusIndicator = ({ nodeId }: Props) => { +const InvocationNodeStatusIndicator = ({ nodeId }: Props) => { const selectNodeExecutionState = useMemo( () => createSelector( @@ -64,7 +64,7 @@ const NodeStatusIndicator = ({ nodeId }: Props) => { ); }; -export default memo(NodeStatusIndicator); +export default memo(InvocationNodeStatusIndicator); type TooltipLabelProps = { nodeExecutionState: NodeExecutionState; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeUnknownFallback.tsx similarity index 88% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeUnknownFallback.tsx index 664a788b5a..7ec59f00f0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeUnknownFallback.tsx @@ -1,8 +1,8 @@ import { Box, Flex, Text } from '@chakra-ui/react'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import { memo } from 'react'; -import NodeCollapseButton from '../Invocation/NodeCollapseButton'; -import NodeWrapper from '../Invocation/NodeWrapper'; +import NodeCollapseButton from '../common/NodeCollapseButton'; +import NodeWrapper from '../common/NodeWrapper'; type Props = { nodeId: string; @@ -12,7 +12,7 @@ type Props = { selected: boolean; }; -const UnknownNodeFallback = ({ +const InvocationNodeUnknownFallback = ({ nodeId, isOpen, label, @@ -72,4 +72,4 @@ const UnknownNodeFallback = ({ ); }; -export default memo(UnknownNodeFallback); +export default memo(InvocationNodeUnknownFallback); diff --git a/invokeai/frontend/web/src/features/nodes/components/nodes/InvocationNodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx similarity index 90% rename from invokeai/frontend/web/src/features/nodes/components/nodes/InvocationNodeWrapper.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx index 26bda27d8b..3c79eac1d3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/nodes/InvocationNodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx @@ -5,7 +5,7 @@ import { InvocationNodeData } from 'features/nodes/types/types'; import { memo, useMemo } from 'react'; import { NodeProps } from 'reactflow'; import InvocationNode from '../Invocation/InvocationNode'; -import UnknownNodeFallback from '../Invocation/UnknownNodeFallback'; +import InvocationNodeUnknownFallback from './InvocationNodeUnknownFallback'; const InvocationNodeWrapper = (props: NodeProps) => { const { data, selected } = props; @@ -23,7 +23,7 @@ const InvocationNodeWrapper = (props: NodeProps) => { if (!nodeTemplate) { return ( - { const dispatch = useAppDispatch(); const label = useFieldLabel(nodeId, fieldName); const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind); + const input = useFieldInputKind(nodeId, fieldName); const skipEvent = useCallback((e: MouseEvent) => { e.preventDefault(); @@ -54,7 +53,6 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => { [fieldName, nodeId] ); - const input = useFieldInputKind(nodeId, fieldName); const mayExpose = useMemo( () => ['any', 'direct'].includes(input ?? '__UNKNOWN_INPUT__'), [input] diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx similarity index 95% rename from invokeai/frontend/web/src/features/nodes/components/fields/FieldHandle.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx index f79a57a4eb..16e06471f3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx @@ -5,8 +5,11 @@ import { FIELDS, HANDLE_TOOLTIP_OPEN_DELAY, colorTokenToCssVar, -} from '../../types/constants'; -import { InputFieldTemplate, OutputFieldTemplate } from '../../types/types'; +} from '../../../../../types/constants'; +import { + InputFieldTemplate, + OutputFieldTemplate, +} from '../../../../../types/types'; export const handleBaseStyles: CSSProperties = { position: 'absolute', diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTitle.tsx similarity index 95% rename from invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTitle.tsx index 8fb22422cd..9ac169f047 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTitle.tsx @@ -7,10 +7,8 @@ import { useEditableControls, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import { - useFieldLabel, - useFieldTemplateTitle, -} from 'features/nodes/hooks/useNodeData'; +import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel'; +import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle'; import { fieldLabelChanged } from 'features/nodes/store/nodesSlice'; import { MouseEvent, memo, useCallback, useEffect, useState } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx similarity index 91% rename from invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx index 03ca843780..341c1af704 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx @@ -1,8 +1,6 @@ import { Flex, Text } from '@chakra-ui/react'; -import { - useFieldData, - useFieldTemplate, -} from 'features/nodes/hooks/useNodeData'; +import { useFieldData } from 'features/nodes/hooks/useFieldData'; +import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate'; import { FIELDS } from 'features/nodes/types/constants'; import { isInputFieldTemplate, diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx similarity index 91% rename from invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx index c64b8c8dfe..df31c3e22f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx @@ -1,14 +1,12 @@ import { Box, Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; import SelectionOverlay from 'common/components/SelectionOverlay'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; -import { - useDoesInputHaveValue, - useFieldInputKind, - useFieldTemplate, - useIsMouseOverField, -} from 'features/nodes/hooks/useNodeData'; +import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue'; +import { useFieldInputKind } from 'features/nodes/hooks/useFieldInputKind'; +import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate'; +import { useIsMouseOverField } from 'features/nodes/hooks/useIsMouseOverField'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; -import { PropsWithChildren, memo, useCallback, useMemo, useState } from 'react'; +import { PropsWithChildren, memo, useMemo } from 'react'; import FieldContextMenu from './FieldContextMenu'; import FieldHandle from './FieldHandle'; import FieldTitle from './FieldTitle'; @@ -24,15 +22,6 @@ const InputField = ({ nodeId, fieldName }: Props) => { const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input'); const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName); const input = useFieldInputKind(nodeId, fieldName); - const [isHovered, setIsHovered] = useState(false); - - const handleMouseOver = useCallback(() => { - setIsHovered(true); - }, []); - - const handleMouseOut = useCallback(() => { - setIsHovered(false); - }, []); const { isConnected, diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx similarity index 79% rename from invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index 7eda868447..9b3ce100c8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -1,31 +1,29 @@ import { Box, Text } from '@chakra-ui/react'; -import { - useFieldData, - useFieldTemplate, -} from 'features/nodes/hooks/useNodeData'; +import { useFieldData } from 'features/nodes/hooks/useFieldData'; +import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate'; import { memo } from 'react'; -import BooleanInputField from './fieldTypes/BooleanInputField'; -import ClipInputField from './fieldTypes/ClipInputField'; -import CollectionInputField from './fieldTypes/CollectionInputField'; -import CollectionItemInputField from './fieldTypes/CollectionItemInputField'; -import ColorInputField from './fieldTypes/ColorInputField'; -import ConditioningInputField from './fieldTypes/ConditioningInputField'; -import ControlInputField from './fieldTypes/ControlInputField'; -import ControlNetModelInputField from './fieldTypes/ControlNetModelInputField'; -import EnumInputField from './fieldTypes/EnumInputField'; -import ImageCollectionInputField from './fieldTypes/ImageCollectionInputField'; -import ImageInputField from './fieldTypes/ImageInputField'; -import LatentsInputField from './fieldTypes/LatentsInputField'; -import LoRAModelInputField from './fieldTypes/LoRAModelInputField'; -import MainModelInputField from './fieldTypes/MainModelInputField'; -import NumberInputField from './fieldTypes/NumberInputField'; -import RefinerModelInputField from './fieldTypes/RefinerModelInputField'; -import SDXLMainModelInputField from './fieldTypes/SDXLMainModelInputField'; -import SchedulerInputField from './fieldTypes/SchedulerInputField'; -import StringInputField from './fieldTypes/StringInputField'; -import UnetInputField from './fieldTypes/UnetInputField'; -import VaeInputField from './fieldTypes/VaeInputField'; -import VaeModelInputField from './fieldTypes/VaeModelInputField'; +import BooleanInputField from './inputs/BooleanInputField'; +import ClipInputField from './inputs/ClipInputField'; +import CollectionInputField from './inputs/CollectionInputField'; +import CollectionItemInputField from './inputs/CollectionItemInputField'; +import ColorInputField from './inputs/ColorInputField'; +import ConditioningInputField from './inputs/ConditioningInputField'; +import ControlInputField from './inputs/ControlInputField'; +import ControlNetModelInputField from './inputs/ControlNetModelInputField'; +import EnumInputField from './inputs/EnumInputField'; +import ImageCollectionInputField from './inputs/ImageCollectionInputField'; +import ImageInputField from './inputs/ImageInputField'; +import LatentsInputField from './inputs/LatentsInputField'; +import LoRAModelInputField from './inputs/LoRAModelInputField'; +import MainModelInputField from './inputs/MainModelInputField'; +import NumberInputField from './inputs/NumberInputField'; +import RefinerModelInputField from './inputs/RefinerModelInputField'; +import SDXLMainModelInputField from './inputs/SDXLMainModelInputField'; +import SchedulerInputField from './inputs/SchedulerInputField'; +import StringInputField from './inputs/StringInputField'; +import UnetInputField from './inputs/UnetInputField'; +import VaeInputField from './inputs/VaeInputField'; +import VaeModelInputField from './inputs/VaeModelInputField'; type InputFieldProps = { nodeId: string; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx similarity index 97% rename from invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index 2f4dc84827..cbf4a19137 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -2,7 +2,7 @@ import { Flex, FormControl, FormLabel, Icon, Tooltip } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import IAIIconButton from 'common/components/IAIIconButton'; import SelectionOverlay from 'common/components/SelectionOverlay'; -import { useIsMouseOverField } from 'features/nodes/hooks/useNodeData'; +import { useIsMouseOverField } from 'features/nodes/hooks/useIsMouseOverField'; import { workflowExposedFieldRemoved } from 'features/nodes/store/nodesSlice'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx similarity index 97% rename from invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx index 2a257d741e..7c88318fa1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx @@ -6,7 +6,7 @@ import { Tooltip, } from '@chakra-ui/react'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; -import { useFieldTemplate } from 'features/nodes/hooks/useNodeData'; +import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { PropsWithChildren, memo } from 'react'; import FieldHandle from './FieldHandle'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BooleanInputField.tsx similarity index 95% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BooleanInputField.tsx index 296f56c9a2..c9f83403f6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BooleanInputField.tsx @@ -4,9 +4,9 @@ import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice'; import { BooleanInputFieldTemplate, BooleanInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { ChangeEvent, memo, useCallback } from 'react'; -import { FieldComponentProps } from './types'; const BooleanInputFieldComponent = ( props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ClipInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ClipInputField.tsx similarity index 86% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ClipInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ClipInputField.tsx index 37c3db3d11..cf5d7fae95 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ClipInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ClipInputField.tsx @@ -1,9 +1,9 @@ import { ClipInputFieldTemplate, ClipInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo } from 'react'; -import { FieldComponentProps } from './types'; const ClipInputFieldComponent = ( _props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/CollectionInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CollectionInputField.tsx similarity index 88% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/CollectionInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CollectionInputField.tsx index 99c88af2cb..7cbc46f28c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/CollectionInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CollectionInputField.tsx @@ -1,9 +1,9 @@ import { CollectionInputFieldTemplate, CollectionInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo } from 'react'; -import { FieldComponentProps } from './types'; const CollectionInputFieldComponent = ( _props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/CollectionItemInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CollectionItemInputField.tsx similarity index 88% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/CollectionItemInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CollectionItemInputField.tsx index 00f753d8d3..e67a20bdfb 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/CollectionItemInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/CollectionItemInputField.tsx @@ -1,9 +1,9 @@ import { CollectionItemInputFieldTemplate, CollectionItemInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo } from 'react'; -import { FieldComponentProps } from './types'; const CollectionItemInputFieldComponent = ( _props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ColorInputField.tsx similarity index 95% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ColorInputField.tsx index 422c3ba48f..c2af279cb5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ColorInputField.tsx @@ -3,10 +3,10 @@ import { fieldColorValueChanged } from 'features/nodes/store/nodesSlice'; import { ColorInputFieldTemplate, ColorInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo, useCallback } from 'react'; import { RgbaColor, RgbaColorPicker } from 'react-colorful'; -import { FieldComponentProps } from './types'; const ColorInputFieldComponent = ( props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ConditioningInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ConditioningInputField.tsx similarity index 88% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ConditioningInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ConditioningInputField.tsx index e280251cd3..9d174f40c5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ConditioningInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ConditioningInputField.tsx @@ -1,9 +1,9 @@ import { ConditioningInputFieldTemplate, ConditioningInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo } from 'react'; -import { FieldComponentProps } from './types'; const ConditioningInputFieldComponent = ( _props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlInputField.tsx similarity index 87% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlInputField.tsx index 6b2b3deafb..346dd49b21 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlInputField.tsx @@ -1,9 +1,9 @@ import { ControlInputFieldTemplate, ControlInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo } from 'react'; -import { FieldComponentProps } from './types'; const ControlInputFieldComponent = ( _props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelInputField.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelInputField.tsx index 3192e7583b..f66c8b0cfd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelInputField.tsx @@ -5,13 +5,13 @@ import { fieldControlNetModelValueChanged } from 'features/nodes/store/nodesSlic import { ControlNetModelInputFieldTemplate, ControlNetModelInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { modelIdToControlNetModelParam } from 'features/parameters/util/modelIdToControlNetModelParam'; import { forEach } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; import { useGetControlNetModelsQuery } from 'services/api/endpoints/models'; -import { FieldComponentProps } from './types'; const ControlNetModelInputFieldComponent = ( props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/EnumInputField.tsx similarity index 95% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/EnumInputField.tsx index f26dcac2d0..84ecf4842e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/EnumInputField.tsx @@ -4,9 +4,9 @@ import { fieldEnumModelValueChanged } from 'features/nodes/store/nodesSlice'; import { EnumInputFieldTemplate, EnumInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { ChangeEvent, memo, useCallback } from 'react'; -import { FieldComponentProps } from './types'; const EnumInputFieldComponent = ( props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageCollectionInputField.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageCollectionInputField.tsx index 4efd0b7775..4a19f08614 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageCollectionInputField.tsx @@ -1,6 +1,7 @@ import { ImageCollectionInputFieldTemplate, ImageCollectionInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo } from 'react'; @@ -11,7 +12,6 @@ import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks'; import { NodesMultiImageDropData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import { FieldComponentProps } from './types'; const ImageCollectionInputFieldComponent = ( props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageInputField.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageInputField.tsx index cb2dfcac66..e04d0d1edc 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageInputField.tsx @@ -11,12 +11,12 @@ import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { ImageInputFieldTemplate, ImageInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo, useCallback, useMemo } from 'react'; import { FaUndo } from 'react-icons/fa'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { PostUploadAction } from 'services/api/types'; -import { FieldComponentProps } from './types'; const ImageInputFieldComponent = ( props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LatentsInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LatentsInputField.tsx similarity index 87% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LatentsInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LatentsInputField.tsx index 5d5225582c..099314654f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LatentsInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LatentsInputField.tsx @@ -1,9 +1,9 @@ import { LatentsInputFieldTemplate, LatentsInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo } from 'react'; -import { FieldComponentProps } from './types'; const LatentsInputFieldComponent = ( _props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelInputField.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelInputField.tsx index 8e4d04a55d..6d0e37d063 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelInputField.tsx @@ -7,13 +7,13 @@ import { fieldLoRAModelValueChanged } from 'features/nodes/store/nodesSlice'; import { LoRAModelInputFieldTemplate, LoRAModelInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { modelIdToLoRAModelParam } from 'features/parameters/util/modelIdToLoRAModelParam'; import { forEach } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; import { useGetLoRAModelsQuery } from 'services/api/endpoints/models'; -import { FieldComponentProps } from './types'; const LoRAModelInputFieldComponent = ( props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelInputField.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelInputField.tsx index f5d2393532..047d2d797a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelInputField.tsx @@ -6,6 +6,7 @@ import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice'; import { MainModelInputFieldTemplate, MainModelInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam'; @@ -18,7 +19,6 @@ import { useGetMainModelsQuery, useGetOnnxModelsQuery, } from 'services/api/endpoints/models'; -import { FieldComponentProps } from './types'; const MainModelInputFieldComponent = ( props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberInputField.tsx similarity index 97% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberInputField.tsx index bd670bd394..1e569d5005 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberInputField.tsx @@ -13,9 +13,9 @@ import { FloatInputFieldValue, IntegerInputFieldTemplate, IntegerInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo, useEffect, useMemo, useState } from 'react'; -import { FieldComponentProps } from './types'; const NumberInputFieldComponent = ( props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelInputField.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelInputField.tsx index d42334c7b2..4298670934 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelInputField.tsx @@ -6,6 +6,7 @@ import { fieldRefinerModelValueChanged } from 'features/nodes/store/nodesSlice'; import { SDXLRefinerModelInputFieldTemplate, SDXLRefinerModelInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam'; @@ -16,7 +17,6 @@ import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { REFINER_BASE_MODELS } from 'services/api/constants'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; -import { FieldComponentProps } from './types'; const RefinerModelInputFieldComponent = ( props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelInputField.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelInputField.tsx index 2193835228..f1721ecd58 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelInputField.tsx @@ -6,6 +6,7 @@ import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice'; import { SDXLMainModelInputFieldTemplate, SDXLMainModelInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam'; @@ -19,7 +20,6 @@ import { useGetMainModelsQuery, useGetOnnxModelsQuery, } from 'services/api/endpoints/models'; -import { FieldComponentProps } from './types'; const ModelInputFieldComponent = ( props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SchedulerInputField.tsx similarity index 97% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SchedulerInputField.tsx index 87f2f5b5e4..557c128942 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SchedulerInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SchedulerInputField.tsx @@ -7,6 +7,7 @@ import { fieldSchedulerValueChanged } from 'features/nodes/store/nodesSlice'; import { SchedulerInputFieldTemplate, SchedulerInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { SCHEDULER_LABEL_MAP, @@ -14,7 +15,6 @@ import { } from 'features/parameters/types/parameterSchemas'; import { map } from 'lodash-es'; import { memo, useCallback } from 'react'; -import { FieldComponentProps } from './types'; const selector = createSelector( [stateSelector], diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringInputField.tsx similarity index 96% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringInputField.tsx index 4561f9cc32..c82b8f612c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringInputField.tsx @@ -5,9 +5,9 @@ import { fieldStringValueChanged } from 'features/nodes/store/nodesSlice'; import { StringInputFieldTemplate, StringInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { ChangeEvent, memo, useCallback } from 'react'; -import { FieldComponentProps } from './types'; const StringInputFieldComponent = ( props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/UnetInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/UnetInputField.tsx similarity index 86% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/UnetInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/UnetInputField.tsx index eaf760d5d4..2beefc7034 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/UnetInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/UnetInputField.tsx @@ -1,9 +1,9 @@ import { UNetInputFieldTemplate, UNetInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo } from 'react'; -import { FieldComponentProps } from './types'; const UNetInputFieldComponent = ( _props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VaeInputField.tsx similarity index 86% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VaeInputField.tsx index 16c59368f9..738267faab 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VaeInputField.tsx @@ -1,9 +1,9 @@ import { VaeInputFieldTemplate, VaeInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { memo } from 'react'; -import { FieldComponentProps } from './types'; const VaeInputFieldComponent = ( _props: FieldComponentProps diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VaeModelInputField.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VaeModelInputField.tsx index 4532369233..529febb6c1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VaeModelInputField.tsx @@ -6,13 +6,13 @@ import { fieldVaeModelValueChanged } from 'features/nodes/store/nodesSlice'; import { VaeModelInputFieldTemplate, VaeModelInputFieldValue, + FieldComponentProps, } from 'features/nodes/types/types'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { modelIdToVAEModelParam } from 'features/parameters/util/modelIdToVAEModelParam'; import { forEach } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; import { useGetVaeModelsQuery } from 'services/api/endpoints/models'; -import { FieldComponentProps } from './types'; const VaeModelInputFieldComponent = ( props: FieldComponentProps< diff --git a/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx similarity index 92% rename from invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx index 7a46c11901..ab0589664c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx @@ -5,9 +5,9 @@ import { notesNodeValueChanged } from 'features/nodes/store/nodesSlice'; import { NotesNodeData } from 'features/nodes/types/types'; import { ChangeEvent, memo, useCallback } from 'react'; import { NodeProps } from 'reactflow'; -import NodeCollapseButton from '../Invocation/NodeCollapseButton'; -import NodeTitle from '../Invocation/NodeTitle'; -import NodeWrapper from '../Invocation/NodeWrapper'; +import NodeWrapper from '../common/NodeWrapper'; +import NodeCollapseButton from '../common/NodeCollapseButton'; +import NodeTitle from '../common/NodeTitle'; const NotesNode = (props: NodeProps) => { const { id: nodeId, data, selected } = props; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeCollapseButton.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeCollapseButton.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeResizer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeResizer.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/NodeResizer.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeResizer.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeTitle.tsx similarity index 95% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeTitle.tsx index eeb5147c74..88ea1b69e1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeTitle.tsx @@ -7,10 +7,8 @@ import { useEditableControls, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import { - useNodeLabel, - useNodeTemplateTitle, -} from 'features/nodes/hooks/useNodeData'; +import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel'; +import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle'; import { nodeLabelChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import { MouseEvent, memo, useCallback, useEffect, useState } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx similarity index 95% rename from invokeai/frontend/web/src/features/nodes/components/Invocation/NodeWrapper.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx index 2d2dc28d56..327c01a806 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx @@ -5,9 +5,12 @@ import { useToken, } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + DRAG_HANDLE_CLASSNAME, + NODE_WIDTH, +} from 'features/nodes/types/constants'; import { contextMenusClosed } from 'features/ui/store/uiSlice'; import { PropsWithChildren, memo, useCallback } from 'react'; -import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../../types/constants'; type NodeWrapperProps = PropsWithChildren & { nodeId: string; diff --git a/invokeai/frontend/web/src/features/nodes/components/editorPanels/BottomLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/BottomLeftPanel.tsx similarity index 75% rename from invokeai/frontend/web/src/features/nodes/components/editorPanels/BottomLeftPanel.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/BottomLeftPanel.tsx index 39aa2444c4..eccc4409af 100644 --- a/invokeai/frontend/web/src/features/nodes/components/editorPanels/BottomLeftPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/BottomLeftPanel.tsx @@ -1,7 +1,7 @@ import { memo } from 'react'; import { Panel } from 'reactflow'; -import ViewportControls from '../ViewportControls'; -import NodeOpacitySlider from '../NodeOpacitySlider'; +import ViewportControls from './ViewportControls'; +import NodeOpacitySlider from './NodeOpacitySlider'; import { Flex } from '@chakra-ui/react'; const BottomLeftPanel = () => ( diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeOpacitySlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx similarity index 92% rename from invokeai/frontend/web/src/features/nodes/components/NodeOpacitySlider.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx index 693940859f..87e49efc55 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeOpacitySlider.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx @@ -7,7 +7,7 @@ import { } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useCallback } from 'react'; -import { nodeOpacityChanged } from '../store/nodesSlice'; +import { nodeOpacityChanged } from 'features/nodes/store/nodesSlice'; export default function NodeOpacitySlider() { const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx index 7416c6c555..8e4f2487be 100644 --- a/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx @@ -14,7 +14,7 @@ import { useReactFlow } from 'reactflow'; import { shouldShowFieldTypeLegendChanged, shouldShowMinimapPanelChanged, -} from '../store/nodesSlice'; +} from 'features/nodes/store/nodesSlice'; const ViewportControls = () => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/nodes/components/editorPanels/MinimapPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/editorPanels/MinimapPanel.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ClearGraphButton.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ClearGraphButton.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditorSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/NodeEditorSettings.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/NodeEditorSettings.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/NodeEditorSettings.tsx index b942b2b3c0..e62f1bac89 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditorSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/NodeEditorSettings.tsx @@ -22,7 +22,7 @@ import { shouldColorEdgesChanged, shouldSnapToGridChanged, shouldValidateGraphChanged, -} from '../store/nodesSlice'; +} from 'features/nodes/store/nodesSlice'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; const selector = createSelector( diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/NodeInvokeButton.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/NodeInvokeButton.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/ReloadSchemaButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/ui/ReloadSchemaButton.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/editorPanels/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx similarity index 69% rename from invokeai/frontend/web/src/features/nodes/components/editorPanels/TopCenterPanel.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx index 240c2057be..675a69325a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/editorPanels/TopCenterPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx @@ -2,10 +2,10 @@ import { HStack } from '@chakra-ui/react'; import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton'; import { memo } from 'react'; import { Panel } from 'reactflow'; -import NodeEditorSettings from '../NodeEditorSettings'; -import ClearGraphButton from '../ui/ClearGraphButton'; -import NodeInvokeButton from '../ui/NodeInvokeButton'; -import ReloadSchemaButton from '../ui/ReloadSchemaButton'; +import NodeEditorSettings from './NodeEditorSettings'; +import ClearGraphButton from './ClearGraphButton'; +import NodeInvokeButton from './NodeInvokeButton'; +import ReloadSchemaButton from './ReloadSchemaButton'; const TopCenterPanel = () => { return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/AddNodeMenu.tsx similarity index 96% rename from invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/AddNodeMenu.tsx index a816762d0f..9075ce62fc 100644 --- a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/AddNodeMenu.tsx @@ -5,12 +5,12 @@ import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect'; +import { useBuildNodeData } from 'features/nodes/hooks/useBuildNodeData'; +import { nodeAdded } from 'features/nodes/store/nodesSlice'; import { map } from 'lodash-es'; import { forwardRef, useCallback } from 'react'; import 'reactflow/dist/style.css'; import { AnyInvocationType } from 'services/events/types'; -import { useBuildNodeData } from '../hooks/useBuildNodeData'; -import { nodeAdded } from '../store/nodesSlice'; type NodeTemplate = { label: string; diff --git a/invokeai/frontend/web/src/features/nodes/components/editorPanels/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx similarity index 82% rename from invokeai/frontend/web/src/features/nodes/components/editorPanels/TopLeftPanel.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx index 2b89db000a..e53a8a391c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/editorPanels/TopLeftPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; import { Panel } from 'reactflow'; -import AddNodeMenu from '../AddNodeMenu'; +import AddNodeMenu from './AddNodeMenu'; const TopLeftPanel = () => ( diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/FieldTypeLegend.tsx similarity index 93% rename from invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/FieldTypeLegend.tsx index a523cc29fe..0afd6a80f7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/FieldTypeLegend.tsx @@ -1,8 +1,8 @@ import { Badge, Flex, Tooltip } from '@chakra-ui/react'; +import { FIELDS } from 'features/nodes/types/constants'; import { map } from 'lodash-es'; import { memo } from 'react'; import 'reactflow/dist/style.css'; -import { FIELDS } from '../types/constants'; const FieldTypeLegend = () => { return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/editorPanels/TopRightPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx similarity index 89% rename from invokeai/frontend/web/src/features/nodes/components/editorPanels/TopRightPanel.tsx rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx index 7facf3973f..903502811d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/editorPanels/TopRightPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx @@ -1,7 +1,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { memo } from 'react'; import { Panel } from 'reactflow'; -import FieldTypeLegend from '../FieldTypeLegend'; +import FieldTypeLegend from './FieldTypeLegend'; const TopRightPanel = () => { const shouldShowFieldTypeLegend = useAppSelector( diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/ImageOutputPreview.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/ImageOutputPreview.tsx deleted file mode 100644 index d35e80fa62..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/panel/ImageOutputPreview.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import IAIDndImage from 'common/components/IAIDndImage'; -import { memo } from 'react'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import { ImageOutput } from 'services/api/types'; - -type Props = { - output: ImageOutput; -}; - -const ImageOutputPreview = ({ output }: Props) => { - const { image, width, height } = output; - const { data: imageDTO } = useGetImageDTOQuery(image.image_name); - - return ; -}; - -export default memo(ImageOutputPreview); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NumberOutputPreview.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/NumberOutputPreview.tsx deleted file mode 100644 index ebe03740b3..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/panel/NumberOutputPreview.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Text } from '@chakra-ui/react'; -import { memo } from 'react'; -import { FloatOutput, IntegerOutput } from 'services/api/types'; - -type Props = { - output: IntegerOutput | FloatOutput; -}; - -const NumberOutputPreview = ({ output }: Props) => { - return {output.value}; -}; - -export default memo(NumberOutputPreview); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/StringOutputPreview.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/StringOutputPreview.tsx deleted file mode 100644 index 1dce0530dd..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/panel/StringOutputPreview.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Text } from '@chakra-ui/react'; -import { memo } from 'react'; -import { StringOutput } from 'services/api/types'; - -type Props = { - output: StringOutput; -}; - -const StringOutputPreview = ({ output }: Props) => { - return {output.value}; -}; - -export default memo(StringOutputPreview); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NodeEditorPanelGroup.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx similarity index 91% rename from invokeai/frontend/web/src/features/nodes/components/panel/NodeEditorPanelGroup.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx index 269108e87a..909dca2ced 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/NodeEditorPanelGroup.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx @@ -1,9 +1,9 @@ -import InspectorPanel from 'features/nodes/components/panel/InspectorPanel'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import { memo, useState } from 'react'; import { Panel, PanelGroup } from 'react-resizable-panels'; import 'reactflow/dist/style.css'; -import WorkflowPanel from './WorkflowPanel'; +import WorkflowPanel from './workflow/WorkflowPanel'; +import InspectorPanel from './inspector/InspectorPanel'; const NodeEditorPanelGroup = () => { const [isTopPanelCollapsed, setIsTopPanelCollapsed] = useState(false); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/ScrollableContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/ScrollableContent.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/panel/ScrollableContent.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/ScrollableContent.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx similarity index 93% rename from invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx index 084f743d19..bb06836a70 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx @@ -23,7 +23,7 @@ const selector = createSelector( defaultSelectorOptions ); -const NodeDataInspector = () => { +const InspectorDataTab = () => { const { data } = useAppSelector(selector); if (!data) { @@ -33,4 +33,4 @@ const NodeDataInspector = () => { return ; }; -export default memo(NodeDataInspector); +export default memo(InspectorDataTab); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NodeResultsInspector.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx similarity index 88% rename from invokeai/frontend/web/src/features/nodes/components/panel/NodeResultsInspector.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx index 6e2a2a3dbd..43148a653d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/NodeResultsInspector.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx @@ -5,11 +5,11 @@ import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { memo } from 'react'; -import ImageOutputPreview from './ImageOutputPreview'; -import NumberOutputPreview from './NumberOutputPreview'; -import ScrollableContent from './ScrollableContent'; -import StringOutputPreview from './StringOutputPreview'; +import ImageOutputPreview from './outputs/ImageOutputPreview'; +import ScrollableContent from '../ScrollableContent'; import { AnyResult } from 'services/events/types'; +import StringOutputPreview from './outputs/StringOutputPreview'; +import NumberOutputPreview from './outputs/NumberOutputPreview'; const selector = createSelector( stateSelector, @@ -32,7 +32,7 @@ const selector = createSelector( defaultSelectorOptions ); -const NodeResultsInspector = () => { +const InspectorOutputsTab = () => { const { node, nes } = useAppSelector(selector); if (!node || !nes) { @@ -96,6 +96,6 @@ const NodeResultsInspector = () => { ); }; -export default memo(NodeResultsInspector); +export default memo(InspectorOutputsTab); const getKey = (result: AnyResult, i: number) => `${result.type}-${i}`; diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorPanel.tsx similarity index 66% rename from invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorPanel.tsx index b67f749027..12970a0e66 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorPanel.tsx @@ -7,9 +7,9 @@ import { Tabs, } from '@chakra-ui/react'; import { memo } from 'react'; -import NodeDataInspector from './NodeDataInspector'; -import NodeResultsInspector from './NodeResultsInspector'; -import NodeTemplateInspector from './NodeTemplateInspector'; +import InspectorDataTab from './InspectorDataTab'; +import InspectorOutputsTab from './InspectorOutputsTab'; +import InspectorTemplateTab from './InspectorTemplateTab'; const InspectorPanel = () => { return ( @@ -29,20 +29,20 @@ const InspectorPanel = () => { sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }} > - Node Outputs - Node Data - Node Template + Outputs + Data + Template - + - + - + diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NodeTemplateInspector.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/panel/NodeTemplateInspector.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/GeneralTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowGeneralTab.tsx similarity index 98% rename from invokeai/frontend/web/src/features/nodes/components/panel/workflow/GeneralTab.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowGeneralTab.tsx index 8dab91c0d5..e36675b71f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/GeneralTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowGeneralTab.tsx @@ -36,7 +36,7 @@ const selector = createSelector( defaultSelectorOptions ); -const WorkflowPanel = () => { +const WorkflowGeneralTab = () => { const { author, name, description, tags, version, contact, notes } = useAppSelector(selector); const dispatch = useAppDispatch(); @@ -139,4 +139,4 @@ const WorkflowPanel = () => { ); }; -export default memo(WorkflowPanel); +export default memo(WorkflowGeneralTab); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/WorkflowTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx similarity index 92% rename from invokeai/frontend/web/src/features/nodes/components/panel/workflow/WorkflowTab.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx index c9400ab5f6..3cbe5ea1ee 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/WorkflowTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx @@ -19,7 +19,7 @@ const useWatchWorkflow = () => { }; }; -const WorkflowWorkflowTab = () => { +const WorkflowJSONTab = () => { const { workflow } = useWatchWorkflow(); return ( @@ -40,4 +40,4 @@ const WorkflowWorkflowTab = () => { ); }; -export default memo(WorkflowWorkflowTab); +export default memo(WorkflowJSONTab); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx similarity index 90% rename from invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx index b77453b749..d1cecefbff 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx @@ -5,7 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { memo } from 'react'; -import LinearViewField from '../../fields/LinearViewField'; +import LinearViewField from '../../flow/nodes/Invocation/fields/LinearViewField'; import ScrollableContent from '../ScrollableContent'; const selector = createSelector( @@ -18,7 +18,7 @@ const selector = createSelector( defaultSelectorOptions ); -const LinearTabContent = () => { +const WorkflowLinearTab = () => { const { fields } = useAppSelector(selector); return ( @@ -61,4 +61,4 @@ const LinearTabContent = () => { ); }; -export default memo(LinearTabContent); +export default memo(WorkflowLinearTab); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/NotesTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowNotesTab.tsx similarity index 94% rename from invokeai/frontend/web/src/features/nodes/components/panel/workflow/NotesTab.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowNotesTab.tsx index d8b19c1645..d1ea0bcea1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/NotesTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowNotesTab.tsx @@ -14,7 +14,7 @@ const selector = createSelector(stateSelector, ({ nodes }) => { }; }); -const WorkflowPanel = () => { +const WorkflowNotesTab = () => { const { notes } = useAppSelector(selector); const dispatch = useAppDispatch(); @@ -48,4 +48,4 @@ const WorkflowPanel = () => { ); }; -export default memo(WorkflowPanel); +export default memo(WorkflowNotesTab); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/WorkflowPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx similarity index 76% rename from invokeai/frontend/web/src/features/nodes/components/panel/WorkflowPanel.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx index 052cf15ad7..df1c0dcc69 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/WorkflowPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx @@ -7,9 +7,9 @@ import { Tabs, } from '@chakra-ui/react'; import { memo } from 'react'; -import GeneralTab from './workflow/GeneralTab'; -import LinearTab from './workflow/LinearTab'; -import WorkflowTab from './workflow/WorkflowTab'; +import WorkflowGeneralTab from './WorkflowGeneralTab'; +import WorkflowLinearTab from './WorkflowLinearTab'; +import WorkflowJSONTab from './WorkflowJSONTab'; const WorkflowPanel = () => { return ( @@ -35,13 +35,13 @@ const WorkflowPanel = () => { - + - + - + diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index e2154f7391..96b2d652e9 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector'; import { useMemo } from 'react'; -import { useFieldType } from './useNodeData'; +import { useFieldType } from './useFieldType.ts'; const selectIsConnectionInProgress = createSelector( stateSelector, diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts new file mode 100644 index 0000000000..f56099ed2b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts @@ -0,0 +1,28 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; + +export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return Boolean(node?.data.inputs[fieldName]?.value); + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const doesFieldHaveValue = useAppSelector(selector); + + return doesFieldHaveValue; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldData.ts new file mode 100644 index 0000000000..ba2c4e2d5c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldData.ts @@ -0,0 +1,28 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; + +export const useFieldData = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return node?.data.inputs[fieldName]; + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const fieldData = useAppSelector(selector); + + return fieldData; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputKind.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputKind.ts new file mode 100644 index 0000000000..159815a6a6 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputKind.ts @@ -0,0 +1,30 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; + +export const useFieldInputKind = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + const fieldTemplate = nodeTemplate?.inputs[fieldName]; + return fieldTemplate?.input; + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const inputKind = useAppSelector(selector); + + return inputKind; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts new file mode 100644 index 0000000000..fcf33c3427 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts @@ -0,0 +1,28 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; + +export const useFieldLabel = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return node?.data.inputs[fieldName]?.label; + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const label = useAppSelector(selector); + + return label; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts new file mode 100644 index 0000000000..e2c7126012 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts @@ -0,0 +1,31 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { map } from 'lodash-es'; +import { useMemo } from 'react'; +import { KIND_MAP } from '../types/constants'; +import { isInvocationNode } from '../types/types'; + +export const useFieldNames = (nodeId: string, kind: 'input' | 'output') => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return []; + } + return map(node.data[KIND_MAP[kind]], (field) => field.name).filter( + (fieldName) => fieldName !== 'is_intermediate' + ); + }, + defaultSelectorOptions + ), + [kind, nodeId] + ); + + const fieldNames = useAppSelector(selector); + return fieldNames; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts new file mode 100644 index 0000000000..93d545aaea --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts @@ -0,0 +1,34 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { KIND_MAP } from '../types/constants'; +import { isInvocationNode } from '../types/types'; + +export const useFieldTemplate = ( + nodeId: string, + fieldName: string, + kind: 'input' | 'output' +) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + return nodeTemplate?.[KIND_MAP[kind]][fieldName]; + }, + defaultSelectorOptions + ), + [fieldName, kind, nodeId] + ); + + const fieldTemplate = useAppSelector(selector); + + return fieldTemplate; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts new file mode 100644 index 0000000000..923c25cc18 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts @@ -0,0 +1,34 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; +import { KIND_MAP } from '../types/constants'; + +export const useFieldTemplateTitle = ( + nodeId: string, + fieldName: string, + kind: 'input' | 'output' +) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + return nodeTemplate?.[KIND_MAP[kind]][fieldName]?.title; + }, + defaultSelectorOptions + ), + [fieldName, kind, nodeId] + ); + + const fieldTemplate = useAppSelector(selector); + + return fieldTemplate; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts new file mode 100644 index 0000000000..f4d78f8954 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts @@ -0,0 +1,33 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { KIND_MAP } from '../types/constants'; +import { isInvocationNode } from '../types/types'; + +export const useFieldType = ( + nodeId: string, + fieldName: string, + kind: 'input' | 'output' +) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return node?.data[KIND_MAP[kind]][fieldName]?.type; + }, + defaultSelectorOptions + ), + [fieldName, kind, nodeId] + ); + + const fieldType = useAppSelector(selector); + + return fieldType; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts b/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts new file mode 100644 index 0000000000..0976ededd1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts @@ -0,0 +1,31 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { some } from 'lodash-es'; +import { useMemo } from 'react'; +import { IMAGE_FIELDS } from '../types/constants'; +import { isInvocationNode } from '../types/types'; + +export const useHasImageOutput = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + return some(node.data.outputs, (output) => + IMAGE_FIELDS.includes(output.type) + ); + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const hasImageOutput = useAppSelector(selector); + return hasImageOutput; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts new file mode 100644 index 0000000000..13b94f207c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts @@ -0,0 +1,27 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; + +export const useIsIntermediate = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + return Boolean(node.data.inputs.is_intermediate?.value); + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const is_intermediate = useAppSelector(selector); + return is_intermediate; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsMouseOverField.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsMouseOverField.ts new file mode 100644 index 0000000000..9108cd12b6 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsMouseOverField.ts @@ -0,0 +1,33 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useCallback, useMemo } from 'react'; +import { mouseOverFieldChanged } from '../store/nodesSlice'; + +export const useIsMouseOverField = (nodeId: string, fieldName: string) => { + const dispatch = useAppDispatch(); + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => + nodes.mouseOverField?.nodeId === nodeId && + nodes.mouseOverField?.fieldName === fieldName, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const isMouseOverField = useAppSelector(selector); + + const handleMouseOver = useCallback(() => { + dispatch(mouseOverFieldChanged({ nodeId, fieldName })); + }, [dispatch, fieldName, nodeId]); + + const handleMouseOut = useCallback(() => { + dispatch(mouseOverFieldChanged(null)); + }, [dispatch]); + + return { isMouseOverField, handleMouseOver, handleMouseOut }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts index daca2aad45..cda8f91dfd 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -1,37 +1,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { map, some } from 'lodash-es'; -import { useCallback, useMemo } from 'react'; -import { mouseOverFieldChanged } from '../store/nodesSlice'; -import { FOOTER_FIELDS, IMAGE_FIELDS } from '../types/constants'; -import { isInvocationNode } from '../types/types'; - -const KIND_MAP = { - input: 'inputs' as const, - output: 'outputs' as const, -}; - -export const useNodeTemplate = (nodeId: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; - return nodeTemplate; - }, - defaultSelectorOptions - ), - [nodeId] - ); - - const nodeTemplate = useAppSelector(selector); - - return nodeTemplate; -}; +import { useMemo } from 'react'; export const useNodeData = (nodeId: string) => { const selector = useMemo( @@ -51,337 +22,3 @@ export const useNodeData = (nodeId: string) => { return nodeData; }; - -export const useFieldLabel = (nodeId: string, fieldName: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - return node?.data.inputs[fieldName]?.label; - }, - defaultSelectorOptions - ), - [fieldName, nodeId] - ); - - const label = useAppSelector(selector); - - return label; -}; - -export const useFieldData = (nodeId: string, fieldName: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - return node?.data.inputs[fieldName]; - }, - defaultSelectorOptions - ), - [fieldName, nodeId] - ); - - const fieldData = useAppSelector(selector); - - return fieldData; -}; - -export const useFieldInputKind = (nodeId: string, fieldName: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; - const fieldTemplate = nodeTemplate?.inputs[fieldName]; - return fieldTemplate?.input; - }, - defaultSelectorOptions - ), - [fieldName, nodeId] - ); - - const inputKind = useAppSelector(selector); - - return inputKind; -}; - -export const useFieldType = ( - nodeId: string, - fieldName: string, - kind: 'input' | 'output' -) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - return node?.data[KIND_MAP[kind]][fieldName]?.type; - }, - defaultSelectorOptions - ), - [fieldName, kind, nodeId] - ); - - const fieldType = useAppSelector(selector); - - return fieldType; -}; - -export const useFieldNames = (nodeId: string, kind: 'input' | 'output') => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return []; - } - return map(node.data[KIND_MAP[kind]], (field) => field.name).filter( - (fieldName) => fieldName !== 'is_intermediate' - ); - }, - defaultSelectorOptions - ), - [kind, nodeId] - ); - - const fieldNames = useAppSelector(selector); - return fieldNames; -}; - -export const useWithFooter = (nodeId: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - return some(node.data.outputs, (output) => - FOOTER_FIELDS.includes(output.type) - ); - }, - defaultSelectorOptions - ), - [nodeId] - ); - - const withFooter = useAppSelector(selector); - return withFooter; -}; - -export const useHasImageOutput = (nodeId: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - return some(node.data.outputs, (output) => - IMAGE_FIELDS.includes(output.type) - ); - }, - defaultSelectorOptions - ), - [nodeId] - ); - - const hasImageOutput = useAppSelector(selector); - return hasImageOutput; -}; - -export const useIsIntermediate = (nodeId: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - return Boolean(node.data.inputs.is_intermediate?.value); - }, - defaultSelectorOptions - ), - [nodeId] - ); - - const is_intermediate = useAppSelector(selector); - return is_intermediate; -}; - -export const useNodeLabel = (nodeId: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - - return node.data.label; - }, - defaultSelectorOptions - ), - [nodeId] - ); - - const label = useAppSelector(selector); - return label; -}; - -export const useNodeTemplateTitle = (nodeId: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - const nodeTemplate = node - ? nodes.nodeTemplates[node.data.type] - : undefined; - - return nodeTemplate?.title; - }, - defaultSelectorOptions - ), - [nodeId] - ); - - const title = useAppSelector(selector); - return title; -}; - -export const useFieldTemplateTitle = ( - nodeId: string, - fieldName: string, - kind: 'input' | 'output' -) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; - return nodeTemplate?.[KIND_MAP[kind]][fieldName]?.title; - }, - defaultSelectorOptions - ), - [fieldName, kind, nodeId] - ); - - const fieldTemplate = useAppSelector(selector); - - return fieldTemplate; -}; - -export const useFieldTemplate = ( - nodeId: string, - fieldName: string, - kind: 'input' | 'output' -) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; - return nodeTemplate?.[KIND_MAP[kind]][fieldName]; - }, - defaultSelectorOptions - ), - [fieldName, kind, nodeId] - ); - - const fieldTemplate = useAppSelector(selector); - - return fieldTemplate; -}; - -export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - return Boolean(node?.data.inputs[fieldName]?.value); - }, - defaultSelectorOptions - ), - [fieldName, nodeId] - ); - - const doesFieldHaveValue = useAppSelector(selector); - - return doesFieldHaveValue; -}; - -export const useIsMouseOverField = (nodeId: string, fieldName: string) => { - const dispatch = useAppDispatch(); - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => - nodes.mouseOverField?.nodeId === nodeId && - nodes.mouseOverField?.fieldName === fieldName, - defaultSelectorOptions - ), - [fieldName, nodeId] - ); - - const isMouseOverField = useAppSelector(selector); - - const handleMouseOver = useCallback(() => { - dispatch(mouseOverFieldChanged({ nodeId, fieldName })); - }, [dispatch, fieldName, nodeId]); - - const handleMouseOut = useCallback(() => { - dispatch(mouseOverFieldChanged(null)); - }, [dispatch]); - - return { isMouseOverField, handleMouseOver, handleMouseOut }; -}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts new file mode 100644 index 0000000000..f9bbe4cc1d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts @@ -0,0 +1,28 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; + +export const useNodeLabel = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + + return node.data.label; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const label = useAppSelector(selector); + return label; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts new file mode 100644 index 0000000000..733c03e6cf --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts @@ -0,0 +1,25 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; + +export const useNodeTemplate = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + return nodeTemplate; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const nodeTemplate = useAppSelector(selector); + + return nodeTemplate; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts new file mode 100644 index 0000000000..4ef3eed5d9 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts @@ -0,0 +1,31 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; + +export const useNodeTemplateTitle = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + const nodeTemplate = node + ? nodes.nodeTemplates[node.data.type] + : undefined; + + return nodeTemplate?.title; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const title = useAppSelector(selector); + return title; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts new file mode 100644 index 0000000000..8bde120005 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts @@ -0,0 +1,31 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { some } from 'lodash-es'; +import { useMemo } from 'react'; +import { FOOTER_FIELDS } from '../types/constants'; +import { isInvocationNode } from '../types/types'; + +export const useWithFooter = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + return some(node.data.outputs, (output) => + FOOTER_FIELDS.includes(output.type) + ); + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const withFooter = useAppSelector(selector); + return withFooter; +}; diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index 07d5543da4..4fbb32fe7f 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -9,6 +9,11 @@ export const DRAG_HANDLE_CLASSNAME = 'node-drag-handle'; export const IMAGE_FIELDS = ['ImageField', 'ImageCollection']; export const FOOTER_FIELDS = IMAGE_FIELDS; +export const KIND_MAP = { + input: 'inputs' as const, + output: 'outputs' as const, +}; + export const COLLECTION_TYPES: FieldType[] = [ 'Collection', 'IntegerCollection', diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index fe0593c5dc..d4618f9269 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -709,3 +709,12 @@ export type FieldIdentifier = { nodeId: string; fieldName: string; }; + +export type FieldComponentProps< + V extends InputFieldValue, + T extends InputFieldTemplate +> = { + nodeId: string; + field: V; + fieldTemplate: T; +}; From adb05cde5baba631b8b027e972d10f8a193aaa06 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 19 Aug 2023 15:52:46 +1000 Subject: [PATCH 21/45] feat(ui): simple partial search for nodes --- .../flow/panels/TopLeftPanel/AddNodeMenu.tsx | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/AddNodeMenu.tsx index 9075ce62fc..f510247500 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/AddNodeMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/AddNodeMenu.tsx @@ -18,6 +18,23 @@ type NodeTemplate = { description: string; tags: string[]; }; +const filter = (value: string, item: NodeTemplate) => { + const regex = new RegExp( + value + .toLowerCase() + .trim() + // strip out regex special characters to avoid errors + .replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '') + .split(' ') + .join('.*'), + 'g' + ); + return ( + regex.test(item.label.toLowerCase()) || + regex.test(item.description.toLowerCase()) || + item.tags.some((tag) => regex.test(tag)) + ); +}; const selector = createSelector( [stateSelector], @@ -45,6 +62,8 @@ const selector = createSelector( tags: ['notes'], }); + data.sort((a, b) => a.label.localeCompare(b.label)); + return { data }; }, defaultSelectorOptions @@ -96,13 +115,9 @@ const AddNodeMenu = () => { maxDropdownHeight={400} nothingFound="No matching nodes" itemComponent={SelectItem} - filter={(value, item: NodeTemplate) => - item.label.toLowerCase().includes(value.toLowerCase().trim()) || - item.value.toLowerCase().includes(value.toLowerCase().trim()) || - item.description.toLowerCase().includes(value.toLowerCase().trim()) || - item.tags.includes(value.toLowerCase().trim()) - } + filter={filter} onChange={handleChange} + hoverOnSearchChange={true} sx={{ width: '24rem', }} From 385765faec0d21c5f88942856fe648d46a5244b5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 19 Aug 2023 17:31:21 +1000 Subject: [PATCH 22/45] fix(ui): fix missing tags on template parse --- invokeai/frontend/web/src/features/nodes/types/types.ts | 5 +---- invokeai/frontend/web/src/features/nodes/util/parseSchema.ts | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index d4618f9269..c1f1462fda 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -495,11 +495,8 @@ export type TypeHints = { export type InvocationSchemaExtra = { output: OpenAPIV3.ReferenceObject; // the output of the invocation - ui?: { - tags?: string[]; - title?: string; - }; title: string; + tags?: string[]; properties: Omit< NonNullable & (_InputField | _OutputField), diff --git a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts index 19201b23bb..a900d0ddae 100644 --- a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts @@ -36,8 +36,8 @@ export const parseSchema = ( >((acc, schema) => { if (isInvocationSchemaObject(schema)) { const type = schema.properties.type.default; - const title = schema.ui?.title ?? schema.title.replace('Invocation', ''); - const tags = schema.ui?.tags ?? []; + const title = schema.title.replace('Invocation', ''); + const tags = schema.tags ?? []; const description = schema.description ?? ''; const inputs = reduce( From a9fdc77edd0ad0707ee920ad2a6b4ec0bb7ff04e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 19 Aug 2023 17:44:30 +1000 Subject: [PATCH 23/45] feat(ui): rename node editor to workflow editor --- invokeai/frontend/web/public/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index f41da82e07..de568a40f0 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -52,7 +52,7 @@ "img2img": "Image To Image", "unifiedCanvas": "Unified Canvas", "linear": "Linear", - "nodes": "Node Editor", + "nodes": "Workflow Editor", "batch": "Batch Manager", "modelManager": "Model Manager", "postprocessing": "Post Processing", From 4be4fc6731bfb6beb28c19bb10bbfd163d909676 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 19 Aug 2023 17:46:16 +1000 Subject: [PATCH 24/45] feat(ui): rework add node select - `space` and `/` open floating add node select - improved filter logic (partial word matches) --- .../features/nodes/components/NodeEditor.tsx | 4 +- .../flow/AddNodePopover/AddNodePopover.tsx | 205 +++++++++++++++++ .../AddNodePopoverSelectItem.tsx | 29 +++ .../BottomLeftPanel/ViewportControls.tsx | 20 +- .../flow/panels/TopLeftPanel/AddNodeMenu.tsx | 155 ------------- .../flow/panels/TopLeftPanel/TopLeftPanel.tsx | 30 ++- .../nodes/components/search/NodeSearch.tsx | 208 ------------------ .../src/features/nodes/store/nodesSlice.ts | 13 ++ .../web/src/features/nodes/store/types.ts | 1 + 9 files changed, 284 insertions(+), 381 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopoverSelectItem.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/AddNodeMenu.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index ffda25c2a6..87d8e4f127 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -9,6 +9,7 @@ import 'reactflow/dist/style.css'; import NodeEditorPanelGroup from './sidePanel/NodeEditorPanelGroup'; import { Flow } from './flow/Flow'; import { AnimatePresence, motion } from 'framer-motion'; +import AddNodePopover from './flow/AddNodePopover/AddNodePopover'; const NodeEditor = () => { const [isPanelCollapsed, setIsPanelCollapsed] = useState(false); @@ -57,9 +58,10 @@ const NodeEditor = () => { opacity: 0, transition: { duration: 0.2 }, }} - style={{ width: '100%', height: '100%' }} + style={{ position: 'relative', width: '100%', height: '100%' }} > + )} 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 new file mode 100644 index 0000000000..05aa60c1ba --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -0,0 +1,205 @@ +import { + Flex, + Popover, + PopoverAnchor, + PopoverBody, + PopoverContent, +} from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppToaster } from 'app/components/Toaster'; +import { stateSelector } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect'; +import { useBuildNodeData } from 'features/nodes/hooks/useBuildNodeData'; +import { + addNodePopoverClosed, + addNodePopoverOpened, + nodeAdded, +} from 'features/nodes/store/nodesSlice'; +import { map } from 'lodash-es'; +import { memo, useCallback, useRef } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { HotkeyCallback } from 'react-hotkeys-hook/dist/types'; +import 'reactflow/dist/style.css'; +import { AnyInvocationType } from 'services/events/types'; +import { AddNodePopoverSelectItem } from './AddNodePopoverSelectItem'; + +type NodeTemplate = { + label: string; + value: string; + description: string; + tags: string[]; +}; + +const filter = (value: string, item: NodeTemplate) => { + const regex = new RegExp( + value + .trim() + .replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '') + .split(' ') + .join('.*'), + 'gi' + ); + return ( + regex.test(item.label) || + regex.test(item.description) || + item.tags.some((tag) => regex.test(tag)) + ); +}; + +const selector = createSelector( + [stateSelector], + ({ nodes }) => { + const data: NodeTemplate[] = map(nodes.nodeTemplates, (template) => { + return { + label: template.title, + value: template.type, + description: template.description, + tags: template.tags, + }; + }); + + data.push({ + label: 'Progress Image', + value: 'current_image', + description: 'Displays the current image in the Node Editor', + tags: ['progress'], + }); + + data.push({ + label: 'Notes', + value: 'notes', + description: 'Add notes about your workflow', + tags: ['notes'], + }); + + data.sort((a, b) => a.label.localeCompare(b.label)); + + return { data }; + }, + defaultSelectorOptions +); + +const AddNodePopover = () => { + const dispatch = useAppDispatch(); + const buildInvocation = useBuildNodeData(); + const toaster = useAppToaster(); + const { data } = useAppSelector(selector); + const isOpen = useAppSelector((state) => state.nodes.isAddNodePopoverOpen); + const inputRef = useRef(null); + + const addNode = useCallback( + (nodeType: AnyInvocationType) => { + const invocation = buildInvocation(nodeType); + + if (!invocation) { + toaster({ + status: 'error', + title: `Unknown Invocation type ${nodeType}`, + }); + return; + } + + dispatch(nodeAdded(invocation)); + }, + [dispatch, buildInvocation, toaster] + ); + + const handleChange = useCallback( + (v: string | null) => { + if (!v) { + return; + } + + addNode(v as AnyInvocationType); + }, + [addNode] + ); + + const onClose = useCallback(() => { + dispatch(addNodePopoverClosed()); + }, [dispatch]); + + const onOpen = useCallback(() => { + dispatch(addNodePopoverOpened()); + }, [dispatch]); + + const handleHotkeyOpen: HotkeyCallback = useCallback( + (e) => { + e.preventDefault(); + onOpen(); + setTimeout(() => { + inputRef.current?.focus(); + }, 0); + }, + [onOpen] + ); + + const handleHotkeyClose: HotkeyCallback = useCallback(() => { + onClose(); + }, [onClose]); + + useHotkeys(['space', '/'], handleHotkeyOpen); + useHotkeys(['escape'], handleHotkeyClose); + + return ( + + + + + + + + + + + ); +}; + +export default memo(AddNodePopover); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopoverSelectItem.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopoverSelectItem.tsx new file mode 100644 index 0000000000..95b033f95c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopoverSelectItem.tsx @@ -0,0 +1,29 @@ +import { Text } from '@chakra-ui/react'; +import { forwardRef } from 'react'; +import 'reactflow/dist/style.css'; + +interface ItemProps extends React.ComponentPropsWithoutRef<'div'> { + value: string; + label: string; + description: string; +} + +export const AddNodePopoverSelectItem = forwardRef( + ({ label, description, ...others }: ItemProps, ref) => { + return ( +
+
+ {label} + + {description} + +
+
+ ); + } +); + +AddNodePopoverSelectItem.displayName = 'AddNodePopoverSelectItem'; 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 8e4f2487be..260655723e 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 @@ -5,14 +5,14 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { FaExpand, - FaInfo, + // FaInfo, FaMapMarkerAlt, FaMinus, FaPlus, } from 'react-icons/fa'; import { useReactFlow } from 'reactflow'; import { - shouldShowFieldTypeLegendChanged, + // shouldShowFieldTypeLegendChanged, shouldShowMinimapPanelChanged, } from 'features/nodes/store/nodesSlice'; @@ -20,9 +20,9 @@ const ViewportControls = () => { const { t } = useTranslation(); const { zoomIn, zoomOut, fitView } = useReactFlow(); const dispatch = useAppDispatch(); - const shouldShowFieldTypeLegend = useAppSelector( - (state) => state.nodes.shouldShowFieldTypeLegend - ); + // const shouldShowFieldTypeLegend = useAppSelector( + // (state) => state.nodes.shouldShowFieldTypeLegend + // ); const shouldShowMinimapPanel = useAppSelector( (state) => state.nodes.shouldShowMinimapPanel ); @@ -39,9 +39,9 @@ const ViewportControls = () => { fitView(); }, [fitView]); - const handleClickedToggleFieldTypeLegend = useCallback(() => { - dispatch(shouldShowFieldTypeLegendChanged(!shouldShowFieldTypeLegend)); - }, [shouldShowFieldTypeLegend, dispatch]); + // const handleClickedToggleFieldTypeLegend = useCallback(() => { + // dispatch(shouldShowFieldTypeLegendChanged(!shouldShowFieldTypeLegend)); + // }, [shouldShowFieldTypeLegend, dispatch]); const handleClickedToggleMiniMapPanel = useCallback(() => { dispatch(shouldShowMinimapPanelChanged(!shouldShowMinimapPanel)); @@ -70,7 +70,7 @@ const ViewportControls = () => { icon={} /> - { onClick={handleClickedToggleFieldTypeLegend} icon={} /> - + */} { - const regex = new RegExp( - value - .toLowerCase() - .trim() - // strip out regex special characters to avoid errors - .replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '') - .split(' ') - .join('.*'), - 'g' - ); - return ( - regex.test(item.label.toLowerCase()) || - regex.test(item.description.toLowerCase()) || - item.tags.some((tag) => regex.test(tag)) - ); -}; - -const selector = createSelector( - [stateSelector], - ({ nodes }) => { - const data: NodeTemplate[] = map(nodes.nodeTemplates, (template) => { - return { - label: template.title, - value: template.type, - description: template.description, - tags: template.tags, - }; - }); - - data.push({ - label: 'Progress Image', - value: 'current_image', - description: 'Displays the current image in the Node Editor', - tags: ['progress'], - }); - - data.push({ - label: 'Notes', - value: 'notes', - description: 'Add notes about your workflow', - tags: ['notes'], - }); - - data.sort((a, b) => a.label.localeCompare(b.label)); - - return { data }; - }, - defaultSelectorOptions -); - -const AddNodeMenu = () => { - const dispatch = useAppDispatch(); - const { data } = useAppSelector(selector); - - const buildInvocation = useBuildNodeData(); - - const toaster = useAppToaster(); - - const addNode = useCallback( - (nodeType: AnyInvocationType) => { - const invocation = buildInvocation(nodeType); - - if (!invocation) { - toaster({ - status: 'error', - title: `Unknown Invocation type ${nodeType}`, - }); - return; - } - - dispatch(nodeAdded(invocation)); - }, - [dispatch, buildInvocation, toaster] - ); - - const handleChange = useCallback( - (v: string | null) => { - if (!v) { - return; - } - - addNode(v as AnyInvocationType); - }, - [addNode] - ); - - return ( - - - - ); -}; - -interface ItemProps extends React.ComponentPropsWithoutRef<'div'> { - value: string; - label: string; - description: string; -} - -const SelectItem = forwardRef( - ({ label, description, ...others }: ItemProps, ref) => { - return ( -
-
- {label} - - {description} - -
-
- ); - } -); - -SelectItem.displayName = 'SelectItem'; - -export default AddNodeMenu; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx index e53a8a391c..5f00604afa 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx @@ -1,11 +1,27 @@ -import { memo } from 'react'; +import { useAppDispatch } from 'app/store/storeHooks'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice'; +import { memo, useCallback } from 'react'; +import { FaPlus } from 'react-icons/fa'; import { Panel } from 'reactflow'; -import AddNodeMenu from './AddNodeMenu'; -const TopLeftPanel = () => ( - - - -); +const TopLeftPanel = () => { + const dispatch = useAppDispatch(); + + const handleOpenAddNodePopover = useCallback(() => { + dispatch(addNodePopoverOpened()); + }, [dispatch]); + + return ( + + } + /> + + ); +}; export default memo(TopLeftPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx b/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx deleted file mode 100644 index 1b9dc38cb6..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import { Box, Flex } from '@chakra-ui/layout'; -import { Tooltip } from '@chakra-ui/tooltip'; -import { useAppToaster } from 'app/components/Toaster'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIInput from 'common/components/IAIInput'; -import { useBuildNodeData } from 'features/nodes/hooks/useBuildNodeData'; -import { InvocationTemplate } from 'features/nodes/types/types'; -import Fuse from 'fuse.js'; -import { map } from 'lodash-es'; -import { - ChangeEvent, - FocusEvent, - KeyboardEvent, - ReactNode, - memo, - useCallback, - useRef, - useState, -} from 'react'; -import { AnyInvocationType } from 'services/events/types'; -import { nodeAdded } from '../../store/nodesSlice'; - -interface NodeListItemProps { - title: string; - description: string; - type: AnyInvocationType; - isSelected: boolean; - addNode: (nodeType: AnyInvocationType) => void; -} - -const NodeListItem = (props: NodeListItemProps) => { - const { title, description, type, isSelected, addNode } = props; - return ( - - addNode(type)} - background={isSelected ? 'base.600' : 'none'} - _hover={{ - background: 'base.600', - cursor: 'pointer', - }} - > - {title} - - - ); -}; - -NodeListItem.displayName = 'NodeListItem'; - -const NodeSearch = () => { - const nodeTemplates = useAppSelector((state) => - map(state.nodes.nodeTemplates) - ); - - const [filteredNodes, setFilteredNodes] = useState< - Fuse.FuseResult[] - >([]); - - const buildInvocation = useBuildNodeData(); - const dispatch = useAppDispatch(); - const toaster = useAppToaster(); - - const [searchText, setSearchText] = useState(''); - const [showNodeList, setShowNodeList] = useState(false); - const [focusedIndex, setFocusedIndex] = useState(-1); - const nodeSearchRef = useRef(null); - - const fuseOptions = { - findAllMatches: true, - threshold: 0, - ignoreLocation: true, - keys: ['title', 'type', 'tags'], - }; - - const fuse = new Fuse(nodeTemplates, fuseOptions); - - const findNode = (e: ChangeEvent) => { - setSearchText(e.target.value); - setFilteredNodes(fuse.search(e.target.value)); - setShowNodeList(true); - }; - - const addNode = useCallback( - (nodeType: AnyInvocationType) => { - const invocation = buildInvocation(nodeType); - - if (!invocation) { - toaster({ - status: 'error', - title: `Unknown Invocation type ${nodeType}`, - }); - return; - } - - dispatch(nodeAdded(invocation)); - }, - [dispatch, buildInvocation, toaster] - ); - - const renderNodeList = () => { - const nodeListToRender: ReactNode[] = []; - - if (searchText.length > 0) { - filteredNodes.forEach(({ item }, index) => { - const { title, description, type } = item; - if (title.toLowerCase().includes(searchText)) { - nodeListToRender.push( - - ); - } - }); - } else { - nodeTemplates.forEach(({ title, description, type }, index) => { - nodeListToRender.push( - - ); - }); - } - - return ( - - {nodeListToRender} - - ); - }; - - const searchKeyHandler = (e: KeyboardEvent) => { - const { key } = e; - let nextIndex = 0; - - if (key === 'ArrowDown') { - setShowNodeList(true); - if (searchText.length > 0) { - nextIndex = (focusedIndex + 1) % filteredNodes.length; - } else { - nextIndex = (focusedIndex + 1) % nodeTemplates.length; - } - } - - if (key === 'ArrowUp') { - setShowNodeList(true); - if (searchText.length > 0) { - nextIndex = - (focusedIndex + filteredNodes.length - 1) % filteredNodes.length; - } else { - nextIndex = - (focusedIndex + nodeTemplates.length - 1) % nodeTemplates.length; - } - } - - // # TODO Handle Blur - // if (key === 'Escape') { - // } - - if (key === 'Enter') { - let selectedNodeType: AnyInvocationType | undefined; - - if (searchText.length > 0) { - selectedNodeType = filteredNodes[focusedIndex]?.item.type; - } else { - selectedNodeType = nodeTemplates[focusedIndex]?.type; - } - - if (selectedNodeType) { - addNode(selectedNodeType); - } - setShowNodeList(false); - } - - setFocusedIndex(nextIndex); - }; - - const searchInputBlurHandler = (e: FocusEvent) => { - if (!e.currentTarget.contains(e.relatedTarget)) setShowNodeList(false); - }; - - return ( - setShowNodeList(true)} - onBlur={searchInputBlurHandler} - ref={nodeSearchRef} - > - - {showNodeList && renderNodeList()} - - ); -}; - -export default memo(NodeSearch); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 2e39b7cfc1..a8ce447c09 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -78,6 +78,7 @@ export const initialNodesState: NodesState = { shouldAnimateEdges: true, shouldSnapToGrid: false, shouldColorEdges: true, + isAddNodePopoverOpen: false, nodeOpacity: 1, selectedNodes: [], selectedEdges: [], @@ -699,6 +700,15 @@ const nodesSlice = createSlice({ }; }); }, + addNodePopoverOpened: (state) => { + state.isAddNodePopoverOpen = true; + }, + addNodePopoverClosed: (state) => { + state.isAddNodePopoverOpen = false; + }, + addNodePopoverToggled: (state) => { + state.isAddNodePopoverOpen = !state.isAddNodePopoverOpen; + }, }, extraReducers: (builder) => { builder.addCase(receivedOpenAPISchema.pending, (state) => { @@ -812,6 +822,9 @@ export const { selectionCopied, selectionPasted, selectedAll, + addNodePopoverOpened, + addNodePopoverClosed, + addNodePopoverToggled, } = nodesSlice.actions; export default nodesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index 1a26f959fd..bcc878d69e 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -33,4 +33,5 @@ export type NodesState = { mouseOverField: FieldIdentifier | null; nodesToCopy: Node[]; edgesToCopy: Edge[]; + isAddNodePopoverOpen: boolean; }; From 4ac41bc4b1e5b7652c94dcc1fe77dcfd3797789d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 19 Aug 2023 17:46:35 +1000 Subject: [PATCH 25/45] feat(ui): adding node selects new node exclusively --- .../web/src/features/nodes/store/nodesSlice.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index a8ce447c09..816a326ae0 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -144,6 +144,18 @@ const nodesSlice = createSlice({ 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); if (!isInvocationNode(node)) { From 5292eda0e4fb538821aebf5a327487830781812c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:00:35 +1000 Subject: [PATCH 26/45] feat(nodes): remove "Loader" from model nodes They are not loaders, they are selectors - remove this to reduce confusion. --- invokeai/app/invocations/model.py | 8 ++++---- invokeai/app/invocations/onnx.py | 2 +- invokeai/app/invocations/sdxl.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index cecca78651..3cae4b3383 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -72,7 +72,7 @@ class LoRAModelField(BaseModel): base_model: BaseModelType = Field(description="Base model") -@title("Main Model Loader") +@title("Main Model") @tags("model") class MainModelLoaderInvocation(BaseInvocation): """Loads a main model, outputting its submodels.""" @@ -179,7 +179,7 @@ class LoraLoaderOutput(BaseInvocationOutput): # fmt: on -@title("LoRA Loader") +@title("LoRA") @tags("lora", "model") class LoraLoaderInvocation(BaseInvocation): """Apply selected lora to unet and text_encoder.""" @@ -257,7 +257,7 @@ class SDXLLoraLoaderOutput(BaseInvocationOutput): # fmt: on -@title("SDXL LoRA Loader") +@title("SDXL LoRA") @tags("sdxl", "lora", "model") class SDXLLoraLoaderInvocation(BaseInvocation): """Apply selected lora to unet and text_encoder.""" @@ -356,7 +356,7 @@ class VaeLoaderOutput(BaseInvocationOutput): vae: VaeField = OutputField(description=FieldDescriptions.vae, title="VAE") -@title("VAE Loader") +@title("VAE") @tags("vae", "model") class VaeLoaderInvocation(BaseInvocation): """Loads a VAE model, outputting a VaeLoaderOutput""" diff --git a/invokeai/app/invocations/onnx.py b/invokeai/app/invocations/onnx.py index a2a5e436f9..b16694357b 100644 --- a/invokeai/app/invocations/onnx.py +++ b/invokeai/app/invocations/onnx.py @@ -406,7 +406,7 @@ class OnnxModelField(BaseModel): model_type: ModelType = Field(description="Model Type") -@title("ONNX Model Loader") +@title("ONNX Main Model") @tags("onnx", "model") class OnnxModelLoaderInvocation(BaseInvocation): """Loads a main model, outputting its submodels.""" diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py index 4efe30a3d9..fc224db14d 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -37,7 +37,7 @@ class SDXLRefinerModelLoaderOutput(BaseInvocationOutput): vae: VaeField = OutputField(description=FieldDescriptions.vae, title="VAE") -@title("SDXL Main Model Loader") +@title("SDXL Main Model") @tags("model", "sdxl") class SDXLModelLoaderInvocation(BaseInvocation): """Loads an sdxl base model, outputting its submodels.""" @@ -122,7 +122,7 @@ class SDXLModelLoaderInvocation(BaseInvocation): ) -@title("SDXL Refiner Model Loader") +@title("SDXL Refiner Model") @tags("model", "sdxl", "refiner") class SDXLRefinerModelLoaderInvocation(BaseInvocation): """Loads an sdxl refiner model, outputting its submodels.""" From 496a2db15c8920fe3d6e48192a96220620dd1152 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:02:41 +1000 Subject: [PATCH 27/45] feat(nodes): make `id`, `type` required in BaseInvocation, BaseInvocationOutput Doing this via these classes' `Config.schema_extra()` method makes it unintrusive and clients will get the correct types for these properties. Shifts the responsibility of correct types to the backend, where previously it was on the client. --- invokeai/app/invocations/baseinvocation.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 1a57225a34..d9bdcc988b 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -393,6 +393,13 @@ class BaseInvocationOutput(BaseModel): toprocess.extend(next_subclasses) return tuple(subclasses) + class Config: + @staticmethod + def schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None: + if "required" not in schema or not isinstance(schema["required"], list): + schema["required"] = list() + schema["required"].extend(["type"]) + class RequiredConnectionException(Exception): """Raised when an field which requires a connection did not receive a value.""" @@ -453,6 +460,9 @@ class BaseInvocation(ABC, BaseModel): schema["title"] = uiconfig.title if uiconfig and hasattr(uiconfig, "tags"): schema["tags"] = uiconfig.tags + if "required" not in schema or not isinstance(schema["required"], list): + schema["required"] = list() + schema["required"].extend(["type", "id"]) @abstractmethod def invoke(self, context: InvocationContext) -> BaseInvocationOutput: From 6e1ddb671e8b50a40a230aa63808285af39c876e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:18:22 +1000 Subject: [PATCH 28/45] feat(nodes): make fields that accept connection input optional in OpenAPI schema Doing this via `BaseInvocation`'s `Config.schema_extra()` means all clients get an accurate OpenAPI schema. Shifts the responsibility of correct types to the backend, where previously it was on the client. --- invokeai/app/invocations/baseinvocation.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index d9bdcc988b..99f48122cf 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -464,6 +464,16 @@ class BaseInvocation(ABC, BaseModel): schema["required"] = list() schema["required"].extend(["type", "id"]) + # nodes may have required fields, that can accept input from connections + # mark them as optional in the schema + for field_name, field in model_class.__fields__.items(): + _input = field.field_info.extra.get("input", None) + if _input in [Input.Connection, Input.Any]: + try: + schema["required"].remove(field_name) + except Exception: + pass + @abstractmethod def invoke(self, context: InvocationContext) -> BaseInvocationOutput: """Invoke with provided context and return outputs.""" From 37dc2d9d4d42923f8469270f9a0a80b6bf6628f5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:20:02 +1000 Subject: [PATCH 29/45] feat(nodes): update vae node tags --- invokeai/app/invocations/latent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 2a23931c3d..ba7f9e754f 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -456,7 +456,7 @@ class DenoiseLatentsInvocation(BaseInvocation): @title("Latents to Image") -@tags("latents", "image", "vae") +@tags("latents", "image", "vae", "l2i") class LatentsToImageInvocation(BaseInvocation): """Generates an image from latents.""" @@ -644,7 +644,7 @@ class ScaleLatentsInvocation(BaseInvocation): @title("Image to Latents") -@tags("latents", "image", "vae") +@tags("latents", "image", "vae", "i2l") class ImageToLatentsInvocation(BaseInvocation): """Encodes an image into latents.""" From cdc49456e89f7918edd1f364ef852a1c4e0be10a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:22:01 +1000 Subject: [PATCH 30/45] feat(api): add additional `class` attribute to invocations and outputs in OpenAPI schema It is `"invocation"` for invocations and `"output"` for outputs. Clients may use this to confidently and positively identify if an OpenAPI schema object is an invocation or output, instead of using a potentially fragile heuristic. --- invokeai/app/api_app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 0a31116878..b34000dc04 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -122,6 +122,7 @@ def custom_openapi(): output_schemas = schema(output_types, ref_prefix="#/components/schemas/") for schema_key, output_schema in output_schemas["definitions"].items(): + output_schema["class"] = "output" openapi_schema["components"]["schemas"][schema_key] = output_schema # TODO: note that we assume the schema_key here is the TYPE.__name__ @@ -130,8 +131,8 @@ def custom_openapi(): # Add Node Editor UI helper schemas ui_config_schemas = schema([UIConfigBase, _InputField, _OutputField], ref_prefix="#/components/schemas/") - for schema_key, output_schema in ui_config_schemas["definitions"].items(): - openapi_schema["components"]["schemas"][schema_key] = output_schema + for schema_key, ui_config_schema in ui_config_schemas["definitions"].items(): + openapi_schema["components"]["schemas"][schema_key] = ui_config_schema # Add a reference to the output type to additionalProperties of the invoker schema for invoker in all_invocations: @@ -140,8 +141,8 @@ def custom_openapi(): output_type_title = output_type_titles[output_type.__name__] invoker_schema = openapi_schema["components"]["schemas"][invoker_name] outputs_ref = {"$ref": f"#/components/schemas/{output_type_title}"} - invoker_schema["output"] = outputs_ref + invoker_schema["class"] = "invocation" from invokeai.backend.model_management.models import get_model_config_enums From bf04e913c28b044b15d79752a71c61753d2ecde2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:22:23 +1000 Subject: [PATCH 31/45] feat(nodes): make primitive outputs not optional, fix primitive invocation defaults --- invokeai/app/invocations/primitives.py | 34 ++++++++------------------ 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index fcf6adef87..607423e570 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -42,9 +42,7 @@ class BooleanCollectionOutput(BaseInvocationOutput): type: Literal["boolean_collection_output"] = "boolean_collection_output" # Outputs - collection: list[bool] = OutputField( - default_factory=list, description="The output boolean collection", ui_type=UIType.BooleanCollection - ) + collection: list[bool] = OutputField(description="The output boolean collection", ui_type=UIType.BooleanCollection) @title("Boolean Primitive") @@ -70,7 +68,7 @@ class BooleanCollectionInvocation(BaseInvocation): # Inputs collection: list[bool] = InputField( - default=False, description="The collection of boolean values", ui_type=UIType.BooleanCollection + default_factory=list, description="The collection of boolean values", ui_type=UIType.BooleanCollection ) def invoke(self, context: InvocationContext) -> BooleanCollectionOutput: @@ -95,9 +93,7 @@ class IntegerCollectionOutput(BaseInvocationOutput): type: Literal["integer_collection_output"] = "integer_collection_output" # Outputs - collection: list[int] = OutputField( - default_factory=list, description="The int collection", ui_type=UIType.IntegerCollection - ) + collection: list[int] = OutputField(description="The int collection", ui_type=UIType.IntegerCollection) @title("Integer Primitive") @@ -148,9 +144,7 @@ class FloatCollectionOutput(BaseInvocationOutput): type: Literal["float_collection_output"] = "float_collection_output" # Outputs - collection: list[float] = OutputField( - default_factory=list, description="The float collection", ui_type=UIType.FloatCollection - ) + collection: list[float] = OutputField(description="The float collection", ui_type=UIType.FloatCollection) @title("Float Primitive") @@ -176,7 +170,7 @@ class FloatCollectionInvocation(BaseInvocation): # Inputs collection: list[float] = InputField( - default=0, description="The collection of float values", ui_type=UIType.FloatCollection + default_factory=list, description="The collection of float values", ui_type=UIType.FloatCollection ) def invoke(self, context: InvocationContext) -> FloatCollectionOutput: @@ -201,9 +195,7 @@ class StringCollectionOutput(BaseInvocationOutput): type: Literal["string_collection_output"] = "string_collection_output" # Outputs - collection: list[str] = OutputField( - default_factory=list, description="The output strings", ui_type=UIType.StringCollection - ) + collection: list[str] = OutputField(description="The output strings", ui_type=UIType.StringCollection) @title("String Primitive") @@ -229,7 +221,7 @@ class StringCollectionInvocation(BaseInvocation): # Inputs collection: list[str] = InputField( - default=0, description="The collection of string values", ui_type=UIType.StringCollection + default_factory=list, description="The collection of string values", ui_type=UIType.StringCollection ) def invoke(self, context: InvocationContext) -> StringCollectionOutput: @@ -262,9 +254,7 @@ class ImageCollectionOutput(BaseInvocationOutput): type: Literal["image_collection_output"] = "image_collection_output" # Outputs - collection: list[ImageField] = OutputField( - default_factory=list, description="The output images", ui_type=UIType.ImageCollection - ) + collection: list[ImageField] = OutputField(description="The output images", ui_type=UIType.ImageCollection) @title("Image Primitive") @@ -334,7 +324,6 @@ class LatentsCollectionOutput(BaseInvocationOutput): type: Literal["latents_collection_output"] = "latents_collection_output" collection: list[LatentsField] = OutputField( - default_factory=list, description=FieldDescriptions.latents, ui_type=UIType.LatentsCollection, ) @@ -365,7 +354,7 @@ class LatentsCollectionInvocation(BaseInvocation): # Inputs collection: list[LatentsField] = InputField( - default=0, description="The collection of latents tensors", ui_type=UIType.LatentsCollection + description="The collection of latents tensors", ui_type=UIType.LatentsCollection ) def invoke(self, context: InvocationContext) -> LatentsCollectionOutput: @@ -410,9 +399,7 @@ class ColorCollectionOutput(BaseInvocationOutput): type: Literal["color_collection_output"] = "color_collection_output" # Outputs - collection: list[ColorField] = OutputField( - default_factory=list, description="The output colors", ui_type=UIType.ColorCollection - ) + collection: list[ColorField] = OutputField(description="The output colors", ui_type=UIType.ColorCollection) @title("Color Primitive") @@ -455,7 +442,6 @@ class ConditioningCollectionOutput(BaseInvocationOutput): # Outputs collection: list[ConditioningField] = OutputField( - default_factory=list, description="The output conditioning tensors", ui_type=UIType.ConditioningCollection, ) From 56245a74064988e51e6304e082312ceefea8751b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:23:23 +1000 Subject: [PATCH 32/45] chore(ui): regen types --- .../frontend/web/src/services/api/schema.d.ts | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index 320ef9d77c..6dce8f0c2b 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -596,8 +596,7 @@ export type components = { type: "boolean_collection"; /** * Collection - * @description The collection of boolean values - * @default false + * @description The collection of boolean values */ collection?: (boolean)[]; }; @@ -616,7 +615,7 @@ export type components = { * Collection * @description The output boolean collection */ - collection?: (boolean)[]; + collection: (boolean)[]; }; /** * Boolean Primitive @@ -843,7 +842,7 @@ export type components = { * Collection * @description The output colors */ - collection?: (components["schemas"]["ColorField"])[]; + collection: (components["schemas"]["ColorField"])[]; }; /** * Color Correct @@ -1045,7 +1044,7 @@ export type components = { * Collection * @description The output conditioning tensors */ - collection?: (components["schemas"]["ConditioningField"])[]; + collection: (components["schemas"]["ConditioningField"])[]; }; /** * ConditioningField @@ -1793,8 +1792,7 @@ export type components = { type: "float_collection"; /** * Collection - * @description The collection of float values - * @default 0 + * @description The collection of float values */ collection?: (number)[]; }; @@ -1813,7 +1811,7 @@ export type components = { * Collection * @description The float collection */ - collection?: (number)[]; + collection: (number)[]; }; /** * Float Primitive @@ -2204,7 +2202,7 @@ export type components = { * Collection * @description The output images */ - collection?: (components["schemas"]["ImageField"])[]; + collection: (components["schemas"]["ImageField"])[]; }; /** * Convert Image Mode @@ -3156,7 +3154,7 @@ export type components = { * Collection * @description The int collection */ - collection?: (number)[]; + collection: (number)[]; }; /** * Integer Primitive @@ -3279,8 +3277,7 @@ export type components = { type: "latents_collection"; /** * Collection - * @description The collection of latents tensors - * @default 0 + * @description The collection of latents tensors */ collection?: (components["schemas"]["LatentsField"])[]; }; @@ -3299,7 +3296,7 @@ export type components = { * Collection * @description Latents tensor */ - collection?: (components["schemas"]["LatentsField"])[]; + collection: (components["schemas"]["LatentsField"])[]; }; /** * LatentsField @@ -3644,7 +3641,7 @@ export type components = { weight: number; }; /** - * LoRA Loader + * LoRA * @description Apply selected lora to unet and text_encoder. */ LoraLoaderInvocation: { @@ -3725,7 +3722,7 @@ export type components = { model_type: components["schemas"]["ModelType"]; }; /** - * Main Model Loader + * Main Model * @description Loads a main model, outputting its submodels. */ MainModelLoaderInvocation: { @@ -4681,7 +4678,7 @@ export type components = { model_type: components["schemas"]["ModelType"]; }; /** - * ONNX Model Loader + * ONNX Main Model * @description Loads a main model, outputting its submodels. */ OnnxModelLoaderInvocation: { @@ -5195,7 +5192,7 @@ export type components = { clip2?: components["schemas"]["ClipField"]; }; /** - * SDXL LoRA Loader + * SDXL LoRA * @description Apply selected lora to unet and text_encoder. */ SDXLLoraLoaderInvocation: { @@ -5271,7 +5268,7 @@ export type components = { clip2?: components["schemas"]["ClipField"]; }; /** - * SDXL Main Model Loader + * SDXL Main Model * @description Loads an sdxl base model, outputting its submodels. */ SDXLModelLoaderInvocation: { @@ -5391,7 +5388,7 @@ export type components = { clip2?: components["schemas"]["ClipField"]; }; /** - * SDXL Refiner Model Loader + * SDXL Refiner Model * @description Loads an sdxl refiner model, outputting its submodels. */ SDXLRefinerModelLoaderInvocation: { @@ -5809,8 +5806,7 @@ export type components = { type: "string_collection"; /** * Collection - * @description The collection of string values - * @default 0 + * @description The collection of string values */ collection?: (string)[]; }; @@ -5829,7 +5825,7 @@ export type components = { * Collection * @description The output strings */ - collection?: (string)[]; + collection: (string)[]; }; /** * String Primitive @@ -6023,7 +6019,7 @@ export type components = { vae: components["schemas"]["ModelInfo"]; }; /** - * VAE Loader + * VAE * @description Loads a VAE model, outputting a VaeLoaderOutput */ VaeLoaderInvocation: { @@ -6193,6 +6189,18 @@ export type components = { ui_hidden: boolean; ui_type?: components["schemas"]["UIType"]; }; + /** + * ControlNetModelFormat + * @description An enumeration. + * @enum {string} + */ + ControlNetModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionOnnxModelFormat * @description An enumeration. @@ -6205,24 +6213,12 @@ export type components = { * @enum {string} */ StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * ControlNetModelFormat - * @description An enumeration. - * @enum {string} - */ - ControlNetModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionXLModelFormat * @description An enumeration. * @enum {string} */ StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusion2ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; From ab76d54c10aa6e1b2c95605a54be22a267faafef Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:24:43 +1000 Subject: [PATCH 33/45] feat(ui): update node schema parsing simplified logic thanks to backend changes --- .../src/features/nodes/types/typeGuards.ts | 9 -- .../web/src/features/nodes/types/types.ts | 37 ++++- .../nodes/util/fieldTemplateBuilders.ts | 69 -------- .../src/features/nodes/util/parseSchema.ts | 148 +++++++++++------- 4 files changed, 128 insertions(+), 135 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/types/typeGuards.ts diff --git a/invokeai/frontend/web/src/features/nodes/types/typeGuards.ts b/invokeai/frontend/web/src/features/nodes/types/typeGuards.ts deleted file mode 100644 index 99c9a28150..0000000000 --- a/invokeai/frontend/web/src/features/nodes/types/typeGuards.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { OpenAPIV3 } from 'openapi-types'; - -export const isReferenceObject = ( - obj: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject -): obj is OpenAPIV3.ReferenceObject => '$ref' in obj; - -export const isSchemaObject = ( - obj: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject -): obj is OpenAPIV3.SchemaObject => !('$ref' in obj); diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index c1f1462fda..1b5d193aed 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -518,6 +518,18 @@ export type InvocationBaseSchemaObject = Omit< > & InvocationSchemaExtra; +export type InvocationOutputSchemaObject = Omit< + OpenAPIV3.SchemaObject, + 'properties' +> & + OpenAPIV3.SchemaObject['properties'] & { + type: Omit & { + default: AnyInvocationType; + }; + } & { + class: 'output'; + }; + export type InvocationFieldSchema = OpenAPIV3.SchemaObject & _InputField; export interface ArraySchemaObject extends InvocationBaseSchemaObject { @@ -528,11 +540,30 @@ export interface NonArraySchemaObject extends InvocationBaseSchemaObject { type?: OpenAPIV3.NonArraySchemaObjectType; } -export type InvocationSchemaObject = ArraySchemaObject | NonArraySchemaObject; +export type InvocationSchemaObject = ( + | ArraySchemaObject + | NonArraySchemaObject +) & { class: 'invocation' }; + +export const isSchemaObject = ( + obj: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject +): obj is OpenAPIV3.SchemaObject => !('$ref' in obj); export const isInvocationSchemaObject = ( - obj: OpenAPIV3.ReferenceObject | InvocationSchemaObject -): obj is InvocationSchemaObject => !('$ref' in obj); + obj: + | OpenAPIV3.ReferenceObject + | OpenAPIV3.SchemaObject + | InvocationSchemaObject +): obj is InvocationSchemaObject => + 'class' in obj && obj.class === 'invocation'; + +export const isInvocationOutputSchemaObject = ( + obj: + | OpenAPIV3.ReferenceObject + | OpenAPIV3.SchemaObject + | InvocationOutputSchemaObject +): obj is InvocationOutputSchemaObject => + 'class' in obj && obj.class === 'output'; export const isInvocationFieldSchema = ( obj: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts index 63a0d51f05..ef96b8b485 100644 --- a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts +++ b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts @@ -1,8 +1,4 @@ -import { logger } from 'app/logging/logger'; -import { parseify } from 'common/util/serialize'; -import { reduce } from 'lodash-es'; import { OpenAPIV3 } from 'openapi-types'; -import { isSchemaObject } from '../types/typeGuards'; import { BooleanInputFieldTemplate, ClipInputFieldTemplate, @@ -24,7 +20,6 @@ import { LatentsInputFieldTemplate, LoRAModelInputFieldTemplate, MainModelInputFieldTemplate, - OutputFieldTemplate, SDXLMainModelInputFieldTemplate, SDXLRefinerModelInputFieldTemplate, SchedulerInputFieldTemplate, @@ -33,7 +28,6 @@ import { VaeInputFieldTemplate, VaeModelInputFieldTemplate, isFieldType, - isInvocationFieldSchema, } from '../types/types'; export type BaseFieldProperties = 'name' | 'title' | 'description'; @@ -628,66 +622,3 @@ export const buildInputFieldTemplate = ( } return; }; - -/** - * Builds invocation output fields from an invocation's output reference object. - * @param openAPI The OpenAPI schema - * @param refObject The output reference object - * @returns A record of outputs - */ -export const buildOutputFieldTemplates = ( - refObject: OpenAPIV3.ReferenceObject, - openAPI: OpenAPIV3.Document -): Record => { - // extract output schema name from ref - const outputSchemaName = refObject.$ref.split('/').slice(-1)[0]; - - if (!outputSchemaName) { - logger('nodes').error( - { refObject: parseify(refObject) }, - 'No output schema name found in ref object' - ); - throw 'No output schema name found in ref object'; - } - - // get the output schema itself - const outputSchema = openAPI.components?.schemas?.[outputSchemaName]; - if (!outputSchema) { - logger('nodes').error({ outputSchemaName }, 'Output schema not found'); - throw 'Output schema not found'; - } - - // console.log('output', outputSchema); - if (isSchemaObject(outputSchema)) { - // console.log('isSchemaObject'); - const outputFields = reduce( - outputSchema.properties as OpenAPIV3.SchemaObject, - (outputsAccumulator, property, propertyName) => { - if ( - !['type', 'id'].includes(propertyName) && - !['object'].includes(property.type) && // TODO: handle objects? - isInvocationFieldSchema(property) - ) { - const fieldType = getFieldType(property); - // console.log('output fieldType', fieldType); - outputsAccumulator[propertyName] = { - fieldKind: 'output', - name: propertyName, - title: property.title ?? '', - description: property.description ?? '', - type: fieldType, - }; - } else { - // console.warn('Unhandled OUTPUT property', property); - } - - return outputsAccumulator; - }, - {} as Record - ); - - return outputFields; - } - - return {}; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts index a900d0ddae..e237ecbfe4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts @@ -1,83 +1,123 @@ -import { filter, reduce } from 'lodash-es'; +import { logger } from 'app/logging/logger'; +import { parseify } from 'common/util/serialize'; +import { reduce } from 'lodash-es'; import { OpenAPIV3 } from 'openapi-types'; +import { AnyInvocationType } from 'services/events/types'; import { InputFieldTemplate, InvocationSchemaObject, InvocationTemplate, + OutputFieldTemplate, isInvocationFieldSchema, + isInvocationOutputSchemaObject, isInvocationSchemaObject, } from '../types/types'; -import { - buildInputFieldTemplate, - buildOutputFieldTemplates, -} from './fieldTemplateBuilders'; +import { buildInputFieldTemplate, getFieldType } from './fieldTemplateBuilders'; const RESERVED_FIELD_NAMES = ['id', 'type', 'metadata']; -const invocationDenylist = [ - 'Graph', - 'InvocationMeta', - 'MetadataAccumulatorInvocation', +const invocationDenylist: AnyInvocationType[] = [ + 'graph', + 'metadata_accumulator', ]; +const isNotInDenylist = (schema: InvocationSchemaObject) => + !invocationDenylist.includes(schema.properties.type.default); + export const parseSchema = ( openAPI: OpenAPIV3.Document ): Record => { - const filteredSchemas = filter( - openAPI.components?.schemas, - (schema, key) => - key.includes('Invocation') && - !key.includes('InvocationOutput') && - !invocationDenylist.some((denylistItem) => key.includes(denylistItem)) - ) as (OpenAPIV3.ReferenceObject | InvocationSchemaObject)[]; + const filteredSchemas = Object.values(openAPI.components?.schemas ?? {}) + .filter(isInvocationSchemaObject) + .filter(isNotInDenylist); const invocations = filteredSchemas.reduce< Record >((acc, schema) => { - if (isInvocationSchemaObject(schema)) { - const type = schema.properties.type.default; - const title = schema.title.replace('Invocation', ''); - const tags = schema.tags ?? []; - const description = schema.description ?? ''; + const type = schema.properties.type.default; + const title = schema.title.replace('Invocation', ''); + const tags = schema.tags ?? []; + const description = schema.description ?? ''; - const inputs = reduce( - schema.properties, - (inputsAccumulator, property, propertyName) => { - if ( - !RESERVED_FIELD_NAMES.includes(propertyName) && - isInvocationFieldSchema(property) && - !property.ui_hidden - ) { - const field = buildInputFieldTemplate( - schema, - property, - propertyName - ); + const inputs = reduce( + schema.properties, + (inputsAccumulator, property, propertyName) => { + if ( + !RESERVED_FIELD_NAMES.includes(propertyName) && + isInvocationFieldSchema(property) && + !property.ui_hidden + ) { + const field = buildInputFieldTemplate(schema, property, propertyName); - if (field) { - inputsAccumulator[propertyName] = field; - } + if (field) { + inputsAccumulator[propertyName] = field; } - return inputsAccumulator; - }, - {} as Record + } + return inputsAccumulator; + }, + {} as Record + ); + + const outputSchemaName = schema.output.$ref.split('/').pop(); + + if (!outputSchemaName) { + logger('nodes').error( + { outputRefObject: parseify(schema.output) }, + 'No output schema name found in ref object' ); - - const rawOutput = (schema as InvocationSchemaObject).output; - const outputs = buildOutputFieldTemplates(rawOutput, openAPI); - - const invocation: InvocationTemplate = { - title, - type, - tags, - description, - inputs, - outputs, - }; - - Object.assign(acc, { [type]: invocation }); + throw 'No output schema name found in ref object'; } + const outputSchema = openAPI.components?.schemas?.[outputSchemaName]; + if (!outputSchema) { + logger('nodes').error({ outputSchemaName }, 'Output schema not found'); + throw 'Output schema not found'; + } + + if (!isInvocationOutputSchemaObject(outputSchema)) { + logger('nodes').error( + { outputSchema: parseify(outputSchema) }, + 'Invalid output schema' + ); + throw 'Invalid output schema'; + } + + const outputs = reduce( + outputSchema.properties as OpenAPIV3.SchemaObject, + (outputsAccumulator, property, propertyName) => { + if ( + !['type', 'id'].includes(propertyName) && + !['object'].includes(property.type) && // TODO: handle objects? + isInvocationFieldSchema(property) + ) { + const fieldType = getFieldType(property); + outputsAccumulator[propertyName] = { + fieldKind: 'output', + name: propertyName, + title: property.title ?? '', + description: property.description ?? '', + type: fieldType, + }; + } else { + logger('nodes').warn({ property }, 'Unhandled output property'); + } + + return outputsAccumulator; + }, + {} as Record + ); + + const invocation: InvocationTemplate = { + title, + type, + tags, + description, + inputs, + outputs, + }; + + Object.assign(acc, { [type]: invocation }); + return acc; }, {}); From f9fc89b3c5c00aa7a2191371df21c455ca1f2255 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:25:12 +1000 Subject: [PATCH 34/45] feat(ui): nodes scheduler type default value -> `"euler"` --- .../frontend/web/src/features/nodes/util/fieldValueBuilders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts index 91e7ca522d..07f8074eb9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts +++ b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts @@ -94,7 +94,7 @@ export const buildInputFieldValue = ( } if (template.type === 'Scheduler') { - fieldValue.value = undefined; + fieldValue.value = 'euler'; } return fieldValue; From 6d111aac9016282121c70376625bbc28e11c639e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 21:55:14 +1000 Subject: [PATCH 35/45] fix(ui): fix node opacity slider hitbox --- .../flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx | 9 +++++---- invokeai/frontend/web/src/theme/components/slider.ts | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) 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 87e49efc55..7818dece72 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,13 +1,13 @@ import { - Box, + Flex, Slider, SliderFilledTrack, SliderThumb, SliderTrack, } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useCallback } from 'react'; import { nodeOpacityChanged } from 'features/nodes/store/nodesSlice'; +import { useCallback } from 'react'; export default function NodeOpacitySlider() { const dispatch = useAppDispatch(); @@ -21,7 +21,7 @@ export default function NodeOpacitySlider() { ); return ( - + - +
); } diff --git a/invokeai/frontend/web/src/theme/components/slider.ts b/invokeai/frontend/web/src/theme/components/slider.ts index 98a2556b9e..6392da6cc1 100644 --- a/invokeai/frontend/web/src/theme/components/slider.ts +++ b/invokeai/frontend/web/src/theme/components/slider.ts @@ -22,8 +22,8 @@ const invokeAIFilledTrack = defineStyle((props) => { const invokeAIThumb = defineStyle((props) => { return { - w: 2, - h: 4, + w: props.orientation === 'horizontal' ? 2 : 4, + h: props.orientation === 'horizontal' ? 4 : 2, bg: mode('base.50', 'base.100')(props), }; }); From d95773f50ffaa07788609fb6290f6dc8fc461ed9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 22:24:42 +1000 Subject: [PATCH 36/45] Revert "feat(nodes): make fields that accept connection input optional in OpenAPI schema" This reverts commit 7325cbdd250153f347e3782265dd42783f7f1d00. --- invokeai/app/invocations/baseinvocation.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 99f48122cf..d9bdcc988b 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -464,16 +464,6 @@ class BaseInvocation(ABC, BaseModel): schema["required"] = list() schema["required"].extend(["type", "id"]) - # nodes may have required fields, that can accept input from connections - # mark them as optional in the schema - for field_name, field in model_class.__fields__.items(): - _input = field.field_info.extra.get("input", None) - if _input in [Input.Connection, Input.Any]: - try: - schema["required"].remove(field_name) - except Exception: - pass - @abstractmethod def invoke(self, context: InvocationContext) -> BaseInvocationOutput: """Invoke with provided context and return outputs.""" From 2dfcba865408483a728f75a0f9fc52a9853ab370 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 23:07:18 +1000 Subject: [PATCH 37/45] fix(ui): fix graphs using old field names --- .../nodes/util/graphBuilders/addDynamicPromptsToGraph.ts | 6 +++--- .../nodes/util/graphBuilders/buildCanvasInpaintGraph.ts | 2 +- .../nodes/util/graphBuilders/buildCanvasOutpaintGraph.ts | 2 +- .../nodes/util/graphBuilders/buildCanvasSDXLInpaintGraph.ts | 2 +- .../util/graphBuilders/buildCanvasSDXLOutpaintGraph.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts index ae3b31c2ad..9c71de5516 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts @@ -108,13 +108,13 @@ export const addDynamicPromptsToGraph = ( // Connect random int to the start of the range of size so the range starts on the random first seed graph.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, + source: { node_id: RANDOM_INT, field: 'value' }, destination: { node_id: NOISE, field: 'seed' }, }); if (metadataAccumulator) { graph.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, + source: { node_id: RANDOM_INT, field: 'value' }, destination: { node_id: METADATA_ACCUMULATOR, field: 'seed' }, }); } @@ -198,7 +198,7 @@ export const addDynamicPromptsToGraph = ( // Connect random int to the start of the range of size so the range starts on the random first seed graph.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, + source: { node_id: RANDOM_INT, field: 'value' }, destination: { node_id: RANGE_OF_SIZE, field: 'start' }, }); } else { diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts index 6b0da8e197..4f83868d67 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts @@ -504,7 +504,7 @@ export const buildCanvasInpaintGraph = ( // Connect random int to the start of the range of size so the range starts on the random first seed graph.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, + source: { node_id: RANDOM_INT, field: 'value' }, destination: { node_id: RANGE_OF_SIZE, field: 'start' }, }); } else { diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasOutpaintGraph.ts index a949c88e5f..379f864549 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasOutpaintGraph.ts @@ -845,7 +845,7 @@ export const buildCanvasOutpaintGraph = ( // Connect random int to the start of the range of size so the range starts on the random first seed graph.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, + source: { node_id: RANDOM_INT, field: 'value' }, destination: { node_id: RANGE_OF_SIZE, field: 'start' }, }); } else { diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLInpaintGraph.ts index ba40a70c83..12ae969f76 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLInpaintGraph.ts @@ -518,7 +518,7 @@ export const buildCanvasSDXLInpaintGraph = ( // Connect random int to the start of the range of size so the range starts on the random first seed graph.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, + source: { node_id: RANDOM_INT, field: 'value' }, destination: { node_id: RANGE_OF_SIZE, field: 'start' }, }); } else { diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLOutpaintGraph.ts index 1cc268c03d..5dc0093f79 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLOutpaintGraph.ts @@ -860,7 +860,7 @@ export const buildCanvasSDXLOutpaintGraph = ( // Connect random int to the start of the range of size so the range starts on the random first seed graph.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, + source: { node_id: RANDOM_INT, field: 'value' }, destination: { node_id: RANGE_OF_SIZE, field: 'start' }, }); } else { From 990b6b5f6a80b138f80d9cbb91ce5afc1fbea465 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 20 Aug 2023 23:07:55 +1000 Subject: [PATCH 38/45] feat(ui): useful tooltips on invoke button --- .../web/src/common/components/IAIButton.tsx | 4 +- .../src/common/components/IAIIconButton.tsx | 4 +- .../src/common/hooks/useIsReadyToInvoke.ts | 119 +++++++++------ .../TopCenterPanel/NodeInvokeButton.tsx | 64 ++++----- .../flow/panels/TopLeftPanel/TopLeftPanel.tsx | 12 +- .../web/src/features/nodes/store/selectors.ts | 92 ------------ .../ProcessButtons/InvokeButton.tsx | 136 +++++++++++------- 7 files changed, 193 insertions(+), 238 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/store/selectors.ts diff --git a/invokeai/frontend/web/src/common/components/IAIButton.tsx b/invokeai/frontend/web/src/common/components/IAIButton.tsx index d1e77537cc..4058296aaf 100644 --- a/invokeai/frontend/web/src/common/components/IAIButton.tsx +++ b/invokeai/frontend/web/src/common/components/IAIButton.tsx @@ -8,8 +8,8 @@ import { import { memo, ReactNode } from 'react'; export interface IAIButtonProps extends ButtonProps { - tooltip?: string; - tooltipProps?: Omit; + tooltip?: TooltipProps['label']; + tooltipProps?: Omit; isChecked?: boolean; children: ReactNode; } diff --git a/invokeai/frontend/web/src/common/components/IAIIconButton.tsx b/invokeai/frontend/web/src/common/components/IAIIconButton.tsx index ed1514055e..0a42430689 100644 --- a/invokeai/frontend/web/src/common/components/IAIIconButton.tsx +++ b/invokeai/frontend/web/src/common/components/IAIIconButton.tsx @@ -9,8 +9,8 @@ import { memo } from 'react'; export type IAIIconButtonProps = IconButtonProps & { role?: string; - tooltip?: string; - tooltipProps?: Omit; + tooltip?: TooltipProps['label']; + tooltipProps?: Omit; isChecked?: boolean; }; diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts index f43ec1851f..ac770e3787 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts @@ -2,71 +2,104 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -// import { validateSeedWeights } from 'common/util/seedWeightPairs'; +import { isInvocationNode } from 'features/nodes/types/types'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { forEach } from 'lodash-es'; -import { NON_REFINER_BASE_MODELS } from 'services/api/constants'; -import { modelsApi } from '../../services/api/endpoints/models'; +import { forEach, map } from 'lodash-es'; +import { getConnectedEdges } from 'reactflow'; -const readinessSelector = createSelector( +const selector = createSelector( [stateSelector, activeTabNameSelector], (state, activeTabName) => { - const { generation, system } = state; - const { initialImage } = generation; + const { generation, system, nodes } = state; + const { initialImage, model } = generation; const { isProcessing, isConnected } = system; - let isReady = true; - const reasonsWhyNotReady: string[] = []; + const reasons: string[] = []; - if (activeTabName === 'img2img' && !initialImage) { - isReady = false; - reasonsWhyNotReady.push('No initial image selected'); - } - - const { isSuccess: mainModelsSuccessfullyLoaded } = - modelsApi.endpoints.getMainModels.select(NON_REFINER_BASE_MODELS)(state); - if (!mainModelsSuccessfullyLoaded) { - isReady = false; - reasonsWhyNotReady.push('Models are not loaded'); - } - - // TODO: job queue // Cannot generate if already processing an image if (isProcessing) { - isReady = false; - reasonsWhyNotReady.push('System Busy'); + reasons.push('System busy'); } // Cannot generate if not connected if (!isConnected) { - isReady = false; - reasonsWhyNotReady.push('System Disconnected'); + reasons.push('System disconnected'); } - // // Cannot generate variations without valid seed weights - // if ( - // shouldGenerateVariations && - // (!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1) - // ) { - // isReady = false; - // reasonsWhyNotReady.push('Seed-Weights badly formatted.'); - // } + if (activeTabName === 'img2img' && !initialImage) { + reasons.push('No initial image selected'); + } - forEach(state.controlNet.controlNets, (controlNet, id) => { - if (!controlNet.model) { - isReady = false; - reasonsWhyNotReady.push(`ControlNet ${id} has no model selected.`); + if (activeTabName === 'nodes' && nodes.shouldValidateGraph) { + nodes.nodes.forEach((node) => { + if (!isInvocationNode(node)) { + return; + } + + const nodeTemplate = nodes.nodeTemplates[node.data.type]; + + if (!nodeTemplate) { + // Node type not found + reasons.push('Missing node template'); + 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('Missing field template'); + return; + } + + if (fieldTemplate.required && !field.value && !hasConnection) { + reasons.push( + `${node.data.label || nodeTemplate.title} -> ${ + field.label || fieldTemplate.title + } missing input` + ); + return; + } + }); + }); + } else { + if (!model) { + reasons.push('No model selected'); } - }); - // All good - return { isReady, reasonsWhyNotReady }; + if (state.controlNet.isEnabled) { + map(state.controlNet.controlNets).forEach((controlNet, i) => { + if (!controlNet.isEnabled) { + return; + } + if (!controlNet.model) { + reasons.push(`ControlNet ${i + 1} has no model selected.`); + } + + if ( + !controlNet.controlImage || + (!controlNet.processedControlImage && + controlNet.processorType !== 'none') + ) { + reasons.push(`ControlNet ${i + 1} has no control image`); + } + }); + } + } + + return { isReady: !reasons.length, isProcessing, reasons }; }, defaultSelectorOptions ); export const useIsReadyToInvoke = () => { - const { isReady } = useAppSelector(readinessSelector); - return isReady; + const { isReady, isProcessing, reasons } = useAppSelector(selector); + return { isReady, isProcessing, reasons }; }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/NodeInvokeButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/NodeInvokeButton.tsx index f207e910b1..decaea19e8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/NodeInvokeButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/NodeInvokeButton.tsx @@ -2,10 +2,9 @@ import { Box } from '@chakra-ui/react'; import { userInvoked } from 'app/store/actions'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton, { IAIButtonProps } from 'common/components/IAIButton'; -import IAIIconButton, { - IAIIconButtonProps, -} from 'common/components/IAIIconButton'; -import { selectIsReadyNodes } from 'features/nodes/store/selectors'; +import { IAIIconButtonProps } from 'common/components/IAIIconButton'; +import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; +import { InvokeButtonTooltipContent } from 'features/parameters/components/ProcessButtons/InvokeButton'; import ProgressBar from 'features/system/components/ProgressBar'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; @@ -14,15 +13,13 @@ import { useTranslation } from 'react-i18next'; import { FaPlay } from 'react-icons/fa'; interface InvokeButton - extends Omit { - iconButton?: boolean; -} + extends Omit {} const NodeInvokeButton = (props: InvokeButton) => { - const { iconButton = false, ...rest } = props; + const { ...rest } = props; const dispatch = useAppDispatch(); const activeTabName = useAppSelector(activeTabNameSelector); - const isReady = useAppSelector(selectIsReadyNodes); + const { isReady, isProcessing } = useIsReadyToInvoke(); const handleInvoke = useCallback(() => { dispatch(userInvoked('nodes')); }, [dispatch]); @@ -58,37 +55,24 @@ const NodeInvokeButton = (props: InvokeButton) => { )} - {iconButton ? ( - } - isDisabled={!isReady} - onClick={handleInvoke} - flexGrow={1} - w="100%" - tooltip={t('parameters.invoke')} - tooltipProps={{ placement: 'bottom' }} - colorScheme="accent" - id="invoke-button" - {...rest} - /> - ) : ( - - Invoke - - )} + } + aria-label={t('parameters.invoke')} + type="submit" + isDisabled={!isReady} + onClick={handleInvoke} + flexGrow={1} + w="100%" + colorScheme="accent" + id="invoke-button" + leftIcon={isProcessing ? undefined : } + fontWeight={700} + isLoading={isProcessing} + loadingText={t('parameters.invoke')} + {...rest} + > + Invoke + ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx index 5f00604afa..67471b7e3d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx @@ -1,8 +1,7 @@ import { useAppDispatch } from 'app/store/storeHooks'; -import IAIIconButton from 'common/components/IAIIconButton'; +import IAIButton from 'common/components/IAIButton'; import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice'; import { memo, useCallback } from 'react'; -import { FaPlus } from 'react-icons/fa'; import { Panel } from 'reactflow'; const TopLeftPanel = () => { @@ -14,12 +13,9 @@ const TopLeftPanel = () => { return ( - } - /> + + Add Node + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts deleted file mode 100644 index 41a608baa3..0000000000 --- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { stateSelector } from 'app/store/store'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -// import { validateSeedWeights } from 'common/util/seedWeightPairs'; -import { every } from 'lodash-es'; -import { getConnectedEdges } from 'reactflow'; -import { isInvocationNode } from '../types/types'; -import { NodesState } from './types'; - -export const selectIsReadyNodes = createSelector( - [stateSelector], - (state) => { - const { nodes, system } = state; - const { isProcessing, isConnected } = system; - - if (isProcessing || !isConnected) { - // Cannot generate if already processing an image - return false; - } - - if (!nodes.shouldValidateGraph) { - return true; - } - - const isGraphReady = every(nodes.nodes, (node) => { - if (!isInvocationNode(node)) { - return true; - } - - const nodeTemplate = nodes.nodeTemplates[node.data.type]; - - if (!nodeTemplate) { - // Node type not found - return false; - } - - const connectedEdges = getConnectedEdges([node], nodes.edges); - - const isNodeValid = every(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) { - // Field type not found - return false; - } - - if (fieldTemplate.required && !field.value && !hasConnection) { - // Required field is empty or does not have a connection - return false; - } - - // ok - return true; - }); - - return isNodeValid; - }); - - return isGraphReady; - }, - defaultSelectorOptions -); - -export const getNodeAndTemplate = (nodeId: string, nodes: NodesState) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; - - return { node, nodeTemplate }; -}; - -export const getInputFieldAndTemplate = ( - nodeId: string, - fieldName: string, - nodes: NodesState -) => { - const node = nodes.nodes - .filter(isInvocationNode) - .find((node) => node.id === nodeId); - const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; - - if (!node || !nodeTemplate) { - return; - } - - const field = node.data.inputs[fieldName]; - const fieldTemplate = nodeTemplate.inputs[fieldName]; - - return { field, fieldTemplate }; -}; diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index 3880f717b9..2332a91c7d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -1,4 +1,12 @@ -import { Box, ChakraProps, Tooltip } from '@chakra-ui/react'; +import { + Box, + ChakraProps, + Divider, + Flex, + ListItem, + Text, + UnorderedList, +} from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { userInvoked } from 'app/store/actions'; import { stateSelector } from 'app/store/store'; @@ -13,7 +21,7 @@ import { clampSymmetrySteps } from 'features/parameters/store/generationSlice'; import ProgressBar from 'features/system/components/ProgressBar'; import { selectIsBusy } from 'features/system/store/systemSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaPlay } from 'react-icons/fa'; @@ -53,9 +61,8 @@ interface InvokeButton export default function InvokeButton(props: InvokeButton) { const { iconButton = false, ...rest } = props; const dispatch = useAppDispatch(); - const isReady = useIsReadyToInvoke(); - const { isBusy, autoAddBoardId, activeTabName } = useAppSelector(selector); - const autoAddBoardName = useBoardName(autoAddBoardId); + const { isReady, isProcessing } = useIsReadyToInvoke(); + const { activeTabName } = useAppSelector(selector); const handleInvoke = useCallback(() => { dispatch(clampSymmetrySteps()); @@ -94,53 +101,80 @@ export default function InvokeButton(props: InvokeButton) { )} - - {iconButton ? ( - } - isDisabled={!isReady || isBusy} - onClick={handleInvoke} - tooltip={t('parameters.invoke')} - tooltipProps={{ placement: 'top' }} - colorScheme="accent" - id="invoke-button" - {...rest} - sx={{ - w: 'full', - flexGrow: 1, - ...(isBusy ? IN_PROGRESS_STYLES : {}), - }} - /> - ) : ( - - Invoke - - )} - + {iconButton ? ( + } + isDisabled={!isReady} + onClick={handleInvoke} + tooltip={} + colorScheme="accent" + isLoading={isProcessing} + id="invoke-button" + {...rest} + sx={{ + w: 'full', + flexGrow: 1, + ...(isProcessing ? IN_PROGRESS_STYLES : {}), + }} + /> + ) : ( + } + aria-label={t('parameters.invoke')} + type="submit" + isDisabled={!isReady} + onClick={handleInvoke} + colorScheme="accent" + id="invoke-button" + leftIcon={isProcessing ? undefined : } + isLoading={isProcessing} + loadingText={t('parameters.invoke')} + {...rest} + sx={{ + w: 'full', + flexGrow: 1, + fontWeight: 700, + ...(isProcessing ? IN_PROGRESS_STYLES : {}), + }} + > + Invoke + + )} ); } + +export const InvokeButtonTooltipContent = memo(() => { + const { isReady, reasons } = useIsReadyToInvoke(); + const { autoAddBoardId } = useAppSelector(selector); + const autoAddBoardName = useBoardName(autoAddBoardId); + + return ( + + + {isReady ? 'Ready to Invoke' : 'Unable to Invoke'} + + {reasons.length > 0 && ( + + {reasons.map((reason, i) => ( + + {reason} + + ))} + + )} + + + Adding images to{' '} + + {autoAddBoardName || 'Uncategorized'} + + + + ); +}); + +InvokeButtonTooltipContent.displayName = 'InvokeButtonTooltipContent'; From 5c305b1eeba851e8a1705e7b1ea076b61ce6573e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 21 Aug 2023 00:32:11 +1000 Subject: [PATCH 39/45] feat(ui): add app error boundary Should catch all app crashes --- invokeai/frontend/web/package.json | 2 + .../frontend/web/src/app/components/App.tsx | 16 ++- .../components/AppErrorBoundaryFallback.tsx | 97 +++++++++++++++++++ .../{ImageMetadataJSON.tsx => DataViewer.tsx} | 25 ++--- .../ImageMetadataViewer.tsx | 8 +- .../sidePanel/inspector/InspectorDataTab.tsx | 4 +- .../inspector/InspectorTemplateTab.tsx | 4 +- .../sidePanel/workflow/WorkflowJSONTab.tsx | 8 +- invokeai/frontend/web/yarn.lock | 12 +++ 9 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx rename invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/{ImageMetadataJSON.tsx => DataViewer.tsx} (81%) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 1f86d2585c..2f60245768 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -84,6 +84,7 @@ "konva": "^9.2.0", "lodash-es": "^4.17.21", "nanostores": "^0.9.2", + "new-github-issue-url": "^1.0.0", "openapi-fetch": "^0.6.1", "overlayscrollbars": "^2.2.0", "overlayscrollbars-react": "^0.5.0", @@ -94,6 +95,7 @@ "react-colorful": "^5.6.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-error-boundary": "^4.0.11", "react-hotkeys-hook": "4.4.0", "react-i18next": "^13.0.1", "react-icons": "^4.10.1", diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index fa45ae93cd..8c7ce65ece 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -16,9 +16,11 @@ import InvokeTabs from 'features/ui/components/InvokeTabs'; import ParametersDrawer from 'features/ui/components/ParametersDrawer'; import i18n from 'i18n'; import { size } from 'lodash-es'; -import { ReactNode, memo, useEffect } from 'react'; +import { ReactNode, memo, useCallback, useEffect } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import GlobalHotkeys from './GlobalHotkeys'; import Toaster from './Toaster'; +import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; const DEFAULT_CONFIG = {}; @@ -32,6 +34,11 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => { const logger = useLogger(); const dispatch = useAppDispatch(); + const handleReset = useCallback(() => { + localStorage.clear(); + location.reload(); + return false; + }, []); useEffect(() => { i18n.changeLanguage(language); @@ -49,7 +56,10 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => { }, [dispatch]); return ( - <> + { - + ); }; diff --git a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx new file mode 100644 index 0000000000..36aa14a17e --- /dev/null +++ b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx @@ -0,0 +1,97 @@ +import { Flex, Heading, Link, Text, useToast } from '@chakra-ui/react'; +import IAIButton from 'common/components/IAIButton'; +import newGithubIssueUrl from 'new-github-issue-url'; +import { useCallback, useMemo } from 'react'; +import { FaCopy, FaExternalLinkAlt } from 'react-icons/fa'; +import { FaArrowRotateLeft } from 'react-icons/fa6'; +import { serializeError } from 'serialize-error'; + +type Props = { + error: Error; + resetErrorBoundary: () => void; +}; + +const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => { + const toast = useToast(); + + const handleCopy = useCallback(() => { + const text = JSON.stringify(serializeError(error), null, 2); + navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``); + toast({ + title: 'Error Copied', + }); + }, [error, toast]); + + const url = useMemo( + () => + newGithubIssueUrl({ + user: 'invoke-ai', + repo: 'InvokeAI', + template: 'BUG_REPORT.yml', + title: `[bug]: ${error.name}: ${error.message}`, + }), + [error.message, error.name] + ); + return ( + + + Something went wrong + + + {error.name}: {error.message} + + + + } + onClick={resetErrorBoundary} + > + Reset UI + + } onClick={handleCopy}> + Copy Error + + + }>Create Issue + + + + + ); +}; + +export default AppErrorBoundaryFallback; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataJSON.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx similarity index 81% rename from invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataJSON.tsx rename to invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx index 69385607de..53b5f20d5f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataJSON.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx @@ -1,34 +1,35 @@ import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react'; +import { isString } from 'lodash-es'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { useCallback, useMemo } from 'react'; import { FaCopy, FaSave } from 'react-icons/fa'; type Props = { label: string; - jsonObject: object; + data: object | string; fileName?: string; }; -const ImageMetadataJSON = (props: Props) => { - const { label, jsonObject, fileName } = props; - const jsonString = useMemo( - () => JSON.stringify(jsonObject, null, 2), - [jsonObject] +const DataViewer = (props: Props) => { + const { label, data, fileName } = props; + const dataString = useMemo( + () => (isString(data) ? data : JSON.stringify(data, null, 2)), + [data] ); const handleCopy = useCallback(() => { - navigator.clipboard.writeText(jsonString); - }, [jsonString]); + navigator.clipboard.writeText(dataString); + }, [dataString]); const handleSave = useCallback(() => { - const blob = new Blob([jsonString]); + const blob = new Blob([dataString]); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `${fileName || label}.json`; document.body.appendChild(a); a.click(); a.remove(); - }, [jsonString, label, fileName]); + }, [dataString, label, fileName]); return ( { }, }} > -
{jsonString}
+
{dataString}
@@ -92,4 +93,4 @@ const ImageMetadataJSON = (props: Props) => { ); }; -export default ImageMetadataJSON; +export default DataViewer; 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 d70aea8a8d..9262d081b5 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx @@ -16,7 +16,7 @@ import { useGetImageMetadataQuery } from 'services/api/endpoints/images'; import { ImageDTO } from 'services/api/types'; import { useDebounce } from 'use-debounce'; import ImageMetadataActions from './ImageMetadataActions'; -import ImageMetadataJSON from './ImageMetadataJSON'; +import DataViewer from './DataViewer'; type ImageMetadataViewerProps = { image: ImageDTO; @@ -79,21 +79,21 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { {metadata ? ( - + ) : ( )} {image ? ( - + ) : ( )} {graph ? ( - + ) : ( )} 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 bb06836a70..a97834f138 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,7 +3,7 @@ import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON'; +import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { memo } from 'react'; const selector = createSelector( @@ -30,7 +30,7 @@ const InspectorDataTab = () => { return ; } - return ; + return ; }; export default memo(InspectorDataTab); 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 b483158b36..525b58b1cb 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 @@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON'; +import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { memo } from 'react'; const selector = createSelector( @@ -34,7 +34,7 @@ const NodeTemplateInspector = () => { return ; } - return ; + return ; }; export default memo(NodeTemplateInspector); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx index 3cbe5ea1ee..21c75a42da 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx @@ -1,7 +1,7 @@ import { Flex } from '@chakra-ui/react'; import { RootState } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; -import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON'; +import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { buildWorkflow } from 'features/nodes/util/buildWorkflow'; import { memo, useMemo } from 'react'; import { useDebounce } from 'use-debounce'; @@ -31,11 +31,7 @@ const WorkflowJSONTab = () => { h: 'full', }} > - + ); }; diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 7511efd0fb..ad55022425 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -4976,6 +4976,11 @@ neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +new-github-issue-url@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/new-github-issue-url/-/new-github-issue-url-1.0.0.tgz#c9e84057c2609b7cbd686d1d8baa53e291292e79" + integrity sha512-wa9jlUFg3v6S3ddijQiB18SY4u9eJYcUe5sHa+6SB8m1UUbtX+H/bBglxOLnhhF1zIHuhWXnKBAa8kBeKRIozQ== + node-fetch@^2.6.11: version "2.6.12" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" @@ -5488,6 +5493,13 @@ react-dropzone@^14.2.3: file-selector "^0.6.0" prop-types "^15.8.1" +react-error-boundary@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c" + integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw== + dependencies: + "@babel/runtime" "^7.12.5" + react-fast-compare@3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f" From fbff22c94bda30689aebc08756136d8c708867dc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:26:16 +1000 Subject: [PATCH 40/45] feat(ui): memoize all components --- .../components/AppErrorBoundaryFallback.tsx | 4 +- .../app/components/ThemeLocaleProvider.tsx | 4 +- .../web/src/app/components/Toaster.ts | 4 +- .../IAIErrorLoadingImageFallback.tsx | 3 +- .../src/common/components/IAIFillSkeleton.tsx | 3 +- .../components/ImageMetadataOverlay.tsx | 3 +- .../common/components/ImageUploadOverlay.tsx | 3 +- .../components/SelectImagePlaceholder.tsx | 3 +- .../common/components/SelectionOverlay.tsx | 3 +- .../ClearCanvasHistoryButtonModal.tsx | 3 +- .../features/canvas/components/IAICanvas.tsx | 4 +- .../IAICanvasBoundingBoxOverlay.tsx | 3 +- .../canvas/components/IAICanvasGrid.tsx | 4 +- .../canvas/components/IAICanvasImage.tsx | 3 +- .../components/IAICanvasIntermediateImage.tsx | 4 +- .../components/IAICanvasMaskCompositer.tsx | 4 +- .../canvas/components/IAICanvasMaskLines.tsx | 3 +- .../components/IAICanvasObjectRenderer.tsx | 3 +- .../canvas/components/IAICanvasResizer.tsx | 4 +- .../components/IAICanvasStagingArea.tsx | 3 +- .../IAICanvasStagingAreaToolbar.tsx | 4 +- .../canvas/components/IAICanvasStatusText.tsx | 3 +- .../components/IAICanvasToolPreview.tsx | 3 +- .../IAICanvasToolbar/IAICanvasBoundingBox.tsx | 4 +- .../IAICanvasToolbar/IAICanvasMaskOptions.tsx | 3 +- .../IAICanvasSettingsButtonPopover.tsx | 4 +- .../IAICanvasToolChooserOptions.tsx | 3 +- .../IAICanvasToolbar/IAICanvasToolbar.tsx | 3 +- .../ParamControlNetFeatureToggle.tsx | 4 +- .../ParamDynamicPromptsCollapse.tsx | 3 +- .../ParamDynamicPromptsCombinatorial.tsx | 4 +- .../components/ParamDynamicPromptsEnabled.tsx | 4 +- .../ParamDynamicPromptsMaxPrompts.tsx | 4 +- .../components/ParamEmbeddingPopover.tsx | 4 +- .../gallery/components/Boards/AutoAddIcon.tsx | 3 +- .../components/Boards/BoardAutoAddSelect.tsx | 4 +- .../components/Boards/BoardContextMenu.tsx | 125 ++--- .../Boards/BoardsList/AddBoardButton.tsx | 4 +- .../Boards/BoardsList/GalleryBoard.tsx | 464 +++++++++--------- .../Boards/BoardsList/GenericBoard.tsx | 4 +- .../Boards/BoardsList/NoBoardBoard.tsx | 2 +- .../CurrentImage/CurrentImageButtons.tsx | 4 +- .../CurrentImage/CurrentImageDisplay.tsx | 3 +- .../CurrentImage/CurrentImageHidden.tsx | 3 +- .../gallery/components/GalleryPinButton.tsx | 3 +- .../components/GallerySettingsPopover.tsx | 4 +- .../MultipleSelectionMenuItems.tsx | 4 +- .../components/ImageFallbackSpinner.tsx | 3 +- .../ImageGrid/ImageGridItemContainer.tsx | 4 +- .../ImageGrid/ImageGridListContainer.tsx | 4 +- .../ImageMetadataViewer/DataViewer.tsx | 4 +- .../ImageMetadataActions.tsx | 4 +- .../ImageMetadataViewer/ImageMetadataItem.tsx | 3 +- .../lora/components/ParamLoraList.tsx | 3 +- .../lora/components/ParamLoraSelect.tsx | 4 +- .../Invocation/fields/FieldTooltipContent.tsx | 4 +- .../nodes/Invocation/fields/InputField.tsx | 2 +- .../sidePanel/ScrollableContent.tsx | 4 +- .../Canvas/GenerationModeStatusText.tsx | 3 +- .../Core/ParamNegativeConditioning.tsx | 4 +- .../Core/ParamPositiveConditioning.tsx | 4 +- .../Parameters/ImageToImage/InitialImage.tsx | 4 +- .../ImageToImage/InitialImageDisplay.tsx | 4 +- .../Upscale/ParamUpscaleSettings.tsx | 4 +- .../ProcessButtons/ProcessButtons.tsx | 3 +- .../ParamSDXLNegativeStyleConditioning.tsx | 4 +- .../ParamSDXLPositiveStyleConditioning.tsx | 4 +- .../components/ParamSDXLRefinerCollapse.tsx | 3 +- .../SDXLImageToImageTabParameters.tsx | 3 +- .../SDXLTextToImageTabParameters.tsx | 3 +- .../components/InvokeAILogoComponent.tsx | 4 +- .../SettingsModal/SettingsModal.tsx | 3 +- .../components/SettingsModal/StyledFlex.tsx | 4 +- .../system/components/StatusIndicator.tsx | 4 +- .../components/PinParametersPanelButton.tsx | 3 +- .../ResizableDrawer/ResizableDrawer.tsx | 4 +- .../ImageToImageTabParameters.tsx | 3 +- .../subpanels/ModelManagerPanel/ModelList.tsx | 56 ++- .../tabs/TextToImage/TextToImageTabMain.tsx | 3 +- .../TextToImage/TextToImageTabParameters.tsx | 3 +- .../UnifiedCanvasSettings.tsx | 3 +- .../UnifiedCanvasToolSelect.tsx | 3 +- .../UnifiedCanvasToolbarBeta.tsx | 3 +- .../UnifiedCanvas/UnifiedCanvasParameters.tsx | 3 +- 84 files changed, 486 insertions(+), 440 deletions(-) diff --git a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx index 36aa14a17e..76a34388eb 100644 --- a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx +++ b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx @@ -1,7 +1,7 @@ import { Flex, Heading, Link, Text, useToast } from '@chakra-ui/react'; import IAIButton from 'common/components/IAIButton'; import newGithubIssueUrl from 'new-github-issue-url'; -import { useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { FaCopy, FaExternalLinkAlt } from 'react-icons/fa'; import { FaArrowRotateLeft } from 'react-icons/fa6'; import { serializeError } from 'serialize-error'; @@ -94,4 +94,4 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => { ); }; -export default AppErrorBoundaryFallback; +export default memo(AppErrorBoundaryFallback); diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx index 621b196ae0..9bcc7c831b 100644 --- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx +++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx @@ -3,7 +3,7 @@ import { createLocalStorageManager, extendTheme, } from '@chakra-ui/react'; -import { ReactNode, useEffect, useMemo } from 'react'; +import { ReactNode, memo, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { theme as invokeAITheme } from 'theme/theme'; @@ -46,4 +46,4 @@ function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { ); } -export default ThemeLocaleProvider; +export default memo(ThemeLocaleProvider); diff --git a/invokeai/frontend/web/src/app/components/Toaster.ts b/invokeai/frontend/web/src/app/components/Toaster.ts index dff2a7c7f5..9d7149023b 100644 --- a/invokeai/frontend/web/src/app/components/Toaster.ts +++ b/invokeai/frontend/web/src/app/components/Toaster.ts @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { toastQueueSelector } from 'features/system/store/systemSelectors'; import { addToast, clearToastQueue } from 'features/system/store/systemSlice'; import { MakeToastArg, makeToast } from 'features/system/util/makeToast'; -import { useCallback, useEffect } from 'react'; +import { memo, useCallback, useEffect } from 'react'; /** * Logical component. Watches the toast queue and makes toasts when the queue is not empty. @@ -44,4 +44,4 @@ export const useAppToaster = () => { return toaster; }; -export default Toaster; +export default memo(Toaster); diff --git a/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx index 2136acc3c3..0a5d4fb12f 100644 --- a/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx +++ b/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx @@ -1,4 +1,5 @@ import { Box, Flex, Icon } from '@chakra-ui/react'; +import { memo } from 'react'; import { FaExclamation } from 'react-icons/fa'; const IAIErrorLoadingImageFallback = () => { @@ -39,4 +40,4 @@ const IAIErrorLoadingImageFallback = () => { ); }; -export default IAIErrorLoadingImageFallback; +export default memo(IAIErrorLoadingImageFallback); diff --git a/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx b/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx index a3c83cb734..8081714432 100644 --- a/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx +++ b/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx @@ -1,4 +1,5 @@ import { Box, Skeleton } from '@chakra-ui/react'; +import { memo } from 'react'; const IAIFillSkeleton = () => { return ( @@ -27,4 +28,4 @@ const IAIFillSkeleton = () => { ); }; -export default IAIFillSkeleton; +export default memo(IAIFillSkeleton); diff --git a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx index 3ef7d8f83e..765dd3c000 100644 --- a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx @@ -1,4 +1,5 @@ import { Badge, Flex } from '@chakra-ui/react'; +import { memo } from 'react'; import { ImageDTO } from 'services/api/types'; type ImageMetadataOverlayProps = { @@ -26,4 +27,4 @@ const ImageMetadataOverlay = ({ imageDTO }: ImageMetadataOverlayProps) => { ); }; -export default ImageMetadataOverlay; +export default memo(ImageMetadataOverlay); diff --git a/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx index b2d5ddb2da..5c91a7ceda 100644 --- a/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx @@ -1,4 +1,5 @@ import { Box, Flex, Heading } from '@chakra-ui/react'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; type ImageUploadOverlayProps = { @@ -87,4 +88,4 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => { ); }; -export default ImageUploadOverlay; +export default memo(ImageUploadOverlay); diff --git a/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx b/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx index a19d447755..2db202ddc0 100644 --- a/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx +++ b/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx @@ -1,4 +1,5 @@ import { Flex, Icon } from '@chakra-ui/react'; +import { memo } from 'react'; import { FaImage } from 'react-icons/fa'; const SelectImagePlaceholder = () => { @@ -19,4 +20,4 @@ const SelectImagePlaceholder = () => { ); }; -export default SelectImagePlaceholder; +export default memo(SelectImagePlaceholder); diff --git a/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx b/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx index eb04c7c56d..aed5e1f083 100644 --- a/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx @@ -1,4 +1,5 @@ import { Box } from '@chakra-ui/react'; +import { memo } from 'react'; type Props = { isSelected: boolean; @@ -40,4 +41,4 @@ const SelectionOverlay = ({ isSelected, isHovered }: Props) => { ); }; -export default SelectionOverlay; +export default memo(SelectionOverlay); diff --git a/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx b/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx index 49a13c401c..a86497aade 100644 --- a/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx @@ -5,6 +5,7 @@ import { clearCanvasHistory } from 'features/canvas/store/canvasSlice'; import { useTranslation } from 'react-i18next'; import { FaTrash } from 'react-icons/fa'; import { isStagingSelector } from '../store/canvasSelectors'; +import { memo } from 'react'; const ClearCanvasHistoryButtonModal = () => { const isStaging = useAppSelector(isStagingSelector); @@ -28,4 +29,4 @@ const ClearCanvasHistoryButtonModal = () => { ); }; -export default ClearCanvasHistoryButtonModal; +export default memo(ClearCanvasHistoryButtonModal); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx index 7a82e64270..82d49625fa 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx @@ -9,7 +9,7 @@ import { import Konva from 'konva'; import { KonvaEventObject } from 'konva/lib/Node'; import { Vector2d } from 'konva/lib/types'; -import { useCallback, useRef } from 'react'; +import { memo, useCallback, useRef } from 'react'; import { Layer, Stage } from 'react-konva'; import useCanvasDragMove from '../hooks/useCanvasDragMove'; import useCanvasHotkeys from '../hooks/useCanvasHotkeys'; @@ -220,4 +220,4 @@ const IAICanvas = () => { ); }; -export default IAICanvas; +export default memo(IAICanvas); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx index e90d2c4d25..22a8848cad 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx @@ -4,6 +4,7 @@ import { isEqual } from 'lodash-es'; import { Group, Rect } from 'react-konva'; import { canvasSelector } from '../store/canvasSelectors'; +import { memo } from 'react'; const selector = createSelector( canvasSelector, @@ -67,4 +68,4 @@ const IAICanvasBoundingBoxOverlay = () => { ); }; -export default IAICanvasBoundingBoxOverlay; +export default memo(IAICanvasBoundingBoxOverlay); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx index 1b97acba71..50a68357fb 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx @@ -6,7 +6,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { isEqual, range } from 'lodash-es'; -import { ReactNode, useCallback, useLayoutEffect, useState } from 'react'; +import { ReactNode, memo, useCallback, useLayoutEffect, useState } from 'react'; import { Group, Line as KonvaLine } from 'react-konva'; const selector = createSelector( @@ -117,4 +117,4 @@ const IAICanvasGrid = () => { return {gridLines}; }; -export default IAICanvasGrid; +export default memo(IAICanvasGrid); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx index eb41857e46..9f8829c280 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx @@ -4,6 +4,7 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import useImage from 'use-image'; import { CanvasImage } from '../store/canvasTypes'; import { $authToken } from 'services/api/client'; +import { memo } from 'react'; type IAICanvasImageProps = { canvasImage: CanvasImage; @@ -25,4 +26,4 @@ const IAICanvasImage = (props: IAICanvasImageProps) => { return ; }; -export default IAICanvasImage; +export default memo(IAICanvasImage); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx index ea5e9a6486..b636ef9528 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx @@ -4,7 +4,7 @@ import { systemSelector } from 'features/system/store/systemSelectors'; import { ImageConfig } from 'konva/lib/shapes/Image'; import { isEqual } from 'lodash-es'; -import { useEffect, useState } from 'react'; +import { memo, useEffect, useState } from 'react'; import { Image as KonvaImage } from 'react-konva'; import { canvasSelector } from '../store/canvasSelectors'; @@ -66,4 +66,4 @@ const IAICanvasIntermediateImage = (props: Props) => { ) : null; }; -export default IAICanvasIntermediateImage; +export default memo(IAICanvasIntermediateImage); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositer.tsx index e374d2aa7b..3949f74ee3 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositer.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositer.tsx @@ -7,7 +7,7 @@ import { Rect } from 'react-konva'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import Konva from 'konva'; import { isNumber } from 'lodash-es'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; export const canvasMaskCompositerSelector = createSelector( canvasSelector, @@ -172,4 +172,4 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => { ); }; -export default IAICanvasMaskCompositer; +export default memo(IAICanvasMaskCompositer); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx index a553653901..ca91e11350 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx @@ -6,6 +6,7 @@ import { isEqual } from 'lodash-es'; import { Group, Line } from 'react-konva'; import { isCanvasMaskLine } from '../store/canvasTypes'; +import { memo } from 'react'; export const canvasLinesSelector = createSelector( [canvasSelector], @@ -52,4 +53,4 @@ const IAICanvasLines = (props: InpaintingCanvasLinesProps) => { ); }; -export default IAICanvasLines; +export default memo(IAICanvasLines); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx index ec1e87cca7..82bc22d85d 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx @@ -12,6 +12,7 @@ import { isCanvasFillRect, } from '../store/canvasTypes'; import IAICanvasImage from './IAICanvasImage'; +import { memo } from 'react'; const selector = createSelector( [canvasSelector], @@ -101,4 +102,4 @@ const IAICanvasObjectRenderer = () => { ); }; -export default IAICanvasObjectRenderer; +export default memo(IAICanvasObjectRenderer); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx index d16a5dab87..2806c9622e 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx @@ -12,7 +12,7 @@ import { setDoesCanvasNeedScaling, } from 'features/canvas/store/canvasSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { useLayoutEffect, useRef } from 'react'; +import { memo, useLayoutEffect, useRef } from 'react'; const canvasResizerSelector = createSelector( canvasSelector, @@ -86,4 +86,4 @@ const IAICanvasResizer = () => { ); }; -export default IAICanvasResizer; +export default memo(IAICanvasResizer); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx index 5355e28762..fa73f020da 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx @@ -6,6 +6,7 @@ import { isEqual } from 'lodash-es'; import { Group, Rect } from 'react-konva'; import IAICanvasImage from './IAICanvasImage'; +import { memo } from 'react'; const selector = createSelector( [canvasSelector], @@ -88,4 +89,4 @@ const IAICanvasStagingArea = (props: Props) => { ); }; -export default IAICanvasStagingArea; +export default memo(IAICanvasStagingArea); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx index 1929bff8f9..0bffda3493 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx @@ -13,7 +13,7 @@ import { } from 'features/canvas/store/canvasSlice'; import { isEqual } from 'lodash-es'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { @@ -207,4 +207,4 @@ const IAICanvasStagingAreaToolbar = () => { ); }; -export default IAICanvasStagingAreaToolbar; +export default memo(IAICanvasStagingAreaToolbar); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx index 8c1dfbb86f..7aa9cad003 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx @@ -7,6 +7,7 @@ import { isEqual } from 'lodash-es'; import { useTranslation } from 'react-i18next'; import roundToHundreth from '../util/roundToHundreth'; import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusTextCursorPos'; +import { memo } from 'react'; const warningColor = 'var(--invokeai-colors-warning-500)'; @@ -162,4 +163,4 @@ const IAICanvasStatusText = () => { ); }; -export default IAICanvasStatusText; +export default memo(IAICanvasStatusText); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx index 8ad58e020c..7227d70c48 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx @@ -10,6 +10,7 @@ import { COLOR_PICKER_SIZE, COLOR_PICKER_STROKE_RADIUS, } from '../util/constants'; +import { memo } from 'react'; const canvasBrushPreviewSelector = createSelector( canvasSelector, @@ -206,4 +207,4 @@ const IAICanvasToolPreview = (props: GroupConfig) => { ); }; -export default IAICanvasToolPreview; +export default memo(IAICanvasToolPreview); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx index 41c281d259..9d63d5b681 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx @@ -19,7 +19,7 @@ import { KonvaEventObject } from 'konva/lib/Node'; import { Vector2d } from 'konva/lib/types'; import { isEqual } from 'lodash-es'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { Group, Rect, Transformer } from 'react-konva'; @@ -313,4 +313,4 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { ); }; -export default IAICanvasBoundingBox; +export default memo(IAICanvasBoundingBox); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx index 25ef295631..76211a2e95 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx @@ -20,6 +20,7 @@ import { } from 'features/canvas/store/canvasSlice'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { isEqual } from 'lodash-es'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -150,4 +151,4 @@ const IAICanvasMaskOptions = () => { ); }; -export default IAICanvasMaskOptions; +export default memo(IAICanvasMaskOptions); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx index ae03df8409..aae2da5632 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx @@ -18,7 +18,7 @@ import { } from 'features/canvas/store/canvasSlice'; import { isEqual } from 'lodash-es'; -import { ChangeEvent } from 'react'; +import { ChangeEvent, memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaWrench } from 'react-icons/fa'; @@ -163,4 +163,4 @@ const IAICanvasSettingsButtonPopover = () => { ); }; -export default IAICanvasSettingsButtonPopover; +export default memo(IAICanvasSettingsButtonPopover); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx index 158e2954af..a3e8f6af8b 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx @@ -18,6 +18,7 @@ import { } from 'features/canvas/store/canvasSlice'; import { systemSelector } from 'features/system/store/systemSelectors'; import { clamp, isEqual } from 'lodash-es'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -252,4 +253,4 @@ const IAICanvasToolChooserOptions = () => { ); }; -export default IAICanvasToolChooserOptions; +export default memo(IAICanvasToolChooserOptions); 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 26ccfe31b6..8035b7ffca 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx @@ -48,6 +48,7 @@ import IAICanvasRedoButton from './IAICanvasRedoButton'; import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover'; import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions'; import IAICanvasUndoButton from './IAICanvasUndoButton'; +import { memo } from 'react'; export const selector = createSelector( [systemSelector, canvasSelector, isStagingSelector], @@ -309,4 +310,4 @@ const IAICanvasToolbar = () => { ); }; -export default IAICanvasToolbar; +export default memo(IAICanvasToolbar); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetFeatureToggle.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetFeatureToggle.tsx index 8eed90ce16..97a54dc7d1 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetFeatureToggle.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetFeatureToggle.tsx @@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISwitch from 'common/components/IAISwitch'; import { isControlNetEnabledToggled } from 'features/controlNet/store/controlNetSlice'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; const selector = createSelector( stateSelector, @@ -36,4 +36,4 @@ const ParamControlNetFeatureToggle = () => { ); }; -export default ParamControlNetFeatureToggle; +export default memo(ParamControlNetFeatureToggle); diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCollapse.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCollapse.tsx index 36d8795615..a213a398c1 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCollapse.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCollapse.tsx @@ -8,6 +8,7 @@ import ParamDynamicPromptsCombinatorial from './ParamDynamicPromptsCombinatorial import ParamDynamicPromptsToggle from './ParamDynamicPromptsEnabled'; import ParamDynamicPromptsMaxPrompts from './ParamDynamicPromptsMaxPrompts'; import { useFeatureStatus } from '../../system/hooks/useFeatureStatus'; +import { memo } from 'react'; const selector = createSelector( stateSelector, @@ -40,4 +41,4 @@ const ParamDynamicPromptsCollapse = () => { ); }; -export default ParamDynamicPromptsCollapse; +export default memo(ParamDynamicPromptsCollapse); diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCombinatorial.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCombinatorial.tsx index 809ec0df10..c028a5d55c 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCombinatorial.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCombinatorial.tsx @@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISwitch from 'common/components/IAISwitch'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { combinatorialToggled } from '../store/dynamicPromptsSlice'; const selector = createSelector( @@ -34,4 +34,4 @@ const ParamDynamicPromptsCombinatorial = () => { ); }; -export default ParamDynamicPromptsCombinatorial; +export default memo(ParamDynamicPromptsCombinatorial); diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsEnabled.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsEnabled.tsx index f92fa410f2..1b31147937 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsEnabled.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsEnabled.tsx @@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISwitch from 'common/components/IAISwitch'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { isEnabledToggled } from '../store/dynamicPromptsSlice'; const selector = createSelector( @@ -33,4 +33,4 @@ const ParamDynamicPromptsToggle = () => { ); }; -export default ParamDynamicPromptsToggle; +export default memo(ParamDynamicPromptsToggle); diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx index 5bee317d22..f374f1cb15 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx @@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider from 'common/components/IAISlider'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { maxPromptsChanged, maxPromptsReset, @@ -60,4 +60,4 @@ const ParamDynamicPromptsMaxPrompts = () => { ); }; -export default ParamDynamicPromptsMaxPrompts; +export default memo(ParamDynamicPromptsMaxPrompts); diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx index 4eb9a67de2..1cc9e60068 100644 --- a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx +++ b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx @@ -13,7 +13,7 @@ import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSe import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { forEach } from 'lodash-es'; -import { PropsWithChildren, useCallback, useMemo, useRef } from 'react'; +import { PropsWithChildren, memo, useCallback, useMemo, useRef } from 'react'; import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models'; import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; @@ -140,4 +140,4 @@ const ParamEmbeddingPopover = (props: Props) => { ); }; -export default ParamEmbeddingPopover; +export default memo(ParamEmbeddingPopover); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddIcon.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddIcon.tsx index ffdde04ef5..4e748d61e8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddIcon.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddIcon.tsx @@ -1,4 +1,5 @@ import { Badge, Flex } from '@chakra-ui/react'; +import { memo } from 'react'; const AutoAddIcon = () => { return ( @@ -20,4 +21,4 @@ const AutoAddIcon = () => { ); }; -export default AutoAddIcon; +export default memo(AutoAddIcon); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardAutoAddSelect.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardAutoAddSelect.tsx index 96d17b548e..f88bd66d59 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardAutoAddSelect.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardAutoAddSelect.tsx @@ -6,7 +6,7 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect'; import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip'; import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice'; -import { useCallback, useRef } from 'react'; +import { memo, useCallback, useRef } from 'react'; import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; const selector = createSelector( @@ -81,4 +81,4 @@ const BoardAutoAddSelect = () => { ); }; -export default BoardAutoAddSelect; +export default memo(BoardAutoAddSelect); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx index c27897ce57..6a012030e8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx @@ -23,71 +23,72 @@ type Props = { setBoardToDelete?: (board?: BoardDTO) => void; }; -const BoardContextMenu = memo( - ({ board, board_id, setBoardToDelete, children }: Props) => { - const dispatch = useAppDispatch(); +const BoardContextMenu = ({ + board, + board_id, + setBoardToDelete, + children, +}: Props) => { + const dispatch = useAppDispatch(); - const selector = useMemo( - () => - createSelector(stateSelector, ({ gallery, system }) => { - const isAutoAdd = gallery.autoAddBoardId === board_id; - const isProcessing = system.isProcessing; - const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick; - return { isAutoAdd, isProcessing, autoAssignBoardOnClick }; - }), - [board_id] - ); + const selector = useMemo( + () => + createSelector(stateSelector, ({ gallery, system }) => { + const isAutoAdd = gallery.autoAddBoardId === board_id; + const isProcessing = system.isProcessing; + const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick; + return { isAutoAdd, isProcessing, autoAssignBoardOnClick }; + }), + [board_id] + ); - const { isAutoAdd, isProcessing, autoAssignBoardOnClick } = - useAppSelector(selector); - const boardName = useBoardName(board_id); + const { isAutoAdd, isProcessing, autoAssignBoardOnClick } = + useAppSelector(selector); + const boardName = useBoardName(board_id); - const handleSetAutoAdd = useCallback(() => { - dispatch(autoAddBoardIdChanged(board_id)); - }, [board_id, dispatch]); + const handleSetAutoAdd = useCallback(() => { + dispatch(autoAddBoardIdChanged(board_id)); + }, [board_id, dispatch]); - const skipEvent = useCallback((e: MouseEvent) => { - e.preventDefault(); - }, []); + const skipEvent = useCallback((e: MouseEvent) => { + e.preventDefault(); + }, []); - return ( - - menuProps={{ size: 'sm', isLazy: true }} - menuButtonProps={{ - bg: 'transparent', - _hover: { bg: 'transparent' }, - }} - renderMenu={() => ( - - - } - isDisabled={isAutoAdd || isProcessing || autoAssignBoardOnClick} - onClick={handleSetAutoAdd} - > - Auto-add to this Board - - {!board && } - {board && ( - - )} - - - )} - > - {children} - - ); - } -); + return ( + + menuProps={{ size: 'sm', isLazy: true }} + menuButtonProps={{ + bg: 'transparent', + _hover: { bg: 'transparent' }, + }} + renderMenu={() => ( + + + } + isDisabled={isAutoAdd || isProcessing || autoAssignBoardOnClick} + onClick={handleSetAutoAdd} + > + Auto-add to this Board + + {!board && } + {board && ( + + )} + + + )} + > + {children} + + ); +}; -BoardContextMenu.displayName = 'HoverableBoard'; - -export default BoardContextMenu; +export default memo(BoardContextMenu); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx index 7a07680878..ebd08e94d5 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx @@ -1,5 +1,5 @@ import IAIIconButton from 'common/components/IAIIconButton'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { FaPlus } from 'react-icons/fa'; import { useCreateBoardMutation } from 'services/api/endpoints/boards'; @@ -24,4 +24,4 @@ const AddBoardButton = () => { ); }; -export default AddBoardButton; +export default memo(AddBoardButton); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx index 696a8b748b..78b633bd99 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx @@ -39,187 +39,188 @@ interface GalleryBoardProps { setBoardToDelete: (board?: BoardDTO) => void; } -const GalleryBoard = memo( - ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => { - const dispatch = useAppDispatch(); - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ gallery, system }) => { - const isSelectedForAutoAdd = - board.board_id === gallery.autoAddBoardId; - const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick; - const isProcessing = system.isProcessing; +const GalleryBoard = ({ + board, + isSelected, + setBoardToDelete, +}: GalleryBoardProps) => { + const dispatch = useAppDispatch(); + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ gallery, system }) => { + const isSelectedForAutoAdd = + board.board_id === gallery.autoAddBoardId; + const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick; + const isProcessing = system.isProcessing; - return { - isSelectedForAutoAdd, - autoAssignBoardOnClick, - isProcessing, - }; - }, - defaultSelectorOptions - ), - [board.board_id] - ); + return { + isSelectedForAutoAdd, + autoAssignBoardOnClick, + isProcessing, + }; + }, + defaultSelectorOptions + ), + [board.board_id] + ); - const { isSelectedForAutoAdd, autoAssignBoardOnClick, isProcessing } = - useAppSelector(selector); - const [isHovered, setIsHovered] = useState(false); - const handleMouseOver = useCallback(() => { - setIsHovered(true); - }, []); - const handleMouseOut = useCallback(() => { - setIsHovered(false); - }, []); + const { isSelectedForAutoAdd, autoAssignBoardOnClick, isProcessing } = + useAppSelector(selector); + const [isHovered, setIsHovered] = useState(false); + const handleMouseOver = useCallback(() => { + setIsHovered(true); + }, []); + const handleMouseOut = useCallback(() => { + setIsHovered(false); + }, []); - const { data: imagesTotal } = useGetBoardImagesTotalQuery(board.board_id); - const { data: assetsTotal } = useGetBoardAssetsTotalQuery(board.board_id); - const tooltip = useMemo(() => { - if (!imagesTotal || !assetsTotal) { - return undefined; + const { data: imagesTotal } = useGetBoardImagesTotalQuery(board.board_id); + const { data: assetsTotal } = useGetBoardAssetsTotalQuery(board.board_id); + const tooltip = useMemo(() => { + if (!imagesTotal || !assetsTotal) { + return undefined; + } + return `${imagesTotal} image${ + imagesTotal > 1 ? 's' : '' + }, ${assetsTotal} asset${assetsTotal > 1 ? 's' : ''}`; + }, [assetsTotal, imagesTotal]); + + const { currentData: coverImage } = useGetImageDTOQuery( + board.cover_image_name ?? skipToken + ); + + const { board_name, board_id } = board; + const [localBoardName, setLocalBoardName] = useState(board_name); + + const handleSelectBoard = useCallback(() => { + dispatch(boardIdSelected(board_id)); + if (autoAssignBoardOnClick && !isProcessing) { + dispatch(autoAddBoardIdChanged(board_id)); + } + }, [board_id, autoAssignBoardOnClick, isProcessing, dispatch]); + + const [updateBoard, { isLoading: isUpdateBoardLoading }] = + useUpdateBoardMutation(); + + const droppableData: AddToBoardDropData = useMemo( + () => ({ + id: board_id, + actionType: 'ADD_TO_BOARD', + context: { boardId: board_id }, + }), + [board_id] + ); + + const handleSubmit = useCallback( + async (newBoardName: string) => { + // empty strings are not allowed + if (!newBoardName.trim()) { + setLocalBoardName(board_name); + return; } - return `${imagesTotal} image${ - imagesTotal > 1 ? 's' : '' - }, ${assetsTotal} asset${assetsTotal > 1 ? 's' : ''}`; - }, [assetsTotal, imagesTotal]); - const { currentData: coverImage } = useGetImageDTOQuery( - board.cover_image_name ?? skipToken - ); - - const { board_name, board_id } = board; - const [localBoardName, setLocalBoardName] = useState(board_name); - - const handleSelectBoard = useCallback(() => { - dispatch(boardIdSelected(board_id)); - if (autoAssignBoardOnClick && !isProcessing) { - dispatch(autoAddBoardIdChanged(board_id)); + // don't updated the board name if it hasn't changed + if (newBoardName === board_name) { + return; } - }, [board_id, autoAssignBoardOnClick, isProcessing, dispatch]); - const [updateBoard, { isLoading: isUpdateBoardLoading }] = - useUpdateBoardMutation(); + try { + const { board_name } = await updateBoard({ + board_id, + changes: { board_name: newBoardName }, + }).unwrap(); - const droppableData: AddToBoardDropData = useMemo( - () => ({ - id: board_id, - actionType: 'ADD_TO_BOARD', - context: { boardId: board_id }, - }), - [board_id] - ); + // update local state + setLocalBoardName(board_name); + } catch { + // revert on error + setLocalBoardName(board_name); + } + }, + [board_id, board_name, updateBoard] + ); - const handleSubmit = useCallback( - async (newBoardName: string) => { - // empty strings are not allowed - if (!newBoardName.trim()) { - setLocalBoardName(board_name); - return; - } + const handleChange = useCallback((newBoardName: string) => { + setLocalBoardName(newBoardName); + }, []); - // don't updated the board name if it hasn't changed - if (newBoardName === board_name) { - return; - } - - try { - const { board_name } = await updateBoard({ - board_id, - changes: { board_name: newBoardName }, - }).unwrap(); - - // update local state - setLocalBoardName(board_name); - } catch { - // revert on error - setLocalBoardName(board_name); - } - }, - [board_id, board_name, updateBoard] - ); - - const handleChange = useCallback((newBoardName: string) => { - setLocalBoardName(newBoardName); - }, []); - - return ( - + - - - {(ref) => ( - - - {coverImage?.thumbnail_url ? ( - ( + + + {coverImage?.thumbnail_url ? ( + + ) : ( + + - ) : ( - - - - )} - {/* + )} + {/* */} - {isSelectedForAutoAdd && } - - } + + + - - + - - - - - Move} - /> + // get rid of the edit border + boxShadow: 'none', + }, + }} + /> + - - )} - - - - ); - } -); -GalleryBoard.displayName = 'HoverableBoard'; + Move} + /> +
+ + )} + +
+ + ); +}; -export default GalleryBoard; +export default memo(GalleryBoard); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GenericBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GenericBoard.tsx index 1698a81ac0..7a95e7fcd9 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GenericBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GenericBoard.tsx @@ -3,7 +3,7 @@ import IAIDroppable from 'common/components/IAIDroppable'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { TypesafeDroppableData } from 'features/dnd/types'; import { BoardId } from 'features/gallery/store/types'; -import { ReactNode } from 'react'; +import { ReactNode, memo } from 'react'; import BoardContextMenu from '../BoardContextMenu'; type GenericBoardProps = { @@ -105,4 +105,4 @@ const GenericBoard = (props: GenericBoardProps) => { ); }; -export default GenericBoard; +export default memo(GenericBoard); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx index fec280db0f..da51a5fe39 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx @@ -156,4 +156,4 @@ const NoBoardBoard = memo(({ isSelected }: Props) => { NoBoardBoard.displayName = 'HoverableBoard'; -export default NoBoardBoard; +export default memo(NoBoardBoard); diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx index d62027769b..0212376507 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx @@ -26,7 +26,7 @@ import { setShouldShowImageDetails, setShouldShowProgressInViewer, } from 'features/ui/store/uiSlice'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { @@ -323,4 +323,4 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { ); }; -export default CurrentImageButtons; +export default memo(CurrentImageButtons); diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx index 1d8863f4d8..1c342d093e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx @@ -2,6 +2,7 @@ import { Flex } from '@chakra-ui/react'; import CurrentImageButtons from './CurrentImageButtons'; import CurrentImagePreview from './CurrentImagePreview'; +import { memo } from 'react'; const CurrentImageDisplay = () => { return ( @@ -22,4 +23,4 @@ const CurrentImageDisplay = () => { ); }; -export default CurrentImageDisplay; +export default memo(CurrentImageDisplay); diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageHidden.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageHidden.tsx index 062cdd7c00..af2a7c5f98 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageHidden.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageHidden.tsx @@ -1,4 +1,5 @@ import { Flex } from '@chakra-ui/react'; +import { memo } from 'react'; import { FaEyeSlash } from 'react-icons/fa'; const CurrentImageHidden = () => { @@ -18,4 +19,4 @@ const CurrentImageHidden = () => { ); }; -export default CurrentImageHidden; +export default memo(CurrentImageHidden); diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryPinButton.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPinButton.tsx index 916dec69a2..f1895c4d6f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryPinButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryPinButton.tsx @@ -5,6 +5,7 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIIconButton from 'common/components/IAIIconButton'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { togglePinGalleryPanel } from 'features/ui/store/uiSlice'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs'; @@ -41,4 +42,4 @@ const GalleryPinButton = () => { ); }; -export default GalleryPinButton; +export default memo(GalleryPinButton); diff --git a/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx index 23cfdcc5fd..2eab78d118 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx @@ -12,7 +12,7 @@ import { setGalleryImageMinimumWidth, shouldAutoSwitchChanged, } from 'features/gallery/store/gallerySlice'; -import { ChangeEvent, useCallback } from 'react'; +import { ChangeEvent, memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { FaWrench } from 'react-icons/fa'; import BoardAutoAddSelect from './Boards/BoardAutoAddSelect'; @@ -101,4 +101,4 @@ const GallerySettingsPopover = () => { ); }; -export default GallerySettingsPopover; +export default memo(GallerySettingsPopover); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx index 0f36273122..bf2b344b4c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx @@ -5,7 +5,7 @@ import { isModalOpenChanged, } from 'features/changeBoardModal/store/slice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; -import { useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { FaFolder, FaTrash } from 'react-icons/fa'; import { MdStar, MdStarBorder } from 'react-icons/md'; import { @@ -74,4 +74,4 @@ const MultipleSelectionMenuItems = () => { ); }; -export default MultipleSelectionMenuItems; +export default memo(MultipleSelectionMenuItems); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx index fd603d3756..95577efc13 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx @@ -1,4 +1,5 @@ import { Flex, Spinner, SpinnerProps } from '@chakra-ui/react'; +import { memo } from 'react'; type ImageFallbackSpinnerProps = SpinnerProps; @@ -23,4 +24,4 @@ const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => { ); }; -export default ImageFallbackSpinner; +export default memo(ImageFallbackSpinner); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridItemContainer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridItemContainer.tsx index a09455ef2c..f55ca1dedf 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridItemContainer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridItemContainer.tsx @@ -1,5 +1,5 @@ import { Box, FlexProps, forwardRef } from '@chakra-ui/react'; -import { PropsWithChildren } from 'react'; +import { PropsWithChildren, memo } from 'react'; type ItemContainerProps = PropsWithChildren & FlexProps; const ItemContainer = forwardRef((props: ItemContainerProps, ref) => ( @@ -8,4 +8,4 @@ const ItemContainer = forwardRef((props: ItemContainerProps, ref) => ( )); -export default ItemContainer; +export default memo(ItemContainer); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridListContainer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridListContainer.tsx index fbbca2b2cf..a93222b58e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridListContainer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridListContainer.tsx @@ -1,7 +1,7 @@ import { FlexProps, Grid, forwardRef } from '@chakra-ui/react'; import { RootState } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; -import { PropsWithChildren } from 'react'; +import { PropsWithChildren, memo } from 'react'; type ListContainerProps = PropsWithChildren & FlexProps; const ListContainer = forwardRef((props: ListContainerProps, ref) => { @@ -23,4 +23,4 @@ const ListContainer = forwardRef((props: ListContainerProps, ref) => { ); }); -export default ListContainer; +export default memo(ListContainer); 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 53b5f20d5f..a98124bb89 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx @@ -1,7 +1,7 @@ import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react'; import { isString } from 'lodash-es'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; -import { useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { FaCopy, FaSave } from 'react-icons/fa'; type Props = { @@ -93,4 +93,4 @@ const DataViewer = (props: Props) => { ); }; -export default DataViewer; +export default memo(DataViewer); 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 c0821c2226..ee5b342d4e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -1,5 +1,5 @@ import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { UnsafeImageMetadata } from 'services/api/types'; import ImageMetadataItem from './ImageMetadataItem'; @@ -206,4 +206,4 @@ const ImageMetadataActions = (props: Props) => { ); }; -export default ImageMetadataActions; +export default memo(ImageMetadataActions); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataItem.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataItem.tsx index d72561351f..c03fd26ba1 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataItem.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataItem.tsx @@ -1,5 +1,6 @@ import { ExternalLinkIcon } from '@chakra-ui/icons'; import { Flex, IconButton, Link, Text, Tooltip } from '@chakra-ui/react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { FaCopy } from 'react-icons/fa'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; @@ -74,4 +75,4 @@ const ImageMetadataItem = ({ ); }; -export default ImageMetadataItem; +export default memo(ImageMetadataItem); diff --git a/invokeai/frontend/web/src/features/lora/components/ParamLoraList.tsx b/invokeai/frontend/web/src/features/lora/components/ParamLoraList.tsx index 5ba4e711ef..83fddef578 100644 --- a/invokeai/frontend/web/src/features/lora/components/ParamLoraList.tsx +++ b/invokeai/frontend/web/src/features/lora/components/ParamLoraList.tsx @@ -5,6 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { map } from 'lodash-es'; import ParamLora from './ParamLora'; +import { memo } from 'react'; const selector = createSelector( stateSelector, @@ -29,4 +30,4 @@ const ParamLoraList = () => { ); }; -export default ParamLoraList; +export default memo(ParamLoraList); diff --git a/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx b/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx index 2046d36ab2..bb485d44b6 100644 --- a/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx @@ -9,7 +9,7 @@ import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectI import { loraAdded } from 'features/lora/store/loraSlice'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { forEach } from 'lodash-es'; -import { useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useGetLoRAModelsQuery } from 'services/api/endpoints/models'; const selector = createSelector( @@ -102,4 +102,4 @@ const ParamLoRASelect = () => { ); }; -export default ParamLoRASelect; +export default memo(ParamLoRASelect); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx index 341c1af704..d2b5b4251e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent.tsx @@ -7,7 +7,7 @@ import { isInputFieldValue, } from 'features/nodes/types/types'; import { startCase } from 'lodash-es'; -import { useMemo } from 'react'; +import { memo, useMemo } from 'react'; interface Props { nodeId: string; @@ -51,4 +51,4 @@ const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => { ); }; -export default FieldTooltipContent; +export default memo(FieldTooltipContent); 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 df31c3e22f..3758ae4114 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 @@ -139,7 +139,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { ); }; -export default InputField; +export default memo(InputField); type InputFieldWrapperProps = PropsWithChildren<{ shouldDim: boolean; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/ScrollableContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/ScrollableContent.tsx index 3b8cf5d520..7e3f4aa249 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/ScrollableContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/ScrollableContent.tsx @@ -1,6 +1,6 @@ import { Box, Flex } from '@chakra-ui/react'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; -import { PropsWithChildren } from 'react'; +import { PropsWithChildren, memo } from 'react'; const ScrollableContent = (props: PropsWithChildren) => { return ( @@ -42,4 +42,4 @@ const ScrollableContent = (props: PropsWithChildren) => { ); }; -export default ScrollableContent; +export default memo(ScrollableContent); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/GenerationModeStatusText.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/GenerationModeStatusText.tsx index 511e90f0f3..eeef340324 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/GenerationModeStatusText.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/GenerationModeStatusText.tsx @@ -1,5 +1,6 @@ import { Box } from '@chakra-ui/react'; import { useCanvasGenerationMode } from 'features/canvas/hooks/useCanvasGenerationMode'; +import { memo } from 'react'; const GENERATION_MODE_NAME_MAP = { txt2img: 'Text to Image', @@ -18,4 +19,4 @@ const GenerationModeStatusText = () => { ); }; -export default GenerationModeStatusText; +export default memo(GenerationModeStatusText); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx index 513ab64930..2aab013f4f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx @@ -5,7 +5,7 @@ import IAITextarea from 'common/components/IAITextarea'; import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton'; import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover'; import { setNegativePrompt } from 'features/parameters/store/generationSlice'; -import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; +import { ChangeEvent, KeyboardEvent, memo, useCallback, useRef } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus'; @@ -109,4 +109,4 @@ const ParamNegativeConditioning = () => { ); }; -export default ParamNegativeConditioning; +export default memo(ParamNegativeConditioning); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx index 59b5138e3e..923ba1546d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx @@ -1,7 +1,7 @@ import { Box, FormControl, useDisclosure } from '@chakra-ui/react'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; +import { ChangeEvent, KeyboardEvent, memo, useCallback, useRef } from 'react'; import { createSelector } from '@reduxjs/toolkit'; import { @@ -159,4 +159,4 @@ const ParamPositiveConditioning = () => { ); }; -export default ParamPositiveConditioning; +export default memo(ParamPositiveConditioning); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImage.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImage.tsx index e8a629e2ac..17786dac43 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImage.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImage.tsx @@ -9,7 +9,7 @@ import { TypesafeDraggableData, TypesafeDroppableData, } from 'features/dnd/types'; -import { useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; const selector = createSelector( @@ -64,4 +64,4 @@ const InitialImage = () => { ); }; -export default InitialImage; +export default memo(InitialImage); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx index 9e23a1a243..0321d3a1fe 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx @@ -6,7 +6,7 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIIconButton from 'common/components/IAIIconButton'; import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; import { clearInitialImage } from 'features/parameters/store/generationSlice'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { FaUndo, FaUpload } from 'react-icons/fa'; import InitialImage from './InitialImage'; import { PostUploadAction } from 'services/api/types'; @@ -95,4 +95,4 @@ const InitialImageDisplay = () => { ); }; -export default InitialImageDisplay; +export default memo(InitialImageDisplay); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/ParamUpscaleSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/ParamUpscaleSettings.tsx index 5824c38123..c858ae66a7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/ParamUpscaleSettings.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/ParamUpscaleSettings.tsx @@ -5,7 +5,7 @@ import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; import { selectIsBusy } from 'features/system/store/systemSelectors'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { FaExpandArrowsAlt } from 'react-icons/fa'; import { ImageDTO } from 'services/api/types'; @@ -59,4 +59,4 @@ const ParamUpscalePopover = (props: Props) => { ); }; -export default ParamUpscalePopover; +export default memo(ParamUpscalePopover); diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/ProcessButtons.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/ProcessButtons.tsx index f132092012..ebd60b14bd 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/ProcessButtons.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/ProcessButtons.tsx @@ -1,6 +1,7 @@ import { Flex } from '@chakra-ui/react'; import CancelButton from './CancelButton'; import InvokeButton from './InvokeButton'; +import { memo } from 'react'; /** * Buttons to start and cancel image generation. @@ -14,4 +15,4 @@ const ProcessButtons = () => { ); }; -export default ProcessButtons; +export default memo(ProcessButtons); diff --git a/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLNegativeStyleConditioning.tsx b/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLNegativeStyleConditioning.tsx index e1533bc886..ff877f5e9a 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLNegativeStyleConditioning.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLNegativeStyleConditioning.tsx @@ -1,7 +1,7 @@ import { Box, FormControl, useDisclosure } from '@chakra-ui/react'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; +import { ChangeEvent, KeyboardEvent, memo, useCallback, useRef } from 'react'; import { createSelector } from '@reduxjs/toolkit'; import { clampSymmetrySteps } from 'features/parameters/store/generationSlice'; @@ -167,4 +167,4 @@ const ParamSDXLNegativeStyleConditioning = () => { ); }; -export default ParamSDXLNegativeStyleConditioning; +export default memo(ParamSDXLNegativeStyleConditioning); diff --git a/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLPositiveStyleConditioning.tsx b/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLPositiveStyleConditioning.tsx index fdd29eaeb7..8ff2f9f19e 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLPositiveStyleConditioning.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLPositiveStyleConditioning.tsx @@ -1,7 +1,7 @@ import { Box, FormControl, useDisclosure } from '@chakra-ui/react'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; +import { ChangeEvent, KeyboardEvent, memo, useCallback, useRef } from 'react'; import { createSelector } from '@reduxjs/toolkit'; import { clampSymmetrySteps } from 'features/parameters/store/generationSlice'; @@ -166,4 +166,4 @@ const ParamSDXLPositiveStyleConditioning = () => { ); }; -export default ParamSDXLPositiveStyleConditioning; +export default memo(ParamSDXLPositiveStyleConditioning); diff --git a/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLRefinerCollapse.tsx b/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLRefinerCollapse.tsx index 3b186006f1..5a3a8dc379 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLRefinerCollapse.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLRefinerCollapse.tsx @@ -12,6 +12,7 @@ import ParamSDXLRefinerScheduler from './SDXLRefiner/ParamSDXLRefinerScheduler'; import ParamSDXLRefinerStart from './SDXLRefiner/ParamSDXLRefinerStart'; import ParamSDXLRefinerSteps from './SDXLRefiner/ParamSDXLRefinerSteps'; import ParamUseSDXLRefiner from './SDXLRefiner/ParamUseSDXLRefiner'; +import { memo } from 'react'; const selector = createSelector( stateSelector, @@ -47,4 +48,4 @@ const ParamSDXLRefinerCollapse = () => { ); }; -export default ParamSDXLRefinerCollapse; +export default memo(ParamSDXLRefinerCollapse); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLImageToImageTabParameters.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLImageToImageTabParameters.tsx index a6ee21ab68..5c2df5c1bb 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLImageToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLImageToImageTabParameters.tsx @@ -6,6 +6,7 @@ import ProcessButtons from 'features/parameters/components/ProcessButtons/Proces import ParamSDXLPromptArea from './ParamSDXLPromptArea'; import ParamSDXLRefinerCollapse from './ParamSDXLRefinerCollapse'; import SDXLImageToImageTabCoreParameters from './SDXLImageToImageTabCoreParameters'; +import { memo } from 'react'; const SDXLImageToImageTabParameters = () => { return ( @@ -22,4 +23,4 @@ const SDXLImageToImageTabParameters = () => { ); }; -export default SDXLImageToImageTabParameters; +export default memo(SDXLImageToImageTabParameters); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLTextToImageTabParameters.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLTextToImageTabParameters.tsx index c562951c4d..46d8ad3558 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLTextToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLTextToImageTabParameters.tsx @@ -6,6 +6,7 @@ import ProcessButtons from 'features/parameters/components/ProcessButtons/Proces import TextToImageTabCoreParameters from 'features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters'; import ParamSDXLPromptArea from './ParamSDXLPromptArea'; import ParamSDXLRefinerCollapse from './ParamSDXLRefinerCollapse'; +import { memo } from 'react'; const SDXLTextToImageTabParameters = () => { return ( @@ -22,4 +23,4 @@ const SDXLTextToImageTabParameters = () => { ); }; -export default SDXLTextToImageTabParameters; +export default memo(SDXLTextToImageTabParameters); diff --git a/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx b/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx index aa4a3dfdfc..a81d898975 100644 --- a/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx +++ b/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx @@ -1,7 +1,7 @@ import { Flex, Image, Text } from '@chakra-ui/react'; import InvokeAILogoImage from 'assets/images/logo.png'; import { AnimatePresence, motion } from 'framer-motion'; -import { useRef } from 'react'; +import { memo, useRef } from 'react'; import { useHoverDirty } from 'react-use'; import { useGetAppVersionQuery } from 'services/api/endpoints/appInfo'; @@ -66,4 +66,4 @@ const InvokeAILogoComponent = ({ showVersion = true }: Props) => { ); }; -export default InvokeAILogoComponent; +export default memo(InvokeAILogoComponent); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 04c71be20f..0026604203 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -39,6 +39,7 @@ import { ChangeEvent, ReactElement, cloneElement, + memo, useCallback, useEffect, } from 'react'; @@ -398,4 +399,4 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { ); }; -export default SettingsModal; +export default memo(SettingsModal); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/StyledFlex.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/StyledFlex.tsx index fd0580f4e2..ec61993c33 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/StyledFlex.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/StyledFlex.tsx @@ -1,5 +1,5 @@ import { Flex } from '@chakra-ui/react'; -import { PropsWithChildren } from 'react'; +import { PropsWithChildren, memo } from 'react'; const StyledFlex = (props: PropsWithChildren) => { return ( @@ -20,4 +20,4 @@ const StyledFlex = (props: PropsWithChildren) => { ); }; -export default StyledFlex; +export default memo(StyledFlex); diff --git a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx index 767b394ac1..4a19ee7fa3 100644 --- a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx @@ -4,7 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { AnimatePresence, motion } from 'framer-motion'; import { ResourceKey } from 'i18next'; -import { useMemo, useRef } from 'react'; +import { memo, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { FaCircle } from 'react-icons/fa'; import { useHoverDirty } from 'react-use'; @@ -125,4 +125,4 @@ const StatusIndicator = () => { ); }; -export default StatusIndicator; +export default memo(StatusIndicator); diff --git a/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx b/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx index 5d4cc4b9d7..dea179a704 100644 --- a/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx @@ -6,6 +6,7 @@ import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvas import { useTranslation } from 'react-i18next'; import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs'; import { setShouldPinParametersPanel } from '../store/uiSlice'; +import { memo } from 'react'; type PinParametersPanelButtonProps = Omit; @@ -55,4 +56,4 @@ const PinParametersPanelButton = (props: PinParametersPanelButtonProps) => { ); }; -export default PinParametersPanelButton; +export default memo(PinParametersPanelButton); diff --git a/invokeai/frontend/web/src/features/ui/components/common/ResizableDrawer/ResizableDrawer.tsx b/invokeai/frontend/web/src/features/ui/components/common/ResizableDrawer/ResizableDrawer.tsx index dbe975b17f..38e2c992dc 100644 --- a/invokeai/frontend/web/src/features/ui/components/common/ResizableDrawer/ResizableDrawer.tsx +++ b/invokeai/frontend/web/src/features/ui/components/common/ResizableDrawer/ResizableDrawer.tsx @@ -14,7 +14,7 @@ import { ResizeCallback, ResizeStartCallback, } from 're-resizable'; -import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; +import { ReactNode, memo, useEffect, useMemo, useRef, useState } from 'react'; import { LangDirection } from './types'; import { getHandleEnables, @@ -193,4 +193,4 @@ const ResizableDrawer = ({ ); }; -export default ResizableDrawer; +export default memo(ResizableDrawer); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx index b8de2f8308..49b4392362 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx @@ -9,6 +9,7 @@ import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Sym import ParamPromptArea from 'features/parameters/components/Parameters/Prompt/ParamPromptArea'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import ImageToImageTabCoreParameters from './ImageToImageTabCoreParameters'; +import { memo } from 'react'; const ImageToImageTabParameters = () => { return ( @@ -28,4 +29,4 @@ const ImageToImageTabParameters = () => { ); }; -export default ImageToImageTabParameters; +export default memo(ImageToImageTabParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelList.tsx index 44626c04c3..9d7153e2db 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelList.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelList.tsx @@ -3,7 +3,7 @@ import { EntityState } from '@reduxjs/toolkit'; import IAIButton from 'common/components/IAIButton'; import IAIInput from 'common/components/IAIInput'; import { forEach } from 'lodash-es'; -import type { ChangeEvent, PropsWithChildren } from 'react'; +import { ChangeEvent, PropsWithChildren, memo } from 'react'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ALL_BASE_MODELS } from 'services/api/constants'; @@ -232,7 +232,7 @@ const ModelList = (props: ModelListProps) => { ); }; -export default ModelList; +export default memo(ModelList); const modelsFilter = < T extends @@ -266,7 +266,7 @@ const modelsFilter = < return filteredModels; }; -const StyledModelContainer = (props: PropsWithChildren) => { +const StyledModelContainer = memo((props: PropsWithChildren) => { return ( { {props.children} ); -}; +}); + +StyledModelContainer.displayName = 'StyledModelContainer'; type ModelListWrapperProps = { title: string; @@ -294,7 +296,7 @@ type ModelListWrapperProps = { selected: ModelListProps; }; -function ModelListWrapper(props: ModelListWrapperProps) { +const ModelListWrapper = memo((props: ModelListWrapperProps) => { const { title, modelList, selected } = props; return ( @@ -313,23 +315,29 @@ function ModelListWrapper(props: ModelListWrapperProps) {
); -} +}); -function FetchingModelsLoader({ loadingMessage }: { loadingMessage?: string }) { - return ( - - - - - {loadingMessage ? loadingMessage : 'Fetching...'} - - - - ); -} +ModelListWrapper.displayName = 'ModelListWrapper'; + +const FetchingModelsLoader = memo( + ({ loadingMessage }: { loadingMessage?: string }) => { + return ( + + + + + {loadingMessage ? loadingMessage : 'Fetching...'} + + + + ); + } +); + +FetchingModelsLoader.displayName = 'FetchingModelsLoader'; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx index 1864e3d043..ca97b79722 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx @@ -1,5 +1,6 @@ import { Box, Flex } from '@chakra-ui/react'; import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay'; +import { memo } from 'react'; const TextToImageTabMain = () => { return ( @@ -25,4 +26,4 @@ const TextToImageTabMain = () => { ); }; -export default TextToImageTabMain; +export default memo(TextToImageTabMain); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx index 75fa063e17..d9b9e0bc39 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx @@ -9,6 +9,7 @@ import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Sym import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import ParamPromptArea from '../../../../parameters/components/Parameters/Prompt/ParamPromptArea'; import TextToImageTabCoreParameters from './TextToImageTabCoreParameters'; +import { memo } from 'react'; const TextToImageTabParameters = () => { return ( @@ -28,4 +29,4 @@ const TextToImageTabParameters = () => { ); }; -export default TextToImageTabParameters; +export default memo(TextToImageTabParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx index a179a95c3f..22737ce0bb 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx @@ -18,6 +18,7 @@ import { FaWrench } from 'react-icons/fa'; import ClearCanvasHistoryButtonModal from 'features/canvas/components/ClearCanvasHistoryButtonModal'; import { isEqual } from 'lodash-es'; import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; export const canvasControlsSelector = createSelector( [canvasSelector], @@ -109,4 +110,4 @@ const UnifiedCanvasSettings = () => { ); }; -export default UnifiedCanvasSettings; +export default memo(UnifiedCanvasSettings); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasToolSelect.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasToolSelect.tsx index a6c4ec7c40..a11f40a257 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasToolSelect.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasToolSelect.tsx @@ -13,6 +13,7 @@ import { } from 'features/canvas/store/canvasSlice'; import { systemSelector } from 'features/system/store/systemSelectors'; import { isEqual } from 'lodash-es'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -161,4 +162,4 @@ const UnifiedCanvasToolSelect = () => { ); }; -export default UnifiedCanvasToolSelect; +export default memo(UnifiedCanvasToolSelect); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbarBeta.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbarBeta.tsx index b5eec3bec3..f33347dce0 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbarBeta.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbarBeta.tsx @@ -14,6 +14,7 @@ import UnifiedCanvasResetCanvas from './UnifiedCanvasToolbar/UnifiedCanvasResetC import UnifiedCanvasResetView from './UnifiedCanvasToolbar/UnifiedCanvasResetView'; import UnifiedCanvasSaveToGallery from './UnifiedCanvasToolbar/UnifiedCanvasSaveToGallery'; import UnifiedCanvasToolSelect from './UnifiedCanvasToolbar/UnifiedCanvasToolSelect'; +import { memo } from 'react'; const UnifiedCanvasToolbarBeta = () => { return ( @@ -51,4 +52,4 @@ const UnifiedCanvasToolbarBeta = () => { ); }; -export default UnifiedCanvasToolbarBeta; +export default memo(UnifiedCanvasToolbarBeta); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx index 9e6dc8fef8..dcf3500239 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx @@ -10,6 +10,7 @@ import ParamSeamPaintingCollapse from 'features/parameters/components/Parameters import ParamPromptArea from 'features/parameters/components/Parameters/Prompt/ParamPromptArea'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import UnifiedCanvasCoreParameters from './UnifiedCanvasCoreParameters'; +import { memo } from 'react'; const UnifiedCanvasParameters = () => { return ( @@ -30,4 +31,4 @@ const UnifiedCanvasParameters = () => { ); }; -export default UnifiedCanvasParameters; +export default memo(UnifiedCanvasParameters); From 01738deb23d7438a998615d34171fceaaf2edcbe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:35:56 +1000 Subject: [PATCH 41/45] feat(ui): add eslint rules - `curly` requires conditionals to use curly braces - `react/jsx-curly-brace-presence` requires string props to *not* have curly braces --- invokeai/frontend/web/.eslintrc.js | 5 +++++ .../web/src/common/components/IAIDndImage.tsx | 8 ++++++-- .../web/src/common/components/ImageUploader.tsx | 4 +++- .../web/src/common/hooks/useResolution.ts | 12 +++++++++--- .../web/src/common/util/dateComparator.ts | 8 ++++++-- .../web/src/common/util/openBase64ImageInTab.ts | 4 +++- .../components/IAICanvasMaskCompositer.tsx | 11 ++++++++--- .../components/IAICanvasObjectRenderer.tsx | 4 +++- .../canvas/components/IAICanvasResizer.tsx | 4 +++- .../components/IAICanvasStagingAreaToolbar.tsx | 4 +++- .../canvas/components/IAICanvasToolPreview.tsx | 4 +++- .../IAICanvasToolbar/IAICanvasBoundingBox.tsx | 8 ++++++-- .../IAICanvasToolbar/IAICanvasToolbar.tsx | 4 +++- .../features/canvas/hooks/useCanvasDragMove.ts | 12 +++++++++--- .../features/canvas/hooks/useCanvasHotkeys.ts | 4 +++- .../features/canvas/hooks/useCanvasMouseDown.ts | 8 ++++++-- .../features/canvas/hooks/useCanvasMouseMove.ts | 12 +++++++++--- .../features/canvas/hooks/useCanvasMouseUp.ts | 4 +++- .../src/features/canvas/hooks/useCanvasZoom.ts | 8 ++++++-- .../features/canvas/hooks/useColorUnderCursor.ts | 8 ++++++-- .../web/src/features/canvas/store/canvasSlice.ts | 16 ++++++++++++---- .../canvas/util/getScaledCursorPosition.ts | 4 +++- .../controlNet/components/ControlNet.tsx | 4 ++-- .../parameters/ParamControlNetWeight.tsx | 2 +- .../components/ParamEmbeddingPopover.tsx | 2 +- .../components/Boards/BoardAutoAddSelect.tsx | 2 +- .../components/Boards/BoardsList/BoardsList.tsx | 2 +- .../SingleSelectionMenuItems.tsx | 8 ++++++-- .../components/ImageGrid/GalleryImage.tsx | 8 ++++++-- .../src/features/nodes/components/NodeEditor.tsx | 4 ++-- .../components/flow/nodes/Notes/NotesNode.tsx | 2 +- .../util/graphBuilders/addSDXLRefinerToGraph.ts | 4 +++- .../Advanced/ParamAdvancedCollapse.tsx | 2 +- .../MaskAdjustment/ParamMaskBlurMethod.tsx | 4 +++- .../ImageToImage/InitialImageDisplay.tsx | 10 +++++----- .../features/parameters/store/generationSlice.ts | 4 +++- .../SDXLImageToImageTabCoreParameters.tsx | 6 +----- .../SDXLUnifiedCanvasTabCoreParameters.tsx | 6 +----- .../ImageToImageTabCoreParameters.tsx | 6 +----- .../AddModelsPanel/AdvancedAddModels.tsx | 4 +++- .../AddModelsPanel/ScanAdvancedAddModels.tsx | 4 +++- .../subpanels/ModelManagerPanel/ModelConvert.tsx | 2 +- .../TextToImage/TextToImageTabCoreParameters.tsx | 6 +----- .../tabs/TextToImage/TextToImageTabMain.tsx | 2 +- .../UnifiedCanvasColorPicker.tsx | 6 ++++-- .../UnifiedCanvasResetView.tsx | 4 +++- .../UnifiedCanvasCoreParameters.tsx | 6 +----- 47 files changed, 174 insertions(+), 92 deletions(-) diff --git a/invokeai/frontend/web/.eslintrc.js b/invokeai/frontend/web/.eslintrc.js index c48e08d45e..7e4d5ead2b 100644 --- a/invokeai/frontend/web/.eslintrc.js +++ b/invokeai/frontend/web/.eslintrc.js @@ -23,6 +23,11 @@ module.exports = { plugins: ['react', '@typescript-eslint', 'eslint-plugin-react-hooks'], root: true, rules: { + curly: 'error', + 'react/jsx-curly-brace-presence': [ + 'error', + { props: 'never', children: 'never' }, + ], 'react-hooks/exhaustive-deps': 'error', 'no-var': 'error', 'brace-style': 'error', diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index aeeb3677cc..e392ad57ae 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -100,14 +100,18 @@ const IAIDndImage = (props: IAIDndImageProps) => { const [isHovered, setIsHovered] = useState(false); const handleMouseOver = useCallback( (e: MouseEvent) => { - if (onMouseOver) onMouseOver(e); + if (onMouseOver) { + onMouseOver(e); + } setIsHovered(true); }, [onMouseOver] ); const handleMouseOut = useCallback( (e: MouseEvent) => { - if (onMouseOut) onMouseOut(e); + if (onMouseOut) { + onMouseOut(e); + } setIsHovered(false); }, [onMouseOut] diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx index c990a9a24e..5f95056a51 100644 --- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx +++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx @@ -150,7 +150,9 @@ const ImageUploader = (props: ImageUploaderProps) => { {...getRootProps({ style: {} })} onKeyDown={(e: KeyboardEvent) => { // Bail out if user hits spacebar - do not open the uploader - if (e.key === ' ') return; + if (e.key === ' ') { + return; + } }} > diff --git a/invokeai/frontend/web/src/common/hooks/useResolution.ts b/invokeai/frontend/web/src/common/hooks/useResolution.ts index 96b95ee074..fb52555be8 100644 --- a/invokeai/frontend/web/src/common/hooks/useResolution.ts +++ b/invokeai/frontend/web/src/common/hooks/useResolution.ts @@ -11,8 +11,14 @@ export default function useResolution(): const tabletResolutions = ['md', 'lg']; const desktopResolutions = ['xl', '2xl']; - if (mobileResolutions.includes(breakpointValue)) return 'mobile'; - if (tabletResolutions.includes(breakpointValue)) return 'tablet'; - if (desktopResolutions.includes(breakpointValue)) return 'desktop'; + if (mobileResolutions.includes(breakpointValue)) { + return 'mobile'; + } + if (tabletResolutions.includes(breakpointValue)) { + return 'tablet'; + } + if (desktopResolutions.includes(breakpointValue)) { + return 'desktop'; + } return 'unknown'; } diff --git a/invokeai/frontend/web/src/common/util/dateComparator.ts b/invokeai/frontend/web/src/common/util/dateComparator.ts index ea0dc28b6d..27af542261 100644 --- a/invokeai/frontend/web/src/common/util/dateComparator.ts +++ b/invokeai/frontend/web/src/common/util/dateComparator.ts @@ -6,7 +6,11 @@ export const dateComparator = (a: string, b: string) => { const dateB = new Date(b); // sort in ascending order - if (dateA > dateB) return 1; - if (dateA < dateB) return -1; + if (dateA > dateB) { + return 1; + } + if (dateA < dateB) { + return -1; + } return 0; }; diff --git a/invokeai/frontend/web/src/common/util/openBase64ImageInTab.ts b/invokeai/frontend/web/src/common/util/openBase64ImageInTab.ts index 0e18ccb45f..71d3bcd661 100644 --- a/invokeai/frontend/web/src/common/util/openBase64ImageInTab.ts +++ b/invokeai/frontend/web/src/common/util/openBase64ImageInTab.ts @@ -5,7 +5,9 @@ type Base64AndCaption = { const openBase64ImageInTab = (images: Base64AndCaption[]) => { const w = window.open(''); - if (!w) return; + if (!w) { + return; + } images.forEach((i) => { const image = new Image(); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositer.tsx index 3949f74ee3..e65f51cade 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositer.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositer.tsx @@ -125,7 +125,9 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => { }, [offset]); useEffect(() => { - if (fillPatternImage) return; + if (fillPatternImage) { + return; + } const image = new Image(); image.onload = () => { @@ -135,7 +137,9 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => { }, [fillPatternImage, maskColorString]); useEffect(() => { - if (!fillPatternImage) return; + if (!fillPatternImage) { + return; + } fillPatternImage.src = getColoredSVG(maskColorString); }, [fillPatternImage, maskColorString]); @@ -151,8 +155,9 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => { !isNumber(stageScale) || !isNumber(stageDimensions.width) || !isNumber(stageDimensions.height) - ) + ) { return null; + } return ( { const { objects } = useAppSelector(selector); - if (!objects) return null; + if (!objects) { + return null; + } return ( diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx index 2806c9622e..e0d8776d8c 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx @@ -42,7 +42,9 @@ const IAICanvasResizer = () => { useLayoutEffect(() => { window.setTimeout(() => { - if (!ref.current) return; + if (!ref.current) { + return; + } const { clientWidth, clientHeight } = ref.current; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx index 0bffda3493..2065e16668 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx @@ -129,7 +129,9 @@ const IAICanvasStagingAreaToolbar = () => { currentStagingAreaImage?.imageName ?? skipToken ); - if (!currentStagingAreaImage) return null; + if (!currentStagingAreaImage) { + return null; + } return ( { clip, } = useAppSelector(canvasBrushPreviewSelector); - if (!shouldDrawBrushPreview) return null; + if (!shouldDrawBrushPreview) { + return null; + } return ( diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx index 9d63d5b681..0f94b1c57a 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx @@ -85,7 +85,9 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { useState(false); useEffect(() => { - if (!transformerRef.current || !shapeRef.current) return; + if (!transformerRef.current || !shapeRef.current) { + return; + } transformerRef.current.nodes([shapeRef.current]); transformerRef.current.getLayer()?.batchDraw(); }, []); @@ -133,7 +135,9 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { * not its width and height. We need to un-scale the width and height before * setting the values. */ - if (!shapeRef.current) return; + if (!shapeRef.current) { + return; + } const rect = shapeRef.current; 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 8035b7ffca..175fba8b4e 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx @@ -167,7 +167,9 @@ const IAICanvasToolbar = () => { const handleResetCanvasView = (shouldScaleTo1 = false) => { const canvasBaseLayer = getCanvasBaseLayer(); - if (!canvasBaseLayer) return; + if (!canvasBaseLayer) { + return; + } const clientRect = canvasBaseLayer.getClientRect({ skipTransform: true, }); diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts index 6861c25842..81e9c0b855 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts @@ -32,13 +32,17 @@ const useCanvasDrag = () => { return { handleDragStart: useCallback(() => { - if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) return; + if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) { + return; + } dispatch(setIsMovingStage(true)); }, [dispatch, isMovingBoundingBox, isStaging, tool]), handleDragMove: useCallback( (e: KonvaEventObject) => { - if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) return; + if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) { + return; + } const newCoordinates = { x: e.target.x(), y: e.target.y() }; @@ -48,7 +52,9 @@ const useCanvasDrag = () => { ), handleDragEnd: useCallback(() => { - if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) return; + if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) { + return; + } dispatch(setIsMovingStage(false)); }, [dispatch, isMovingBoundingBox, isStaging, tool]), }; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts index 6f4669a42a..1641360e5e 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts @@ -134,7 +134,9 @@ const useInpaintingCanvasHotkeys = () => { useHotkeys( ['space'], (e: KeyboardEvent) => { - if (e.repeat) return; + if (e.repeat) { + return; + } canvasStage?.container().focus(); diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts index 67bf7a8539..d98a44edd9 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts @@ -38,7 +38,9 @@ const useCanvasMouseDown = (stageRef: MutableRefObject) => { return useCallback( (e: KonvaEventObject) => { - if (!stageRef.current) return; + if (!stageRef.current) { + return; + } stageRef.current.container().focus(); @@ -54,7 +56,9 @@ const useCanvasMouseDown = (stageRef: MutableRefObject) => { const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - if (!scaledCursorPosition) return; + if (!scaledCursorPosition) { + return; + } e.evt.preventDefault(); diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts index abeab825e4..088356006e 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts @@ -41,11 +41,15 @@ const useCanvasMouseMove = ( const { updateColorUnderCursor } = useColorPicker(); return useCallback(() => { - if (!stageRef.current) return; + if (!stageRef.current) { + return; + } const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - if (!scaledCursorPosition) return; + if (!scaledCursorPosition) { + return; + } dispatch(setCursorPosition(scaledCursorPosition)); @@ -56,7 +60,9 @@ const useCanvasMouseMove = ( return; } - if (!isDrawing || tool === 'move' || isStaging) return; + if (!isDrawing || tool === 'move' || isStaging) { + return; + } didMouseMoveRef.current = true; dispatch( diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts index 8e70543c6f..d99d63c223 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts @@ -47,7 +47,9 @@ const useCanvasMouseUp = ( if (!didMouseMoveRef.current && isDrawing && stageRef.current) { const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - if (!scaledCursorPosition) return; + if (!scaledCursorPosition) { + return; + } /** * Extend the current line. diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts index 3d6a1d7804..f58211ca2c 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts @@ -35,13 +35,17 @@ const useCanvasWheel = (stageRef: MutableRefObject) => { return useCallback( (e: KonvaEventObject) => { // stop default scrolling - if (!stageRef.current || isMoveStageKeyHeld) return; + if (!stageRef.current || isMoveStageKeyHeld) { + return; + } e.evt.preventDefault(); const cursorPos = stageRef.current.getPointerPosition(); - if (!cursorPos) return; + if (!cursorPos) { + return; + } const mousePointTo = { x: (cursorPos.x - stageRef.current.x()) / stageScale, diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts b/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts index 64289a1fd3..0ade036987 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts @@ -16,11 +16,15 @@ const useColorPicker = () => { return { updateColorUnderCursor: () => { - if (!stage || !canvasBaseLayer) return; + if (!stage || !canvasBaseLayer) { + return; + } const position = stage.getPointerPosition(); - if (!position) return; + if (!position) { + return; + } const pixelRatio = Konva.pixelRatio; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index 11f829221a..83341d017e 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -397,7 +397,9 @@ export const canvasSlice = createSlice({ const { tool, layer, brushColor, brushSize, shouldRestrictStrokesToBox } = state; - if (tool === 'move' || tool === 'colorPicker') return; + if (tool === 'move' || tool === 'colorPicker') { + return; + } const newStrokeWidth = brushSize / 2; @@ -434,14 +436,18 @@ export const canvasSlice = createSlice({ addPointToCurrentLine: (state, action: PayloadAction) => { const lastLine = state.layerState.objects.findLast(isCanvasAnyLine); - if (!lastLine) return; + if (!lastLine) { + return; + } lastLine.points.push(...action.payload); }, undo: (state) => { const targetState = state.pastLayerStates.pop(); - if (!targetState) return; + if (!targetState) { + return; + } state.futureLayerStates.unshift(cloneDeep(state.layerState)); @@ -454,7 +460,9 @@ export const canvasSlice = createSlice({ redo: (state) => { const targetState = state.futureLayerStates.shift(); - if (!targetState) return; + if (!targetState) { + return; + } state.pastLayerStates.push(cloneDeep(state.layerState)); diff --git a/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts b/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts index 03a4d749bf..4cfd7dc8f1 100644 --- a/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts +++ b/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts @@ -5,7 +5,9 @@ const getScaledCursorPosition = (stage: Stage) => { const stageTransform = stage.getAbsoluteTransform().copy(); - if (!pointerPosition || !stageTransform) return; + if (!pointerPosition || !stageTransform) { + return; + } const scaledCursorPosition = stageTransform.invert().point(pointerPosition); diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index 3252207edc..6a49fd9727 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -91,8 +91,8 @@ const ControlNet = (props: ControlNetProps) => { > diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx index c08283e1f9..6725c47bb8 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx @@ -23,7 +23,7 @@ const ParamControlNetWeight = (props: ParamControlNetWeightProps) => { return ( { { label="Auto-Add Board" inputRef={inputRef} autoFocus - placeholder={'Select a Board'} + placeholder="Select a Board" value={autoAddBoardId} data={boards} nothingFound="No matching Boards" diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx index cb3474f6bd..4bbd9533fa 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx @@ -41,7 +41,7 @@ const BoardsList = (props: Props) => { <> { }, [copyImageToClipboard, imageDTO.image_url]); const handleStarImage = useCallback(() => { - if (imageDTO) starImages({ imageDTOs: [imageDTO] }); + if (imageDTO) { + starImages({ imageDTOs: [imageDTO] }); + } }, [starImages, imageDTO]); const handleUnstarImage = useCallback(() => { - if (imageDTO) unstarImages({ imageDTOs: [imageDTO] }); + if (imageDTO) { + unstarImages({ imageDTOs: [imageDTO] }); + } }, [unstarImages, imageDTO]); return ( 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 5dbbf011e8..40af91d53a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -88,8 +88,12 @@ const GalleryImage = (props: HoverableImageProps) => { }, []); const starIcon = useMemo(() => { - if (imageDTO?.starred) return ; - if (!imageDTO?.starred && isHovered) return ; + if (imageDTO?.starred) { + return ; + } + if (!imageDTO?.starred && isHovered) { + return ; + } }, [imageDTO?.starred, isHovered]); if (!imageDTO) { diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 87d8e4f127..5d41ada814 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -34,7 +34,7 @@ const NodeEditor = () => { /> { style={{ position: 'absolute', width: '100%', height: '100%' }} > ) => { <> + diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/MaskAdjustment/ParamMaskBlurMethod.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/MaskAdjustment/ParamMaskBlurMethod.tsx index fa20dcdbcc..62d0605640 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/MaskAdjustment/ParamMaskBlurMethod.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/MaskAdjustment/ParamMaskBlurMethod.tsx @@ -21,7 +21,9 @@ export default function ParamMaskBlurMethod() { const { t } = useTranslation(); const handleMaskBlurMethodChange = (v: string | null) => { - if (!v) return; + if (!v) { + return; + } dispatch(setMaskBlurMethod(v as MaskBlurMethods)); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx index 0321d3a1fe..960b4cd773 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx @@ -40,7 +40,7 @@ const InitialImageDisplay = () => { return ( { } {...getUploadButtonProps()} /> } onClick={handleReset} isDisabled={isResetButtonDisabled} diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index d8495c5751..c7ab83cc00 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -313,7 +313,9 @@ export const generationSlice = createSlice({ }); builder.addCase(setShouldShowAdvancedOptions, (state, action) => { const advancedOptionsStatus = action.payload; - if (!advancedOptionsStatus) state.clipSkip = 0; + if (!advancedOptionsStatus) { + state.clipSkip = 0; + } }); }, }); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLImageToImageTabCoreParameters.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLImageToImageTabCoreParameters.tsx index 4d7c919655..dae462ad47 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLImageToImageTabCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLImageToImageTabCoreParameters.tsx @@ -32,11 +32,7 @@ const SDXLImageToImageTabCoreParameters = () => { const { shouldUseSliders, activeLabel } = useAppSelector(selector); return ( - + { const { shouldUseSliders, activeLabel } = useAppSelector(selector); return ( - + { const { shouldUseSliders, activeLabel } = useAppSelector(selector); return ( - + { - if (!v) return; + if (!v) { + return; + } setAdvancedAddMode(v as ManualAddMode); }} /> diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/ScanAdvancedAddModels.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/ScanAdvancedAddModels.tsx index 32906c3396..997341279d 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/ScanAdvancedAddModels.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/ScanAdvancedAddModels.tsx @@ -74,7 +74,9 @@ export default function ScanAdvancedAddModels() { value={advancedAddMode} data={advancedAddModeData} onChange={(v) => { - if (!v) return; + if (!v) { + return; + } setAdvancedAddMode(v as ManualAddMode); if (v === 'checkpoint') { setIsCheckpoint(true); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelConvert.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelConvert.tsx index 045745e206..56dcdfd52f 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelConvert.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelConvert.tsx @@ -111,7 +111,7 @@ export default function ModelConvert(props: ModelConvertProps) { acceptButtonText={`${t('modelManager.convert')}`} triggerComponent={ { const { shouldUseSliders, activeLabel } = useAppSelector(selector); return ( - + { return ( { - if (layer === 'base') + if (layer === 'base') { return `rgba(${brushColor.r},${brushColor.g},${brushColor.b},${brushColor.a})`; - if (layer === 'mask') + } + if (layer === 'mask') { return `rgba(${maskColor.r},${maskColor.g},${maskColor.b},${maskColor.a})`; + } }; useHotkeys( diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasResetView.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasResetView.tsx index fa002a788e..7d0478476e 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasResetView.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasResetView.tsx @@ -31,7 +31,9 @@ export default function UnifiedCanvasResetView() { const handleResetCanvasView = (shouldScaleTo1 = false) => { const canvasBaseLayer = getCanvasBaseLayer(); - if (!canvasBaseLayer) return; + if (!canvasBaseLayer) { + return; + } const clientRect = canvasBaseLayer.getClientRect({ skipTransform: true, }); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx index 6e4ce7d5d0..36f7ff7320 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx @@ -30,11 +30,7 @@ const UnifiedCanvasCoreParameters = () => { const { shouldUseSliders, activeLabel } = useAppSelector(selector); return ( - + Date: Tue, 22 Aug 2023 03:31:29 +1200 Subject: [PATCH 42/45] feat: Add hotkey for Add Nodes (Shift+A) Standard with other tools like Blender --- invokeai/frontend/web/public/locales/en.json | 5 ++++ .../flow/panels/TopLeftPanel/TopLeftPanel.tsx | 5 ++++ .../components/HotkeysModal/HotkeysModal.tsx | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index de568a40f0..fca2a1a153 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -133,6 +133,7 @@ "generalHotkeys": "General Hotkeys", "galleryHotkeys": "Gallery Hotkeys", "unifiedCanvasHotkeys": "Unified Canvas Hotkeys", + "nodesHotkeys": "Nodes Hotkeys", "invoke": { "title": "Invoke", "desc": "Generate an image" @@ -332,6 +333,10 @@ "acceptStagingImage": { "title": "Accept Staging Image", "desc": "Accept Current Staging Area Image" + }, + "addNodes": { + "title": "Add Nodes", + "desc": "Opens the add node menu" } }, "modelManager": { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx index 67471b7e3d..a5f1539b64 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx @@ -2,6 +2,7 @@ import { useAppDispatch } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice'; import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import { Panel } from 'reactflow'; const TopLeftPanel = () => { @@ -11,6 +12,10 @@ const TopLeftPanel = () => { dispatch(addNodePopoverOpened()); }, [dispatch]); + useHotkeys(['shift+a'], () => { + handleOpenAddNodePopover(); + }); + return ( diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx b/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx index 4da46fdac9..12c25dcb6b 100644 --- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx @@ -286,6 +286,14 @@ export default function HotkeysModal({ children }: HotkeysModalProps) { }, ]; + const nodesHotkeys = [ + { + title: t('hotkeys.addNodes.title'), + desc: t('hotkeys.addNodes.desc'), + hotkey: 'Shift + A', + }, + ]; + const renderHotkeyModalItems = (hotkeys: HotkeyList[]) => ( {hotkeys.map((hotkey, i) => ( @@ -377,6 +385,22 @@ export default function HotkeysModal({ children }: HotkeysModalProps) { {renderHotkeyModalItems(unifiedCanvasHotkeys)} + + + + +

{t('hotkeys.nodesHotkeys')}

+ +
+
+ + {renderHotkeyModalItems(nodesHotkeys)} + +
From 4da861e9804799c4a4b8c31c6072d4a0d2b0229c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:01:50 +1000 Subject: [PATCH 43/45] chore: clean up .gitignore --- .gitignore | 33 ------------------- .../inspector/outputs/ImageOutputPreview.tsx | 17 ++++++++++ .../inspector/outputs/NumberOutputPreview.tsx | 13 ++++++++ .../inspector/outputs/StringOutputPreview.tsx | 13 ++++++++ 4 files changed, 43 insertions(+), 33 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/ImageOutputPreview.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/NumberOutputPreview.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/StringOutputPreview.tsx diff --git a/.gitignore b/.gitignore index ca136a17e5..44a0864b5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,4 @@ -# ignore default image save location and model symbolic link .idea/ -embeddings/ -outputs/ -models/ldm/stable-diffusion-v1/model.ckpt -**/restoration/codeformer/weights - -# ignore user models config -configs/models.user.yaml -config/models.user.yml -invokeai.init -.version -.last_model # ignore the Anaconda/Miniconda installer used while building Docker image anaconda.sh @@ -186,38 +174,17 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -src **/__pycache__/ -outputs -# Logs and associated folders -# created from generated embeddings. -logs -testtube -checkpoints # If it's a Mac .DS_Store -invokeai/frontend/yarn.lock -invokeai/frontend/node_modules - # Let the frontend manage its own gitignore !invokeai/frontend/web/* # Scratch folder .scratch/ .vscode/ -gfpgan/ -models/ldm/stable-diffusion-v1/*.sha256 - -# GFPGAN model files -gfpgan/ - -# config file (will be created by installer) -configs/models.yaml - -# ignore initfile -.invokeai # ignore environment.yml and requirements.txt # these are links to the real files in environments-and-requirements diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/ImageOutputPreview.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/ImageOutputPreview.tsx new file mode 100644 index 0000000000..4d89fcbc69 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/ImageOutputPreview.tsx @@ -0,0 +1,17 @@ +import IAIDndImage from 'common/components/IAIDndImage'; +import { memo } from 'react'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import { ImageOutput } from 'services/api/types'; + +type Props = { + output: ImageOutput; +}; + +const ImageOutputPreview = ({ output }: Props) => { + const { image } = output; + const { data: imageDTO } = useGetImageDTOQuery(image.image_name); + + return ; +}; + +export default memo(ImageOutputPreview); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/NumberOutputPreview.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/NumberOutputPreview.tsx new file mode 100644 index 0000000000..ebe03740b3 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/NumberOutputPreview.tsx @@ -0,0 +1,13 @@ +import { Text } from '@chakra-ui/react'; +import { memo } from 'react'; +import { FloatOutput, IntegerOutput } from 'services/api/types'; + +type Props = { + output: IntegerOutput | FloatOutput; +}; + +const NumberOutputPreview = ({ output }: Props) => { + return {output.value}; +}; + +export default memo(NumberOutputPreview); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/StringOutputPreview.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/StringOutputPreview.tsx new file mode 100644 index 0000000000..1dce0530dd --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/outputs/StringOutputPreview.tsx @@ -0,0 +1,13 @@ +import { Text } from '@chakra-ui/react'; +import { memo } from 'react'; +import { StringOutput } from 'services/api/types'; + +type Props = { + output: StringOutput; +}; + +const StringOutputPreview = ({ output }: Props) => { + return {output.value}; +}; + +export default memo(StringOutputPreview); From be6ba57775aa8703362370f4d560321ce59a3dda Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:14:46 +1000 Subject: [PATCH 44/45] chore: flake8 --- invokeai/app/services/invocation_stats.py | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/app/services/invocation_stats.py b/invokeai/app/services/invocation_stats.py index 6c0b90626e..b42d128b51 100644 --- a/invokeai/app/services/invocation_stats.py +++ b/invokeai/app/services/invocation_stats.py @@ -34,7 +34,6 @@ from abc import ABC, abstractmethod from contextlib import AbstractContextManager from dataclasses import dataclass, field from typing import Dict -from pydantic import ValidationError import torch From 0c639bd75157ed211682ad0676f68f9c6b26952a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:26:11 +1000 Subject: [PATCH 45/45] fix(tests): fix tests --- tests/nodes/test_graph_execution_state.py | 4 ++-- tests/nodes/test_node_graph.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/nodes/test_graph_execution_state.py b/tests/nodes/test_graph_execution_state.py index cc3cc0288b..cdb9815f83 100644 --- a/tests/nodes/test_graph_execution_state.py +++ b/tests/nodes/test_graph_execution_state.py @@ -116,14 +116,14 @@ def test_graph_state_expands_iterator(mock_services): graph.add_node(AddInvocation(id="3", b=1)) graph.add_edge(create_edge("0", "collection", "1", "collection")) graph.add_edge(create_edge("1", "item", "2", "a")) - graph.add_edge(create_edge("2", "a", "3", "a")) + graph.add_edge(create_edge("2", "value", "3", "a")) g = GraphExecutionState(graph=graph) while not g.is_complete(): invoke_next(g, mock_services) prepared_add_nodes = g.source_prepared_mapping["3"] - results = set([g.results[n].a for n in prepared_add_nodes]) + results = set([g.results[n].value for n in prepared_add_nodes]) expected = set([1, 11, 21]) assert results == expected diff --git a/tests/nodes/test_node_graph.py b/tests/nodes/test_node_graph.py index f03e23e1c2..fe6709827f 100644 --- a/tests/nodes/test_node_graph.py +++ b/tests/nodes/test_node_graph.py @@ -477,13 +477,13 @@ def test_graph_expands_subgraph(): n1_2 = SubtractInvocation(id="2", b=3) n1.graph.add_node(n1_1) n1.graph.add_node(n1_2) - n1.graph.add_edge(create_edge("1", "a", "2", "a")) + n1.graph.add_edge(create_edge("1", "value", "2", "a")) g.add_node(n1) n2 = AddInvocation(id="2", b=5) g.add_node(n2) - g.add_edge(create_edge("1.2", "a", "2", "a")) + g.add_edge(create_edge("1.2", "value", "2", "a")) dg = g.nx_graph_flat() assert set(dg.nodes) == set(["1.1", "1.2", "2"]) @@ -500,14 +500,14 @@ def test_graph_subgraph_t2i(): g.add_node(n1) - n2 = IntegerInvocation(id="2", a=512) - n3 = IntegerInvocation(id="3", a=256) + n2 = IntegerInvocation(id="2", value=512) + n3 = IntegerInvocation(id="3", value=256) g.add_node(n2) g.add_node(n3) - g.add_edge(create_edge("2", "a", "1.width", "a")) - g.add_edge(create_edge("3", "a", "1.height", "a")) + g.add_edge(create_edge("2", "value", "1.width", "value")) + g.add_edge(create_edge("3", "value", "1.height", "value")) n4 = ShowImageInvocation(id="4") g.add_node(n4)