diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
index 6bb27c0eaf..29df0bf542 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -35,7 +35,7 @@ import { addInvocationErrorEventListener } from 'app/store/middleware/listenerMi
import { addInvocationStartedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted';
import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall';
import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad';
-import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged';
+import { addSocketQueueEventsListeners } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents';
import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested';
import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested';
import type { AppDispatch, RootState } from 'app/store/store';
@@ -99,7 +99,7 @@ addSocketConnectedEventListener(startAppListening);
addSocketDisconnectedEventListener(startAppListening);
addModelLoadEventListener(startAppListening);
addModelInstallEventListener(startAppListening);
-addSocketQueueItemStatusChangedEventListener(startAppListening);
+addSocketQueueEventsListeners(startAppListening);
addBulkDownloadListeners(startAppListening);
// Boards
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
index 9e988b8bd4..4aa6020d0a 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
@@ -1,10 +1,11 @@
+import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
layerAdded,
layerImageAdded,
+ stagingAreaCanceledStaging,
stagingAreaImageAccepted,
- stagingAreaReset,
} from 'features/controlLayers/store/canvasV2Slice';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
@@ -13,25 +14,15 @@ import { assert } from 'tsafe';
export const addStagingListeners = (startAppListening: AppStartListening) => {
startAppListening({
- actionCreator: stagingAreaReset,
- effect: async (_, { dispatch, getState }) => {
+ matcher: isAnyOf(stagingAreaCanceledStaging, stagingAreaImageAccepted),
+ effect: async (_, { dispatch }) => {
const log = logger('canvas');
- const stagingArea = getState().canvasV2.stagingArea;
-
- if (!stagingArea) {
- // Should not happen
- return;
- }
-
- if (stagingArea.batchIds.length === 0) {
- return;
- }
try {
const req = dispatch(
- queueApi.endpoints.cancelByBatchIds.initiate(
- { batch_ids: stagingArea.batchIds },
- { fixedCacheKey: 'cancelByBatchIds' }
+ queueApi.endpoints.cancelByBatchOrigin.initiate(
+ { origin: 'canvas' },
+ { fixedCacheKey: 'cancelByBatchOrigin' }
)
);
const { canceled } = await req.unwrap();
@@ -59,7 +50,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
actionCreator: stagingAreaImageAccepted,
effect: async (action, api) => {
const { imageDTO } = action.payload;
- const { layers, stagingArea, selectedEntityIdentifier } = api.getState().canvasV2;
+ const { layers, selectedEntityIdentifier, bbox } = api.getState().canvasV2;
let layer = layers.entities.find((layer) => layer.id === selectedEntityIdentifier?.id);
if (!layer) {
@@ -73,13 +64,11 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
}
assert(layer, 'No layer found to stage image');
- assert(stagingArea, 'Staging should be defined');
- const { x, y } = stagingArea.bbox;
+ const { x, y } = bbox;
const { id } = layer;
api.dispatch(layerImageAdded({ id, imageDTO, pos: { x, y } }));
- api.dispatch(stagingAreaReset());
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
index 21f0fe1e53..d15a48da48 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
@@ -1,12 +1,7 @@
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { getNodeManager } from 'features/controlLayers/konva/nodeManager';
-import {
- stagingAreaBatchIdAdded,
- stagingAreaInitialized,
- stagingAreaReset,
-} from 'features/controlLayers/store/canvasV2Slice';
-import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
+import { stagingAreaCanceledStaging, stagingAreaStartedStaging } from 'features/controlLayers/store/canvasV2Slice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
@@ -19,20 +14,13 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
enqueueRequested.match(action) && action.payload.tabName === 'generation',
effect: async (action, { getState, dispatch }) => {
const state = getState();
- const { shouldShowProgressInViewer } = state.ui;
const model = state.canvasV2.params.model;
const { prepend } = action.payload;
- let didInitializeStagingArea = false;
-
- if (state.canvasV2.stagingArea === null) {
- dispatch(
- stagingAreaInitialized({
- batchIds: [],
- bbox: state.canvasV2.bbox,
- })
- );
- didInitializeStagingArea = true;
+ let didStartStaging = false;
+ if (!state.canvasV2.stagingArea.isStaging) {
+ dispatch(stagingAreaStartedStaging());
+ didStartStaging = true;
}
try {
@@ -57,23 +45,11 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
fixedCacheKey: 'enqueueBatch',
})
);
-
- const enqueueResult = await req.unwrap();
req.reset();
-
- if (shouldShowProgressInViewer) {
- dispatch(isImageViewerOpenChanged(true));
- }
- // TODO(psyche): update the backend schema, this is always provided
- const batchId = enqueueResult.batch.batch_id;
- assert(batchId, 'No batch ID found in enqueue result');
- dispatch(stagingAreaBatchIdAdded({ batchId }));
+ await req.unwrap();
} catch {
- if (didInitializeStagingArea) {
- // We initialized the staging area in this listener, and there was a problem at some point. This means
- // there only possible canvas batch id is the one we just added, so we can reset the staging area without
- // losing any data.
- dispatch(stagingAreaReset());
+ if (didStartStaging && getState().canvasV2.stagingArea.isStaging) {
+ dispatch(stagingAreaCanceledStaging());
}
}
},
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts
index 3f5e1333b9..6d614c5f40 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts
@@ -30,6 +30,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
graph,
workflow: builtWorkflow,
runs: state.canvasV2.params.iterations,
+ origin: 'workflows',
},
prepend: action.payload.prepend,
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts
index 14af8bdbf4..cc81dbdf75 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts
@@ -12,19 +12,21 @@ const log = logger('socketio');
export const addGeneratorProgressEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketGeneratorProgress,
- effect: (action, { getState }) => {
+ effect: (action) => {
log.trace(parseify(action.payload), `Generator progress`);
- const { invocation_source_id, step, total_steps, progress_image, batch_id } = action.payload.data;
- const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
- if (nes) {
- nes.status = zNodeStatus.enum.IN_PROGRESS;
- nes.progress = (step + 1) / total_steps;
- nes.progressImage = progress_image ?? null;
- upsertExecutionState(nes.nodeId, nes);
+ const { invocation_source_id, step, total_steps, progress_image, origin } = action.payload.data;
+
+ if (origin === 'workflows') {
+ const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
+ if (nes) {
+ nes.status = zNodeStatus.enum.IN_PROGRESS;
+ nes.progress = (step + 1) / total_steps;
+ nes.progressImage = progress_image ?? null;
+ upsertExecutionState(nes.nodeId, nes);
+ }
}
- const isCanvasQueueItem = getState().canvasV2.stagingArea?.batchIds.includes(batch_id);
- if (isCanvasQueueItem) {
+ if (origin === 'canvas') {
$lastProgressEvent.set(action.payload.data);
}
},
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts
index 53aa9acf0e..e963023522 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts
@@ -3,13 +3,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import { stagingAreaImageAdded } from 'features/controlLayers/store/canvasV2Slice';
-import {
- boardIdSelected,
- galleryViewChanged,
- imageSelected,
- isImageViewerOpenChanged,
- offsetChanged,
-} from 'features/gallery/store/gallerySlice';
+import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants';
@@ -17,7 +11,6 @@ import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images';
import { getCategories, getListImagesUrl } from 'services/api/util';
import { socketInvocationComplete } from 'services/events/actions';
-import { assert } from 'tsafe';
// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them
const nodeTypeDenylist = ['load_image', 'image'];
@@ -35,7 +28,7 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
// This complete event has an associated image output
if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) {
const { image_name } = data.result.image;
- const { canvasV2, gallery } = getState();
+ const { gallery, canvasV2 } = getState();
// This populates the `getImageDTO` cache
const imageDTORequest = dispatch(
@@ -47,11 +40,21 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
const imageDTO = await imageDTORequest.unwrap();
imageDTORequest.unsubscribe();
- // Add canvas images to the staging area
- if (canvasV2.stagingArea?.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) {
- const stagingArea = getState().canvasV2.stagingArea;
- assert(stagingArea, 'Staging should be defined');
- dispatch(stagingAreaImageAdded({ imageDTO }));
+ // handle tab-specific logic
+ if (data.origin === 'canvas') {
+ if (data.invocation_source_id === CANVAS_OUTPUT && canvasV2.stagingArea.isStaging) {
+ dispatch(stagingAreaImageAdded({ imageDTO }));
+ }
+ } else if (data.origin === 'workflows') {
+ const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
+ if (nes) {
+ nes.status = zNodeStatus.enum.COMPLETED;
+ if (nes.progress !== null) {
+ nes.progress = 1;
+ }
+ nes.outputs.push(result);
+ upsertExecutionState(nes.nodeId, nes);
+ }
}
if (!imageDTO.is_intermediate) {
@@ -106,20 +109,9 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
}
dispatch(imageSelected(imageDTO));
- dispatch(isImageViewerOpenChanged(true));
}
}
}
-
- const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
- if (nes) {
- nes.status = zNodeStatus.enum.COMPLETED;
- if (nes.progress !== null) {
- nes.progress = 1;
- }
- nes.outputs.push(result);
- upsertExecutionState(nes.nodeId, nes);
- }
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.tsx b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx
similarity index 89%
rename from invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.tsx
rename to invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx
index 2d117ef140..5ba1013bb7 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.tsx
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx
@@ -12,7 +12,15 @@ import { socketQueueItemStatusChanged } from 'services/events/actions';
const log = logger('socketio');
-export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => {
+export const addSocketQueueEventsListeners = (startAppListening: AppStartListening) => {
+ // When the queue is cleared or canvas batch is canceled, we should clear the last canvas progress event
+ startAppListening({
+ matcher: queueApi.endpoints.clearQueue.matchFulfilled,
+ effect: () => {
+ $lastProgressEvent.set(null);
+ },
+ });
+
startAppListening({
actionCreator: socketQueueItemStatusChanged,
effect: async (action, { dispatch, getState }) => {
@@ -29,13 +37,11 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
error_type,
error_message,
error_traceback,
- batch_id,
+ origin,
} = action.payload.data;
log.debug(action.payload, `Queue item ${item_id} status updated: ${status}`);
- const isCanvasQueueItem = getState().canvasV2.stagingArea?.batchIds.includes(batch_id);
-
// Update this specific queue item in the list of queue items (this is the queue item DTO, without the session)
dispatch(
queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => {
@@ -96,7 +102,7 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
} else if (status === 'failed' && error_type) {
const isLocal = getState().config.isLocal ?? true;
const sessionId = session_id;
- if (isCanvasQueueItem) {
+ if (origin === 'canvas') {
$lastProgressEvent.set(null);
}
@@ -115,9 +121,7 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
/>
),
});
- } else if (status === 'completed' && isCanvasQueueItem) {
- $lastProgressEvent.set(null);
- } else if (status === 'canceled' && isCanvasQueueItem) {
+ } else if (status === 'canceled' && origin === 'canvas') {
$lastProgressEvent.set(null);
}
},
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
index 44aaca714f..f8e00a9176 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
@@ -3,13 +3,12 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
$shouldShowStagedImage,
+ stagingAreaCanceledStaging,
stagingAreaImageAccepted,
stagingAreaImageDiscarded,
stagingAreaNextImageSelected,
stagingAreaPreviousImageSelected,
- stagingAreaReset,
} from 'features/controlLayers/store/canvasV2Slice';
-import type { CanvasV2State } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@@ -25,29 +24,23 @@ import {
} from 'react-icons/pi';
export const StagingAreaToolbar = memo(() => {
- const stagingArea = useAppSelector((s) => s.canvasV2.stagingArea);
+ const isStaging = useAppSelector((s) => s.canvasV2.stagingArea.isStaging);
- if (!stagingArea || stagingArea.images.length === 0) {
+ if (!isStaging) {
return null;
}
- return ;
+ return ;
});
StagingAreaToolbar.displayName = 'StagingAreaToolbar';
-type Props = {
- stagingArea: NonNullable;
-};
-
-export const StagingAreaToolbarContent = memo(({ stagingArea }: Props) => {
+export const StagingAreaToolbarContent = memo(() => {
const dispatch = useAppDispatch();
+ const stagingArea = useAppSelector((s) => s.canvasV2.stagingArea);
const shouldShowStagedImage = useStore($shouldShowStagedImage);
const images = useMemo(() => stagingArea.images, [stagingArea]);
- const imageDTO = useMemo(() => {
- if (stagingArea.selectedImageIndex === null) {
- return null;
- }
+ const selectedImageDTO = useMemo(() => {
return images[stagingArea.selectedImageIndex] ?? null;
}, [images, stagingArea.selectedImageIndex]);
@@ -64,29 +57,26 @@ export const StagingAreaToolbarContent = memo(({ stagingArea }: Props) => {
}, [dispatch]);
const onAccept = useCallback(() => {
- if (!imageDTO || !stagingArea) {
+ if (!selectedImageDTO) {
return;
}
- dispatch(stagingAreaImageAccepted({ imageDTO }));
- }, [dispatch, imageDTO, stagingArea]);
+ dispatch(stagingAreaImageAccepted({ imageDTO: selectedImageDTO }));
+ }, [dispatch, selectedImageDTO]);
const onDiscardOne = useCallback(() => {
- if (!imageDTO || !stagingArea) {
+ if (!selectedImageDTO) {
return;
}
if (images.length === 1) {
- dispatch(stagingAreaReset());
+ dispatch(stagingAreaCanceledStaging());
} else {
- dispatch(stagingAreaImageDiscarded({ imageDTO }));
+ dispatch(stagingAreaImageDiscarded({ imageDTO: selectedImageDTO }));
}
- }, [dispatch, imageDTO, images.length, stagingArea]);
+ }, [dispatch, selectedImageDTO, images.length]);
const onDiscardAll = useCallback(() => {
- if (!stagingArea) {
- return;
- }
- dispatch(stagingAreaReset());
- }, [dispatch, stagingArea]);
+ dispatch(stagingAreaCanceledStaging());
+ }, [dispatch]);
const onToggleShouldShowStagedImage = useCallback(() => {
$shouldShowStagedImage.set(!shouldShowStagedImage);
@@ -117,6 +107,14 @@ export const StagingAreaToolbarContent = memo(({ stagingArea }: Props) => {
preventDefault: true,
});
+ const counterText = useMemo(() => {
+ if (images.length > 0) {
+ return `${(stagingArea.selectedImageIndex ?? 0) + 1} of ${images.length}`;
+ } else {
+ return `0 of 0`;
+ }
+ }, [images.length, stagingArea.selectedImageIndex]);
+
return (
<>
@@ -128,11 +126,9 @@ export const StagingAreaToolbarContent = memo(({ stagingArea }: Props) => {
colorScheme="invokeBlue"
isDisabled={images.length <= 1 || !shouldShowStagedImage}
/>
-
+
{
icon={}
onClick={onAccept}
colorScheme="invokeBlue"
+ isDisabled={!selectedImageDTO}
/>
{
}
onClick={onSaveStagingImage}
colorScheme="invokeBlue"
+ isDisabled={!selectedImageDTO || !selectedImageDTO.is_intermediate}
/>
{
onClick={onDiscardOne}
colorScheme="invokeBlue"
fontSize={16}
- isDisabled={images.length <= 1}
+ isDisabled={!selectedImageDTO}
/>
{
onClick={onDiscardAll}
colorScheme="error"
fontSize={16}
- isDisabled={images.length === 0}
/>
>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
index d6b14e9549..6ec0641279 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
@@ -43,7 +43,7 @@ export const ToolChooser: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
- const isStaging = useAppSelector((s) => s.canvasV2.stagingArea !== null);
+ const isStaging = useAppSelector((s) => s.canvasV2.stagingArea.isStaging);
const isDrawingToolDisabled = useMemo(
() => !getIsDrawingToolEnabled(selectedEntityIdentifier),
[selectedEntityIdentifier]
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts
index 941d34a106..e216fc5a33 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts
@@ -76,6 +76,7 @@ export type StateApi = {
getInpaintMaskState: () => CanvasV2State['inpaintMask'];
getStagingAreaState: () => CanvasV2State['stagingArea'];
getLastProgressEvent: () => InvocationDenoiseProgressEvent | null;
+ resetLastProgressEvent: () => void;
onInpaintMaskImageCached: (imageDTO: ImageDTO) => void;
onRegionMaskImageCached: (id: string, imageDTO: ImageDTO) => void;
onLayerImageCached: (imageDTO: ImageDTO) => void;
@@ -280,8 +281,10 @@ export class KonvaNodeManager {
renderStagingArea() {
this.preview.stagingArea.render(
this.stateApi.getStagingAreaState(),
+ this.stateApi.getBbox(),
this.stateApi.getShouldShowStagedImage(),
- this.stateApi.getLastProgressEvent()
+ this.stateApi.getLastProgressEvent(),
+ this.stateApi.resetLastProgressEvent
);
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts
index 17051e79cf..beddbcc10b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts
@@ -18,18 +18,18 @@ export class CanvasPreview {
documentSizeOverlay: CanvasDocumentSizeOverlay,
stagingArea: CanvasStagingArea
) {
- this.layer = new Konva.Layer({ listening: true });
-
- this.bbox = bbox;
- this.layer.add(this.bbox.group);
-
- this.tool = tool;
- this.layer.add(this.tool.group);
+ this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false });
this.documentSizeOverlay = documentSizeOverlay;
this.layer.add(this.documentSizeOverlay.group);
this.stagingArea = stagingArea;
this.layer.add(this.stagingArea.group);
+
+ this.bbox = bbox;
+ this.layer.add(this.bbox.group);
+
+ this.tool = tool;
+ this.layer.add(this.tool.group);
}
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts
index fa692558d8..24763e238e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts
@@ -307,6 +307,9 @@ export const initializeRenderer = (
getStagingAreaState,
getShouldShowStagedImage: $shouldShowStagedImage.get,
getLastProgressEvent: $lastProgressEvent.get,
+ resetLastProgressEvent: () => {
+ $lastProgressEvent.set(null);
+ },
// Read-write state
setTool,
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts
index 28b64f898b..6bd5617aa1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts
@@ -2,7 +2,6 @@ import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/ren
import type { CanvasV2State } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
-import { assert } from 'tsafe';
export class CanvasStagingArea {
group: Konva.Group;
@@ -17,13 +16,54 @@ export class CanvasStagingArea {
async render(
stagingArea: CanvasV2State['stagingArea'],
+ bbox: CanvasV2State['bbox'],
shouldShowStagedImage: boolean,
- lastProgressEvent: InvocationDenoiseProgressEvent | null
+ lastProgressEvent: InvocationDenoiseProgressEvent | null,
+ resetLastProgressEvent: () => void
) {
- if (stagingArea && lastProgressEvent) {
+ const imageDTO = stagingArea.images[stagingArea.selectedImageIndex];
+
+ if (imageDTO) {
+ if (this.image) {
+ if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) {
+ await this.image.updateImageSource(imageDTO.image_name);
+ }
+ this.image.konvaImageGroup.x(bbox.x);
+ this.image.konvaImageGroup.y(bbox.y);
+ this.image.konvaImageGroup.visible(shouldShowStagedImage);
+ this.progressImage?.konvaImageGroup.visible(false);
+ } else {
+ const { image_name, width, height } = imageDTO;
+ this.image = new KonvaImage({
+ imageObject: {
+ id: 'staging-area-image',
+ type: 'image',
+ x: bbox.x,
+ y: bbox.y,
+ width,
+ height,
+ filters: [],
+ image: {
+ name: image_name,
+ width,
+ height,
+ },
+ },
+ onLoad: () => {
+ resetLastProgressEvent();
+ },
+ });
+ this.group.add(this.image.konvaImageGroup);
+ await this.image.updateImageSource(imageDTO.image_name);
+ this.image.konvaImageGroup.visible(shouldShowStagedImage);
+ this.progressImage?.konvaImageGroup.visible(false);
+ }
+ }
+
+ if (stagingArea.isStaging && lastProgressEvent) {
const { invocation, step, progress_image } = lastProgressEvent;
const { dataURL } = progress_image;
- const { x, y, width, height } = stagingArea.bbox;
+ const { x, y, width, height } = bbox;
const progressImageId = `${invocation.id}_${step}`;
if (this.progressImage) {
if (
@@ -42,47 +82,16 @@ export class CanvasStagingArea {
this.image?.konvaImageGroup.visible(false);
this.progressImage.konvaImageGroup.visible(true);
}
- } else if (stagingArea && stagingArea.selectedImageIndex !== null) {
- const imageDTO = stagingArea.images[stagingArea.selectedImageIndex];
- assert(imageDTO, 'Image must exist');
- if (this.image) {
- if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) {
- await this.image.updateImageSource(imageDTO.image_name);
- }
- this.image.konvaImageGroup.x(stagingArea.bbox.x);
- this.image.konvaImageGroup.y(stagingArea.bbox.y);
- this.image.konvaImageGroup.visible(shouldShowStagedImage);
- this.progressImage?.konvaImageGroup.visible(false);
- } else {
- const { image_name, width, height } = imageDTO;
- this.image = new KonvaImage({
- imageObject: {
- id: 'staging-area-image',
- type: 'image',
- x: stagingArea.bbox.x,
- y: stagingArea.bbox.y,
- width,
- height,
- filters: [],
- image: {
- name: image_name,
- width,
- height,
- },
- },
- });
- this.group.add(this.image.konvaImageGroup);
- await this.image.updateImageSource(imageDTO.image_name);
- this.image.konvaImageGroup.visible(shouldShowStagedImage);
- this.progressImage?.konvaImageGroup.visible(false);
- }
- } else {
+ }
+
+ if (!imageDTO && !lastProgressEvent) {
if (this.image) {
this.image.konvaImageGroup.visible(false);
}
if (this.progressImage) {
this.progressImage.konvaImageGroup.visible(false);
}
+ resetLastProgressEvent();
}
}
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
index 02804d7bb4..41df1dc7fb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
@@ -121,7 +121,11 @@ const initialState: CanvasV2State = {
refinerNegativeAestheticScore: 2.5,
refinerStart: 0.8,
},
- stagingArea: null,
+ stagingArea: {
+ isStaging: false,
+ images: [],
+ selectedImageIndex: 0,
+ },
};
export const canvasV2Slice = createSlice({
@@ -332,12 +336,11 @@ export const {
imLinePointAdded,
imRectAdded,
// Staging
- stagingAreaInitialized,
+ stagingAreaStartedStaging,
stagingAreaImageAdded,
- stagingAreaBatchIdAdded,
stagingAreaImageDiscarded,
stagingAreaImageAccepted,
- stagingAreaReset,
+ stagingAreaCanceledStaging,
stagingAreaNextImageSelected,
stagingAreaPreviousImageSelected,
} = canvasV2Slice.actions;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts
index 78d07b0fb8..9e6168d966 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts
@@ -1,16 +1,11 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
-import type { CanvasV2State, Rect } from 'features/controlLayers/store/types';
+import type { CanvasV2State } from 'features/controlLayers/store/types';
import type { ImageDTO } from 'services/api/types';
export const stagingAreaReducers = {
- stagingAreaInitialized: (state, action: PayloadAction<{ bbox: Rect; batchIds: string[] }>) => {
- const { bbox, batchIds } = action.payload;
- state.stagingArea = {
- bbox,
- batchIds,
- selectedImageIndex: null,
- images: [],
- };
+ stagingAreaStartedStaging: (state) => {
+ state.stagingArea.isStaging = true;
+ state.stagingArea.selectedImageIndex = 0;
// When we start staging, the user should not be interacting with the stage except to move it around. Set the tool
// to view.
state.tool.selectedBuffer = state.tool.selected;
@@ -18,67 +13,41 @@ export const stagingAreaReducers = {
},
stagingAreaImageAdded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => {
const { imageDTO } = action.payload;
- if (!state.stagingArea) {
- // Should not happen
- return;
- }
state.stagingArea.images.push(imageDTO);
- if (!state.stagingArea.selectedImageIndex) {
- state.stagingArea.selectedImageIndex = state.stagingArea.images.length - 1;
- }
+ state.stagingArea.selectedImageIndex = state.stagingArea.images.length - 1;
},
stagingAreaNextImageSelected: (state) => {
- if (!state.stagingArea) {
- // Should not happen
- return;
- }
- if (state.stagingArea.selectedImageIndex === null) {
- if (state.stagingArea.images.length > 0) {
- state.stagingArea.selectedImageIndex = 0;
- }
- return;
- }
state.stagingArea.selectedImageIndex = (state.stagingArea.selectedImageIndex + 1) % state.stagingArea.images.length;
},
stagingAreaPreviousImageSelected: (state) => {
- if (!state.stagingArea) {
- // Should not happen
- return;
- }
- if (state.stagingArea.selectedImageIndex === null) {
- if (state.stagingArea.images.length > 0) {
- state.stagingArea.selectedImageIndex = 0;
- }
- return;
- }
state.stagingArea.selectedImageIndex =
(state.stagingArea.selectedImageIndex - 1 + state.stagingArea.images.length) % state.stagingArea.images.length;
},
- stagingAreaBatchIdAdded: (state, action: PayloadAction<{ batchId: string }>) => {
- const { batchId } = action.payload;
- if (!state.stagingArea) {
- // Should not happen
- return;
- }
- state.stagingArea.batchIds.push(batchId);
- },
stagingAreaImageDiscarded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => {
const { imageDTO } = action.payload;
- if (!state.stagingArea) {
- // Should not happen
- return;
- }
state.stagingArea.images = state.stagingArea.images.filter((image) => image.image_name !== imageDTO.image_name);
+ state.stagingArea.selectedImageIndex = Math.min(
+ state.stagingArea.selectedImageIndex,
+ state.stagingArea.images.length - 1
+ );
+ if (state.stagingArea.images.length === 0) {
+ state.stagingArea.isStaging = false;
+ }
},
stagingAreaImageAccepted: (state, _: PayloadAction<{ imageDTO: ImageDTO }>) => {
// When we finish staging, reset the tool back to the previous selection.
+ state.stagingArea.isStaging = false;
+ state.stagingArea.images = [];
+ state.stagingArea.selectedImageIndex = 0;
if (state.tool.selectedBuffer) {
state.tool.selected = state.tool.selectedBuffer;
state.tool.selectedBuffer = null;
}
},
- stagingAreaReset: (state) => {
- state.stagingArea = null;
+ stagingAreaCanceledStaging: (state) => {
+ state.stagingArea.isStaging = false;
+ state.stagingArea.images = [];
+ state.stagingArea.selectedImageIndex = 0;
// When we finish staging, reset the tool back to the previous selection.
if (state.tool.selectedBuffer) {
state.tool.selected = state.tool.selectedBuffer;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index ea55c6b581..a5beb70ead 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -883,11 +883,10 @@ export type CanvasV2State = {
refinerStart: number;
};
stagingArea: {
- bbox: Rect;
+ isStaging: boolean;
images: ImageDTO[];
- selectedImageIndex: number | null;
- batchIds: string[];
- } | null;
+ selectedImageIndex: number;
+ };
};
export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number };
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts
index bb282863b9..3cd80862ab 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts
@@ -107,6 +107,7 @@ export const prepareLinearUIBatch = (state: RootState, g: Graph, prepend: boolea
graph: g.getGraph(),
runs: 1,
data,
+ origin: 'canvas',
},
};
diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
index e7edf9811f..f64b2a30a3 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
@@ -276,6 +276,26 @@ export const queueApi = api.injectEndpoints({
},
invalidatesTags: ['SessionQueueStatus', 'BatchStatus'],
}),
+ cancelByBatchOrigin: build.mutation<
+ paths['/api/v1/queue/{queue_id}/cancel_by_origin']['put']['responses']['200']['content']['application/json'],
+ paths['/api/v1/queue/{queue_id}/cancel_by_origin']['put']['parameters']['query']
+ >({
+ query: (params) => ({
+ url: buildQueueUrl('cancel_by_origin'),
+ method: 'PUT',
+ params,
+ }),
+ onQueryStarted: async (arg, api) => {
+ const { dispatch, queryFulfilled } = api;
+ try {
+ await queryFulfilled;
+ resetListQueryData(dispatch);
+ } catch {
+ // no-op
+ }
+ },
+ invalidatesTags: ['SessionQueueStatus', 'BatchStatus'],
+ }),
listQueueItems: build.query<
EntityState & {
has_more: boolean;