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'];