From b24c2cb535f9288f5f9b5caa87b6dbff02d9b85d Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 22 Sep 2025 12:38:57 -0400 Subject: [PATCH 1/8] introduce new canvas_workflows that can be used to indicate inputs and outputs of the canvas, build UI where use can select a workflow with these nodes to run against canvas --- invokeai/app/invocations/canvas_workflow.py | 52 ++++ invokeai/frontend/web/public/locales/en.json | 10 + invokeai/frontend/web/src/app/store/store.ts | 3 + .../components/StagingArea/shared.ts | 23 +- .../store/canvasWorkflowSlice.ts | 185 ++++++++++++++ .../WorkflowListMenuTrigger.tsx | 7 +- .../sidePanel/viewMode/EmptyState.tsx | 6 +- .../WorkflowLibrary/WorkflowLibraryModal.tsx | 15 +- .../workflow/WorkflowLibrary/WorkflowList.tsx | 16 +- .../WorkflowLibrary/WorkflowListItem.tsx | 233 ++++++++++-------- .../nodes/store/workflowLibraryModal.ts | 49 +++- .../generation/buildCanvasWorkflowGraph.ts | 202 +++++++++++++++ .../components/CanvasWorkflowTrigger.tsx | 65 +++++ .../queue/components/QueueControls.tsx | 2 + .../features/queue/hooks/useEnqueueCanvas.ts | 58 +++++ .../ParametersPanelCanvas.tsx | 72 +++++- .../frontend/web/src/services/api/schema.ts | 97 +++++++- 17 files changed, 964 insertions(+), 131 deletions(-) create mode 100644 invokeai/app/invocations/canvas_workflow.py create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCanvasWorkflowGraph.ts create mode 100644 invokeai/frontend/web/src/features/queue/components/CanvasWorkflowTrigger.tsx diff --git a/invokeai/app/invocations/canvas_workflow.py b/invokeai/app/invocations/canvas_workflow.py new file mode 100644 index 00000000000..779a13a1033 --- /dev/null +++ b/invokeai/app/invocations/canvas_workflow.py @@ -0,0 +1,52 @@ +"""Canvas workflow bridge invocations.""" + +from invokeai.app.invocations.baseinvocation import ( + BaseInvocation, + Classification, + invocation, +) +from invokeai.app.invocations.fields import ImageField, Input, InputField, WithBoard, WithMetadata +from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.services.shared.invocation_context import InvocationContext + + +@invocation( + "canvas_composite_raster_input", + title="Canvas Composite Input", + tags=["canvas", "workflow", "canvas-workflow-input"], + category="canvas", + version="1.0.0", + classification=Classification.Beta, +) +class CanvasCompositeRasterInputInvocation(BaseInvocation, WithMetadata, WithBoard): + """Provides the flattened canvas raster layer to a workflow.""" + + image: ImageField = InputField( + description="The flattened canvas raster layer.", + input=Input.Direct, + ) + + def invoke(self, context: InvocationContext) -> ImageOutput: + image_dto = context.images.get_dto(self.image.image_name) + return ImageOutput.build(image_dto=image_dto) + + +@invocation( + "canvas_workflow_output", + title="Canvas Workflow Output", + tags=["canvas", "workflow", "canvas-workflow-output"], + category="canvas", + version="1.0.0", + classification=Classification.Beta, +) +class CanvasWorkflowOutputInvocation(BaseInvocation, WithMetadata, WithBoard): + """Designates the workflow image output used by the canvas.""" + + image: ImageField = InputField( + description="The workflow's resulting image.", + input=Input.Connection, + ) + + def invoke(self, context: InvocationContext) -> ImageOutput: + image_dto = context.images.get_dto(self.image.image_name) + return ImageOutput.build(image_dto=image_dto) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index f7a310fc321..b4c318cfb35 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2128,6 +2128,16 @@ "recalculateRects": "Recalculate Rects", "clipToBbox": "Clip Strokes to Bbox", "outputOnlyMaskedRegions": "Output Only Generated Regions", + "canvasWorkflowLabel": "Canvas Workflow", + "canvasWorkflowInstructions": "Select a workflow containing the canvas composite input and canvas workflow output nodes to drive custom canvas generation.", + "canvasWorkflowSelectedDescription": "This workflow is currently configured for canvas generation.", + "canvasWorkflowSelectButton": "Select Workflow", + "canvasWorkflowSelected": "Canvas workflow selected", + "canvasWorkflowModalTitle": "Select Canvas Workflow", + "canvasWorkflowModalDescription": "Choose a workflow containing the canvas composite input and canvas workflow output nodes. Only workflows that meet these requirements can be used from the canvas.", + "selectCanvasWorkflowTooltip": "Select a workflow to run from the canvas", + "changeCanvasWorkflowTooltip": "Change canvas workflow", + "canvasWorkflowChangeButton": "Change Workflow", "addLayer": "Add Layer", "duplicate": "Duplicate", "moveToFront": "Move to Front", diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 12fcfa5a406..568c3dafa57 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -24,6 +24,7 @@ import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/sli import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice'; import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasWorkflowSliceConfig } from 'features/controlLayers/store/canvasWorkflowSlice'; import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice'; import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; @@ -65,6 +66,7 @@ const log = logger('system'); const SLICE_CONFIGS = { [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig, [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig, + [canvasWorkflowSliceConfig.slice.reducerPath]: canvasWorkflowSliceConfig, [canvasSliceConfig.slice.reducerPath]: canvasSliceConfig, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig, [configSliceConfig.slice.reducerPath]: configSliceConfig, @@ -91,6 +93,7 @@ const ALL_REDUCERS = { [api.reducerPath]: api.reducer, [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer, [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer, + [canvasWorkflowSliceConfig.slice.reducerPath]: canvasWorkflowSliceConfig.slice.reducer, // Undoable! [canvasSliceConfig.slice.reducerPath]: undoable( canvasSliceConfig.slice.reducer, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts index fe98408df58..216f1f70113 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts @@ -21,13 +21,28 @@ export const getOutputImageName = (item: S['SessionQueueItem']) => { )?.[1][0]; const output = nodeId ? item.session.results[nodeId] : undefined; - if (!output) { + const getImageNameFromOutput = (result?: S['GraphExecutionState']['results'][string]) => { + if (!result) { + return null; + } + for (const [_name, value] of objectEntries(result)) { + if (isImageField(value)) { + return value.image_name; + } + } return null; + }; + + const imageName = getImageNameFromOutput(output); + if (imageName) { + return imageName; } - for (const [_name, value] of objectEntries(output)) { - if (isImageField(value)) { - return value.image_name; + // Fallback: search all results for an image field. Custom workflows may not have a canvas_output-prefixed node id. + for (const result of Object.values(item.session.results)) { + const fallbackName = getImageNameFromOutput(result); + if (fallbackName) { + return fallbackName; } } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts new file mode 100644 index 00000000000..a56f1874b3e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts @@ -0,0 +1,185 @@ +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { logger } from 'app/logging/logger'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; +import { deepClone } from 'common/util/deepClone'; +import { parseify } from 'common/util/serialize'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import type { Templates } from 'features/nodes/store/types'; +import type { WorkflowV3 } from 'features/nodes/types/workflow'; +import { zWorkflowV3 } from 'features/nodes/types/workflow'; +import { serializeError } from 'serialize-error'; +import { workflowsApi } from 'services/api/endpoints/workflows'; +import { z } from 'zod'; + +const log = logger('canvas'); + +const zCanvasWorkflowState = z.object({ + selectedWorkflowId: z.string().nullable(), + workflow: zWorkflowV3.nullable(), + inputNodeId: z.string().nullable(), + outputNodeId: z.string().nullable(), + status: z.enum(['idle', 'loading', 'succeeded', 'failed']), + error: z.string().nullable(), +}); + +export type CanvasWorkflowState = z.infer; + +const getInitialState = (): CanvasWorkflowState => ({ + selectedWorkflowId: null, + workflow: null, + inputNodeId: null, + outputNodeId: null, + status: 'idle', + error: null, +}); + +type ValidateResult = { + inputNodeId: string; + outputNodeId: string; +}; + +const INPUT_TAG = 'canvas-workflow-input'; +const OUTPUT_TAG = 'canvas-workflow-output'; + +const validateCanvasWorkflow = (workflow: WorkflowV3, templates: Templates): ValidateResult => { + const invocationNodes = workflow.nodes.filter( + (node): node is WorkflowV3['nodes'][number] => node.type === 'invocation' + ); + + const inputNodes = invocationNodes.filter((node) => { + const template = templates[node.data.type]; + return Boolean(template && template.tags.includes(INPUT_TAG)); + }); + + const outputNodes = invocationNodes.filter((node) => { + const template = templates[node.data.type]; + return Boolean(template && template.tags.includes(OUTPUT_TAG)); + }); + + if (inputNodes.length !== 1) { + throw new Error('A canvas workflow must include exactly one input node.'); + } + + if (outputNodes.length !== 1) { + throw new Error('A canvas workflow must include exactly one output node.'); + } + + const inputNode = inputNodes[0]!; + const outputNode = outputNodes[0]!; + + const inputTemplate = templates[inputNode.data.type]; + if (!inputTemplate) { + throw new Error(`Input node template "${inputNode.data.type}" not found.`); + } + if (!('image' in inputTemplate.inputs)) { + throw new Error('Canvas input node must expose an image field.'); + } + + const outputTemplate = templates[outputNode.data.type]; + if (!outputTemplate) { + throw new Error(`Output node template "${outputNode.data.type}" not found.`); + } + if (!('image' in outputTemplate.inputs)) { + throw new Error('Canvas output node must accept an image input field named "image".'); + } + + return { inputNodeId: inputNode.id, outputNodeId: outputNode.id }; +}; + +export const selectCanvasWorkflow = createAsyncThunk< + { workflowId: string; workflow: WorkflowV3; inputNodeId: string; outputNodeId: string }, + string, + { rejectValue: string } +>('canvasWorkflow/select', async (workflowId, { dispatch, rejectWithValue }) => { + const request = dispatch(workflowsApi.endpoints.getWorkflow.initiate(workflowId, { subscribe: false })); + try { + const result = await request.unwrap(); + const workflow = zWorkflowV3.parse(deepClone(result.workflow)); + const templates = $templates.get(); + if (!Object.keys(templates).length) { + throw new Error('Invocation templates are not yet available.'); + } + const { inputNodeId, outputNodeId } = validateCanvasWorkflow(workflow, templates); + return { workflowId: result.workflow_id, workflow, inputNodeId, outputNodeId }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unable to load workflow.'; + log.error({ error: serializeError(error as Error) }, 'Failed to load canvas workflow'); + return rejectWithValue(message); + } finally { + request.unsubscribe(); + } +}); + +const slice = createSlice({ + name: 'canvasWorkflow', + initialState: getInitialState(), + reducers: { + canvasWorkflowCleared: () => getInitialState(), + }, + extraReducers(builder) { + builder + .addCase(selectCanvasWorkflow.pending, (state) => { + state.status = 'loading'; + state.error = null; + }) + .addCase(selectCanvasWorkflow.fulfilled, (state, action) => { + state.selectedWorkflowId = action.payload.workflowId; + state.workflow = action.payload.workflow; + state.inputNodeId = action.payload.inputNodeId; + state.outputNodeId = action.payload.outputNodeId; + state.status = 'succeeded'; + state.error = null; + }) + .addCase(selectCanvasWorkflow.rejected, (state, action) => { + state.status = 'failed'; + state.error = action.payload ?? action.error.message ?? 'Unable to load workflow.'; + }); + }, +}); + +export const { canvasWorkflowCleared } = slice.actions; + +export const canvasWorkflowSliceConfig: SliceConfig = { + slice, + schema: zCanvasWorkflowState, + getInitialState, + persistConfig: { + migrate: (state) => { + const parsed = zCanvasWorkflowState.safeParse(state); + if (!parsed.success) { + log.warn({ error: parseify(parsed.error) }, 'Failed to migrate canvas workflow state, resetting to defaults'); + return getInitialState(); + } + return { + ...parsed.data, + status: 'idle', + error: null, + } satisfies CanvasWorkflowState; + }, + persistDenylist: ['status', 'error'], + }, +}; + +export const selectCanvasWorkflowSlice = (state: RootState) => state.canvasWorkflow; + +export const selectCanvasWorkflowStatus = (state: RootState) => selectCanvasWorkflowSlice(state).status; + +export const selectCanvasWorkflowError = (state: RootState) => selectCanvasWorkflowSlice(state).error; + +export const selectCanvasWorkflowSelection = (state: RootState) => selectCanvasWorkflowSlice(state).selectedWorkflowId; + +export const selectCanvasWorkflowData = (state: RootState) => selectCanvasWorkflowSlice(state).workflow; + +export const selectCanvasWorkflowNodeIds = (state: RootState) => ({ + inputNodeId: selectCanvasWorkflowSlice(state).inputNodeId, + outputNodeId: selectCanvasWorkflowSlice(state).outputNodeId, +}); + +export const selectIsCanvasWorkflowActive = (state: RootState) => { + const sliceState = selectCanvasWorkflowSlice(state); + return ( + Boolean(sliceState.workflow && sliceState.inputNodeId && sliceState.outputNodeId) && + (sliceState.status === 'succeeded' || sliceState.status === 'idle') + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger.tsx index f83c460ded4..6642f5634b3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger.tsx @@ -2,6 +2,7 @@ import { Button, Text } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { selectWorkflowName } from 'features/nodes/store/selectors'; import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiFolderOpenFill } from 'react-icons/pi'; @@ -10,8 +11,12 @@ export const WorkflowListMenuTrigger = () => { const { t } = useTranslation(); const workflowName = useAppSelector(selectWorkflowName); + const onClick = useCallback(() => { + workflowLibraryModal.open(); + }, [workflowLibraryModal]); + return ( - + + + + ); + } return ( diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index a7964ce12ae..b224fdb2cd3 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -4166,6 +4166,47 @@ export type components = { */ type: "canny_edge_detection"; }; + /** + * Canvas Composite Input + * @description Provides the flattened canvas raster layer to a workflow. + */ + CanvasCompositeRasterInputInvocation: { + /** + * @description The board to save the image to + * @default null + */ + board?: components["schemas"]["BoardField"] | null; + /** + * @description Optional metadata to be saved with the image + * @default null + */ + metadata?: components["schemas"]["MetadataField"] | null; + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** @description The flattened canvas raster layer. */ + image: components["schemas"]["ImageField"]; + /** + * type + * @default canvas_composite_raster_input + * @constant + */ + type: "canvas_composite_raster_input"; + }; /** * Canvas Paste Back * @description Combines two images by using the mask provided. Intended for use on the Unified Canvas. @@ -4286,6 +4327,50 @@ export type components = { */ type: "canvas_v2_mask_and_crop"; }; + /** + * Canvas Workflow Output + * @description Designates the workflow image output used by the canvas. + */ + CanvasWorkflowOutputInvocation: { + /** + * @description The board to save the image to + * @default null + */ + board?: components["schemas"]["BoardField"] | null; + /** + * @description Optional metadata to be saved with the image + * @default null + */ + metadata?: components["schemas"]["MetadataField"] | null; + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * @description The workflow's resulting image. + * @default null + */ + image?: components["schemas"]["ImageField"] | null; + /** + * type + * @default canvas_workflow_output + * @constant + */ + type: "canvas_workflow_output"; + }; /** * Center Pad or Crop Image * @description Pad or crop an image's sides from the center by specified pixels. Positive values are outside of the image. @@ -9351,7 +9436,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; + [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasCompositeRasterInputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CanvasWorkflowOutputInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; }; /** * Edges @@ -12184,7 +12269,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasCompositeRasterInputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CanvasWorkflowOutputInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -12242,7 +12327,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasCompositeRasterInputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CanvasWorkflowOutputInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -12289,8 +12374,10 @@ export type components = { calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; canny_edge_detection: components["schemas"]["ImageOutput"]; + canvas_composite_raster_input: components["schemas"]["ImageOutput"]; canvas_paste_back: components["schemas"]["ImageOutput"]; canvas_v2_mask_and_crop: components["schemas"]["ImageOutput"]; + canvas_workflow_output: components["schemas"]["ImageOutput"]; clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; cogview4_denoise: components["schemas"]["LatentsOutput"]; cogview4_i2l: components["schemas"]["LatentsOutput"]; @@ -12535,7 +12622,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasCompositeRasterInputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CanvasWorkflowOutputInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -12604,7 +12691,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasCompositeRasterInputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CanvasWorkflowOutputInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node From f441ada690c59bdd29a37fde1ca893056df32db5 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 29 Sep 2025 19:47:08 -0400 Subject: [PATCH 2/8] feat(ui): implement exposed fields for canvas workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for displaying workflow exposed fields in the canvas parameters panel. Uses a shadow slice pattern to maintain complete state isolation between canvas and workflow tabs. Key features: - Shadow nodes slice mirrors workflow nodes structure for canvas workflows - Context providers redirect field component selectors to canvas workflow data - Middleware intercepts field mutations and routes to appropriate slice - Filters out canvas input nodes from exposed fields - Always displays fields in view mode - Each field wrapped with correct node context for proper data access 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../listeners/canvasWorkflowFieldChanged.ts | 57 ++++ .../listeners/canvasWorkflowRehydrated.ts | 39 +++ invokeai/frontend/web/src/app/store/store.ts | 9 + .../CanvasWorkflowContainerElement.tsx | 77 +++++ .../CanvasWorkflowElementContext.tsx | 38 +++ .../components/CanvasWorkflowFieldsPanel.tsx | 36 +++ .../CanvasWorkflowFormElementComponent.tsx | 59 ++++ .../CanvasWorkflowInvocationContext.tsx | 213 ++++++++++++++ .../components/CanvasWorkflowModeContext.tsx | 14 + .../CanvasWorkflowRootContainer.tsx | 62 ++++ .../store/canvasWorkflowNodesSlice.ts | 274 ++++++++++++++++++ .../store/canvasWorkflowSlice.ts | 27 +- .../flow/nodes/Invocation/context.tsx | 2 +- .../sidePanel/builder/ContainerElement.tsx | 4 +- .../sidePanel/builder/DividerElement.tsx | 4 +- .../sidePanel/builder/HeadingElement.tsx | 4 +- .../sidePanel/builder/NodeFieldElement.tsx | 4 +- .../sidePanel/builder/TextElement.tsx | 4 +- .../sidePanel/builder/use-element.ts | 12 +- .../features/nodes/hooks/useWorkflowMode.ts | 27 ++ .../ParametersPanelCanvas.tsx | 46 +-- 21 files changed, 975 insertions(+), 37 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowModeContext.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowRootContainer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useWorkflowMode.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts new file mode 100644 index 00000000000..8167fe9f77b --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts @@ -0,0 +1,57 @@ +import type { AppStartListening } from 'app/store/store'; +import * as canvasWorkflowNodesActions from 'features/controlLayers/store/canvasWorkflowNodesSlice'; +import * as nodesActions from 'features/nodes/store/nodesSlice'; + +/** + * Listens for field value changes on nodes and redirects them to the canvas workflow nodes slice + * if the node belongs to a canvas workflow (exists in canvasWorkflowNodes but not in nodes). + */ +export const addCanvasWorkflowFieldChangedListener = (startListening: AppStartListening) => { + // List of all field mutation actions from nodesSlice + const fieldMutationActions = [ + nodesActions.fieldStringValueChanged, + nodesActions.fieldIntegerValueChanged, + nodesActions.fieldFloatValueChanged, + nodesActions.fieldBooleanValueChanged, + nodesActions.fieldModelIdentifierValueChanged, + nodesActions.fieldEnumModelValueChanged, + nodesActions.fieldSchedulerValueChanged, + nodesActions.fieldBoardValueChanged, + nodesActions.fieldImageValueChanged, + nodesActions.fieldColorValueChanged, + nodesActions.fieldImageCollectionValueChanged, + nodesActions.fieldStringCollectionValueChanged, + nodesActions.fieldIntegerCollectionValueChanged, + nodesActions.fieldFloatCollectionValueChanged, + nodesActions.fieldFloatGeneratorValueChanged, + nodesActions.fieldIntegerGeneratorValueChanged, + nodesActions.fieldStringGeneratorValueChanged, + nodesActions.fieldImageGeneratorValueChanged, + nodesActions.fieldValueReset, + ]; + + for (const actionCreator of fieldMutationActions) { + startListening({ + actionCreator, + effect: (action: any, { dispatch, getState }: any) => { + const state = getState(); + const { nodeId } = action.payload; + + // Check if this node exists in canvas workflow nodes + const canvasWorkflowNode = state.canvasWorkflowNodes.nodes.find((n: any) => n.id === nodeId); + const regularNode = state.nodes.present.nodes.find((n: any) => n.id === nodeId); + + // If the node exists in canvas workflow but NOT in regular nodes, redirect the action + if (canvasWorkflowNode && !regularNode) { + // Get the corresponding action from canvasWorkflowNodesSlice + const actionName = actionCreator.type.split('/').pop() as keyof typeof canvasWorkflowNodesActions; + const canvasWorkflowAction = canvasWorkflowNodesActions[actionName]; + + if (canvasWorkflowAction && typeof canvasWorkflowAction === 'function') { + dispatch(canvasWorkflowAction(action.payload as any)); + } + } + }, + }); + } +}; \ No newline at end of file diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated.ts new file mode 100644 index 00000000000..a4c0f28d211 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated.ts @@ -0,0 +1,39 @@ +import type { AppStartListening } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; +import { selectCanvasWorkflow } from 'features/controlLayers/store/canvasWorkflowSlice'; +import { getFormFieldInitialValues } from 'features/nodes/store/nodesSlice'; +import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; +import type { AnyNode } from 'features/nodes/types/invocation'; +import { REMEMBER_REHYDRATED } from 'redux-remember'; + +/** + * When the app rehydrates from storage, we need to populate the canvasWorkflowNodes + * shadow slice if a canvas workflow was previously selected. + * + * This ensures that exposed fields are visible when the page loads with a workflow already selected. + */ +export const addCanvasWorkflowRehydratedListener = (startListening: AppStartListening) => { + startListening({ + type: REMEMBER_REHYDRATED, + effect: async (_action, { dispatch, getState }) => { + const state = getState(); + const { workflow, inputNodeId } = state.canvasWorkflow; + + // If there's a canvas workflow already selected, we need to load it into shadow nodes + if (workflow && inputNodeId) { + // Manually dispatch the fulfilled action to populate shadow nodes + // We can't use the thunk because the workflow is already loaded + dispatch({ + type: selectCanvasWorkflow.fulfilled.type, + payload: { + workflow, + inputNodeId, + outputNodeId: state.canvasWorkflow.outputNodeId, + workflowId: state.canvasWorkflow.selectedWorkflowId, + fieldValues: state.canvasWorkflow.fieldValues, + }, + }); + } + }, + }); +}; \ No newline at end of file diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 568c3dafa57..7e27ae8fda6 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -25,6 +25,7 @@ import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSe import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { canvasWorkflowSliceConfig } from 'features/controlLayers/store/canvasWorkflowSlice'; +import { canvasWorkflowNodesSliceConfig } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice'; import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; @@ -57,6 +58,8 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { addArchivedOrDeletedBoardListener } from './middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener'; import { addImageUploadedFulfilledListener } from './middleware/listenerMiddleware/listeners/imageUploaded'; +import { addCanvasWorkflowFieldChangedListener } from './middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged'; +import { addCanvasWorkflowRehydratedListener } from './middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated'; export const listenerMiddleware = createListenerMiddleware(); @@ -67,6 +70,7 @@ const SLICE_CONFIGS = { [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig, [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig, [canvasWorkflowSliceConfig.slice.reducerPath]: canvasWorkflowSliceConfig, + [canvasWorkflowNodesSliceConfig.slice.reducerPath]: canvasWorkflowNodesSliceConfig, [canvasSliceConfig.slice.reducerPath]: canvasSliceConfig, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig, [configSliceConfig.slice.reducerPath]: configSliceConfig, @@ -94,6 +98,7 @@ const ALL_REDUCERS = { [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer, [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer, [canvasWorkflowSliceConfig.slice.reducerPath]: canvasWorkflowSliceConfig.slice.reducer, + [canvasWorkflowNodesSliceConfig.slice.reducerPath]: canvasWorkflowNodesSliceConfig.slice.reducer, // Undoable! [canvasSliceConfig.slice.reducerPath]: undoable( canvasSliceConfig.slice.reducer, @@ -292,3 +297,7 @@ addAppConfigReceivedListener(startAppListening); addAdHocPostProcessingRequestedListener(startAppListening); addSetDefaultSettingsListener(startAppListening); + +// Canvas workflow fields +addCanvasWorkflowFieldChangedListener(startAppListening); +addCanvasWorkflowRehydratedListener(startAppListening); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx new file mode 100644 index 00000000000..b69e6f587ae --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx @@ -0,0 +1,77 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; +import { + ContainerContextProvider, + DepthContextProvider, + useContainerContext, + useDepthContext, +} from 'features/nodes/components/sidePanel/builder/contexts'; +import { isContainerElement } from 'features/nodes/types/workflow'; +import { CONTAINER_CLASS_NAME } from 'features/nodes/types/workflow'; +import { memo } from 'react'; + +import { CanvasWorkflowFormElementComponent } from './CanvasWorkflowFormElementComponent'; + +const containerViewModeSx: SystemStyleObject = { + gap: 2, + '&[data-self-layout="column"]': { + flexDir: 'column', + alignItems: 'stretch', + }, + '&[data-self-layout="row"]': { + flexDir: 'row', + alignItems: 'flex-start', + overflowX: 'auto', + overflowY: 'visible', + h: 'min-content', + flexShrink: 0, + }, + '&[data-parent-layout="column"]': { + w: 'full', + h: 'min-content', + }, + '&[data-parent-layout="row"]': { + flex: '1 1 0', + minW: 32, + }, +}; + +/** + * Container element for canvas workflow fields. + * This reads from the canvas workflow nodes slice. + */ +export const CanvasWorkflowContainerElement = memo(({ id }: { id: string }) => { + const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice); + const el = nodesState.form.elements[id]; + const depth = useDepthContext(); + const containerCtx = useContainerContext(); + + if (!el || !isContainerElement(el)) { + return null; + } + + const { data } = el; + const { children, layout } = data; + + return ( + + + + {children.map((childId) => ( + + ))} + + + + ); +}); +CanvasWorkflowContainerElement.displayName = 'CanvasWorkflowContainerElement'; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx new file mode 100644 index 00000000000..1f99392a93d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx @@ -0,0 +1,38 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; +import type { FormElement } from 'features/nodes/types/workflow'; +import type { PropsWithChildren } from 'react'; +import { createContext, memo, useContext, useMemo } from 'react'; + +/** + * Context that provides element lookup from canvas workflow nodes instead of regular nodes. + * This ensures that when viewing canvas workflow fields, we read from the shadow slice. + */ + +type CanvasWorkflowElementContextValue = { + getElement: (id: string) => FormElement | undefined; +}; + +const CanvasWorkflowElementContext = createContext(null); + +export const CanvasWorkflowElementProvider = memo(({ children }: PropsWithChildren) => { + const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice); + + const value = useMemo( + () => ({ + getElement: (id: string) => nodesState.form.elements[id], + }), + [nodesState.form.elements] + ); + + return {children}; +}); +CanvasWorkflowElementProvider.displayName = 'CanvasWorkflowElementProvider'; + +/** + * Hook to get an element, using canvas workflow context if available, + * otherwise falls back to regular nodes. + */ +export const useCanvasWorkflowElement = (): ((id: string) => FormElement | undefined) | null => { + return useContext(CanvasWorkflowElementContext)?.getElement ?? null; +}; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx new file mode 100644 index 00000000000..780bf3eb8fa --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx @@ -0,0 +1,36 @@ +import { Flex, Text } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasWorkflowModeProvider } from 'features/controlLayers/components/CanvasWorkflowModeContext'; +import { CanvasWorkflowRootContainer } from 'features/controlLayers/components/CanvasWorkflowRootContainer'; +import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; +import { memo } from 'react'; + +/** + * Renders the exposed fields for a canvas workflow. + * + * This component renders the workflow's form in view mode. + * Each field element is wrapped with the appropriate InvocationNodeContext + * in CanvasWorkflowFormElementComponent. + */ +export const CanvasWorkflowFieldsPanel = memo(() => { + const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice); + + // Check if form is empty + const rootElement = nodesState.form.elements[nodesState.form.rootElementId]; + if (!rootElement || !('data' in rootElement) || !rootElement.data || !('children' in rootElement.data) || rootElement.data.children.length === 0) { + return ( + + No fields exposed in this workflow + + ); + } + + return ( + + + + + + ); +}); +CanvasWorkflowFieldsPanel.displayName = 'CanvasWorkflowFieldsPanel'; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx new file mode 100644 index 00000000000..46609267699 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx @@ -0,0 +1,59 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; +import { DividerElement } from 'features/nodes/components/sidePanel/builder/DividerElement'; +import { HeadingElement } from 'features/nodes/components/sidePanel/builder/HeadingElement'; +import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode'; +import { TextElement } from 'features/nodes/components/sidePanel/builder/TextElement'; +import { + isContainerElement, + isDividerElement, + isHeadingElement, + isNodeFieldElement, + isTextElement, +} from 'features/nodes/types/workflow'; +import { memo } from 'react'; +import type { Equals } from 'tsafe'; +import { assert } from 'tsafe'; + +import { CanvasWorkflowContainerElement } from './CanvasWorkflowContainerElement'; +import { CanvasWorkflowInvocationNodeContextProvider } from './CanvasWorkflowInvocationContext'; + +/** + * Renders a form element from canvas workflow nodes. + * Recursively handles all element types. + */ +export const CanvasWorkflowFormElementComponent = memo(({ id }: { id: string }) => { + const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice); + const el = nodesState.form.elements[id]; + + if (!el) { + return null; + } + + if (isContainerElement(el)) { + return ; + } + + if (isNodeFieldElement(el)) { + return ( + + + + ); + } + + if (isDividerElement(el)) { + return ; + } + + if (isHeadingElement(el)) { + return ; + } + + if (isTextElement(el)) { + return ; + } + + assert>(false, `Unhandled type for element with id ${id}`); +}); +CanvasWorkflowFormElementComponent.displayName = 'CanvasWorkflowFormElementComponent'; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx new file mode 100644 index 00000000000..eaec0a31519 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx @@ -0,0 +1,213 @@ +import { useStore } from '@nanostores/react'; +import type { Selector } from '@reduxjs/toolkit'; +import { createSelector } from '@reduxjs/toolkit'; +import type { RootState } from 'app/store/store'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectEdges, selectNodeFieldElements, selectNodes } from 'features/nodes/store/selectors'; +import { InvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; +import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation'; +import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; +import type { PropsWithChildren } from 'react'; +import { memo, useMemo } from 'react'; + +/** + * Provides InvocationNodeContext for canvas workflow nodes. + * + * This is a wrapper around InvocationNodeContextProvider that redirects + * node selectors to use the canvasWorkflowNodes slice instead of the nodes slice. + * This allows all existing field components to work without modification. + */ + +const getSelectorFromCache = (cache: Map, key: string, fallback: () => T): T => { + let selector = cache.get(key); + if (!selector) { + selector = fallback(); + cache.set(key, selector); + } + return selector as T; +}; + +// Create custom selectors that read from canvasWorkflowNodes instead of nodes +const selectCanvasWorkflowNodes = (state: RootState) => state.canvasWorkflowNodes.nodes; +const selectCanvasWorkflowEdges = (state: RootState) => state.canvasWorkflowNodes.edges; +const selectCanvasWorkflowNodeFieldElements = (state: RootState) => { + const form = state.canvasWorkflowNodes.form; + return Object.values(form.elements).filter((el) => el.type === 'node-field'); +}; + +export const CanvasWorkflowInvocationNodeContextProvider = memo( + ({ nodeId, children }: PropsWithChildren<{ nodeId: string }>) => { + const templates = useStore($templates); + + const value = useMemo(() => { + const cache: Map> = new Map(); + + const selectNodeSafe = getSelectorFromCache(cache, 'selectNodeSafe', () => + createSelector(selectCanvasWorkflowNodes, (nodes) => { + return (nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) ?? null) as InvocationNode | null; + }) + ); + const selectNodeDataSafe = getSelectorFromCache(cache, 'selectNodeDataSafe', () => + createSelector(selectNodeSafe, (node) => { + return node?.data ?? null; + }) + ); + const selectNodeTypeSafe = getSelectorFromCache(cache, 'selectNodeTypeSafe', () => + createSelector(selectNodeDataSafe, (data) => { + return data?.type ?? null; + }) + ); + const selectNodeTemplateSafe = getSelectorFromCache(cache, 'selectNodeTemplateSafe', () => + createSelector(selectNodeTypeSafe, (type) => { + return type ? (templates[type] ?? null) : null; + }) + ); + const selectNodeInputsSafe = getSelectorFromCache(cache, 'selectNodeInputsSafe', () => + createSelector(selectNodeDataSafe, (data) => { + return data?.inputs ?? null; + }) + ); + const buildSelectInputFieldSafe = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectInputFieldSafe-${fieldName}`, () => + createSelector(selectNodeInputsSafe, (inputs) => { + return inputs?.[fieldName] ?? null; + }) + ); + const buildSelectInputFieldTemplateSafe = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectInputFieldTemplateSafe-${fieldName}`, () => + createSelector(selectNodeTemplateSafe, (template) => { + return template?.inputs?.[fieldName] ?? null; + }) + ); + const buildSelectOutputFieldTemplateSafe = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectOutputFieldTemplateSafe-${fieldName}`, () => + createSelector(selectNodeTemplateSafe, (template) => { + return template?.outputs?.[fieldName] ?? null; + }) + ); + + const selectNodeOrThrow = getSelectorFromCache(cache, 'selectNodeOrThrow', () => + createSelector(selectCanvasWorkflowNodes, (nodes) => { + const node = nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) as InvocationNode | undefined; + if (node === undefined) { + throw new Error(`Cannot find node with id ${nodeId}`); + } + return node; + }) + ); + const selectNodeDataOrThrow = getSelectorFromCache(cache, 'selectNodeDataOrThrow', () => + createSelector(selectNodeOrThrow, (node) => { + return node.data; + }) + ); + const selectNodeTypeOrThrow = getSelectorFromCache(cache, 'selectNodeTypeOrThrow', () => + createSelector(selectNodeDataOrThrow, (data) => { + return data.type; + }) + ); + const selectNodeTemplateOrThrow = getSelectorFromCache(cache, 'selectNodeTemplateOrThrow', () => + createSelector(selectNodeTypeOrThrow, (type) => { + const template = templates[type]; + if (template === undefined) { + throw new Error(`Cannot find template for node with id ${nodeId} with type ${type}`); + } + return template; + }) + ); + const selectNodeInputsOrThrow = getSelectorFromCache(cache, 'selectNodeInputsOrThrow', () => + createSelector(selectNodeDataOrThrow, (data) => { + return data.inputs; + }) + ); + const buildSelectInputFieldOrThrow = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectInputFieldOrThrow-${fieldName}`, () => + createSelector(selectNodeInputsOrThrow, (inputs) => { + const field = inputs[fieldName]; + if (field === undefined) { + console.error(`[CanvasWorkflowContext] Cannot find input field with name ${fieldName} in node ${nodeId}. Available fields:`, Object.keys(inputs)); + throw new Error(`Cannot find input field with name ${fieldName} in node ${nodeId}`); + } + return field; + }) + ); + const buildSelectInputFieldTemplateOrThrow = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectInputFieldTemplateOrThrow-${fieldName}`, () => + createSelector(selectNodeTemplateOrThrow, (template) => { + const fieldTemplate = template.inputs[fieldName]; + if (fieldTemplate === undefined) { + throw new Error(`Cannot find input field template with name ${fieldName} in node ${nodeId}`); + } + return fieldTemplate; + }) + ); + const buildSelectOutputFieldTemplateOrThrow = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectOutputFieldTemplateOrThrow-${fieldName}`, () => + createSelector(selectNodeTemplateOrThrow, (template) => { + const fieldTemplate = template.outputs[fieldName]; + if (fieldTemplate === undefined) { + throw new Error(`Cannot find output field template with name ${fieldName} in node ${nodeId}`); + } + return fieldTemplate; + }) + ); + + const buildSelectIsInputFieldConnected = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectIsInputFieldConnected-${fieldName}`, () => + createSelector(selectCanvasWorkflowEdges, (edges) => { + return edges.some((edge) => { + return edge.target === nodeId && edge.targetHandle === fieldName; + }); + }) + ); + + const buildSelectIsInputFieldAddedToForm = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectIsInputFieldAddedToForm-${fieldName}`, () => + createSelector(selectCanvasWorkflowNodeFieldElements, (nodeFieldElements) => { + return nodeFieldElements.some( + (el: any) => el.data.fieldIdentifier.nodeId === nodeId && el.data.fieldIdentifier.fieldName === fieldName + ); + }) + ); + + const selectNodeNeedsUpdate = getSelectorFromCache(cache, 'selectNodeNeedsUpdate', () => + createSelector([selectNodeDataSafe, selectNodeTemplateSafe], (data, template) => { + if (!data || !template) { + return false; + } + return getNeedsUpdate(data, template); + }) + ); + + return { + nodeId, + + selectNodeSafe, + selectNodeDataSafe, + selectNodeTypeSafe, + selectNodeTemplateSafe, + selectNodeInputsSafe, + + buildSelectInputFieldSafe, + buildSelectInputFieldTemplateSafe, + buildSelectOutputFieldTemplateSafe, + buildSelectIsInputFieldAddedToForm, + + selectNodeOrThrow, + selectNodeDataOrThrow, + selectNodeTypeOrThrow, + selectNodeTemplateOrThrow, + selectNodeInputsOrThrow, + + buildSelectInputFieldOrThrow, + buildSelectInputFieldTemplateOrThrow, + buildSelectOutputFieldTemplateOrThrow, + + buildSelectIsInputFieldConnected, + selectNodeNeedsUpdate, + }; + }, [nodeId, templates]); + + return {children}; + } +); +CanvasWorkflowInvocationNodeContextProvider.displayName = 'CanvasWorkflowInvocationNodeContextProvider'; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowModeContext.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowModeContext.tsx new file mode 100644 index 00000000000..a59bd25044c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowModeContext.tsx @@ -0,0 +1,14 @@ +import { CanvasWorkflowModeContext } from 'features/nodes/hooks/useWorkflowMode'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +/** + * Context provider to override the workflow mode for canvas workflows. + * Canvas workflows should always render fields in view mode, regardless of + * the workflow tab's current mode. + */ + +export const CanvasWorkflowModeProvider = memo(({ children }: PropsWithChildren) => { + return {children}; +}); +CanvasWorkflowModeProvider.displayName = 'CanvasWorkflowModeProvider'; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowRootContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowRootContainer.tsx new file mode 100644 index 00000000000..c61e7b5b8da --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowRootContainer.tsx @@ -0,0 +1,62 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Box } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; +import { + ContainerContextProvider, + DepthContextProvider, +} from 'features/nodes/components/sidePanel/builder/contexts'; +import { isContainerElement } from 'features/nodes/types/workflow'; +import { ROOT_CONTAINER_CLASS_NAME } from 'features/nodes/types/workflow'; +import { memo } from 'react'; + +import { CanvasWorkflowFormElementComponent } from './CanvasWorkflowFormElementComponent'; + +const rootViewModeSx: SystemStyleObject = { + position: 'relative', + alignItems: 'center', + borderRadius: 'base', + w: 'full', + h: 'full', + gap: 2, + display: 'flex', + flex: 1, + maxW: '768px', + '&[data-self-layout="column"]': { + flexDir: 'column', + alignItems: 'stretch', + }, + '&[data-self-layout="row"]': { + flexDir: 'row', + alignItems: 'flex-start', + }, +}; + +/** + * Root container for canvas workflow fields. + * This reads from the canvas workflow nodes slice instead of the main nodes slice. + */ +export const CanvasWorkflowRootContainer = memo(() => { + const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice); + const el = nodesState.form.elements[nodesState.form.rootElementId]; + + if (!el || !isContainerElement(el)) { + return null; + } + + const { id, data } = el; + const { children, layout } = data; + + return ( + + + + {children.map((childId) => ( + + ))} + + + + ); +}); +CanvasWorkflowRootContainer.displayName = 'CanvasWorkflowRootContainer'; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts new file mode 100644 index 00000000000..86180da0c4d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts @@ -0,0 +1,274 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; +import { deepClone } from 'common/util/deepClone'; +import { getFormFieldInitialValues } from 'features/nodes/store/nodesSlice'; +import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; +import type { NodesState } from 'features/nodes/store/types'; +import { zNodesState } from 'features/nodes/store/types'; +import type { StatefulFieldValue } from 'features/nodes/types/field'; +import type { AnyNode } from 'features/nodes/types/invocation'; +import { isInvocationNode } from 'features/nodes/types/invocation'; +import type { WorkflowV3 } from 'features/nodes/types/workflow'; +import { z } from 'zod'; + +import { selectCanvasWorkflow } from './canvasWorkflowSlice'; + +/** + * This slice holds a shadow copy of canvas workflow nodes in the same format as the nodes slice. + * This allows the existing field components to work without modification. + * + * The nodes in this slice are completely separate from the workflow tab nodes. + */ + +const getInitialState = (): NodesState => ({ + _version: 1, + formFieldInitialValues: {}, + name: '', + author: '', + description: '', + version: '', + contact: '', + tags: '', + notes: '', + exposedFields: [], + meta: { version: '3.0.0', category: 'user' }, + form: { + elements: { + root: { + id: 'root', + type: 'container', + data: { + layout: 'column', + children: [], + }, + }, + }, + rootElementId: 'root', + }, + nodes: [], + edges: [], + id: undefined, +}); + +type FieldValueAction = PayloadAction<{ + nodeId: string; + fieldName: string; + value: T; +}>; + +const fieldValueReducer = ( + state: NodesState, + action: FieldValueAction, + schema: z.ZodType +) => { + const { nodeId, fieldName, value } = action.payload; + const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId); + const node = state.nodes?.[nodeIndex]; + if (!isInvocationNode(node)) { + return; + } + const field = node.data?.inputs[fieldName]; + if (!field) { + return; + } + const result = schema.safeParse(value); + if (!result.success) { + return; + } + field.value = result.data; +}; + +const slice = createSlice({ + name: 'canvasWorkflowNodes', + initialState: getInitialState(), + reducers: { + canvasWorkflowNodesCleared: () => getInitialState(), + // Field value mutations - these update the shadow nodes when fields are changed + fieldStringValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldIntegerValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldFloatValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldBooleanValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldModelIdentifierValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldEnumModelValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldSchedulerValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldBoardValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldImageValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldColorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldImageCollectionValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldStringCollectionValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldIntegerCollectionValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldFloatCollectionValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldFloatGeneratorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldIntegerGeneratorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldStringGeneratorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldImageGeneratorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + fieldValueReset: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, z.any()); + }, + }, + extraReducers(builder) { + builder.addCase(selectCanvasWorkflow.fulfilled, (state, action) => { + const { workflow, inputNodeId } = action.payload; + const { nodes, edges, ...workflowExtra } = workflow; + + // Filter out form elements that reference the canvas input node + // The input node is the canvas_composite_raster_input that will be populated by the graph builder + const filteredForm = { + ...workflowExtra.form, + elements: { ...workflowExtra.form.elements }, + }; + + const rootElement = filteredForm.elements[filteredForm.rootElementId]; + if (rootElement && 'data' in rootElement && rootElement.data && 'children' in rootElement.data) { + // Recursively filter out node field elements for the canvas input node + const filterNodeFields = (elementId: string): boolean => { + const element = filteredForm.elements[elementId]; + if (!element) { + return false; + } + + if (element.type === 'node-field') { + const nodeId = element.data.fieldIdentifier.nodeId; + // Exclude fields from the canvas input node only + if (nodeId === inputNodeId) { + delete filteredForm.elements[elementId]; + return false; + } + } + + if ('data' in element && element.data && 'children' in element.data) { + // Filter children and update the container + const filteredChildren = element.data.children.filter(filterNodeFields); + filteredForm.elements[elementId] = { + ...element, + data: { + ...element.data, + children: filteredChildren, + }, + } as any; + } + + return true; + }; + + // Start filtering from root + const filteredChildren = rootElement.data.children.filter(filterNodeFields); + filteredForm.elements[filteredForm.rootElementId] = { + ...rootElement, + data: { + ...rootElement.data, + children: filteredChildren, + }, + } as any; + } + + const formFieldInitialValues = getFormFieldInitialValues(filteredForm, nodes); + + const loadedNodes = nodes.map((node: AnyNode) => ({ ...SHARED_NODE_PROPERTIES, ...node })); + console.log('[canvasWorkflowNodesSlice] Loading nodes:', loadedNodes.map((n: any) => ({ id: n.id, type: n.type, inputs: n.data?.inputs ? Object.keys(n.data.inputs) : [] }))); + + // Load the canvas workflow into shadow nodes with filtered form + return { + ...getInitialState(), + ...deepClone(workflowExtra), + form: filteredForm, + formFieldInitialValues, + nodes: loadedNodes, + edges, + }; + }); + builder.addCase(selectCanvasWorkflow.rejected, (state) => { + return getInitialState(); + }); + }, +}); + +export const { + canvasWorkflowNodesCleared, + fieldStringValueChanged, + fieldIntegerValueChanged, + fieldFloatValueChanged, + fieldBooleanValueChanged, + fieldModelIdentifierValueChanged, + fieldEnumModelValueChanged, + fieldSchedulerValueChanged, + fieldBoardValueChanged, + fieldImageValueChanged, + fieldColorValueChanged, + fieldImageCollectionValueChanged, + fieldStringCollectionValueChanged, + fieldIntegerCollectionValueChanged, + fieldFloatCollectionValueChanged, + fieldFloatGeneratorValueChanged, + fieldIntegerGeneratorValueChanged, + fieldStringGeneratorValueChanged, + fieldImageGeneratorValueChanged, + fieldValueReset, +} = slice.actions; + +export const canvasWorkflowNodesSliceConfig: SliceConfig = { + slice, + schema: zNodesState, + getInitialState, + persistConfig: { + migrate: (state) => state as NodesState, + // We don't persist this slice - it's derived from canvasWorkflow + persistDenylist: [ + '_version', + 'formFieldInitialValues', + 'name', + 'author', + 'description', + 'version', + 'contact', + 'tags', + 'notes', + 'exposedFields', + 'meta', + 'form', + 'nodes', + 'edges', + 'id', + ], + }, +}; + +export const selectCanvasWorkflowNodesSlice = (state: RootState) => state.canvasWorkflowNodes; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts index a56f1874b3e..bbe3c3d6ff5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts @@ -1,11 +1,13 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; -import { $templates } from 'features/nodes/store/nodesSlice'; +import { $templates, getFormFieldInitialValues } from 'features/nodes/store/nodesSlice'; import type { Templates } from 'features/nodes/store/types'; +import type { StatefulFieldValue } from 'features/nodes/types/field'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; import { zWorkflowV3 } from 'features/nodes/types/workflow'; import { serializeError } from 'serialize-error'; @@ -19,6 +21,7 @@ const zCanvasWorkflowState = z.object({ workflow: zWorkflowV3.nullable(), inputNodeId: z.string().nullable(), outputNodeId: z.string().nullable(), + fieldValues: z.record(z.string(), z.any()), status: z.enum(['idle', 'loading', 'succeeded', 'failed']), error: z.string().nullable(), }); @@ -30,6 +33,7 @@ const getInitialState = (): CanvasWorkflowState => ({ workflow: null, inputNodeId: null, outputNodeId: null, + fieldValues: {}, status: 'idle', error: null, }); @@ -88,7 +92,13 @@ const validateCanvasWorkflow = (workflow: WorkflowV3, templates: Templates): Val }; export const selectCanvasWorkflow = createAsyncThunk< - { workflowId: string; workflow: WorkflowV3; inputNodeId: string; outputNodeId: string }, + { + workflowId: string; + workflow: WorkflowV3; + inputNodeId: string; + outputNodeId: string; + fieldValues: Record; + }, string, { rejectValue: string } >('canvasWorkflow/select', async (workflowId, { dispatch, rejectWithValue }) => { @@ -101,7 +111,8 @@ export const selectCanvasWorkflow = createAsyncThunk< throw new Error('Invocation templates are not yet available.'); } const { inputNodeId, outputNodeId } = validateCanvasWorkflow(workflow, templates); - return { workflowId: result.workflow_id, workflow, inputNodeId, outputNodeId }; + const fieldValues = getFormFieldInitialValues(workflow.form, workflow.nodes); + return { workflowId: result.workflow_id, workflow, inputNodeId, outputNodeId, fieldValues }; } catch (error) { const message = error instanceof Error ? error.message : 'Unable to load workflow.'; log.error({ error: serializeError(error as Error) }, 'Failed to load canvas workflow'); @@ -116,6 +127,12 @@ const slice = createSlice({ initialState: getInitialState(), reducers: { canvasWorkflowCleared: () => getInitialState(), + canvasWorkflowFieldValueChanged: ( + state, + action: PayloadAction<{ elementId: string; value: StatefulFieldValue }> + ) => { + state.fieldValues[action.payload.elementId] = action.payload.value; + }, }, extraReducers(builder) { builder @@ -128,6 +145,7 @@ const slice = createSlice({ state.workflow = action.payload.workflow; state.inputNodeId = action.payload.inputNodeId; state.outputNodeId = action.payload.outputNodeId; + state.fieldValues = action.payload.fieldValues; state.status = 'succeeded'; state.error = null; }) @@ -138,7 +156,7 @@ const slice = createSlice({ }, }); -export const { canvasWorkflowCleared } = slice.actions; +export const { canvasWorkflowCleared, canvasWorkflowFieldValueChanged } = slice.actions; export const canvasWorkflowSliceConfig: SliceConfig = { slice, @@ -153,6 +171,7 @@ export const canvasWorkflowSliceConfig: SliceConfig = { } return { ...parsed.data, + fieldValues: parsed.data.fieldValues ?? {}, status: 'idle', error: null, } satisfies CanvasWorkflowState; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/context.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/context.tsx index 8618511d77d..ec458840542 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/context.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/context.tsx @@ -47,7 +47,7 @@ type InvocationNodeContextValue = { selectNodeNeedsUpdate: Selector; }; -const InvocationNodeContext = createContext(null); +export const InvocationNodeContext = createContext(null); const getSelectorFromCache = (cache: Map, key: string, fallback: () => T): T => { let selector = cache.get(key); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx index 76799396086..a2ecbc7d32c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx @@ -18,7 +18,7 @@ import { NodeFieldElement } from 'features/nodes/components/sidePanel/builder/No import { TextElement } from 'features/nodes/components/sidePanel/builder/TextElement'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; import { selectFormRootElement } from 'features/nodes/store/selectors'; -import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice'; +import { useWorkflowMode } from 'features/nodes/hooks/useWorkflowMode'; import type { ContainerElement } from 'features/nodes/types/workflow'; import { CONTAINER_CLASS_NAME, @@ -36,7 +36,7 @@ import { assert } from 'tsafe'; const ContainerElementComponent = memo(({ id }: { id: string }) => { const el = useElement(id); - const mode = useAppSelector(selectWorkflowMode); + const mode = useWorkflowMode(); if (!el || !isContainerElement(el)) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx index 9fdbb8b1b33..df17b4ef82c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx @@ -2,13 +2,13 @@ import { useAppSelector } from 'app/store/storeHooks'; import { DividerElementEditMode } from 'features/nodes/components/sidePanel/builder/DividerElementEditMode'; import { DividerElementViewMode } from 'features/nodes/components/sidePanel/builder/DividerElementViewMode'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; -import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice'; +import { useWorkflowMode } from 'features/nodes/hooks/useWorkflowMode'; import { isDividerElement } from 'features/nodes/types/workflow'; import { memo } from 'react'; export const DividerElement = memo(({ id }: { id: string }) => { const el = useElement(id); - const mode = useAppSelector(selectWorkflowMode); + const mode = useWorkflowMode(); if (!el || !isDividerElement(el)) { return; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx index 997a0b139d9..72993040c56 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx @@ -2,13 +2,13 @@ import { useAppSelector } from 'app/store/storeHooks'; import { HeadingElementEditMode } from 'features/nodes/components/sidePanel/builder/HeadingElementEditMode'; import { HeadingElementViewMode } from 'features/nodes/components/sidePanel/builder/HeadingElementViewMode'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; -import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice'; +import { useWorkflowMode } from 'features/nodes/hooks/useWorkflowMode'; import { isHeadingElement } from 'features/nodes/types/workflow'; import { memo } from 'react'; export const HeadingElement = memo(({ id }: { id: string }) => { const el = useElement(id); - const mode = useAppSelector(selectWorkflowMode); + const mode = useWorkflowMode(); if (!el || !isHeadingElement(el)) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx index dbfe4bbf989..f7b9dfa68ff 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx @@ -3,13 +3,13 @@ import { InvocationNodeContextProvider } from 'features/nodes/components/flow/no import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode'; import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; -import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice'; +import { useWorkflowMode } from 'features/nodes/hooks/useWorkflowMode'; import { isNodeFieldElement } from 'features/nodes/types/workflow'; import { memo } from 'react'; export const NodeFieldElement = memo(({ id }: { id: string }) => { const el = useElement(id); - const mode = useAppSelector(selectWorkflowMode); + const mode = useWorkflowMode(); if (!el || !isNodeFieldElement(el)) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx index 809d27f355f..9d241621b5c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx @@ -2,13 +2,13 @@ import { useAppSelector } from 'app/store/storeHooks'; import { TextElementEditMode } from 'features/nodes/components/sidePanel/builder/TextElementEditMode'; import { TextElementViewMode } from 'features/nodes/components/sidePanel/builder/TextElementViewMode'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; -import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice'; +import { useWorkflowMode } from 'features/nodes/hooks/useWorkflowMode'; import { isTextElement } from 'features/nodes/types/workflow'; import { memo } from 'react'; export const TextElement = memo(({ id }: { id: string }) => { const el = useElement(id); - const mode = useAppSelector(selectWorkflowMode); + const mode = useWorkflowMode(); if (!el || !isTextElement(el)) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-element.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-element.ts index 7697d468796..b26314b3f15 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-element.ts +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-element.ts @@ -1,10 +1,18 @@ import { useAppSelector } from 'app/store/storeHooks'; +import { useCanvasWorkflowElement } from 'features/controlLayers/components/CanvasWorkflowElementContext'; import { buildSelectElement } from 'features/nodes/store/selectors'; import type { FormElement } from 'features/nodes/types/workflow'; import { useMemo } from 'react'; export const useElement = (id: string): FormElement | undefined => { + const canvasGetElement = useCanvasWorkflowElement(); const selector = useMemo(() => buildSelectElement(id), [id]); - const element = useAppSelector(selector); - return element; + const regularElement = useAppSelector(selector); + + // If we're in canvas workflow context, use that; otherwise use regular nodes + if (canvasGetElement) { + return canvasGetElement(id); + } + + return regularElement; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowMode.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowMode.ts new file mode 100644 index 00000000000..d8240b9510e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowMode.ts @@ -0,0 +1,27 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice'; +import type { WorkflowMode } from 'features/nodes/store/types'; +import { createContext, useContext } from 'react'; + +// Create a context to detect if we're in canvas workflow +const CanvasWorkflowModeContext = createContext(null); + +export { CanvasWorkflowModeContext }; + +/** + * Returns the appropriate workflow mode. + * If in canvas workflow context, always returns 'view'. + * Otherwise returns the workflow tab's current mode. + */ +export const useWorkflowMode = (): WorkflowMode => { + const canvasMode = useContext(CanvasWorkflowModeContext); + const workflowTabMode = useAppSelector(selectWorkflowMode); + + // If we're in canvas workflow context, use 'view' mode + if (canvasMode !== null) { + return canvasMode; + } + + // Otherwise use the workflow tab's mode + return workflowTabMode; +}; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx index 7073c9b304e..4a3ec2f7156 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx @@ -2,6 +2,7 @@ import { Box, Button, Flex, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; +import { CanvasWorkflowFieldsPanel } from 'features/controlLayers/components/CanvasWorkflowFieldsPanel'; import { canvasWorkflowCleared, selectCanvasWorkflow, @@ -68,28 +69,33 @@ export const ParametersPanelCanvas = memo(() => { if (workflowState.workflow) { return ( - - - {workflowState.workflow.name || t('controlLayers.canvasWorkflowLabel')} - - {workflowState.workflow.description && ( - - {workflowState.workflow.description} + + + + {workflowState.workflow.name || t('controlLayers.canvasWorkflowLabel')} - )} - {workflowState.error && ( - - {workflowState.error} - - )} - - - + {workflowState.workflow.description && ( + + {workflowState.workflow.description} + + )} + {workflowState.error && ( + + {workflowState.error} + + )} + + + + + + + ); } From 1d0b31529c5f6f10e9e77f07137295100f9d0134 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 29 Sep 2025 20:16:08 -0400 Subject: [PATCH 3/8] cleanup types, lint --- .../listeners/canvasWorkflowFieldChanged.ts | 141 +++++++++++----- .../listeners/canvasWorkflowRehydrated.ts | 8 +- invokeai/frontend/web/src/app/store/store.ts | 4 +- .../CanvasWorkflowContainerElement.tsx | 5 +- .../CanvasWorkflowElementContext.tsx | 2 +- .../components/CanvasWorkflowFieldsPanel.tsx | 10 +- .../CanvasWorkflowFormElementComponent.tsx | 2 +- .../CanvasWorkflowInvocationContext.tsx | 27 +-- .../components/CanvasWorkflowModeContext.tsx | 2 +- .../CanvasWorkflowRootContainer.tsx | 10 +- .../store/canvasWorkflowNodesSlice.ts | 158 +++++++++++------- .../sidePanel/builder/ContainerElement.tsx | 2 +- .../sidePanel/builder/DividerElement.tsx | 1 - .../sidePanel/builder/HeadingElement.tsx | 1 - .../sidePanel/builder/NodeFieldElement.tsx | 1 - .../sidePanel/builder/TextElement.tsx | 1 - .../features/nodes/hooks/useWorkflowMode.ts | 4 +- .../generation/buildCanvasWorkflowGraph.ts | 7 +- 18 files changed, 251 insertions(+), 135 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts index 8167fe9f77b..0ee4ad51e4f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts @@ -1,57 +1,122 @@ -import type { AppStartListening } from 'app/store/store'; +import type { ActionCreatorWithPayload } from '@reduxjs/toolkit'; +import type { AppStartListening, RootState } from 'app/store/store'; import * as canvasWorkflowNodesActions from 'features/controlLayers/store/canvasWorkflowNodesSlice'; import * as nodesActions from 'features/nodes/store/nodesSlice'; +import type { AnyNode } from 'features/nodes/types/invocation'; /** * Listens for field value changes on nodes and redirects them to the canvas workflow nodes slice - * if the node belongs to a canvas workflow (exists in canvasWorkflowNodes but not in nodes). + * if the node belongs to a canvas workflow. */ export const addCanvasWorkflowFieldChangedListener = (startListening: AppStartListening) => { - // List of all field mutation actions from nodesSlice - const fieldMutationActions = [ - nodesActions.fieldStringValueChanged, - nodesActions.fieldIntegerValueChanged, - nodesActions.fieldFloatValueChanged, - nodesActions.fieldBooleanValueChanged, - nodesActions.fieldModelIdentifierValueChanged, - nodesActions.fieldEnumModelValueChanged, - nodesActions.fieldSchedulerValueChanged, - nodesActions.fieldBoardValueChanged, - nodesActions.fieldImageValueChanged, - nodesActions.fieldColorValueChanged, - nodesActions.fieldImageCollectionValueChanged, - nodesActions.fieldStringCollectionValueChanged, - nodesActions.fieldIntegerCollectionValueChanged, - nodesActions.fieldFloatCollectionValueChanged, - nodesActions.fieldFloatGeneratorValueChanged, - nodesActions.fieldIntegerGeneratorValueChanged, - nodesActions.fieldStringGeneratorValueChanged, - nodesActions.fieldImageGeneratorValueChanged, - nodesActions.fieldValueReset, + // List of all field mutation actions from nodesSlice with their canvas workflow counterparts + const fieldMutationActionPairs: Array<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + nodesAction: ActionCreatorWithPayload; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + canvasAction: ActionCreatorWithPayload; + }> = [ + { + nodesAction: nodesActions.fieldStringValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldStringValueChanged, + }, + { + nodesAction: nodesActions.fieldIntegerValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldIntegerValueChanged, + }, + { + nodesAction: nodesActions.fieldFloatValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldFloatValueChanged, + }, + { + nodesAction: nodesActions.fieldBooleanValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldBooleanValueChanged, + }, + { + nodesAction: nodesActions.fieldModelIdentifierValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldModelIdentifierValueChanged, + }, + { + nodesAction: nodesActions.fieldEnumModelValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldEnumModelValueChanged, + }, + { + nodesAction: nodesActions.fieldSchedulerValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldSchedulerValueChanged, + }, + { + nodesAction: nodesActions.fieldBoardValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldBoardValueChanged, + }, + { + nodesAction: nodesActions.fieldImageValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldImageValueChanged, + }, + { + nodesAction: nodesActions.fieldColorValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldColorValueChanged, + }, + { + nodesAction: nodesActions.fieldImageCollectionValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldImageCollectionValueChanged, + }, + { + nodesAction: nodesActions.fieldStringCollectionValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldStringCollectionValueChanged, + }, + { + nodesAction: nodesActions.fieldIntegerCollectionValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldIntegerCollectionValueChanged, + }, + { + nodesAction: nodesActions.fieldFloatCollectionValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldFloatCollectionValueChanged, + }, + { + nodesAction: nodesActions.fieldFloatGeneratorValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldFloatGeneratorValueChanged, + }, + { + nodesAction: nodesActions.fieldIntegerGeneratorValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldIntegerGeneratorValueChanged, + }, + { + nodesAction: nodesActions.fieldStringGeneratorValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldStringGeneratorValueChanged, + }, + { + nodesAction: nodesActions.fieldImageGeneratorValueChanged, + canvasAction: canvasWorkflowNodesActions.fieldImageGeneratorValueChanged, + }, + { nodesAction: nodesActions.fieldValueReset, canvasAction: canvasWorkflowNodesActions.fieldValueReset }, ]; - for (const actionCreator of fieldMutationActions) { + for (const { nodesAction, canvasAction } of fieldMutationActionPairs) { startListening({ - actionCreator, - effect: (action: any, { dispatch, getState }: any) => { - const state = getState(); + actionCreator: nodesAction, + effect: (action, { dispatch, getState }) => { + const state = getState() as RootState; const { nodeId } = action.payload; // Check if this node exists in canvas workflow nodes - const canvasWorkflowNode = state.canvasWorkflowNodes.nodes.find((n: any) => n.id === nodeId); - const regularNode = state.nodes.present.nodes.find((n: any) => n.id === nodeId); + const canvasWorkflowNode = state.canvasWorkflowNodes.nodes.find((n: AnyNode) => n.id === nodeId); + const regularNode = state.nodes.present.nodes.find((n: AnyNode) => n.id === nodeId); - // If the node exists in canvas workflow but NOT in regular nodes, redirect the action - if (canvasWorkflowNode && !regularNode) { - // Get the corresponding action from canvasWorkflowNodesSlice - const actionName = actionCreator.type.split('/').pop() as keyof typeof canvasWorkflowNodesActions; - const canvasWorkflowAction = canvasWorkflowNodesActions[actionName]; + console.log('[canvasWorkflowFieldChanged] Field changed:', { + nodeId, + hasCanvasNode: !!canvasWorkflowNode, + hasRegularNode: !!regularNode, + action: action.type, + payload: action.payload, + }); - if (canvasWorkflowAction && typeof canvasWorkflowAction === 'function') { - dispatch(canvasWorkflowAction(action.payload as any)); - } + // If the node exists in canvas workflow, redirect the action + // This ensures canvas workflow fields always update the canvas workflow nodes slice + if (canvasWorkflowNode) { + console.log('[canvasWorkflowFieldChanged] Redirecting to canvas workflow nodes:', canvasAction.type); + dispatch(canvasAction(action.payload)); } }, }); } -}; \ No newline at end of file +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated.ts index a4c0f28d211..984cd5525f9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated.ts @@ -1,9 +1,5 @@ import type { AppStartListening } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; import { selectCanvasWorkflow } from 'features/controlLayers/store/canvasWorkflowSlice'; -import { getFormFieldInitialValues } from 'features/nodes/store/nodesSlice'; -import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; -import type { AnyNode } from 'features/nodes/types/invocation'; import { REMEMBER_REHYDRATED } from 'redux-remember'; /** @@ -15,7 +11,7 @@ import { REMEMBER_REHYDRATED } from 'redux-remember'; export const addCanvasWorkflowRehydratedListener = (startListening: AppStartListening) => { startListening({ type: REMEMBER_REHYDRATED, - effect: async (_action, { dispatch, getState }) => { + effect: (_action, { dispatch, getState }) => { const state = getState(); const { workflow, inputNodeId } = state.canvasWorkflow; @@ -36,4 +32,4 @@ export const addCanvasWorkflowRehydratedListener = (startListening: AppStartList } }, }); -}; \ No newline at end of file +}; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 7e27ae8fda6..9573634fd3f 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -24,8 +24,8 @@ import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/sli import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice'; import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { canvasWorkflowSliceConfig } from 'features/controlLayers/store/canvasWorkflowSlice'; import { canvasWorkflowNodesSliceConfig } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; +import { canvasWorkflowSliceConfig } from 'features/controlLayers/store/canvasWorkflowSlice'; import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice'; import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; @@ -57,9 +57,9 @@ import { actionSanitizer } from './middleware/devtools/actionSanitizer'; import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { addArchivedOrDeletedBoardListener } from './middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener'; -import { addImageUploadedFulfilledListener } from './middleware/listenerMiddleware/listeners/imageUploaded'; import { addCanvasWorkflowFieldChangedListener } from './middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged'; import { addCanvasWorkflowRehydratedListener } from './middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated'; +import { addImageUploadedFulfilledListener } from './middleware/listenerMiddleware/listeners/imageUploaded'; export const listenerMiddleware = createListenerMiddleware(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx index b69e6f587ae..bbfee270436 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx @@ -8,8 +8,7 @@ import { useContainerContext, useDepthContext, } from 'features/nodes/components/sidePanel/builder/contexts'; -import { isContainerElement } from 'features/nodes/types/workflow'; -import { CONTAINER_CLASS_NAME } from 'features/nodes/types/workflow'; +import { CONTAINER_CLASS_NAME, isContainerElement } from 'features/nodes/types/workflow'; import { memo } from 'react'; import { CanvasWorkflowFormElementComponent } from './CanvasWorkflowFormElementComponent'; @@ -74,4 +73,4 @@ export const CanvasWorkflowContainerElement = memo(({ id }: { id: string }) => { ); }); -CanvasWorkflowContainerElement.displayName = 'CanvasWorkflowContainerElement'; \ No newline at end of file +CanvasWorkflowContainerElement.displayName = 'CanvasWorkflowContainerElement'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx index 1f99392a93d..e022d43bdb9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx @@ -35,4 +35,4 @@ CanvasWorkflowElementProvider.displayName = 'CanvasWorkflowElementProvider'; */ export const useCanvasWorkflowElement = (): ((id: string) => FormElement | undefined) | null => { return useContext(CanvasWorkflowElementContext)?.getElement ?? null; -}; \ No newline at end of file +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx index 780bf3eb8fa..5145f8bc1d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx @@ -17,7 +17,13 @@ export const CanvasWorkflowFieldsPanel = memo(() => { // Check if form is empty const rootElement = nodesState.form.elements[nodesState.form.rootElementId]; - if (!rootElement || !('data' in rootElement) || !rootElement.data || !('children' in rootElement.data) || rootElement.data.children.length === 0) { + if ( + !rootElement || + !('data' in rootElement) || + !rootElement.data || + !('children' in rootElement.data) || + rootElement.data.children.length === 0 + ) { return ( No fields exposed in this workflow @@ -33,4 +39,4 @@ export const CanvasWorkflowFieldsPanel = memo(() => { ); }); -CanvasWorkflowFieldsPanel.displayName = 'CanvasWorkflowFieldsPanel'; \ No newline at end of file +CanvasWorkflowFieldsPanel.displayName = 'CanvasWorkflowFieldsPanel'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx index 46609267699..97d34e0440c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx @@ -56,4 +56,4 @@ export const CanvasWorkflowFormElementComponent = memo(({ id }: { id: string }) assert>(false, `Unhandled type for element with id ${id}`); }); -CanvasWorkflowFormElementComponent.displayName = 'CanvasWorkflowFormElementComponent'; \ No newline at end of file +CanvasWorkflowFormElementComponent.displayName = 'CanvasWorkflowFormElementComponent'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx index eaec0a31519..e89fd3a6263 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx @@ -2,10 +2,9 @@ import { useStore } from '@nanostores/react'; import type { Selector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectEdges, selectNodeFieldElements, selectNodes } from 'features/nodes/store/selectors'; import { InvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; -import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import type { InvocationNode } from 'features/nodes/types/invocation'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; import type { PropsWithChildren } from 'react'; import { memo, useMemo } from 'react'; @@ -32,7 +31,9 @@ const selectCanvasWorkflowNodes = (state: RootState) => state.canvasWorkflowNode const selectCanvasWorkflowEdges = (state: RootState) => state.canvasWorkflowNodes.edges; const selectCanvasWorkflowNodeFieldElements = (state: RootState) => { const form = state.canvasWorkflowNodes.form; - return Object.values(form.elements).filter((el) => el.type === 'node-field'); + return Object.values(form.elements).filter( + (el): el is Extract => el.type === 'node-field' + ); }; export const CanvasWorkflowInvocationNodeContextProvider = memo( @@ -40,11 +41,12 @@ export const CanvasWorkflowInvocationNodeContextProvider = memo( const templates = useStore($templates); const value = useMemo(() => { - const cache: Map> = new Map(); + const cache: Map> = new Map(); const selectNodeSafe = getSelectorFromCache(cache, 'selectNodeSafe', () => createSelector(selectCanvasWorkflowNodes, (nodes) => { - return (nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) ?? null) as InvocationNode | null; + return (nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) ?? + null) as InvocationNode | null; }) ); const selectNodeDataSafe = getSelectorFromCache(cache, 'selectNodeDataSafe', () => @@ -88,7 +90,9 @@ export const CanvasWorkflowInvocationNodeContextProvider = memo( const selectNodeOrThrow = getSelectorFromCache(cache, 'selectNodeOrThrow', () => createSelector(selectCanvasWorkflowNodes, (nodes) => { - const node = nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) as InvocationNode | undefined; + const node = nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) as + | InvocationNode + | undefined; if (node === undefined) { throw new Error(`Cannot find node with id ${nodeId}`); } @@ -124,7 +128,10 @@ export const CanvasWorkflowInvocationNodeContextProvider = memo( createSelector(selectNodeInputsOrThrow, (inputs) => { const field = inputs[fieldName]; if (field === undefined) { - console.error(`[CanvasWorkflowContext] Cannot find input field with name ${fieldName} in node ${nodeId}. Available fields:`, Object.keys(inputs)); + console.error( + `[CanvasWorkflowContext] Cannot find input field with name ${fieldName} in node ${nodeId}. Available fields:`, + Object.keys(inputs) + ); throw new Error(`Cannot find input field with name ${fieldName} in node ${nodeId}`); } return field; @@ -164,7 +171,7 @@ export const CanvasWorkflowInvocationNodeContextProvider = memo( getSelectorFromCache(cache, `buildSelectIsInputFieldAddedToForm-${fieldName}`, () => createSelector(selectCanvasWorkflowNodeFieldElements, (nodeFieldElements) => { return nodeFieldElements.some( - (el: any) => el.data.fieldIdentifier.nodeId === nodeId && el.data.fieldIdentifier.fieldName === fieldName + (el) => el.data.fieldIdentifier.nodeId === nodeId && el.data.fieldIdentifier.fieldName === fieldName ); }) ); @@ -210,4 +217,4 @@ export const CanvasWorkflowInvocationNodeContextProvider = memo( return {children}; } ); -CanvasWorkflowInvocationNodeContextProvider.displayName = 'CanvasWorkflowInvocationNodeContextProvider'; \ No newline at end of file +CanvasWorkflowInvocationNodeContextProvider.displayName = 'CanvasWorkflowInvocationNodeContextProvider'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowModeContext.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowModeContext.tsx index a59bd25044c..14a2646cc31 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowModeContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowModeContext.tsx @@ -11,4 +11,4 @@ import { memo } from 'react'; export const CanvasWorkflowModeProvider = memo(({ children }: PropsWithChildren) => { return {children}; }); -CanvasWorkflowModeProvider.displayName = 'CanvasWorkflowModeProvider'; \ No newline at end of file +CanvasWorkflowModeProvider.displayName = 'CanvasWorkflowModeProvider'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowRootContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowRootContainer.tsx index c61e7b5b8da..f232553e432 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowRootContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowRootContainer.tsx @@ -2,12 +2,8 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Box } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; -import { - ContainerContextProvider, - DepthContextProvider, -} from 'features/nodes/components/sidePanel/builder/contexts'; -import { isContainerElement } from 'features/nodes/types/workflow'; -import { ROOT_CONTAINER_CLASS_NAME } from 'features/nodes/types/workflow'; +import { ContainerContextProvider, DepthContextProvider } from 'features/nodes/components/sidePanel/builder/contexts'; +import { isContainerElement, ROOT_CONTAINER_CLASS_NAME } from 'features/nodes/types/workflow'; import { memo } from 'react'; import { CanvasWorkflowFormElementComponent } from './CanvasWorkflowFormElementComponent'; @@ -59,4 +55,4 @@ export const CanvasWorkflowRootContainer = memo(() => { ); }); -CanvasWorkflowRootContainer.displayName = 'CanvasWorkflowRootContainer'; \ No newline at end of file +CanvasWorkflowRootContainer.displayName = 'CanvasWorkflowRootContainer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts index 86180da0c4d..b9757c69340 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts @@ -4,14 +4,56 @@ import type { RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; import { deepClone } from 'common/util/deepClone'; import { getFormFieldInitialValues } from 'features/nodes/store/nodesSlice'; -import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; import type { NodesState } from 'features/nodes/store/types'; import { zNodesState } from 'features/nodes/store/types'; -import type { StatefulFieldValue } from 'features/nodes/types/field'; +import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; +import type { + BoardFieldValue, + BooleanFieldValue, + ColorFieldValue, + EnumFieldValue, + FloatFieldCollectionValue, + FloatFieldValue, + FloatGeneratorFieldValue, + ImageFieldCollectionValue, + ImageFieldValue, + ImageGeneratorFieldValue, + IntegerFieldCollectionValue, + IntegerFieldValue, + IntegerGeneratorFieldValue, + ModelIdentifierFieldValue, + SchedulerFieldValue, + StatefulFieldValue, + StringFieldCollectionValue, + StringFieldValue, + StringGeneratorFieldValue, +} from 'features/nodes/types/field'; +import { + zBoardFieldValue, + zBooleanFieldValue, + zColorFieldValue, + zEnumFieldValue, + zFloatFieldCollectionValue, + zFloatFieldValue, + zFloatGeneratorFieldValue, + zImageFieldCollectionValue, + zImageFieldValue, + zImageGeneratorFieldValue, + zIntegerFieldCollectionValue, + zIntegerFieldValue, + zIntegerGeneratorFieldValue, + zModelIdentifierFieldValue, + zSchedulerFieldValue, + zStatefulFieldValue, + zStringFieldCollectionValue, + zStringFieldValue, + zStringGeneratorFieldValue, +} from 'features/nodes/types/field'; import type { AnyNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; -import type { WorkflowV3 } from 'features/nodes/types/workflow'; -import { z } from 'zod'; +import type { ContainerElement } from 'features/nodes/types/workflow'; +import { isContainerElement } from 'features/nodes/types/workflow'; +import type { z } from 'zod'; import { selectCanvasWorkflow } from './canvasWorkflowSlice'; @@ -86,62 +128,63 @@ const slice = createSlice({ reducers: { canvasWorkflowNodesCleared: () => getInitialState(), // Field value mutations - these update the shadow nodes when fields are changed - fieldStringValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldValueReset: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zStatefulFieldValue); }, - fieldIntegerValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldStringValueChanged: (state, action: FieldValueAction) => { + console.log('[canvasWorkflowNodesSlice] fieldStringValueChanged:', action.payload); + fieldValueReducer(state, action, zStringFieldValue); }, - fieldFloatValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldStringCollectionValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zStringFieldCollectionValue); }, - fieldBooleanValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldIntegerValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zIntegerFieldValue); }, - fieldModelIdentifierValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldFloatValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zFloatFieldValue); }, - fieldEnumModelValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldFloatCollectionValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zFloatFieldCollectionValue); }, - fieldSchedulerValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldIntegerCollectionValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zIntegerFieldCollectionValue); }, - fieldBoardValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldBooleanValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zBooleanFieldValue); }, - fieldImageValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldBoardValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zBoardFieldValue); }, - fieldColorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldImageValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zImageFieldValue); }, - fieldImageCollectionValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldImageCollectionValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zImageFieldCollectionValue); }, - fieldStringCollectionValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldColorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zColorFieldValue); }, - fieldIntegerCollectionValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldModelIdentifierValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zModelIdentifierFieldValue); }, - fieldFloatCollectionValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldEnumModelValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zEnumFieldValue); }, - fieldFloatGeneratorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldSchedulerValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zSchedulerFieldValue); }, - fieldIntegerGeneratorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldFloatGeneratorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zFloatGeneratorFieldValue); }, - fieldStringGeneratorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldIntegerGeneratorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zIntegerGeneratorFieldValue); }, - fieldImageGeneratorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldStringGeneratorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zStringGeneratorFieldValue); }, - fieldValueReset: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, z.any()); + fieldImageGeneratorValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zImageGeneratorFieldValue); }, }, extraReducers(builder) { @@ -174,36 +217,39 @@ const slice = createSlice({ } } - if ('data' in element && element.data && 'children' in element.data) { + if (isContainerElement(element)) { // Filter children and update the container const filteredChildren = element.data.children.filter(filterNodeFields); - filteredForm.elements[elementId] = { + const updatedElement: ContainerElement = { ...element, data: { ...element.data, children: filteredChildren, }, - } as any; + }; + filteredForm.elements[elementId] = updatedElement; } return true; }; // Start filtering from root - const filteredChildren = rootElement.data.children.filter(filterNodeFields); - filteredForm.elements[filteredForm.rootElementId] = { - ...rootElement, - data: { - ...rootElement.data, - children: filteredChildren, - }, - } as any; + if (isContainerElement(rootElement)) { + const filteredChildren = rootElement.data.children.filter(filterNodeFields); + const updatedRootElement: ContainerElement = { + ...rootElement, + data: { + ...rootElement.data, + children: filteredChildren, + }, + }; + filteredForm.elements[filteredForm.rootElementId] = updatedRootElement; + } } const formFieldInitialValues = getFormFieldInitialValues(filteredForm, nodes); const loadedNodes = nodes.map((node: AnyNode) => ({ ...SHARED_NODE_PROPERTIES, ...node })); - console.log('[canvasWorkflowNodesSlice] Loading nodes:', loadedNodes.map((n: any) => ({ id: n.id, type: n.type, inputs: n.data?.inputs ? Object.keys(n.data.inputs) : [] }))); // Load the canvas workflow into shadow nodes with filtered form return { @@ -215,7 +261,7 @@ const slice = createSlice({ edges, }; }); - builder.addCase(selectCanvasWorkflow.rejected, (state) => { + builder.addCase(selectCanvasWorkflow.rejected, () => { return getInitialState(); }); }, @@ -271,4 +317,4 @@ export const canvasWorkflowNodesSliceConfig: SliceConfig = { }, }; -export const selectCanvasWorkflowNodesSlice = (state: RootState) => state.canvasWorkflowNodes; \ No newline at end of file +export const selectCanvasWorkflowNodesSlice = (state: RootState) => state.canvasWorkflowNodes; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx index a2ecbc7d32c..93094a8f1e7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx @@ -17,8 +17,8 @@ import { HeadingElement } from 'features/nodes/components/sidePanel/builder/Head import { NodeFieldElement } from 'features/nodes/components/sidePanel/builder/NodeFieldElement'; import { TextElement } from 'features/nodes/components/sidePanel/builder/TextElement'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; -import { selectFormRootElement } from 'features/nodes/store/selectors'; import { useWorkflowMode } from 'features/nodes/hooks/useWorkflowMode'; +import { selectFormRootElement } from 'features/nodes/store/selectors'; import type { ContainerElement } from 'features/nodes/types/workflow'; import { CONTAINER_CLASS_NAME, diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx index df17b4ef82c..a9747322e28 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx @@ -1,4 +1,3 @@ -import { useAppSelector } from 'app/store/storeHooks'; import { DividerElementEditMode } from 'features/nodes/components/sidePanel/builder/DividerElementEditMode'; import { DividerElementViewMode } from 'features/nodes/components/sidePanel/builder/DividerElementViewMode'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx index 72993040c56..c0678b2ad8c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx @@ -1,4 +1,3 @@ -import { useAppSelector } from 'app/store/storeHooks'; import { HeadingElementEditMode } from 'features/nodes/components/sidePanel/builder/HeadingElementEditMode'; import { HeadingElementViewMode } from 'features/nodes/components/sidePanel/builder/HeadingElementViewMode'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx index f7b9dfa68ff..70e6242c268 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx @@ -1,4 +1,3 @@ -import { useAppSelector } from 'app/store/storeHooks'; import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context'; import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode'; import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode'; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx index 9d241621b5c..feea5db71af 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx @@ -1,4 +1,3 @@ -import { useAppSelector } from 'app/store/storeHooks'; import { TextElementEditMode } from 'features/nodes/components/sidePanel/builder/TextElementEditMode'; import { TextElementViewMode } from 'features/nodes/components/sidePanel/builder/TextElementViewMode'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowMode.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowMode.ts index d8240b9510e..180eaa09d98 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowMode.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowMode.ts @@ -1,6 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; -import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice'; import type { WorkflowMode } from 'features/nodes/store/types'; +import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice'; import { createContext, useContext } from 'react'; // Create a context to detect if we're in canvas workflow @@ -24,4 +24,4 @@ export const useWorkflowMode = (): WorkflowMode => { // Otherwise use the workflow tab's mode return workflowTabMode; -}; \ No newline at end of file +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCanvasWorkflowGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCanvasWorkflowGraph.ts index 4549742afd6..350e82cfc0e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCanvasWorkflowGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCanvasWorkflowGraph.ts @@ -51,6 +51,8 @@ const buildInvocationNodes = ( ): Record => { const invocations: Record = {}; + const canvasWorkflowNodes = state.canvasWorkflowNodes.nodes; + for (const node of workflow.nodes) { if (node.type !== 'invocation') { continue; @@ -62,7 +64,10 @@ const buildInvocationNodes = ( continue; } - const transformedInputs = Object.entries(data.inputs).reduce>((acc, [name, field]) => { + const canvasWorkflowNode = canvasWorkflowNodes.find((n) => n.id === id); + const nodeInputs = canvasWorkflowNode?.type === 'invocation' ? canvasWorkflowNode.data.inputs : data.inputs; + + const transformedInputs = Object.entries(nodeInputs).reduce>((acc, [name, field]) => { const fieldTemplate = template.inputs[name]; if (!fieldTemplate) { log.warn({ nodeId: id, field: name }, 'Canvas workflow field template not found; skipping field'); From 4d30a039fc1ce5869b7d54f1851330474d6c9071 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 29 Sep 2025 20:35:21 -0400 Subject: [PATCH 4/8] error handling --- .../store/canvasWorkflowSlice.ts | 69 +++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts index bbe3c3d6ff5..34bca0fb7b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts @@ -61,12 +61,24 @@ const validateCanvasWorkflow = (workflow: WorkflowV3, templates: Templates): Val return Boolean(template && template.tags.includes(OUTPUT_TAG)); }); - if (inputNodes.length !== 1) { - throw new Error('A canvas workflow must include exactly one input node.'); + if (inputNodes.length === 0) { + throw new Error('A canvas workflow must include at least one input node with the "canvas-workflow-input" tag.'); } - if (outputNodes.length !== 1) { - throw new Error('A canvas workflow must include exactly one output node.'); + if (inputNodes.length > 1) { + throw new Error( + `A canvas workflow must include exactly one input node, but found ${inputNodes.length}. Remove extra input nodes.` + ); + } + + if (outputNodes.length === 0) { + throw new Error('A canvas workflow must include at least one output node with the "canvas-workflow-output" tag.'); + } + + if (outputNodes.length > 1) { + throw new Error( + `A canvas workflow must include exactly one output node, but found ${outputNodes.length}. Remove extra output nodes.` + ); } const inputNode = inputNodes[0]!; @@ -88,6 +100,55 @@ const validateCanvasWorkflow = (workflow: WorkflowV3, templates: Templates): Val throw new Error('Canvas output node must accept an image input field named "image".'); } + // Validate that all nodes have valid templates + for (const node of invocationNodes) { + const template = templates[node.data.type]; + if (!template) { + throw new Error( + `Node "${node.data.label || node.id}" uses invocation type "${node.data.type}" which is not available. This workflow may have been created with a different version of InvokeAI.` + ); + } + } + + // Validate that required fields without connections have values + const edges = workflow.edges.filter((edge) => edge.type === 'default'); + for (const node of invocationNodes) { + if (node.type !== 'invocation') { + continue; + } + + const template = templates[node.data.type]; + if (!template) { + continue; // Already validated above + } + + for (const [fieldName, fieldTemplate] of Object.entries(template.inputs)) { + // Skip if field is connected (will get value from connection) + const isConnected = edges.some((edge) => edge.target === node.id && edge.targetHandle === fieldName); + if (isConnected) { + continue; + } + + // Check if field is required + if (fieldTemplate.required) { + const fieldInstance = node.data.inputs[fieldName]; + if (!fieldInstance) { + throw new Error( + `Node "${node.data.label || node.id}" is missing required field "${fieldTemplate.title || fieldName}".` + ); + } + + // Check if field has a value (not null/undefined/empty) + const value = fieldInstance.value; + if (value === null || value === undefined || value === '') { + throw new Error( + `Node "${node.data.label || node.id}" has required field "${fieldTemplate.title || fieldName}" with no value. Please provide a value or connect this field.` + ); + } + } + } + } + return { inputNodeId: inputNode.id, outputNodeId: outputNode.id }; }; From c08f7ff401a691422c4d214055882d0292578a13 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 29 Sep 2025 20:41:08 -0400 Subject: [PATCH 5/8] fix circ dep --- .../CanvasWorkflowContainerElement.tsx | 76 ------------------- .../CanvasWorkflowFormElementComponent.tsx | 72 +++++++++++++++++- 2 files changed, 71 insertions(+), 77 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx deleted file mode 100644 index bbfee270436..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowContainerElement.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { Flex } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; -import { - ContainerContextProvider, - DepthContextProvider, - useContainerContext, - useDepthContext, -} from 'features/nodes/components/sidePanel/builder/contexts'; -import { CONTAINER_CLASS_NAME, isContainerElement } from 'features/nodes/types/workflow'; -import { memo } from 'react'; - -import { CanvasWorkflowFormElementComponent } from './CanvasWorkflowFormElementComponent'; - -const containerViewModeSx: SystemStyleObject = { - gap: 2, - '&[data-self-layout="column"]': { - flexDir: 'column', - alignItems: 'stretch', - }, - '&[data-self-layout="row"]': { - flexDir: 'row', - alignItems: 'flex-start', - overflowX: 'auto', - overflowY: 'visible', - h: 'min-content', - flexShrink: 0, - }, - '&[data-parent-layout="column"]': { - w: 'full', - h: 'min-content', - }, - '&[data-parent-layout="row"]': { - flex: '1 1 0', - minW: 32, - }, -}; - -/** - * Container element for canvas workflow fields. - * This reads from the canvas workflow nodes slice. - */ -export const CanvasWorkflowContainerElement = memo(({ id }: { id: string }) => { - const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice); - const el = nodesState.form.elements[id]; - const depth = useDepthContext(); - const containerCtx = useContainerContext(); - - if (!el || !isContainerElement(el)) { - return null; - } - - const { data } = el; - const { children, layout } = data; - - return ( - - - - {children.map((childId) => ( - - ))} - - - - ); -}); -CanvasWorkflowContainerElement.displayName = 'CanvasWorkflowContainerElement'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx index 97d34e0440c..4ba7da78192 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx @@ -1,9 +1,18 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; +import { + ContainerContextProvider, + DepthContextProvider, + useContainerContext, + useDepthContext, +} from 'features/nodes/components/sidePanel/builder/contexts'; import { DividerElement } from 'features/nodes/components/sidePanel/builder/DividerElement'; import { HeadingElement } from 'features/nodes/components/sidePanel/builder/HeadingElement'; import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode'; import { TextElement } from 'features/nodes/components/sidePanel/builder/TextElement'; +import { CONTAINER_CLASS_NAME } from 'features/nodes/types/workflow'; import { isContainerElement, isDividerElement, @@ -15,9 +24,70 @@ import { memo } from 'react'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; -import { CanvasWorkflowContainerElement } from './CanvasWorkflowContainerElement'; import { CanvasWorkflowInvocationNodeContextProvider } from './CanvasWorkflowInvocationContext'; +const containerViewModeSx: SystemStyleObject = { + gap: 2, + '&[data-self-layout="column"]': { + flexDir: 'column', + alignItems: 'stretch', + }, + '&[data-self-layout="row"]': { + flexDir: 'row', + alignItems: 'flex-start', + overflowX: 'auto', + overflowY: 'visible', + h: 'min-content', + flexShrink: 0, + }, + '&[data-parent-layout="column"]': { + w: 'full', + h: 'min-content', + }, + '&[data-parent-layout="row"]': { + flex: '1 1 0', + minW: 32, + }, +}; + +/** + * Container element for canvas workflow fields. + * This reads from the canvas workflow nodes slice. + */ +const CanvasWorkflowContainerElement = memo(({ id }: { id: string }) => { + const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice); + const el = nodesState.form.elements[id]; + const depth = useDepthContext(); + const containerCtx = useContainerContext(); + + if (!el || !isContainerElement(el)) { + return null; + } + + const { data } = el; + const { children, layout } = data; + + return ( + + + + {children.map((childId) => ( + + ))} + + + + ); +}); +CanvasWorkflowContainerElement.displayName = 'CanvasWorkflowContainerElement'; + /** * Renders a form element from canvas workflow nodes. * Recursively handles all element types. From 6299db7df0f446e8aae86c6ff6bcf82a087bdf70 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 29 Sep 2025 20:53:16 -0400 Subject: [PATCH 6/8] knip cleanup --- .../listeners/canvasWorkflowFieldChanged.ts | 10 -------- .../CanvasWorkflowElementContext.tsx | 2 +- .../CanvasWorkflowFormElementComponent.tsx | 2 +- .../CanvasWorkflowInvocationContext.tsx | 4 ---- .../components/StagingArea/shared.test.ts | 5 ++-- .../store/canvasWorkflowNodesSlice.ts | 3 --- .../store/canvasWorkflowSlice.ts | 23 +------------------ .../nodes/store/workflowLibraryModal.ts | 5 ---- 8 files changed, 6 insertions(+), 48 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts index 0ee4ad51e4f..cf600c9fb7b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts @@ -100,20 +100,10 @@ export const addCanvasWorkflowFieldChangedListener = (startListening: AppStartLi // Check if this node exists in canvas workflow nodes const canvasWorkflowNode = state.canvasWorkflowNodes.nodes.find((n: AnyNode) => n.id === nodeId); - const regularNode = state.nodes.present.nodes.find((n: AnyNode) => n.id === nodeId); - - console.log('[canvasWorkflowFieldChanged] Field changed:', { - nodeId, - hasCanvasNode: !!canvasWorkflowNode, - hasRegularNode: !!regularNode, - action: action.type, - payload: action.payload, - }); // If the node exists in canvas workflow, redirect the action // This ensures canvas workflow fields always update the canvas workflow nodes slice if (canvasWorkflowNode) { - console.log('[canvasWorkflowFieldChanged] Redirecting to canvas workflow nodes:', canvasAction.type); dispatch(canvasAction(action.payload)); } }, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx index e022d43bdb9..8ca3da420ae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx @@ -15,7 +15,7 @@ type CanvasWorkflowElementContextValue = { const CanvasWorkflowElementContext = createContext(null); -export const CanvasWorkflowElementProvider = memo(({ children }: PropsWithChildren) => { +const CanvasWorkflowElementProvider = memo(({ children }: PropsWithChildren) => { const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice); const value = useMemo( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx index 4ba7da78192..9320d60a6de 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFormElementComponent.tsx @@ -12,8 +12,8 @@ import { DividerElement } from 'features/nodes/components/sidePanel/builder/Divi import { HeadingElement } from 'features/nodes/components/sidePanel/builder/HeadingElement'; import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode'; import { TextElement } from 'features/nodes/components/sidePanel/builder/TextElement'; -import { CONTAINER_CLASS_NAME } from 'features/nodes/types/workflow'; import { + CONTAINER_CLASS_NAME, isContainerElement, isDividerElement, isHeadingElement, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx index e89fd3a6263..4d7c01f8ab1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowInvocationContext.tsx @@ -128,10 +128,6 @@ export const CanvasWorkflowInvocationNodeContextProvider = memo( createSelector(selectNodeInputsOrThrow, (inputs) => { const field = inputs[fieldName]; if (field === undefined) { - console.error( - `[CanvasWorkflowContext] Cannot find input field with name ${fieldName} in node ${nodeId}. Available fields:`, - Object.keys(inputs) - ); throw new Error(`Cannot find input field with name ${fieldName} in node ${nodeId}`); } return field; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts index f16b9023164..ae050491bc9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts @@ -64,7 +64,7 @@ describe('StagingAreaApi Utility Functions', () => { expect(getOutputImageName(queueItem)).toBe('test-output.png'); }); - it('should return null when no canvas output node found', () => { + it('should use fallback when no canvas output node found', () => { const queueItem = { item_id: 1, status: 'completed', @@ -93,7 +93,8 @@ describe('StagingAreaApi Utility Functions', () => { }, } as unknown as S['SessionQueueItem']; - expect(getOutputImageName(queueItem)).toBe(null); + // Fallback mechanism finds image in other nodes when no canvas_output node exists + expect(getOutputImageName(queueItem)).toBe('test-output.png'); }); it('should return null when output node has no results', () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts index b9757c69340..5279244daf4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts @@ -126,13 +126,11 @@ const slice = createSlice({ name: 'canvasWorkflowNodes', initialState: getInitialState(), reducers: { - canvasWorkflowNodesCleared: () => getInitialState(), // Field value mutations - these update the shadow nodes when fields are changed fieldValueReset: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zStatefulFieldValue); }, fieldStringValueChanged: (state, action: FieldValueAction) => { - console.log('[canvasWorkflowNodesSlice] fieldStringValueChanged:', action.payload); fieldValueReducer(state, action, zStringFieldValue); }, fieldStringCollectionValueChanged: (state, action: FieldValueAction) => { @@ -268,7 +266,6 @@ const slice = createSlice({ }); export const { - canvasWorkflowNodesCleared, fieldStringValueChanged, fieldIntegerValueChanged, fieldFloatValueChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts index 34bca0fb7b2..c2a964b4798 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts @@ -217,7 +217,7 @@ const slice = createSlice({ }, }); -export const { canvasWorkflowCleared, canvasWorkflowFieldValueChanged } = slice.actions; +export const { canvasWorkflowCleared } = slice.actions; export const canvasWorkflowSliceConfig: SliceConfig = { slice, @@ -242,24 +242,3 @@ export const canvasWorkflowSliceConfig: SliceConfig = { }; export const selectCanvasWorkflowSlice = (state: RootState) => state.canvasWorkflow; - -export const selectCanvasWorkflowStatus = (state: RootState) => selectCanvasWorkflowSlice(state).status; - -export const selectCanvasWorkflowError = (state: RootState) => selectCanvasWorkflowSlice(state).error; - -export const selectCanvasWorkflowSelection = (state: RootState) => selectCanvasWorkflowSlice(state).selectedWorkflowId; - -export const selectCanvasWorkflowData = (state: RootState) => selectCanvasWorkflowSlice(state).workflow; - -export const selectCanvasWorkflowNodeIds = (state: RootState) => ({ - inputNodeId: selectCanvasWorkflowSlice(state).inputNodeId, - outputNodeId: selectCanvasWorkflowSlice(state).outputNodeId, -}); - -export const selectIsCanvasWorkflowActive = (state: RootState) => { - const sliceState = selectCanvasWorkflowSlice(state); - return ( - Boolean(sliceState.workflow && sliceState.inputNodeId && sliceState.outputNodeId) && - (sliceState.status === 'succeeded' || sliceState.status === 'idle') - ); -}; diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowLibraryModal.ts b/invokeai/frontend/web/src/features/nodes/store/workflowLibraryModal.ts index caf557ac71a..b5462385fa3 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowLibraryModal.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowLibraryModal.ts @@ -40,8 +40,3 @@ export const useWorkflowLibraryModal = () => { }; export { $isWorkflowLibraryModalOpen }; - -export const openWorkflowLibraryModal = (context?: WorkflowLibraryContext) => { - $workflowLibraryContext.set(context ?? defaultContext); - $isWorkflowLibraryModalOpen.set(true); -}; From 8d78b1f1c87e3f8579a96dccc85bca75557c943a Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 6 Oct 2025 09:23:58 -0400 Subject: [PATCH 7/8] feat(ui): implement action router pattern for canvas workflow fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a Symbol-based action routing system to resolve field mutation conflicts between canvas workflows and nodes workflows. This allows both workflows to share the same field action creators while ensuring updates are routed to the correct slice. ## Changes ### Core Action Router System - Add `actionRouter.ts` with Symbol-based metadata injection and checking utilities - Add `workflowContext.ts` to provide workflow context and avoid circular dependencies - Enhance `useAppDispatch` to automatically inject workflow routing metadata based on context ### Canvas Workflow Integration - Add `CanvasWorkflowElementProvider` wrapping `WorkflowContext` for canvas tab - Refactor `canvasWorkflowNodesSlice` to use `extraReducers` instead of local reducers - Listen for `nodesSlice` field actions and filter by workflow routing metadata - Update persistence config to persist nodes/edges while excluding workflow metadata - Filter out input node fields from workflow form (populated by graph builder) ### Nodes Workflow Integration - Add `NodesWorkflowProvider` to mark nodes/workflow editor context - Wrap `NodeEditor` with provider in workflows tab layout - Add workflow routing check in `nodesSlice` field reducers ### DND Image Drop Fixes - Enhance `setNodeImageFieldImage` to check active tab when routing DND actions - Pass `getState` through DND handler to enable routing logic - Ensure drops are routed to correct slice based on active tab ### Validation Improvements - Refactor canvas workflow validation to use existing `getInvocationNodeErrors` utility - Skip validation for input node's image field (populated at runtime) - Remove custom validation logic in favor of shared validator ### Cleanup - Remove `canvasWorkflowFieldChanged` listener (replaced by action router) - Remove unused metadata code from canvas workflow graph builder - Fix TypeScript type assertions in workflow selection handlers ## Fixes - Field inputs now work correctly in canvas workflow panel - DND image drops now route to correct workflow based on active tab - Canvas workflow validation properly handles runtime-populated fields - Persistence config correctly saves field changes while excluding metadata 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../listeners/canvasWorkflowFieldChanged.ts | 112 ---------- invokeai/frontend/web/src/app/store/store.ts | 2 - .../frontend/web/src/app/store/storeHooks.ts | 25 ++- .../web/src/app/store/workflowContext.ts | 15 ++ .../CanvasWorkflowElementContext.tsx | 13 +- .../components/CanvasWorkflowFieldsPanel.tsx | 13 +- .../store/canvasWorkflowNodesSlice.ts | 209 +++++++++--------- .../store/canvasWorkflowSlice.ts | 145 ++++++++---- invokeai/frontend/web/src/features/dnd/dnd.ts | 4 +- .../web/src/features/imageActions/actions.ts | 33 ++- .../components/NodesWorkflowProvider.tsx | 15 ++ .../src/features/nodes/store/actionRouter.ts | 40 ++++ .../src/features/nodes/store/nodesSlice.ts | 8 +- .../generation/buildCanvasWorkflowGraph.ts | 15 -- .../components/CanvasWorkflowTrigger.tsx | 6 +- .../ParametersPanelCanvas.tsx | 6 +- .../ui/layouts/workflows-tab-auto-layout.tsx | 11 +- 17 files changed, 369 insertions(+), 303 deletions(-) delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts create mode 100644 invokeai/frontend/web/src/app/store/workflowContext.ts create mode 100644 invokeai/frontend/web/src/features/nodes/components/NodesWorkflowProvider.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/store/actionRouter.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts deleted file mode 100644 index cf600c9fb7b..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { ActionCreatorWithPayload } from '@reduxjs/toolkit'; -import type { AppStartListening, RootState } from 'app/store/store'; -import * as canvasWorkflowNodesActions from 'features/controlLayers/store/canvasWorkflowNodesSlice'; -import * as nodesActions from 'features/nodes/store/nodesSlice'; -import type { AnyNode } from 'features/nodes/types/invocation'; - -/** - * Listens for field value changes on nodes and redirects them to the canvas workflow nodes slice - * if the node belongs to a canvas workflow. - */ -export const addCanvasWorkflowFieldChangedListener = (startListening: AppStartListening) => { - // List of all field mutation actions from nodesSlice with their canvas workflow counterparts - const fieldMutationActionPairs: Array<{ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - nodesAction: ActionCreatorWithPayload; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - canvasAction: ActionCreatorWithPayload; - }> = [ - { - nodesAction: nodesActions.fieldStringValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldStringValueChanged, - }, - { - nodesAction: nodesActions.fieldIntegerValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldIntegerValueChanged, - }, - { - nodesAction: nodesActions.fieldFloatValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldFloatValueChanged, - }, - { - nodesAction: nodesActions.fieldBooleanValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldBooleanValueChanged, - }, - { - nodesAction: nodesActions.fieldModelIdentifierValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldModelIdentifierValueChanged, - }, - { - nodesAction: nodesActions.fieldEnumModelValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldEnumModelValueChanged, - }, - { - nodesAction: nodesActions.fieldSchedulerValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldSchedulerValueChanged, - }, - { - nodesAction: nodesActions.fieldBoardValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldBoardValueChanged, - }, - { - nodesAction: nodesActions.fieldImageValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldImageValueChanged, - }, - { - nodesAction: nodesActions.fieldColorValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldColorValueChanged, - }, - { - nodesAction: nodesActions.fieldImageCollectionValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldImageCollectionValueChanged, - }, - { - nodesAction: nodesActions.fieldStringCollectionValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldStringCollectionValueChanged, - }, - { - nodesAction: nodesActions.fieldIntegerCollectionValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldIntegerCollectionValueChanged, - }, - { - nodesAction: nodesActions.fieldFloatCollectionValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldFloatCollectionValueChanged, - }, - { - nodesAction: nodesActions.fieldFloatGeneratorValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldFloatGeneratorValueChanged, - }, - { - nodesAction: nodesActions.fieldIntegerGeneratorValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldIntegerGeneratorValueChanged, - }, - { - nodesAction: nodesActions.fieldStringGeneratorValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldStringGeneratorValueChanged, - }, - { - nodesAction: nodesActions.fieldImageGeneratorValueChanged, - canvasAction: canvasWorkflowNodesActions.fieldImageGeneratorValueChanged, - }, - { nodesAction: nodesActions.fieldValueReset, canvasAction: canvasWorkflowNodesActions.fieldValueReset }, - ]; - - for (const { nodesAction, canvasAction } of fieldMutationActionPairs) { - startListening({ - actionCreator: nodesAction, - effect: (action, { dispatch, getState }) => { - const state = getState() as RootState; - const { nodeId } = action.payload; - - // Check if this node exists in canvas workflow nodes - const canvasWorkflowNode = state.canvasWorkflowNodes.nodes.find((n: AnyNode) => n.id === nodeId); - - // If the node exists in canvas workflow, redirect the action - // This ensures canvas workflow fields always update the canvas workflow nodes slice - if (canvasWorkflowNode) { - dispatch(canvasAction(action.payload)); - } - }, - }); - } -}; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 9573634fd3f..4ddcac28cf8 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -57,7 +57,6 @@ import { actionSanitizer } from './middleware/devtools/actionSanitizer'; import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { addArchivedOrDeletedBoardListener } from './middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener'; -import { addCanvasWorkflowFieldChangedListener } from './middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged'; import { addCanvasWorkflowRehydratedListener } from './middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated'; import { addImageUploadedFulfilledListener } from './middleware/listenerMiddleware/listeners/imageUploaded'; @@ -299,5 +298,4 @@ addAdHocPostProcessingRequestedListener(startAppListening); addSetDefaultSettingsListener(startAppListening); // Canvas workflow fields -addCanvasWorkflowFieldChangedListener(startAppListening); addCanvasWorkflowRehydratedListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/storeHooks.ts b/invokeai/frontend/web/src/app/store/storeHooks.ts index cd0e41e55d1..46a1a72d473 100644 --- a/invokeai/frontend/web/src/app/store/storeHooks.ts +++ b/invokeai/frontend/web/src/app/store/storeHooks.ts @@ -1,8 +1,31 @@ import type { AppStore, AppThunkDispatch, RootState } from 'app/store/store'; +import { useIsCanvasWorkflow } from 'app/store/workflowContext'; +import { injectCanvasWorkflowKey, injectNodesWorkflowKey } from 'features/nodes/store/actionRouter'; +import { useCallback } from 'react'; import type { TypedUseSelectorHook } from 'react-redux'; import { useDispatch, useSelector, useStore } from 'react-redux'; // Use throughout your app instead of plain `useDispatch` and `useSelector` -export const useAppDispatch = () => useDispatch(); +export const useAppDispatch = (): AppThunkDispatch => { + const isCanvasWorkflow = useIsCanvasWorkflow(); + const dispatch = useDispatch(); + + return useCallback( + ((action: Parameters[0]) => { + // Inject workflow routing metadata into actions + if (typeof action === 'object' && action !== null && 'type' in action) { + if (isCanvasWorkflow) { + injectCanvasWorkflowKey(action); + } else { + injectNodesWorkflowKey(action); + } + } + + return dispatch(action); + }) as AppThunkDispatch, + [dispatch, isCanvasWorkflow] + ); +}; + export const useAppSelector: TypedUseSelectorHook = useSelector; export const useAppStore = () => useStore.withTypes()(); diff --git a/invokeai/frontend/web/src/app/store/workflowContext.ts b/invokeai/frontend/web/src/app/store/workflowContext.ts new file mode 100644 index 00000000000..a59c1fa55a5 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/workflowContext.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; + +/** + * Context to track whether we're in a canvas workflow or nodes workflow. + * This is used by the useAppDispatch hook to inject the appropriate action routing metadata. + */ +export const WorkflowContext = createContext<{ isCanvasWorkflow: boolean } | null>(null); + +/** + * Hook to check if we're in a canvas workflow context. + */ +export const useIsCanvasWorkflow = (): boolean => { + const context = useContext(WorkflowContext); + return context?.isCanvasWorkflow ?? false; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx index 8ca3da420ae..cf81af7eeac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx @@ -1,4 +1,5 @@ import { useAppSelector } from 'app/store/storeHooks'; +import { WorkflowContext } from 'app/store/workflowContext'; import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; import type { FormElement } from 'features/nodes/types/workflow'; import type { PropsWithChildren } from 'react'; @@ -15,17 +16,23 @@ type CanvasWorkflowElementContextValue = { const CanvasWorkflowElementContext = createContext(null); -const CanvasWorkflowElementProvider = memo(({ children }: PropsWithChildren) => { +export const CanvasWorkflowElementProvider = memo(({ children }: PropsWithChildren) => { const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice); - const value = useMemo( + const elementValue = useMemo( () => ({ getElement: (id: string) => nodesState.form.elements[id], }), [nodesState.form.elements] ); - return {children}; + const workflowValue = useMemo(() => ({ isCanvasWorkflow: true }), []); + + return ( + + {children} + + ); }); CanvasWorkflowElementProvider.displayName = 'CanvasWorkflowElementProvider'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx index 5145f8bc1d4..36289c6b267 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx @@ -1,5 +1,6 @@ import { Flex, Text } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasWorkflowElementProvider } from 'features/controlLayers/components/CanvasWorkflowElementContext'; import { CanvasWorkflowModeProvider } from 'features/controlLayers/components/CanvasWorkflowModeContext'; import { CanvasWorkflowRootContainer } from 'features/controlLayers/components/CanvasWorkflowRootContainer'; import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice'; @@ -32,11 +33,13 @@ export const CanvasWorkflowFieldsPanel = memo(() => { } return ( - - - - - + + + + + + + ); }); CanvasWorkflowFieldsPanel.displayName = 'CanvasWorkflowFieldsPanel'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts index 5279244daf4..ae0ddb8a67c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowNodesSlice.ts @@ -1,33 +1,15 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; +import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; import { deepClone } from 'common/util/deepClone'; +import { isCanvasWorkflowAction } from 'features/nodes/store/actionRouter'; import { getFormFieldInitialValues } from 'features/nodes/store/nodesSlice'; +import * as nodesSliceActions from 'features/nodes/store/nodesSlice'; import type { NodesState } from 'features/nodes/store/types'; import { zNodesState } from 'features/nodes/store/types'; import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; -import type { - BoardFieldValue, - BooleanFieldValue, - ColorFieldValue, - EnumFieldValue, - FloatFieldCollectionValue, - FloatFieldValue, - FloatGeneratorFieldValue, - ImageFieldCollectionValue, - ImageFieldValue, - ImageGeneratorFieldValue, - IntegerFieldCollectionValue, - IntegerFieldValue, - IntegerGeneratorFieldValue, - ModelIdentifierFieldValue, - SchedulerFieldValue, - StatefulFieldValue, - StringFieldCollectionValue, - StringFieldValue, - StringGeneratorFieldValue, -} from 'features/nodes/types/field'; +import type { StatefulFieldValue } from 'features/nodes/types/field'; import { zBoardFieldValue, zBooleanFieldValue, @@ -125,67 +107,9 @@ const fieldValueReducer = ( const slice = createSlice({ name: 'canvasWorkflowNodes', initialState: getInitialState(), - reducers: { - // Field value mutations - these update the shadow nodes when fields are changed - fieldValueReset: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zStatefulFieldValue); - }, - fieldStringValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zStringFieldValue); - }, - fieldStringCollectionValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zStringFieldCollectionValue); - }, - fieldIntegerValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zIntegerFieldValue); - }, - fieldFloatValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zFloatFieldValue); - }, - fieldFloatCollectionValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zFloatFieldCollectionValue); - }, - fieldIntegerCollectionValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zIntegerFieldCollectionValue); - }, - fieldBooleanValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zBooleanFieldValue); - }, - fieldBoardValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zBoardFieldValue); - }, - fieldImageValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zImageFieldValue); - }, - fieldImageCollectionValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zImageFieldCollectionValue); - }, - fieldColorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zColorFieldValue); - }, - fieldModelIdentifierValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zModelIdentifierFieldValue); - }, - fieldEnumModelValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zEnumFieldValue); - }, - fieldSchedulerValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zSchedulerFieldValue); - }, - fieldFloatGeneratorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zFloatGeneratorFieldValue); - }, - fieldIntegerGeneratorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zIntegerGeneratorFieldValue); - }, - fieldStringGeneratorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zStringGeneratorFieldValue); - }, - fieldImageGeneratorValueChanged: (state, action: FieldValueAction) => { - fieldValueReducer(state, action, zImageGeneratorFieldValue); - }, - }, + reducers: {}, extraReducers(builder) { + // addCase must come before addMatcher builder.addCase(selectCanvasWorkflow.fulfilled, (state, action) => { const { workflow, inputNodeId } = action.payload; const { nodes, edges, ...workflowExtra } = workflow; @@ -262,30 +186,103 @@ const slice = createSlice({ builder.addCase(selectCanvasWorkflow.rejected, () => { return getInitialState(); }); + + // Listen for field mutation actions from nodesSlice and handle them if they're for canvas workflow + // addMatcher must come after addCase + builder.addMatcher( + isAnyOf( + nodesSliceActions.fieldValueReset, + nodesSliceActions.fieldStringValueChanged, + nodesSliceActions.fieldStringCollectionValueChanged, + nodesSliceActions.fieldIntegerValueChanged, + nodesSliceActions.fieldFloatValueChanged, + nodesSliceActions.fieldFloatCollectionValueChanged, + nodesSliceActions.fieldIntegerCollectionValueChanged, + nodesSliceActions.fieldBooleanValueChanged, + nodesSliceActions.fieldBoardValueChanged, + nodesSliceActions.fieldImageValueChanged, + nodesSliceActions.fieldImageCollectionValueChanged, + nodesSliceActions.fieldColorValueChanged, + nodesSliceActions.fieldModelIdentifierValueChanged, + nodesSliceActions.fieldEnumModelValueChanged, + nodesSliceActions.fieldSchedulerValueChanged, + nodesSliceActions.fieldFloatGeneratorValueChanged, + nodesSliceActions.fieldIntegerGeneratorValueChanged, + nodesSliceActions.fieldStringGeneratorValueChanged, + nodesSliceActions.fieldImageGeneratorValueChanged + ), + ( + state, + action: PayloadAction<{ nodeId: string; fieldName: string; value: StatefulFieldValue }> & UnknownAction + ) => { + // Only handle if this is a canvas workflow action + if (!isCanvasWorkflowAction(action)) { + return; + } + + // Determine which schema to use based on action type + const actionType = action.type; + let schema; + if (actionType.includes('String')) { + schema = zStringFieldValue; + } else if (actionType.includes('StringCollection')) { + schema = zStringFieldCollectionValue; + } else if ( + actionType.includes('Integer') && + !actionType.includes('Generator') && + !actionType.includes('Collection') + ) { + schema = zIntegerFieldValue; + } else if (actionType.includes('IntegerCollection')) { + schema = zIntegerFieldCollectionValue; + } else if ( + actionType.includes('Float') && + !actionType.includes('Generator') && + !actionType.includes('Collection') + ) { + schema = zFloatFieldValue; + } else if (actionType.includes('FloatCollection')) { + schema = zFloatFieldCollectionValue; + } else if (actionType.includes('Boolean')) { + schema = zBooleanFieldValue; + } else if (actionType.includes('Board')) { + schema = zBoardFieldValue; + } else if ( + actionType.includes('Image') && + !actionType.includes('Generator') && + !actionType.includes('Collection') + ) { + schema = zImageFieldValue; + } else if (actionType.includes('ImageCollection')) { + schema = zImageFieldCollectionValue; + } else if (actionType.includes('Color')) { + schema = zColorFieldValue; + } else if (actionType.includes('ModelIdentifier')) { + schema = zModelIdentifierFieldValue; + } else if (actionType.includes('Enum')) { + schema = zEnumFieldValue; + } else if (actionType.includes('Scheduler')) { + schema = zSchedulerFieldValue; + } else if (actionType.includes('FloatGenerator')) { + schema = zFloatGeneratorFieldValue; + } else if (actionType.includes('IntegerGenerator')) { + schema = zIntegerGeneratorFieldValue; + } else if (actionType.includes('StringGenerator')) { + schema = zStringGeneratorFieldValue; + } else if (actionType.includes('ImageGenerator')) { + schema = zImageGeneratorFieldValue; + } else { + schema = zStatefulFieldValue; + } + + fieldValueReducer(state, action, schema); + } + ); }, }); -export const { - fieldStringValueChanged, - fieldIntegerValueChanged, - fieldFloatValueChanged, - fieldBooleanValueChanged, - fieldModelIdentifierValueChanged, - fieldEnumModelValueChanged, - fieldSchedulerValueChanged, - fieldBoardValueChanged, - fieldImageValueChanged, - fieldColorValueChanged, - fieldImageCollectionValueChanged, - fieldStringCollectionValueChanged, - fieldIntegerCollectionValueChanged, - fieldFloatCollectionValueChanged, - fieldFloatGeneratorValueChanged, - fieldIntegerGeneratorValueChanged, - fieldStringGeneratorValueChanged, - fieldImageGeneratorValueChanged, - fieldValueReset, -} = slice.actions; +// No need to export these actions anymore - they are handled via action routing +// export const { ... } = slice.actions; export const canvasWorkflowNodesSliceConfig: SliceConfig = { slice, @@ -293,7 +290,7 @@ export const canvasWorkflowNodesSliceConfig: SliceConfig = { getInitialState, persistConfig: { migrate: (state) => state as NodesState, - // We don't persist this slice - it's derived from canvasWorkflow + // Only persist nodes and edges - field changes should persist, but not other workflow metadata persistDenylist: [ '_version', 'formFieldInitialValues', @@ -307,8 +304,6 @@ export const canvasWorkflowNodesSliceConfig: SliceConfig = { 'exposedFields', 'meta', 'form', - 'nodes', - 'edges', 'id', ], }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts index c2a964b4798..d29486bcf69 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts @@ -6,11 +6,16 @@ import type { SliceConfig } from 'app/store/types'; import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; import { $templates, getFormFieldInitialValues } from 'features/nodes/store/nodesSlice'; -import type { Templates } from 'features/nodes/store/types'; +import type { NodesState, Templates } from 'features/nodes/store/types'; +import { getInvocationNodeErrors } from 'features/nodes/store/util/fieldValidators'; import type { StatefulFieldValue } from 'features/nodes/types/field'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; -import { zWorkflowV3 } from 'features/nodes/types/workflow'; +import { isWorkflowInvocationNode, zWorkflowV3 } from 'features/nodes/types/workflow'; +import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow'; import { serializeError } from 'serialize-error'; +import { boardsApi } from 'services/api/endpoints/boards'; +import { imagesApi } from 'services/api/endpoints/images'; +import { modelsApi } from 'services/api/endpoints/models'; import { workflowsApi } from 'services/api/endpoints/workflows'; import { z } from 'zod'; @@ -46,10 +51,29 @@ type ValidateResult = { const INPUT_TAG = 'canvas-workflow-input'; const OUTPUT_TAG = 'canvas-workflow-output'; -const validateCanvasWorkflow = (workflow: WorkflowV3, templates: Templates): ValidateResult => { - const invocationNodes = workflow.nodes.filter( - (node): node is WorkflowV3['nodes'][number] => node.type === 'invocation' - ); +const validateCanvasWorkflow = async ( + workflow: unknown, + templates: Templates, + checkImageAccess: (name: string) => Promise, + checkBoardAccess: (id: string) => Promise, + checkModelAccess: (key: string) => Promise +): Promise<{ workflow: WorkflowV3; inputNodeId: string; outputNodeId: string }> => { + // First, use the robust validateWorkflow utility to handle parsing, migration, and general validation + const { workflow: validatedWorkflow, warnings } = await validateWorkflow({ + workflow, + templates, + checkImageAccess, + checkBoardAccess, + checkModelAccess, + }); + + // Log any warnings from validation + if (warnings.length > 0) { + log.warn({ warnings }, 'Canvas workflow validation warnings'); + } + + // Now perform canvas-specific validation + const invocationNodes = validatedWorkflow.nodes.filter(isWorkflowInvocationNode); const inputNodes = invocationNodes.filter((node) => { const template = templates[node.data.type]; @@ -100,56 +124,37 @@ const validateCanvasWorkflow = (workflow: WorkflowV3, templates: Templates): Val throw new Error('Canvas output node must accept an image input field named "image".'); } - // Validate that all nodes have valid templates - for (const node of invocationNodes) { - const template = templates[node.data.type]; - if (!template) { - throw new Error( - `Node "${node.data.label || node.id}" uses invocation type "${node.data.type}" which is not available. This workflow may have been created with a different version of InvokeAI.` - ); - } - } + // Validate that required fields without connections have values using the existing field validator + // Create a temporary nodes state for validation - only nodes and edges are used by the validator + const tempNodesState = { + nodes: invocationNodes, + edges: validatedWorkflow.edges, + } as NodesState; - // Validate that required fields without connections have values - const edges = workflow.edges.filter((edge) => edge.type === 'default'); for (const node of invocationNodes) { - if (node.type !== 'invocation') { - continue; - } - - const template = templates[node.data.type]; - if (!template) { - continue; // Already validated above - } + const errors = getInvocationNodeErrors(node.id, templates, tempNodesState); - for (const [fieldName, fieldTemplate] of Object.entries(template.inputs)) { - // Skip if field is connected (will get value from connection) - const isConnected = edges.some((edge) => edge.target === node.id && edge.targetHandle === fieldName); - if (isConnected) { - continue; + // Filter out "no image input" errors for the input node - it will be populated by the graph builder + const relevantErrors = errors.filter((error) => { + if (error.type === 'field-error' && error.nodeId === inputNode.id && error.fieldName === 'image') { + return false; // Skip validation for the input node's image field } + return true; + }); - // Check if field is required - if (fieldTemplate.required) { - const fieldInstance = node.data.inputs[fieldName]; - if (!fieldInstance) { - throw new Error( - `Node "${node.data.label || node.id}" is missing required field "${fieldTemplate.title || fieldName}".` - ); - } - - // Check if field has a value (not null/undefined/empty) - const value = fieldInstance.value; - if (value === null || value === undefined || value === '') { - throw new Error( - `Node "${node.data.label || node.id}" has required field "${fieldTemplate.title || fieldName}" with no value. Please provide a value or connect this field.` - ); + if (relevantErrors.length > 0) { + const firstError = relevantErrors[0]; + if (firstError) { + if (firstError.type === 'field-error') { + throw new Error(`${firstError.prefix}: ${firstError.issue}`); + } else { + throw new Error(`Node "${node.id}": ${firstError.issue}`); } } } } - return { inputNodeId: inputNode.id, outputNodeId: outputNode.id }; + return { workflow: validatedWorkflow, inputNodeId: inputNode.id, outputNodeId: outputNode.id }; }; export const selectCanvasWorkflow = createAsyncThunk< @@ -166,12 +171,56 @@ export const selectCanvasWorkflow = createAsyncThunk< const request = dispatch(workflowsApi.endpoints.getWorkflow.initiate(workflowId, { subscribe: false })); try { const result = await request.unwrap(); - const workflow = zWorkflowV3.parse(deepClone(result.workflow)); const templates = $templates.get(); if (!Object.keys(templates).length) { throw new Error('Invocation templates are not yet available.'); } - const { inputNodeId, outputNodeId } = validateCanvasWorkflow(workflow, templates); + + // Define access check functions for workflow validation + const checkImageAccess = async (name: string): Promise => { + const imageRequest = dispatch(imagesApi.endpoints.getImageDTO.initiate(name)); + try { + await imageRequest.unwrap(); + return true; + } catch { + return false; + } finally { + imageRequest.unsubscribe(); + } + }; + + const checkBoardAccess = async (id: string): Promise => { + const boardsRequest = dispatch(boardsApi.endpoints.listAllBoards.initiate({})); + try { + const boards = await boardsRequest.unwrap(); + return boards.some((board) => board.board_id === id); + } catch { + return false; + } finally { + boardsRequest.unsubscribe(); + } + }; + + const checkModelAccess = async (key: string): Promise => { + const modelRequest = dispatch(modelsApi.endpoints.getModelConfig.initiate(key)); + try { + await modelRequest.unwrap(); + return true; + } catch { + return false; + } finally { + modelRequest.unsubscribe(); + } + }; + + // Use validateWorkflow to parse, migrate, and validate the workflow + const { workflow, inputNodeId, outputNodeId } = await validateCanvasWorkflow( + result.workflow, + templates, + checkImageAccess, + checkBoardAccess, + checkModelAccess + ); const fieldValues = getFormFieldInitialValues(workflow.form, workflow.nodes); return { workflowId: result.workflow_id, workflow, inputNodeId, outputNodeId, fieldValues }; } catch (error) { diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts index 0aef104869f..e8594c8123a 100644 --- a/invokeai/frontend/web/src/features/dnd/dnd.ts +++ b/invokeai/frontend/web/src/features/dnd/dnd.ts @@ -288,10 +288,10 @@ export const setNodeImageFieldImageDndTarget: DndTarget { + handler: ({ sourceData, targetData, dispatch, getState }) => { const { imageDTO } = sourceData.payload; const { fieldIdentifier } = targetData.payload; - setNodeImageFieldImage({ fieldIdentifier, imageDTO, dispatch }); + setNodeImageFieldImage({ fieldIdentifier, imageDTO, dispatch, getState }); }, }; //#endregion diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index 14d27e900c1..801b99eeeeb 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -35,6 +35,7 @@ import { import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; import type { BoardId } from 'features/gallery/store/types'; +import { injectCanvasWorkflowKey, injectNodesWorkflowKey } from 'features/nodes/store/actionRouter'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import type { FieldIdentifier } from 'features/nodes/types/field'; import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; @@ -71,9 +72,37 @@ export const setNodeImageFieldImage = (arg: { imageDTO: ImageDTO; fieldIdentifier: FieldIdentifier; dispatch: AppDispatch; + getState?: AppGetState; }) => { - const { imageDTO, fieldIdentifier, dispatch } = arg; - dispatch(fieldImageValueChanged({ ...fieldIdentifier, value: imageDTO })); + const { imageDTO, fieldIdentifier, dispatch, getState } = arg; + + // Create the action + const action = fieldImageValueChanged({ ...fieldIdentifier, value: imageDTO }); + + // Only inject workflow routing metadata if getState is available (DND path) + // Otherwise, the action will be handled by useAppDispatch (UI component path) + if (getState) { + // Check if this node belongs to canvas workflow or regular nodes workflow + // We need to check the active tab to determine which workflow is currently being used + const state = getState(); + const activeTab = state.ui.activeTab; + const canvasWorkflowNode = state.canvasWorkflowNodes.nodes.find((n) => n.id === fieldIdentifier.nodeId); + + // Determine which workflow to route to based on active tab and node presence + // Priority: active tab determines the workflow, as the same workflow might be loaded in both slices + const shouldUseCanvasWorkflow = activeTab === 'canvas' && canvasWorkflowNode; + + // Inject the appropriate workflow routing metadata + if (shouldUseCanvasWorkflow) { + // This is a canvas workflow node - inject canvas workflow key + injectCanvasWorkflowKey(action); + } else { + // This is a regular nodes workflow node - inject nodes workflow key + injectNodesWorkflowKey(action); + } + } + + dispatch(action); }; export const setComparisonImage = (arg: { image_name: string; dispatch: AppDispatch }) => { diff --git a/invokeai/frontend/web/src/features/nodes/components/NodesWorkflowProvider.tsx b/invokeai/frontend/web/src/features/nodes/components/NodesWorkflowProvider.tsx new file mode 100644 index 00000000000..350190eb296 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/NodesWorkflowProvider.tsx @@ -0,0 +1,15 @@ +import { WorkflowContext } from 'app/store/workflowContext'; +import type { PropsWithChildren } from 'react'; +import { memo, useMemo } from 'react'; + +/** + * Provider that marks the nodes/workflow editor context. + * This ensures field actions are routed to the nodes slice, not the canvas workflow slice. + */ +export const NodesWorkflowProvider = memo(({ children }: PropsWithChildren) => { + const value = useMemo(() => ({ isCanvasWorkflow: false }), []); + + return {children}; +}); + +NodesWorkflowProvider.displayName = 'NodesWorkflowProvider'; diff --git a/invokeai/frontend/web/src/features/nodes/store/actionRouter.ts b/invokeai/frontend/web/src/features/nodes/store/actionRouter.ts new file mode 100644 index 00000000000..723b486a476 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/actionRouter.ts @@ -0,0 +1,40 @@ +import type { UnknownAction } from '@reduxjs/toolkit'; +import { isPlainObject } from '@reduxjs/toolkit'; + +// Use symbols to prevent collisions with other metadata +const CANVAS_WORKFLOW_KEY = Symbol('CANVAS_WORKFLOW_KEY'); +const NODES_WORKFLOW_KEY = Symbol('NODES_WORKFLOW_KEY'); + +type ActionWithMeta = UnknownAction & { meta?: Record }; + +/** + * Injects metadata into an action to mark it as belonging to the canvas workflow. + */ +export const injectCanvasWorkflowKey = (action: UnknownAction): void => { + const actionWithMeta = action as ActionWithMeta; + Object.assign(action, { meta: { ...(actionWithMeta.meta || {}), [CANVAS_WORKFLOW_KEY]: true } }); +}; + +/** + * Injects metadata into an action to mark it as belonging to the nodes workflow. + */ +export const injectNodesWorkflowKey = (action: UnknownAction): void => { + const actionWithMeta = action as ActionWithMeta; + Object.assign(action, { meta: { ...(actionWithMeta.meta || {}), [NODES_WORKFLOW_KEY]: true } }); +}; + +/** + * Type guard to check if an action is marked as belonging to the canvas workflow. + */ +export const isCanvasWorkflowAction = (action: UnknownAction): boolean => { + const actionWithMeta = action as ActionWithMeta; + return isPlainObject(actionWithMeta.meta) && actionWithMeta.meta?.[CANVAS_WORKFLOW_KEY] === true; +}; + +/** + * Type guard to check if an action is marked as belonging to the nodes workflow. + */ +export const isNodesWorkflowAction = (action: UnknownAction): boolean => { + const actionWithMeta = action as ActionWithMeta; + return isPlainObject(actionWithMeta.meta) && actionWithMeta.meta?.[NODES_WORKFLOW_KEY] === true; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 20c27d2cd6e..01fb0daea95 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -19,6 +19,7 @@ import { removeElement, reparentElement, } from 'features/nodes/components/sidePanel/builder/form-manipulation'; +import { isNodesWorkflowAction } from 'features/nodes/store/actionRouter'; import { type NodesState, zNodesState } from 'features/nodes/store/types'; import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; import type { @@ -152,9 +153,14 @@ const getField = (nodeId: string, fieldName: string, state: NodesState) => { const fieldValueReducer = ( state: NodesState, - action: FieldValueAction, + action: FieldValueAction & UnknownAction, schema: z.ZodType ) => { + // Only handle actions marked for nodes workflow + if (!isNodesWorkflowAction(action)) { + return; + } + const { nodeId, fieldName, value } = action.payload; const field = getField(nodeId, fieldName, state); if (!field) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCanvasWorkflowGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCanvasWorkflowGraph.ts index 350e82cfc0e..76ef3e184a2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCanvasWorkflowGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCanvasWorkflowGraph.ts @@ -3,7 +3,6 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasWorkflowState } from 'features/controlLayers/store/canvasWorkflowSlice'; -import { selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; import { $templates } from 'features/nodes/store/nodesSlice'; import type { Templates } from 'features/nodes/store/types'; @@ -17,7 +16,6 @@ import { selectCanvasOutputFields, selectPresetModifiedPrompts, } from 'features/nodes/util/graph/graphBuilderUtils'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { AnyInvocation, AnyInvocationInputField, AnyInvocationOutputField } from 'services/api/types'; import { assert } from 'tsafe'; @@ -186,19 +184,6 @@ export const buildCanvasWorkflowGraph = async ({ state, manager, workflowState } assert(outputNode, 'Canvas workflow output node missing from graph'); g.updateNode(outputNode, selectCanvasOutputFields(state)); - if (selectActiveTab(state) === 'canvas') { - g.upsertMetadata(selectCanvasMetadata(state)); - } - - g.setMetadataReceivingNode(outputNode); - g.upsertMetadata({ - positive_prompt: prompts.positive, - negative_prompt: prompts.negative ?? '', - }); - g.addEdgeToMetadata(seed, 'value', 'seed'); - g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt'); - g.updateNode(positivePrompt, { value: prompts.positive }); - return { g, seed, diff --git a/invokeai/frontend/web/src/features/queue/components/CanvasWorkflowTrigger.tsx b/invokeai/frontend/web/src/features/queue/components/CanvasWorkflowTrigger.tsx index 7c57adb729d..bf77fd87790 100644 --- a/invokeai/frontend/web/src/features/queue/components/CanvasWorkflowTrigger.tsx +++ b/invokeai/frontend/web/src/features/queue/components/CanvasWorkflowTrigger.tsx @@ -24,12 +24,14 @@ export const CanvasWorkflowTrigger = memo(() => { return; } isProcessingRef.current = true; - const result = await dispatch(selectCanvasWorkflow(workflowId)); + const result = (await dispatch(selectCanvasWorkflow(workflowId))) as + | ReturnType + | ReturnType; if (selectCanvasWorkflow.fulfilled.match(result)) { toast({ status: 'success', title: t('controlLayers.canvasWorkflowSelected') }); workflowLibraryModal.close(); } else { - const message = result.payload ?? result.error.message ?? t('common.error'); + const message = result.payload ?? result.error?.message ?? t('common.error'); toast({ status: 'error', title: `${t('common.error')}: ${message}` }); } isProcessingRef.current = false; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx index 4a3ec2f7156..de90d94cfd9 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx @@ -50,12 +50,14 @@ export const ParametersPanelCanvas = memo(() => { return; } isProcessingRef.current = true; - const result = await dispatch(selectCanvasWorkflow(workflowId)); + const result = (await dispatch(selectCanvasWorkflow(workflowId))) as + | ReturnType + | ReturnType; if (selectCanvasWorkflow.fulfilled.match(result)) { toast({ status: 'success', title: t('controlLayers.canvasWorkflowSelected') }); workflowLibraryModal.close(); } else { - const message = result.payload ?? result.error.message ?? t('common.error'); + const message = result.payload ?? result.error?.message ?? t('common.error'); toast({ status: 'error', title: message }); } isProcessingRef.current = false; diff --git a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx index 06ae423e447..41bc05b4c48 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx @@ -4,6 +4,7 @@ import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent' import { GalleryPanel } from 'features/gallery/components/Gallery'; import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel'; import NodeEditor from 'features/nodes/components/NodeEditor'; +import { NodesWorkflowProvider } from 'features/nodes/components/NodesWorkflowProvider'; import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel'; import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons'; import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons'; @@ -53,9 +54,17 @@ const tabComponents = { [DOCKVIEW_TAB_LAUNCHPAD_ID]: DockviewTabLaunchpad, }; +// Wrap NodeEditor with NodesWorkflowProvider to ensure action routing works correctly +const NodeEditorWithProvider = memo(() => ( + + + +)); +NodeEditorWithProvider.displayName = 'NodeEditorWithProvider'; + const mainPanelComponents: AutoLayoutDockviewComponents = { [LAUNCHPAD_PANEL_ID]: withPanelContainer(WorkflowsLaunchpadPanel), - [WORKSPACE_PANEL_ID]: withPanelContainer(NodeEditor), + [WORKSPACE_PANEL_ID]: withPanelContainer(NodeEditorWithProvider), [VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel), }; From 71a66192acfd97b7be3140f51671d577953c7b51 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 6 Oct 2025 10:39:49 -0400 Subject: [PATCH 8/8] lint --- .../src/features/controlLayers/store/canvasWorkflowSlice.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts index d29486bcf69..5c62da245c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowSlice.ts @@ -3,7 +3,6 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; -import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; import { $templates, getFormFieldInitialValues } from 'features/nodes/store/nodesSlice'; import type { NodesState, Templates } from 'features/nodes/store/types'; @@ -43,11 +42,6 @@ const getInitialState = (): CanvasWorkflowState => ({ error: null, }); -type ValidateResult = { - inputNodeId: string; - outputNodeId: string; -}; - const INPUT_TAG = 'canvas-workflow-input'; const OUTPUT_TAG = 'canvas-workflow-output';