diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx index ee6db90ec1..5acd0c0530 100644 --- a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import 'reactflow/dist/style.css'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { Tooltip, Menu, @@ -10,7 +10,7 @@ import { MenuItem, IconButton, } from '@chakra-ui/react'; -import { FaPlus } from 'react-icons/fa'; +import { FaEllipsisV, FaPlus } from 'react-icons/fa'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { nodeAdded } from '../store/nodesSlice'; import { cloneDeep, map } from 'lodash'; @@ -18,8 +18,10 @@ import { RootState } from 'app/store'; import { useBuildInvocation } from '../hooks/useBuildInvocation'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/hooks/useToastWatcher'; +import { IAIIconButton } from 'exports'; +import { AnyInvocationType } from 'services/events/types'; -export const AddNodeMenu = () => { +const AddNodeMenu = () => { const dispatch = useAppDispatch(); const invocationTemplates = useAppSelector( @@ -29,7 +31,7 @@ export const AddNodeMenu = () => { const buildInvocation = useBuildInvocation(); const addNode = useCallback( - (nodeType: string) => { + (nodeType: AnyInvocationType) => { const invocation = buildInvocation(nodeType); if (!invocation) { @@ -47,9 +49,13 @@ export const AddNodeMenu = () => { ); return ( - - } /> - + + } + /> + {map(invocationTemplates, ({ title, description, type }, key) => { return ( @@ -61,3 +67,5 @@ export const AddNodeMenu = () => { ); }; + +export default memo(AddNodeMenu); diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx index cc5b430382..b13db30690 100644 --- a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx @@ -1,5 +1,5 @@ import { Tooltip } from '@chakra-ui/react'; -import { CSSProperties, useMemo } from 'react'; +import { CSSProperties, memo, useMemo } from 'react'; import { Handle, Position, @@ -19,11 +19,11 @@ const handleBaseStyles: CSSProperties = { }; const inputHandleStyles: CSSProperties = { - left: '-1.7rem', + left: '-1rem', }; const outputHandleStyles: CSSProperties = { - right: '-1.7rem', + right: '-0.5rem', }; const requiredConnectionStyles: CSSProperties = { @@ -38,13 +38,14 @@ type FieldHandleProps = { styles?: CSSProperties; }; -export const FieldHandle = (props: FieldHandleProps) => { +const FieldHandle = (props: FieldHandleProps) => { const { nodeId, field, isValidConnection, handleType, styles } = props; const { name, title, type, description } = field; + console.log(props); + return ( { ); }; + +export default memo(FieldHandle); diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx index a420376016..4f46b03b7a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx @@ -2,8 +2,9 @@ import 'reactflow/dist/style.css'; import { Tooltip, Badge, HStack } from '@chakra-ui/react'; import { map } from 'lodash'; import { FIELDS } from '../types/constants'; +import { memo } from 'react'; -export const FieldTypeLegend = () => { +const FieldTypeLegend = () => { return ( {map(FIELDS, ({ title, description, color }, key) => ( @@ -16,3 +17,5 @@ export const FieldTypeLegend = () => { ); }; + +export default memo(FieldTypeLegend); diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index ad21e92b6d..173458d285 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -1,15 +1,12 @@ import { Background, - Controls, MiniMap, OnConnect, OnEdgesChange, OnNodesChange, ReactFlow, - ConnectionLineType, OnConnectStart, OnConnectEnd, - Panel, } from 'reactflow'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { RootState } from 'app/store'; @@ -22,10 +19,10 @@ import { } from '../store/nodesSlice'; import { useCallback } from 'react'; import { InvocationComponent } from './InvocationComponent'; -import { AddNodeMenu } from './AddNodeMenu'; -import { FieldTypeLegend } from './FieldTypeLegend'; -import { Button } from '@chakra-ui/react'; -import { nodesGraphBuilt } from 'services/thunks/session'; +import TopLeftPanel from './panels/TopLeftPanel'; +import TopRightPanel from './panels/TopRightPanel'; +import TopCenterPanel from './panels/TopCenterPanel'; +import BottomLeftPanel from './panels/BottomLeftPanel.tsx'; const nodeTypes = { invocation: InvocationComponent }; @@ -69,10 +66,6 @@ export const Flow = () => { [dispatch] ); - const handleInvoke = useCallback(() => { - dispatch(nodesGraphBuilt()); - }, [dispatch]); - return ( { style: { strokeWidth: 2 }, }} > - - - - - - - - - + + + + - ); diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx new file mode 100644 index 0000000000..2a61d4cc2b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx @@ -0,0 +1,39 @@ +import { Flex, Heading, Tooltip, Icon } from '@chakra-ui/react'; +import { InvocationTemplate } from 'features/nodes/types/types'; +import { memo, MutableRefObject } from 'react'; +import { FaInfoCircle } from 'react-icons/fa'; + +interface IAINodeHeaderProps { + nodeId: string; + template: InvocationTemplate; +} + +const IAINodeHeader = (props: IAINodeHeaderProps) => { + const { nodeId, template } = props; + return ( + + + + {template.title} + + + + + + + ); +}; + +export default memo(IAINodeHeader); diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx new file mode 100644 index 0000000000..8c7aec680c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx @@ -0,0 +1,146 @@ +import { + InputFieldTemplate, + InputFieldValue, + InvocationTemplate, +} from 'features/nodes/types/types'; +import { memo, ReactNode, useCallback } from 'react'; +import { map } from 'lodash'; +import { useAppSelector } from 'app/storeHooks'; +import { RootState } from 'app/store'; +import { + Box, + Flex, + FormControl, + FormLabel, + HStack, + Tooltip, + Divider, +} from '@chakra-ui/react'; +import FieldHandle from '../FieldHandle'; +import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; +import InputFieldComponent from '../InputFieldComponent'; +import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; + +interface IAINodeInputProps { + nodeId: string; + + input: InputFieldValue; + template?: InputFieldTemplate | undefined; + connected: boolean; +} + +function IAINodeInput(props: IAINodeInputProps) { + const { nodeId, input, template, connected } = props; + const isValidConnection = useIsValidConnection(); + + return ( + + + {!template ? ( + + Unknown input: {input.name} + + ) : ( + <> + + + + {template?.title} + + + + + + {!['never', 'directOnly'].includes( + template?.inputRequirement ?? '' + ) && ( + + )} + + )} + + + ); +} + +interface IAINodeInputsProps { + nodeId: string; + template: InvocationTemplate; + inputs: Record; +} + +const IAINodeInputs = (props: IAINodeInputsProps) => { + const { nodeId, template, inputs } = props; + + const edges = useAppSelector((state: RootState) => state.nodes.edges); + + const renderIAINodeInputs = useCallback(() => { + const IAINodeInputsToRender: ReactNode[] = []; + const inputSockets = map(inputs); + + inputSockets.forEach((inputSocket, index) => { + const inputTemplate = template.inputs[inputSocket.name]; + + const isConnected = Boolean( + edges.filter((connectedInput) => { + return ( + connectedInput.target === nodeId && + connectedInput.targetHandle === inputSocket.name + ); + }).length + ); + + if (index < inputSockets.length) { + IAINodeInputsToRender.push(); + } + + IAINodeInputsToRender.push( + + ); + }); + + return ( + + {IAINodeInputsToRender} + + ); + }, [edges, inputs, nodeId, template.inputs]); + + return renderIAINodeInputs(); +}; + +export default memo(IAINodeInputs); diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx new file mode 100644 index 0000000000..38a3a169b8 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx @@ -0,0 +1,97 @@ +import { + InvocationTemplate, + OutputFieldTemplate, + OutputFieldValue, +} from 'features/nodes/types/types'; +import { memo, ReactNode, useCallback } from 'react'; +import { map } from 'lodash'; +import { useAppSelector } from 'app/storeHooks'; +import { RootState } from 'app/store'; +import { Box, Flex, FormControl, FormLabel, HStack } from '@chakra-ui/react'; +import FieldHandle from '../FieldHandle'; +import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; + +interface IAINodeOutputProps { + nodeId: string; + output: OutputFieldValue; + template?: OutputFieldTemplate | undefined; + connected: boolean; +} + +function IAINodeOutput(props: IAINodeOutputProps) { + const { nodeId, output, template, connected } = props; + const isValidConnection = useIsValidConnection(); + + return ( + + + {!template ? ( + + + Unknown Output: {output.name} + + + ) : ( + <> + + {template?.title} + + + + )} + + + ); +} + +interface IAINodeOutputsProps { + nodeId: string; + template: InvocationTemplate; + outputs: Record; +} + +const IAINodeOutputs = (props: IAINodeOutputsProps) => { + const { nodeId, template, outputs } = props; + + const edges = useAppSelector((state: RootState) => state.nodes.edges); + + const renderIAINodeOutputs = useCallback(() => { + const IAINodeOutputsToRender: ReactNode[] = []; + const outputSockets = map(outputs); + + outputSockets.forEach((outputSocket) => { + const outputTemplate = template.outputs[outputSocket.name]; + + const isConnected = Boolean( + edges.filter((connectedInput) => { + return ( + connectedInput.source === nodeId && + connectedInput.sourceHandle === outputSocket.name + ); + }).length + ); + + IAINodeOutputsToRender.push( + + ); + }); + + return {IAINodeOutputsToRender}; + }, [edges, nodeId, outputs, template.outputs]); + + return renderIAINodeOutputs(); +}; + +export default memo(IAINodeOutputs); diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx new file mode 100644 index 0000000000..0fa13c82b6 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx @@ -0,0 +1,23 @@ +import { memo } from 'react'; +import { NodeResizeControl, NodeResizerProps } from 'reactflow'; + +const IAINodeResizer = (props: NodeResizerProps) => { + const { ...rest } = props; + return ( + + ); +}; + +export default memo(IAINodeResizer); diff --git a/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx index 58ca432ffc..21e4b9fcfb 100644 --- a/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx @@ -1,13 +1,14 @@ import { Box } from '@chakra-ui/react'; +import { memo } from 'react'; import { InputFieldTemplate, InputFieldValue } from '../types/types'; -import { ArrayInputFieldComponent } from './fields/ArrayInputField.tsx'; -import { BooleanInputFieldComponent } from './fields/BooleanInputFieldComponent'; -import { EnumInputFieldComponent } from './fields/EnumInputFieldComponent'; -import { ImageInputFieldComponent } from './fields/ImageInputFieldComponent'; -import { LatentsInputFieldComponent } from './fields/LatentsInputFieldComponent'; -import { ModelInputFieldComponent } from './fields/ModelInputFieldComponent'; -import { NumberInputFieldComponent } from './fields/NumberInputFieldComponent'; -import { StringInputFieldComponent } from './fields/StringInputFieldComponent'; +import ArrayInputFieldComponent from './fields/ArrayInputFieldComponent'; +import BooleanInputFieldComponent from './fields/BooleanInputFieldComponent'; +import EnumInputFieldComponent from './fields/EnumInputFieldComponent'; +import ImageInputFieldComponent from './fields/ImageInputFieldComponent'; +import LatentsInputFieldComponent from './fields/LatentsInputFieldComponent'; +import ModelInputFieldComponent from './fields/ModelInputFieldComponent'; +import NumberInputFieldComponent from './fields/NumberInputFieldComponent'; +import StringInputFieldComponent from './fields/StringInputFieldComponent'; type InputFieldComponentProps = { nodeId: string; @@ -16,7 +17,7 @@ type InputFieldComponentProps = { }; // build an individual input element based on the schema -export const InputFieldComponent = (props: InputFieldComponentProps) => { +const InputFieldComponent = (props: InputFieldComponentProps) => { const { nodeId, field, template } = props; const { type, value } = field; @@ -105,3 +106,5 @@ export const InputFieldComponent = (props: InputFieldComponentProps) => { return Unknown field type: {type}; }; + +export default memo(InputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx index 5f06ee9352..c0ddf1c3b3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx @@ -1,242 +1,98 @@ -import { NodeProps, useReactFlow } from 'reactflow'; -import { - Box, - Flex, - FormControl, - FormLabel, - Heading, - HStack, - Tooltip, - Icon, - Code, - Text, -} from '@chakra-ui/react'; -import { FaExclamationCircle, FaInfoCircle } from 'react-icons/fa'; -import { InvocationValue } from '../types/types'; -import { InputFieldComponent } from './InputFieldComponent'; -import { FieldHandle } from './FieldHandle'; -import { isEqual, map, size } from 'lodash'; -import { memo, useMemo, useRef } from 'react'; -import { useIsValidConnection } from '../hooks/useIsValidConnection'; -import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'app/store'; -import { useAppSelector } from 'app/storeHooks'; -import { useGetInvocationTemplate } from '../hooks/useInvocationTemplate'; +import { NodeProps } from 'reactflow'; +import { Box, Flex, Icon, useToken } from '@chakra-ui/react'; +import { FaExclamationCircle } from 'react-icons/fa'; +import { InvocationTemplate, InvocationValue } from '../types/types'; -const connectedInputFieldsSelector = createSelector( - [(state: RootState) => state.nodes.edges], - (edges) => { - // return edges.map((e) => e.targetHandle); - return edges; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, +import { memo, PropsWithChildren, useMemo } from 'react'; +import IAINodeOutputs from './IAINode/IAINodeOutputs'; +import IAINodeInputs from './IAINode/IAINodeInputs'; +import IAINodeHeader from './IAINode/IAINodeHeader'; +import IAINodeResizer from './IAINode/IAINodeResizer'; +import { RootState } from 'app/store'; +import { AnyInvocationType } from 'services/events/types'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/storeHooks'; + +type InvocationComponentWrapperProps = PropsWithChildren & { + selected: boolean; +}; + +const InvocationComponentWrapper = (props: InvocationComponentWrapperProps) => { + const [nodeSelectedOutline, nodeShadow] = useToken('shadows', [ + 'nodeSelectedOutline', + 'dark-lg', + ]); + + return ( + + {props.children} + + ); +}; + +const makeTemplateSelector = (type: AnyInvocationType) => + createSelector( + [(state: RootState) => state.nodes], + (nodes) => { + const template = nodes.invocationTemplates[type]; + if (!template) { + return; + } + return template; }, - } -); + { + memoizeOptions: { + resultEqualityCheck: ( + a: InvocationTemplate | undefined, + b: InvocationTemplate | undefined + ) => a !== undefined && b !== undefined && a.type === b.type, + }, + } + ); export const InvocationComponent = memo((props: NodeProps) => { const { id: nodeId, data, selected } = props; const { type, inputs, outputs } = data; - const isValidConnection = useIsValidConnection(); + const templateSelector = useMemo(() => makeTemplateSelector(type), [type]); - const connectedInputs = useAppSelector(connectedInputFieldsSelector); - const getInvocationTemplate = useGetInvocationTemplate(); - // TODO: determine if a field/handle is connected and disable the input if so + const template = useAppSelector(templateSelector); - const template = useRef(getInvocationTemplate(type)); - - if (!template.current) { + if (!template) { return ( - + + - + ); } return ( - - - <> - {nodeId} - - - {template.current.title} - - - - - - {map(inputs, (input, i) => { - const { id: fieldId } = input; - const inputTemplate = template.current?.inputs[input.name]; - - if (!inputTemplate) { - return ( - - - - Unknown input: {input.name} - - - - ); - } - - const isConnected = Boolean( - connectedInputs.filter((connectedInput) => { - return ( - connectedInput.target === nodeId && - connectedInput.targetHandle === input.name - ); - }).length - ); - - return ( - - - - {inputTemplate?.title} - - - - - - - {!['never', 'directOnly'].includes( - inputTemplate?.inputRequirement ?? '' - ) && ( - - )} - - ); - })} - {map(outputs).map((output, i) => { - const outputTemplate = template.current?.outputs[output.name]; - - const isConnected = Boolean( - connectedInputs.filter((connectedInput) => { - return ( - connectedInput.source === nodeId && - connectedInput.sourceHandle === output.name - ); - }).length - ); - - if (!outputTemplate) { - return ( - - - - Unknown output: {output.name} - - - - ); - } - - return ( - - - - {outputTemplate?.title} Output - - - - - ); - })} - + + + + + - - + + ); }); diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 02ca6e664f..0afd4ce6c5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -3,15 +3,9 @@ import { Box } from '@chakra-ui/react'; import { ReactFlowProvider } from 'reactflow'; import { Flow } from './Flow'; -import { useAppSelector } from 'app/storeHooks'; -import { RootState } from 'app/store'; -import { buildNodesGraph } from '../util/nodesGraphBuilder/buildNodesGraph'; +import { memo } from 'react'; const NodeEditor = () => { - const state = useAppSelector((state: RootState) => state); - - const graph = buildNodesGraph(state); - return ( { - - {JSON.stringify(graph, null, 2)} - ); }; -export default NodeEditor; +export default memo(NodeEditor); diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx new file mode 100644 index 0000000000..88a125e542 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx @@ -0,0 +1,30 @@ +import { Box } from '@chakra-ui/react'; +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { memo } from 'react'; +import { buildNodesGraph } from '../util/nodesGraphBuilder/buildNodesGraph'; + +const NodeGraphOverlay = () => { + const state = useAppSelector((state: RootState) => state); + const graph = buildNodesGraph(state); + + return ( + + {JSON.stringify(graph, null, 2)} + + ); +}; + +export default memo(NodeGraphOverlay); diff --git a/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx b/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx new file mode 100644 index 0000000000..249e8d4c78 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx @@ -0,0 +1,59 @@ +import { ButtonGroup } from '@chakra-ui/react'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { IAIIconButton } from 'exports'; +import { memo, useCallback } from 'react'; +import { FaCode, FaExpand, FaMinus, FaPlus } from 'react-icons/fa'; +import { useReactFlow } from 'reactflow'; +import { shouldShowGraphOverlayChanged } from '../store/nodesSlice'; + +const ViewportControls = () => { + const { zoomIn, zoomOut, fitView } = useReactFlow(); + const dispatch = useAppDispatch(); + const shouldShowGraphOverlay = useAppSelector( + (state) => state.nodes.shouldShowGraphOverlay + ); + + const handleClickedZoomIn = useCallback(() => { + zoomIn(); + }, [zoomIn]); + + const handleClickedZoomOut = useCallback(() => { + zoomOut(); + }, [zoomOut]); + + const handleClickedFitView = useCallback(() => { + fitView(); + }, [fitView]); + + const handleClickedToggleGraphOverlay = useCallback(() => { + dispatch(shouldShowGraphOverlayChanged(!shouldShowGraphOverlay)); + }, [shouldShowGraphOverlay, dispatch]); + + return ( + + } + /> + } + /> + } + /> + } + /> + + ); +}; + +export default memo(ViewportControls); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx similarity index 56% rename from invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx rename to invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx index d9717f14a4..6f437dfcd8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx @@ -1,14 +1,17 @@ import { ArrayInputFieldTemplate, ArrayInputFieldValue, -} from 'features/nodes/types'; -import { FaImage, FaList } from 'react-icons/fa'; +} from 'features/nodes/types/types'; +import { memo } from 'react'; +import { FaList } from 'react-icons/fa'; import { FieldComponentProps } from './types'; -export const ArrayInputFieldComponent = ( +const ArrayInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; return ; }; + +export default memo(ArrayInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx index f9fe404f82..ceb2364e46 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx @@ -4,11 +4,11 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { BooleanInputFieldTemplate, BooleanInputFieldValue, -} from 'features/nodes/types'; -import { ChangeEvent } from 'react'; +} from 'features/nodes/types/types'; +import { ChangeEvent, memo } from 'react'; import { FieldComponentProps } from './types'; -export const BooleanInputFieldComponent = ( +const BooleanInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; @@ -29,3 +29,5 @@ export const BooleanInputFieldComponent = ( ); }; + +export default memo(BooleanInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx index 8de8e17484..15602e7cad 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx @@ -4,11 +4,11 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { EnumInputFieldTemplate, EnumInputFieldValue, -} from 'features/nodes/types'; -import { ChangeEvent } from 'react'; +} from 'features/nodes/types/types'; +import { ChangeEvent, memo } from 'react'; import { FieldComponentProps } from './types'; -export const EnumInputFieldComponent = ( +const EnumInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field, template } = props; @@ -33,3 +33,5 @@ export const EnumInputFieldComponent = ( ); }; + +export default memo(EnumInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index 599fa61e38..1dc0296139 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -8,13 +8,13 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { ImageInputFieldTemplate, ImageInputFieldValue, -} from 'features/nodes/types'; -import { DragEvent, useCallback, useState } from 'react'; +} from 'features/nodes/types/types'; +import { DragEvent, memo, useCallback, useState } from 'react'; import { FaImage } from 'react-icons/fa'; import { ImageType } from 'services/api'; import { FieldComponentProps } from './types'; -export const ImageInputFieldComponent = ( +const ImageInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; @@ -62,3 +62,5 @@ export const ImageInputFieldComponent = ( ); }; + +export default memo(ImageInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx index ed053aab7c..2de0a07eb5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx @@ -1,13 +1,16 @@ import { LatentsInputFieldTemplate, LatentsInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; +import { memo } from 'react'; import { FieldComponentProps } from './types'; -export const LatentsInputFieldComponent = ( +const LatentsInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; return null; }; + +export default memo(LatentsInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx index 5aaf83a186..14f2816e1c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx @@ -6,13 +6,13 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { ModelInputFieldTemplate, ModelInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; import { selectModelsById, selectModelsIds, } from 'features/system/store/modelSlice'; import { isEqual, map } from 'lodash'; -import { ChangeEvent } from 'react'; +import { ChangeEvent, memo } from 'react'; import { FieldComponentProps } from './types'; const availableModelsSelector = createSelector( @@ -28,7 +28,7 @@ const availableModelsSelector = createSelector( } ); -export const ModelInputFieldComponent = ( +const ModelInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; @@ -55,3 +55,5 @@ export const ModelInputFieldComponent = ( ); }; + +export default memo(ModelInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx index 57b8527e00..f3c563f4fa 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx @@ -12,10 +12,11 @@ import { FloatInputFieldValue, IntegerInputFieldTemplate, IntegerInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; +import { memo } from 'react'; import { FieldComponentProps } from './types'; -export const NumberInputFieldComponent = ( +const NumberInputFieldComponent = ( props: FieldComponentProps< IntegerInputFieldValue | FloatInputFieldValue, IntegerInputFieldTemplate | FloatInputFieldTemplate @@ -39,3 +40,5 @@ export const NumberInputFieldComponent = ( ); }; + +export default memo(NumberInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx index 7ed3b5d435..f371e8e58d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx @@ -4,11 +4,11 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { StringInputFieldTemplate, StringInputFieldValue, -} from 'features/nodes/types'; -import { ChangeEvent } from 'react'; +} from 'features/nodes/types/types'; +import { ChangeEvent, memo } from 'react'; import { FieldComponentProps } from './types'; -export const StringInputFieldComponent = ( +const StringInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; @@ -27,3 +27,5 @@ export const StringInputFieldComponent = ( return ; }; + +export default memo(StringInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx new file mode 100644 index 0000000000..fefad5f490 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx @@ -0,0 +1,11 @@ +import { memo } from 'react'; +import { Panel } from 'reactflow'; +import ViewportControls from '../ViewportControls'; + +const BottomLeftPanel = () => ( + + + +); + +export default memo(BottomLeftPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx new file mode 100644 index 0000000000..dfedb284ec --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx @@ -0,0 +1,23 @@ +import { useAppDispatch } from 'app/storeHooks'; +import IAIButton from 'common/components/IAIButton'; +import { memo, useCallback } from 'react'; +import { Panel } from 'reactflow'; +import { nodesGraphBuilt } from 'services/thunks/session'; + +const TopCenterPanel = () => { + const dispatch = useAppDispatch(); + + const handleInvoke = useCallback(() => { + dispatch(nodesGraphBuilt()); + }, [dispatch]); + + return ( + + + Will it blend? + + + ); +}; + +export default memo(TopCenterPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx new file mode 100644 index 0000000000..2b89db000a --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx @@ -0,0 +1,11 @@ +import { memo } from 'react'; +import { Panel } from 'reactflow'; +import AddNodeMenu from '../AddNodeMenu'; + +const TopLeftPanel = () => ( + + + +); + +export default memo(TopLeftPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx new file mode 100644 index 0000000000..7e51e3e00e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx @@ -0,0 +1,21 @@ +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { memo } from 'react'; +import { Panel } from 'reactflow'; +import FieldTypeLegend from '../FieldTypeLegend'; +import NodeGraphOverlay from '../NodeGraphOverlay'; + +const TopRightPanel = () => { + const shouldShowGraphOverlay = useAppSelector( + (state: RootState) => state.nodes.shouldShowGraphOverlay + ); + + return ( + + + {shouldShowGraphOverlay && } + + ); +}; + +export default memo(TopRightPanel); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts index 19eeac8378..a5422c3e95 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts @@ -1,7 +1,9 @@ +import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store'; import { useAppSelector } from 'app/storeHooks'; import { reduce } from 'lodash'; -import { Node } from 'reactflow'; +import { useCallback } from 'react'; +import { Node, useReactFlow } from 'reactflow'; import { AnyInvocationType } from 'services/events/types'; import { v4 as uuidv4 } from 'uuid'; import { @@ -11,68 +13,82 @@ import { } from '../types/types'; import { buildInputFieldValue } from '../util/fieldValueBuilders'; +const templatesSelector = createSelector( + [(state: RootState) => state.nodes], + (nodes) => nodes.invocationTemplates, + { memoizeOptions: { resultEqualityCheck: (a, b) => true } } +); + export const useBuildInvocation = () => { - const invocationTemplates = useAppSelector( - (state: RootState) => state.nodes.invocationTemplates - ); + const invocationTemplates = useAppSelector(templatesSelector); - return (type: AnyInvocationType) => { - const template = invocationTemplates[type]; + const flow = useReactFlow(); - if (template === undefined) { - console.error(`Unable to find template ${type}.`); - return; - } + return useCallback( + (type: AnyInvocationType) => { + const template = invocationTemplates[type]; - const nodeId = uuidv4(); + if (template === undefined) { + console.error(`Unable to find template ${type}.`); + return; + } - const inputs = reduce( - template.inputs, - (inputsAccumulator, inputTemplate, inputName) => { - const fieldId = uuidv4(); + const nodeId = uuidv4(); - const inputFieldValue: InputFieldValue = buildInputFieldValue( - fieldId, - inputTemplate - ); + const inputs = reduce( + template.inputs, + (inputsAccumulator, inputTemplate, inputName) => { + const fieldId = uuidv4(); - inputsAccumulator[inputName] = inputFieldValue; + const inputFieldValue: InputFieldValue = buildInputFieldValue( + fieldId, + inputTemplate + ); - return inputsAccumulator; - }, - {} as Record - ); + inputsAccumulator[inputName] = inputFieldValue; - const outputs = reduce( - template.outputs, - (outputsAccumulator, outputTemplate, outputName) => { - const fieldId = uuidv4(); + return inputsAccumulator; + }, + {} as Record + ); - const outputFieldValue: OutputFieldValue = { - id: fieldId, - name: outputName, - type: outputTemplate.type, - }; + const outputs = reduce( + template.outputs, + (outputsAccumulator, outputTemplate, outputName) => { + const fieldId = uuidv4(); - outputsAccumulator[outputName] = outputFieldValue; + const outputFieldValue: OutputFieldValue = { + id: fieldId, + name: outputName, + type: outputTemplate.type, + }; - return outputsAccumulator; - }, - {} as Record - ); + outputsAccumulator[outputName] = outputFieldValue; - const invocation: Node = { - id: nodeId, - type: 'invocation', - position: { x: 0, y: 0 }, - data: { + return outputsAccumulator; + }, + {} as Record + ); + + const { x, y } = flow.project({ + x: window.innerWidth / 2.5, + y: window.innerHeight / 8, + }); + + const invocation: Node = { id: nodeId, - type, - inputs, - outputs, - }, - }; + type: 'invocation', + position: { x: x, y: y }, + data: { + id: nodeId, + type, + inputs, + outputs, + }, + }; - return invocation; - }; + return invocation; + }, + [invocationTemplates, flow] + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts deleted file mode 100644 index f58e82b897..0000000000 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useAppSelector } from 'app/storeHooks'; -import { invocationTemplatesSelector } from '../store/selectors/invocationTemplatesSelector'; - -export const useGetInvocationTemplate = () => { - const invocationTemplates = useAppSelector(invocationTemplatesSelector); - - return (invocationType: string) => { - const template = invocationTemplates[invocationType]; - - if (!template) { - return; - } - - return template; - }; -}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 1ce806de57..f4c4f9522d 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -24,6 +24,7 @@ export type NodesState = { invocationTemplates: Record; connectionStartParams: OnConnectStartParams | null; lastGraph: Graph | null; + shouldShowGraphOverlay: boolean; }; export const initialNodesState: NodesState = { @@ -33,6 +34,7 @@ export const initialNodesState: NodesState = { invocationTemplates: {}, connectionStartParams: null, lastGraph: null, + shouldShowGraphOverlay: false, }; const nodesSlice = createSlice({ @@ -77,6 +79,9 @@ const nodesSlice = createSlice({ state.nodes[nodeIndex].data.inputs[fieldName].value = value; } }, + shouldShowGraphOverlayChanged: (state, action: PayloadAction) => { + state.shouldShowGraphOverlay = action.payload; + }, }, extraReducers(builder) { builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => { @@ -98,6 +103,7 @@ export const { connectionMade, connectionStarted, connectionEnded, + shouldShowGraphOverlayChanged, } = nodesSlice.actions; export default nodesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index 0fb8acf5bf..01497651e3 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -22,46 +22,55 @@ const getColorTokenCssVariable = (color: string) => export const FIELDS: Record = { integer: { + color: 'red', colorCssVar: getColorTokenCssVariable('red'), title: 'Integer', description: 'Integers are whole numbers, without a decimal point.', }, float: { + color: 'orange', colorCssVar: getColorTokenCssVariable('orange'), title: 'Float', description: 'Floats are numbers with a decimal point.', }, string: { + color: 'yellow', colorCssVar: getColorTokenCssVariable('yellow'), title: 'String', description: 'Strings are text.', }, boolean: { + color: 'green', colorCssVar: getColorTokenCssVariable('green'), title: 'Boolean', description: 'Booleans are true or false.', }, enum: { + color: 'blue', colorCssVar: getColorTokenCssVariable('blue'), title: 'Enum', description: 'Enums are values that may be one of a number of options.', }, image: { + color: 'purple', colorCssVar: getColorTokenCssVariable('purple'), title: 'Image', description: 'Images may be passed between nodes.', }, latents: { + color: 'pink', colorCssVar: getColorTokenCssVariable('pink'), title: 'Latents', description: 'Latents may be passed between nodes.', }, model: { + color: 'teal', colorCssVar: getColorTokenCssVariable('teal'), title: 'Model', description: 'Models are models.', }, array: { + color: 'gray', colorCssVar: getColorTokenCssVariable('gray'), title: 'Array', description: 'TODO: Array type description.', diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 1f35712d39..4b5548e351 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -39,6 +39,7 @@ export type InvocationTemplate = { }; export type FieldUIConfig = { + color: string; colorCssVar: string; title: string; description: string; diff --git a/invokeai/frontend/web/src/theme/theme.ts b/invokeai/frontend/web/src/theme/theme.ts index ad6687a3bf..1ac868c272 100644 --- a/invokeai/frontend/web/src/theme/theme.ts +++ b/invokeai/frontend/web/src/theme/theme.ts @@ -64,6 +64,7 @@ export const theme: ThemeOverride = { working: `0 0 7px var(--invokeai-colors-working-400)`, error: `0 0 7px var(--invokeai-colors-error-400)`, }, + nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-base-500)`, }, colors: { ...invokeAIThemeColors,