From b24c2cb535f9288f5f9b5caa87b6dbff02d9b85d Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 22 Sep 2025 12:38:57 -0400 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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/6] 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/6] 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); -};