diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 5f8e2070c37..22be6ebc1c8 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1962,7 +1962,6 @@
"recalculateRects": "Recalculate Rects",
"clipToBbox": "Clip Strokes to Bbox",
"outputOnlyMaskedRegions": "Output Only Generated Regions",
- "saveAllImagesToGallery": "Save All Images to Gallery",
"addLayer": "Add Layer",
"duplicate": "Duplicate",
"moveToFront": "Move to Front",
@@ -2087,9 +2086,9 @@
"resetCanvasLayers": "Reset Canvas Layers",
"resetGenerationSettings": "Reset Generation Settings",
"replaceCurrent": "Replace Current",
- "controlLayerEmptyState": "Upload an image, drag an image from the gallery onto this layer, pull the bounding box into this layer, or draw on the canvas to get started.",
- "referenceImageEmptyStateWithCanvasOptions": "Upload an image, drag an image from the gallery onto this Reference Image or pull the bounding box into this Reference Image to get started.",
- "referenceImageEmptyState": "Upload an image or drag an image from the gallery onto this Reference Image to get started.",
+ "controlLayerEmptyState": "Upload an image, drag an image from the gallery onto this layer, pull the bounding box into this layer, or draw on the canvas to get started.",
+ "referenceImageEmptyStateWithCanvasOptions": "Upload an image, drag an image from the gallery onto this Reference Image or pull the bounding box into this Reference Image to get started.",
+ "referenceImageEmptyState": "Upload an image or drag an image from the gallery onto this Reference Image to get started.",
"uploadOrDragAnImage": "Drag an image from the gallery or upload an image.",
"imageNoise": "Image Noise",
"denoiseLimit": "Denoise Limit",
@@ -2332,7 +2331,8 @@
"alert": "Preserving Masked Region"
},
"saveAllImagesToGallery": {
- "alert": "Saving All Images to Gallery"
+ "label": "Send New Generations to Gallery",
+ "alert": "Sending new generations to Gallery, bypassing Canvas"
},
"isolatedStagingPreview": "Isolated Staging Preview",
"isolatedPreview": "Isolated Preview",
diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
index 662994ec416..59f73f109b3 100644
--- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
@@ -1,10 +1,6 @@
import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys';
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal';
-import {
- NewCanvasSessionDialog,
- NewGallerySessionDialog,
-} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
@@ -50,8 +46,6 @@ export const GlobalModalIsolator = memo(() => {
-
-
diff --git a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
index b27613b84b1..d0e043381d1 100644
--- a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
+++ b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
@@ -21,7 +21,6 @@ import { $isStylePresetsMenuOpen, activeStylePresetIdChanged } from 'features/st
import { toast } from 'features/toast/toast';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import { LAUNCHPAD_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
-import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { atom } from 'nanostores';
import { useCallback, useEffect } from 'react';
@@ -165,7 +164,6 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
// Go to the generate tab, open the launchpad
await navigationApi.focusPanel('generate', LAUNCHPAD_PANEL_ID);
store.dispatch(paramsReset());
- store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
break;
case 'canvas':
// Go to the canvas tab, open the launchpad
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
index 7d2e8f7520e..f99a458cd7f 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
@@ -1,7 +1,7 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice';
import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice';
@@ -152,7 +152,8 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
if (modelBase !== state.params.model?.base) {
// Sync generate tab settings whenever the model base changes
dispatch(syncedToOptimalDimension());
- if (!selectIsStaging(state)) {
+ const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state);
+ if (!isStaging) {
// Canvas tab only syncs if not staging
dispatch(bboxSyncedToOptimalDimension());
}
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts
index 38978baf3d9..56a8958d1fa 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts
@@ -1,7 +1,7 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { isNil } from 'es-toolkit';
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
heightChanged,
setCfgRescaleMultiplier,
@@ -115,7 +115,8 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
}
const setSizeOptions = { updateAspectRatio: true, clamp: true };
- const isStaging = selectIsStaging(getState());
+ const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state);
+
const activeTab = selectActiveTab(getState());
if (activeTab === 'generate') {
if (isParameterWidth(width)) {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx
index a07f635ec68..5a4da84bfe1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx
@@ -13,7 +13,7 @@ export const CanvasAlertsSaveAllImagesToGallery = memo(() => {
}
return (
-
+
{t('controlLayers.settings.saveAllImagesToGallery.alert')}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx
index a5fc3fd0fd9..c7ec2151a3b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx
@@ -57,21 +57,21 @@ const CanvasAlertsSelectedEntityStatusContent = memo(({ entityIdentifier, adapte
const alert = useMemo(() => {
if (isFiltering) {
return {
- status: 'info',
+ status: 'warning',
title: t('controlLayers.HUD.entityStatus.isFiltering', { title }),
};
}
if (isTransforming) {
return {
- status: 'info',
+ status: 'warning',
title: t('controlLayers.HUD.entityStatus.isTransforming', { title }),
};
}
if (isEmpty) {
return {
- status: 'info',
+ status: 'warning',
title: t('controlLayers.HUD.entityStatus.isEmpty', { title }),
};
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerSettingsEmptyState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerSettingsEmptyState.tsx
index b1150417f36..596886599ed 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerSettingsEmptyState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerSettingsEmptyState.tsx
@@ -5,7 +5,6 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti
import { usePullBboxIntoLayer } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { replaceCanvasEntityObjectsWithImage } from 'features/imageActions/actions';
-import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
import { memo, useCallback, useMemo } from 'react';
import { Trans } from 'react-i18next';
import type { ImageDTO } from 'services/api/types';
@@ -21,9 +20,6 @@ export const ControlLayerSettingsEmptyState = memo(() => {
[dispatch, entityIdentifier, getState]
);
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
- const onClickGalleryButton = useCallback(() => {
- dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }, [dispatch]);
const pullBboxIntoLayer = usePullBboxIntoLayer(entityIdentifier);
const components = useMemo(
@@ -31,14 +27,11 @@ export const ControlLayerSettingsEmptyState = memo(() => {
UploadButton: (
),
- GalleryButton: (
-
- ),
PullBboxButton: (
),
}),
- [isBusy, onClickGalleryButton, pullBboxIntoLayer, uploadApi]
+ [isBusy, pullBboxIntoLayer, uploadApi]
);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
deleted file mode 100644
index bac86eb7e98..00000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
-import { buildUseBoolean } from 'common/hooks/useBoolean';
-import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import {
- selectSystemShouldConfirmOnNewSession,
- shouldConfirmOnNewSessionToggled,
-} from 'features/system/store/systemSlice';
-import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
-import { memo, useCallback } from 'react';
-import { useTranslation } from 'react-i18next';
-
-const [useNewGallerySessionDialog] = buildUseBoolean(false);
-const [useNewCanvasSessionDialog] = buildUseBoolean(false);
-
-const useNewGallerySession = () => {
- const dispatch = useAppDispatch();
- const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
- const newSessionDialog = useNewGallerySessionDialog();
-
- const newGallerySessionImmediate = useCallback(() => {
- dispatch(generateSessionReset());
- dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }, [dispatch]);
-
- const newGallerySessionWithDialog = useCallback(() => {
- if (shouldConfirmOnNewSession) {
- newSessionDialog.setTrue();
- return;
- }
- newGallerySessionImmediate();
- }, [newGallerySessionImmediate, newSessionDialog, shouldConfirmOnNewSession]);
-
- return { newGallerySessionImmediate, newGallerySessionWithDialog };
-};
-
-const useNewCanvasSession = () => {
- const dispatch = useAppDispatch();
- const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
- const newSessionDialog = useNewCanvasSessionDialog();
-
- const newCanvasSessionImmediate = useCallback(() => {
- dispatch(canvasSessionReset());
- dispatch(activeTabCanvasRightPanelChanged('layers'));
- }, [dispatch]);
-
- const newCanvasSessionWithDialog = useCallback(() => {
- if (shouldConfirmOnNewSession) {
- newSessionDialog.setTrue();
- return;
- }
-
- newCanvasSessionImmediate();
- }, [newCanvasSessionImmediate, newSessionDialog, shouldConfirmOnNewSession]);
-
- return { newCanvasSessionImmediate, newCanvasSessionWithDialog };
-};
-
-export const NewGallerySessionDialog = memo(() => {
- useAssertSingleton('NewGallerySessionDialog');
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
-
- const dialog = useNewGallerySessionDialog();
- const { newGallerySessionImmediate } = useNewGallerySession();
-
- const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
- const onToggleConfirm = useCallback(() => {
- dispatch(shouldConfirmOnNewSessionToggled());
- }, [dispatch]);
-
- return (
-
-
- {t('controlLayers.newGallerySessionDesc')}
- {t('common.areYouSure')}
-
- {t('common.dontAskMeAgain')}
-
-
-
-
- );
-});
-
-NewGallerySessionDialog.displayName = 'NewGallerySessionDialog';
-
-export const NewCanvasSessionDialog = memo(() => {
- useAssertSingleton('NewCanvasSessionDialog');
- const { t } = useTranslation();
-
- const dispatch = useAppDispatch();
-
- const dialog = useNewCanvasSessionDialog();
- const { newCanvasSessionImmediate } = useNewCanvasSession();
-
- const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
- const onToggleConfirm = useCallback(() => {
- dispatch(shouldConfirmOnNewSessionToggled());
- }, [dispatch]);
-
- return (
-
-
- {t('controlLayers.newCanvasSessionDesc')}
- {t('common.areYouSure')}
-
- {t('common.dontAskMeAgain')}
-
-
-
-
- );
-});
-
-NewCanvasSessionDialog.displayName = 'NewCanvasSessionDialog';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageState.tsx
index 81aec80fecb..70333c06869 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageState.tsx
@@ -6,7 +6,6 @@ import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { setGlobalReferenceImage } from 'features/imageActions/actions';
-import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
import { memo, useCallback, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { ImageDTO } from 'services/api/types';
@@ -22,9 +21,6 @@ export const RefImageNoImageState = memo(() => {
[dispatch, id]
);
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
- const onClickGalleryButton = useCallback(() => {
- dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }, [dispatch]);
const dndTargetData = useMemo(
() => setGlobalReferenceImageDndTarget.getData({ id }),
@@ -34,9 +30,8 @@ export const RefImageNoImageState = memo(() => {
const components = useMemo(
() => ({
UploadButton: ,
- GalleryButton: ,
}),
- [onClickGalleryButton, uploadApi]
+ [uploadApi]
);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions.tsx
index 329c6411b24..f15b87d6b93 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions.tsx
@@ -8,7 +8,6 @@ import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { setGlobalReferenceImage } from 'features/imageActions/actions';
-import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
import { memo, useCallback, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { ImageDTO } from 'services/api/types';
@@ -25,9 +24,6 @@ export const RefImageNoImageStateWithCanvasOptions = memo(() => {
[dispatch, id]
);
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
- const onClickGalleryButton = useCallback(() => {
- dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }, [dispatch]);
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
const dndTargetData = useMemo(
@@ -40,14 +36,11 @@ export const RefImageNoImageStateWithCanvasOptions = memo(() => {
UploadButton: (
),
- GalleryButton: (
-
- ),
PullBboxButton: (
),
}),
- [isBusy, onClickGalleryButton, pullBboxIntoIPAdapter, uploadApi]
+ [isBusy, pullBboxIntoIPAdapter, uploadApi]
);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState.tsx
index add9558f594..07ec79f300b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState.tsx
@@ -9,7 +9,6 @@ import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dn
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { setRegionalGuidanceReferenceImage } from 'features/imageActions/actions';
-import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
import { memo, useCallback, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
@@ -31,9 +30,6 @@ export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImag
[dispatch, entityIdentifier, referenceImageId]
);
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
- const onClickGalleryButton = useCallback(() => {
- dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }, [dispatch]);
const onDeleteIPAdapter = useCallback(() => {
dispatch(rgRefImageDeleted({ entityIdentifier, referenceImageId }));
}, [dispatch, entityIdentifier, referenceImageId]);
@@ -53,14 +49,11 @@ export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImag
UploadButton: (
),
- GalleryButton: (
-
- ),
PullBboxButton: (
),
}),
- [isBusy, onClickGalleryButton, pullBboxIntoIPAdapter, uploadApi]
+ [isBusy, pullBboxIntoIPAdapter, uploadApi]
);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsSaveAllImagesToGalleryCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsSaveAllImagesToGalleryCheckbox.tsx
index cfd6a924f72..c1c0d72d02d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsSaveAllImagesToGalleryCheckbox.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsSaveAllImagesToGalleryCheckbox.tsx
@@ -16,7 +16,7 @@ export const CanvasSettingsSaveAllImagesToGalleryCheckbox = memo(() => {
}, [dispatch]);
return (
- {t('controlLayers.saveAllImagesToGallery')}
+ {t('controlLayers.settings.saveAllImagesToGallery.label')}
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
index ceac8d3a415..ee31573d32c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -64,10 +64,6 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, index }: Props) =>
}
}, [autoSwitch, dispatch]);
- const onLoad = useCallback(() => {
- ctx.onImageLoad(item.item_id);
- }, [ctx, item.item_id]);
-
return (
onDoubleClick={onDoubleClick}
>
- {imageDTO && }
+ {imageDTO && }
{!imageLoaded && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
index 60a8458871d..aed3f9c8750 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
@@ -149,8 +149,8 @@ export const StagingAreaItemsList = memo(() => {
return;
}
- return canvasManager.stagingArea.connectToSession(ctx.$selectedItemId, ctx.$progressData, ctx.$isPending);
- }, [canvasManager, ctx.$progressData, ctx.$selectedItemId, ctx.$isPending]);
+ return canvasManager.stagingArea.connectToSession(ctx.$items, ctx.$selectedItemId, ctx.$progressData);
+ }, [canvasManager, ctx.$progressData, ctx.$selectedItemId, ctx.$items]);
useEffect(() => {
return ctx.$selectedItemIndex.listen((index) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
index 38f3388a6aa..f38c37bb701 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -1,11 +1,13 @@
import { useStore } from '@nanostores/react';
-import { useAppStore } from 'app/store/storeHooks';
+import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
+import { loadImage } from 'features/controlLayers/konva/util';
import { selectStagingAreaAutoSwitch } from 'features/controlLayers/store/canvasSettingsSlice';
import {
- buildSelectSessionQueueItems,
+ buildSelectCanvasQueueItems,
canvasQueueItemDiscarded,
canvasSessionReset,
+ selectCanvasSessionId,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { ProgressImage } from 'features/nodes/types/common';
import type { Atom, MapStore, StoreValue, WritableAtom } from 'nanostores';
@@ -86,11 +88,9 @@ const setProgress = ($progressData: ProgressDataMap, data: S['InvocationProgress
export type ProgressDataMap = MapStore>;
type CanvasSessionContextValue = {
- session: { id: string; type: 'simple' | 'advanced' };
$items: Atom;
$itemCount: Atom;
$hasItems: Atom;
- $isPending: Atom;
$progressData: ProgressDataMap;
$selectedItemId: WritableAtom;
$selectedItem: Atom;
@@ -100,472 +100,385 @@ type CanvasSessionContextValue = {
selectPrev: () => void;
selectFirst: () => void;
selectLast: () => void;
- onImageLoad: (itemId: number) => void;
discard: (itemId: number) => void;
discardAll: () => void;
};
const CanvasSessionContext = createContext(null);
-export const CanvasSessionContextProvider = memo(
- ({ id, type, children }: PropsWithChildren<{ id: string; type: 'simple' | 'advanced' }>) => {
- /**
- * For best performance and interop with the Canvas, which is outside react but needs to interact with the react
- * app, all canvas session state is packaged as nanostores atoms. The trickiest part is syncing the queue items
- * with a nanostores atom.
- */
- const session = useMemo(() => ({ type, id }), [type, id]);
-
- /**
- * App store
- */
- const store = useAppStore();
-
- const socket = useStore($socket);
-
- /**
- * Track the last completed item. Used to implement autoswitch.
- */
- const $lastCompletedItemId = useState(() => atom(null))[0];
-
- /**
- * Track the last started item. Used to implement autoswitch.
- */
- const $lastStartedItemId = useState(() => atom(null))[0];
-
- /**
- * Manually-synced atom containing queue items for the current session. This is populated from the RTK Query cache
- * and kept in sync with it via a redux subscription.
- */
- const $items = useState(() => atom([]))[0];
-
- /**
- * An internal flag used to work around race conditions with auto-switch switching to queue items before their
- * output images have fully loaded.
- */
- const $lastLoadedItemId = useState(() => atom(null))[0];
-
- /**
- * An ephemeral store of progress events and images for all items in the current session.
- */
- const $progressData = useState(() => map>({}))[0];
-
- /**
- * The currently selected queue item's ID, or null if one is not selected.
- */
- const $selectedItemId = useState(() => atom(null))[0];
-
- /**
- * The number of items. Computed from the queue items array.
- */
- const $itemCount = useState(() => computed([$items], (items) => items.length))[0];
-
- /**
- * Whether there are any items. Computed from the queue items array.
- */
- const $hasItems = useState(() => computed([$items], (items) => items.length > 0))[0];
-
- /**
- * Whether there are any pending or in-progress items. Computed from the queue items array.
- */
- const $isPending = useState(() =>
- computed([$items], (items) => items.some((item) => item.status === 'pending' || item.status === 'in_progress'))
- )[0];
-
- /**
- * The currently selected queue item, or null if one is not selected.
- */
- const $selectedItem = useState(() =>
- computed([$items, $selectedItemId], (items, selectedItemId) => {
- if (items.length === 0) {
- return null;
- }
- if (selectedItemId === null) {
- return null;
- }
- return items.find(({ item_id }) => item_id === selectedItemId) ?? null;
- })
- )[0];
-
- /**
- * The currently selected queue item's index in the list of items, or null if one is not selected.
- */
- const $selectedItemIndex = useState(() =>
- computed([$items, $selectedItemId], (items, selectedItemId) => {
- if (items.length === 0) {
- return null;
- }
- if (selectedItemId === null) {
- return null;
- }
- return items.findIndex(({ item_id }) => item_id === selectedItemId) ?? null;
- })
- )[0];
-
- /**
- * The currently selected queue item's output image name, or null if one is not selected or there is no output
- * image recorded.
- */
- const $selectedItemOutputImageDTO = useState(() =>
- computed([$selectedItemId, $progressData], (selectedItemId, progressData) => {
- if (selectedItemId === null) {
- return null;
- }
- const datum = progressData[selectedItemId];
- if (!datum) {
- return null;
- }
- return datum.imageDTO;
- })
- )[0];
-
- /**
- * A redux selector to select all queue items from the RTK Query cache.
- */
- const selectQueueItems = useMemo(() => buildSelectSessionQueueItems(session.id), [session.id]);
-
- const discard = useCallback(
- (itemId: number) => {
- store.dispatch(canvasQueueItemDiscarded({ itemId }));
- },
- [store]
- );
-
- const discardAll = useCallback(() => {
- store.dispatch(canvasSessionReset());
- }, [store]);
+export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildren) => {
+ /**
+ * For best performance and interop with the Canvas, which is outside react but needs to interact with the react
+ * app, all canvas session state is packaged as nanostores atoms. The trickiest part is syncing the queue items
+ * with a nanostores atom.
+ */
+
+ /**
+ * App store
+ */
+ const store = useAppStore();
+
+ const sessionId = useAppSelector(selectCanvasSessionId);
+
+ const socket = useStore($socket);
+
+ /**
+ * Track the last completed item. Used to implement autoswitch.
+ */
+ const $lastCompletedItemId = useState(() => atom(null))[0];
+
+ /**
+ * Manually-synced atom containing queue items for the current session. This is populated from the RTK Query cache
+ * and kept in sync with it via a redux subscription.
+ */
+ const $items = useState(() => atom([]))[0];
+
+ /**
+ * An ephemeral store of progress events and images for all items in the current session.
+ */
+ const $progressData = useState(() => map>({}))[0];
+
+ /**
+ * The currently selected queue item's ID, or null if one is not selected.
+ */
+ const $selectedItemId = useState(() => atom(null))[0];
+
+ /**
+ * The number of items. Computed from the queue items array.
+ */
+ const $itemCount = useState(() => computed([$items], (items) => items.length))[0];
+
+ /**
+ * Whether there are any items. Computed from the queue items array.
+ */
+ const $hasItems = useState(() => computed([$items], (items) => items.length > 0))[0];
+
+ /**
+ * Whether there are any pending or in-progress items. Computed from the queue items array.
+ */
+ const $isPending = useState(() =>
+ computed([$items], (items) => items.some((item) => item.status === 'pending' || item.status === 'in_progress'))
+ )[0];
- const selectNext = useCallback(() => {
- const selectedItemId = $selectedItemId.get();
+ /**
+ * The currently selected queue item, or null if one is not selected.
+ */
+ const $selectedItem = useState(() =>
+ computed([$items, $selectedItemId], (items, selectedItemId) => {
+ if (items.length === 0) {
+ return null;
+ }
if (selectedItemId === null) {
- return;
+ return null;
}
- const items = $items.get();
- const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
- const nextIndex = (currentIndex + 1) % items.length;
- const nextItem = items[nextIndex];
- if (!nextItem) {
- return;
+ return items.find(({ item_id }) => item_id === selectedItemId) ?? null;
+ })
+ )[0];
+
+ /**
+ * The currently selected queue item's index in the list of items, or null if one is not selected.
+ */
+ const $selectedItemIndex = useState(() =>
+ computed([$items, $selectedItemId], (items, selectedItemId) => {
+ if (items.length === 0) {
+ return null;
}
- $selectedItemId.set(nextItem.item_id);
- }, [$items, $selectedItemId]);
+ if (selectedItemId === null) {
+ return null;
+ }
+ return items.findIndex(({ item_id }) => item_id === selectedItemId) ?? null;
+ })
+ )[0];
- const selectPrev = useCallback(() => {
- const selectedItemId = $selectedItemId.get();
+ /**
+ * The currently selected queue item's output image name, or null if one is not selected or there is no output
+ * image recorded.
+ */
+ const $selectedItemOutputImageDTO = useState(() =>
+ computed([$selectedItemId, $progressData], (selectedItemId, progressData) => {
if (selectedItemId === null) {
- return;
+ return null;
}
- const items = $items.get();
- const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
- const prevIndex = (currentIndex - 1 + items.length) % items.length;
- const prevItem = items[prevIndex];
- if (!prevItem) {
- return;
+ const datum = progressData[selectedItemId];
+ if (!datum) {
+ return null;
}
- $selectedItemId.set(prevItem.item_id);
- }, [$items, $selectedItemId]);
+ return datum.imageDTO;
+ })
+ )[0];
+
+ /**
+ * A redux selector to select all queue items from the RTK Query cache.
+ */
+ const selectQueueItems = useMemo(() => buildSelectCanvasQueueItems(sessionId), [sessionId]);
+
+ const discard = useCallback(
+ (itemId: number) => {
+ store.dispatch(canvasQueueItemDiscarded({ itemId }));
+ },
+ [store]
+ );
- const selectFirst = useCallback(() => {
- const items = $items.get();
- const first = items.at(0);
- if (!first) {
+ const discardAll = useCallback(() => {
+ store.dispatch(canvasSessionReset());
+ }, [store]);
+
+ const selectNext = useCallback(() => {
+ const selectedItemId = $selectedItemId.get();
+ if (selectedItemId === null) {
+ return;
+ }
+ const items = $items.get();
+ const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
+ const nextIndex = (currentIndex + 1) % items.length;
+ const nextItem = items[nextIndex];
+ if (!nextItem) {
+ return;
+ }
+ $selectedItemId.set(nextItem.item_id);
+ }, [$items, $selectedItemId]);
+
+ const selectPrev = useCallback(() => {
+ const selectedItemId = $selectedItemId.get();
+ if (selectedItemId === null) {
+ return;
+ }
+ const items = $items.get();
+ const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
+ const prevItem = items[prevIndex];
+ if (!prevItem) {
+ return;
+ }
+ $selectedItemId.set(prevItem.item_id);
+ }, [$items, $selectedItemId]);
+
+ const selectFirst = useCallback(() => {
+ const items = $items.get();
+ const first = items.at(0);
+ if (!first) {
+ return;
+ }
+ $selectedItemId.set(first.item_id);
+ }, [$items, $selectedItemId]);
+
+ const selectLast = useCallback(() => {
+ const items = $items.get();
+ const last = items.at(-1);
+ if (!last) {
+ return;
+ }
+ $selectedItemId.set(last.item_id);
+ }, [$items, $selectedItemId]);
+
+ // Set up socket listeners
+ useEffect(() => {
+ if (!socket) {
+ return;
+ }
+
+ const onProgress = (data: S['InvocationProgressEvent']) => {
+ if (data.destination !== sessionId) {
return;
}
- $selectedItemId.set(first.item_id);
- }, [$items, $selectedItemId]);
+ setProgress($progressData, data);
+ };
- const selectLast = useCallback(() => {
- const items = $items.get();
- const last = items.at(-1);
- if (!last) {
+ const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
+ if (data.destination !== sessionId) {
return;
}
- $selectedItemId.set(last.item_id);
- }, [$items, $selectedItemId]);
-
- const onImageLoad = useCallback(
- (itemId: number) => {
- const progressData = $progressData.get();
- const current = progressData[itemId];
- if (current) {
- const next = { ...current, imageLoaded: true };
- $progressData.setKey(itemId, next);
- } else {
- $progressData.setKey(itemId, {
- ...getInitialProgressData(itemId),
- imageLoaded: true,
- });
- }
- if (
- $lastCompletedItemId.get() === itemId &&
- selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish'
- ) {
- $selectedItemId.set(itemId);
- $lastCompletedItemId.set(null);
- }
- },
- [$lastCompletedItemId, $progressData, $selectedItemId, store]
- );
+ if (data.status === 'completed') {
+ /**
+ * There is an unpleasant bit of indirection here. When an item is completed, and auto-switch is set to
+ * switch_on_finish, we want to load the image and switch to it. In this socket handler, we don't have
+ * access to the full queue item, which we need to get the output image and load it. We get the full
+ * queue items as part of the list query, so it's rather inefficient to fetch it again here.
+ *
+ * To reduce the number of extra network requests, we instead store this item as the last completed item.
+ * Then in the progress data sync effect, we process the queue item load its image.
+ */
+ $lastCompletedItemId.set(data.item_id);
+ }
+ if (data.status === 'in_progress' && selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_start') {
+ $selectedItemId.set(data.item_id);
+ }
+ };
- // Set up socket listeners
- useEffect(() => {
- if (!socket) {
- return;
+ socket.on('invocation_progress', onProgress);
+ socket.on('queue_item_status_changed', onQueueItemStatusChanged);
+
+ return () => {
+ socket.off('invocation_progress', onProgress);
+ socket.off('queue_item_status_changed', onQueueItemStatusChanged);
+ };
+ }, [$progressData, $selectedItemId, sessionId, socket, $lastCompletedItemId, store]);
+
+ // Set up state subscriptions and effects
+ useEffect(() => {
+ let _prevItems: readonly S['SessionQueueItem'][] = [];
+ // Seed the $items atom with the initial query cache state
+ $items.set(selectQueueItems(store.getState()));
+
+ // Manually keep the $items atom in sync as the query cache is updated
+ const unsubReduxSyncToItemsAtom = store.subscribe(() => {
+ const prevItems = $items.get();
+ const items = selectQueueItems(store.getState());
+ if (items !== prevItems) {
+ _prevItems = prevItems;
+ $items.set(items);
}
+ });
- const onProgress = (data: S['InvocationProgressEvent']) => {
- if (data.destination !== session.id) {
- return;
+ // Handle cases that could result in a nonexistent queue item being selected.
+ const unsubEnsureSelectedItemIdExists = effect([$items, $selectedItemId], (items, selectedItemId) => {
+ if (items.length === 0) {
+ // If there are no items, cannot have a selected item.
+ $selectedItemId.set(null);
+ } else if (selectedItemId === null && items.length > 0) {
+ // If there is no selected item but there are items, select the first one.
+ $selectedItemId.set(items[0]?.item_id ?? null);
+ return;
+ } else if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
+ // If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll
+ // the above case, selecting the first item if there are any.
+ let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
+ if (prevIndex >= items.length) {
+ prevIndex = items.length - 1;
}
- setProgress($progressData, data);
- };
+ const nextItem = items[prevIndex];
+ $selectedItemId.set(nextItem?.item_id ?? null);
+ }
- const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
- if (data.destination !== session.id) {
- return;
- }
- if (data.status === 'completed') {
- $lastCompletedItemId.set(data.item_id);
+ if (items !== _prevItems) {
+ _prevItems = items;
+ }
+ });
+
+ // Sync progress data - remove canceled/failed items, update progress data with new images, and load images
+ const unsubSyncProgressData = $items.subscribe(async (items) => {
+ const progressData = $progressData.get();
+
+ const toDelete: number[] = [];
+ const toUpdate: ProgressData[] = [];
+
+ for (const [id, datum] of objectEntries(progressData)) {
+ if (!datum) {
+ toDelete.push(id);
+ continue;
}
- if (data.status === 'in_progress') {
- $lastStartedItemId.set(data.item_id);
+ const item = items.find(({ item_id }) => item_id === datum.itemId);
+ if (!item) {
+ toDelete.push(datum.itemId);
+ } else if (item.status === 'canceled' || item.status === 'failed') {
+ toUpdate.push({
+ ...datum,
+ progressEvent: null,
+ progressImage: null,
+ imageDTO: null,
+ });
}
- };
-
- socket.on('invocation_progress', onProgress);
- socket.on('queue_item_status_changed', onQueueItemStatusChanged);
-
- return () => {
- socket.off('invocation_progress', onProgress);
- socket.off('queue_item_status_changed', onQueueItemStatusChanged);
- };
- }, [$lastCompletedItemId, $lastStartedItemId, $progressData, $selectedItemId, session.id, socket]);
-
- // Set up state subscriptions and effects
- useEffect(() => {
- let _prevItems: readonly S['SessionQueueItem'][] = [];
- // Seed the $items atom with the initial query cache state
- $items.set(selectQueueItems(store.getState()));
-
- // Manually keep the $items atom in sync as the query cache is updated
- const unsubReduxSyncToItemsAtom = store.subscribe(() => {
- const prevItems = $items.get();
- const items = selectQueueItems(store.getState());
- if (items !== prevItems) {
- _prevItems = prevItems;
- $items.set(items);
+ }
+
+ for (const item of items) {
+ const datum = progressData[item.item_id];
+
+ if (datum?.imageDTO) {
+ continue;
}
- });
-
- // Handle cases that could result in a nonexistent queue item being selected.
- const unsubEnsureSelectedItemIdExists = effect(
- [$items, $selectedItemId, $lastStartedItemId],
- (items, selectedItemId, lastStartedItemId) => {
- if (items.length === 0) {
- // If there are no items, cannot have a selected item.
- $selectedItemId.set(null);
- } else if (selectedItemId === null && items.length > 0) {
- // If there is no selected item but there are items, select the first one.
- $selectedItemId.set(items[0]?.item_id ?? null);
- return;
- } else if (
- selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_start' &&
- items.findIndex(({ item_id }) => item_id === lastStartedItemId) !== -1
- ) {
- $selectedItemId.set(lastStartedItemId);
- $lastStartedItemId.set(null);
- } else if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
- // If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll
- // the above case, selecting the first item if there are any.
- let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
- if (prevIndex >= items.length) {
- prevIndex = items.length - 1;
- }
- const nextItem = items[prevIndex];
- $selectedItemId.set(nextItem?.item_id ?? null);
- }
-
- if (items !== _prevItems) {
- _prevItems = items;
- }
+ const outputImageName = getOutputImageName(item);
+ if (!outputImageName) {
+ continue;
}
- );
-
- // Clean up the progress data when a queue item is discarded.
- const unsubCleanUpProgressData = $items.subscribe(async (items) => {
- const progressData = $progressData.get();
-
- const toDelete: number[] = [];
- const toUpdate: ProgressData[] = [];
-
- for (const [id, datum] of objectEntries(progressData)) {
- if (!datum) {
- toDelete.push(id);
- continue;
- }
- const item = items.find(({ item_id }) => item_id === datum.itemId);
- if (!item) {
- toDelete.push(datum.itemId);
- } else if (item.status === 'canceled' || item.status === 'failed') {
- toUpdate.push({
- ...datum,
- progressEvent: null,
- progressImage: null,
- imageDTO: null,
- });
- }
+ const imageDTO = await getImageDTOSafe(outputImageName);
+ if (!imageDTO) {
+ continue;
}
- for (const item of items) {
- const datum = progressData[item.item_id];
-
- if (datum) {
- if (datum.imageDTO) {
- continue;
- }
- const outputImageName = getOutputImageName(item);
- if (!outputImageName) {
- continue;
- }
- const imageDTO = await getImageDTOSafe(outputImageName);
- if (!imageDTO) {
- continue;
- }
- toUpdate.push({
- ...datum,
- imageDTO,
- });
- } else {
- const outputImageName = getOutputImageName(item);
- if (!outputImageName) {
- continue;
- }
- const imageDTO = await getImageDTOSafe(outputImageName);
- if (!imageDTO) {
- continue;
- }
- toUpdate.push({
- ...getInitialProgressData(item.item_id),
- imageDTO,
- });
- }
+ // This is the load logic mentioned in the comment in the QueueItemStatusChangedEvent handler above.
+ if (
+ $lastCompletedItemId.get() === item.item_id &&
+ selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish'
+ ) {
+ loadImage(imageDTO.image_url, true).then(() => {
+ $selectedItemId.set(item.item_id);
+ $lastCompletedItemId.set(null);
+ });
}
- for (const itemId of toDelete) {
- $progressData.setKey(itemId, undefined);
- }
+ toUpdate.push({
+ ...getInitialProgressData(item.item_id),
+ ...datum,
+ imageDTO,
+ });
+ }
- for (const datum of toUpdate) {
- $progressData.setKey(datum.itemId, datum);
- }
- });
-
- // We only want to auto-switch to completed queue items once their images have fully loaded to prevent flashes
- // of fallback content and/or progress images. The only surefire way to determine when images have fully loaded
- // is via the image elements' `onLoad` callback. Images set `$lastLoadedItemId` to their queue item ID in their
- // `onLoad` handler, and we listen for that here. If auto-switch is enabled, we then switch the to the item.
- //
- // TODO: This isn't perfect... we set $lastLoadedItemId in the mini preview component, but the full view
- // component still needs to retrieve the image from the browser cache... can result in a flash of the progress
- // image...
- const unsubHandleAutoSwitch = $lastLoadedItemId.listen((lastLoadedItemId) => {
- if (lastLoadedItemId === null) {
- return;
- }
- if (selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish') {
- $selectedItemId.set(lastLoadedItemId);
- }
- $lastLoadedItemId.set(null);
- });
-
- // Create an RTK Query subscription. Without this, the query cache selector will never return anything bc RTK
- // doesn't know we care about it.
- const { unsubscribe: unsubQueueItemsQuery } = store.dispatch(
- queueApi.endpoints.listAllQueueItems.initiate({ destination: session.id })
- );
-
- // const unsubListener = store.dispatch(
- // addAppListener({
- // matcher: queueApi.endpoints.cancelQueueItem.matchFulfilled,
- // effect: ({ payload }, { getState }) => {
- // const { item_id } = payload;
-
- // const items = selectQueueItems(getState());
- // if (items.length === 0) {
- // $selectedItemId.set(null);
- // } else if ($selectedItemId.get() === null) {
- // $selectedItemId.set(items[0].item_id);
- // }
- // },
- // })
- // );
-
- // Clean up all subscriptions and top-level (i.e. non-computed/derived state)
- return () => {
- unsubHandleAutoSwitch();
- unsubQueueItemsQuery();
- unsubReduxSyncToItemsAtom();
- unsubEnsureSelectedItemIdExists();
- unsubCleanUpProgressData();
- $items.set([]);
- $progressData.set({});
- $selectedItemId.set(null);
- };
- }, [
+ for (const itemId of toDelete) {
+ $progressData.setKey(itemId, undefined);
+ }
+
+ for (const datum of toUpdate) {
+ $progressData.setKey(datum.itemId, datum);
+ }
+ });
+
+ // Create an RTK Query subscription. Without this, the query cache selector will never return anything bc RTK
+ // doesn't know we care about it.
+ const { unsubscribe: unsubQueueItemsQuery } = store.dispatch(
+ queueApi.endpoints.listAllQueueItems.initiate({ destination: sessionId })
+ );
+
+ // Clean up all subscriptions and top-level (i.e. non-computed/derived state)
+ return () => {
+ unsubQueueItemsQuery();
+ unsubReduxSyncToItemsAtom();
+ unsubEnsureSelectedItemIdExists();
+ unsubSyncProgressData();
+ $items.set([]);
+ $progressData.set({});
+ $selectedItemId.set(null);
+ };
+ }, [$items, $progressData, $selectedItemId, selectQueueItems, sessionId, store, $lastCompletedItemId]);
+
+ const value = useMemo(
+ () => ({
$items,
- $lastLoadedItemId,
- $lastStartedItemId,
+ $hasItems,
+ $isPending,
$progressData,
$selectedItemId,
- selectQueueItems,
- session.id,
- store,
- ]);
-
- const value = useMemo(
- () => ({
- session,
- $items,
- $hasItems,
- $isPending,
- $progressData,
- $selectedItemId,
- $selectedItem,
- $selectedItemIndex,
- $selectedItemOutputImageDTO,
- $itemCount,
- selectNext,
- selectPrev,
- selectFirst,
- selectLast,
- onImageLoad,
- discard,
- discardAll,
- }),
- [
- $items,
- $hasItems,
- $isPending,
- $progressData,
- $selectedItem,
- $selectedItemId,
- $selectedItemIndex,
- session,
- $selectedItemOutputImageDTO,
- $itemCount,
- selectNext,
- selectPrev,
- selectFirst,
- selectLast,
- onImageLoad,
- discard,
- discardAll,
- ]
- );
+ $selectedItem,
+ $selectedItemIndex,
+ $selectedItemOutputImageDTO,
+ $itemCount,
+ selectNext,
+ selectPrev,
+ selectFirst,
+ selectLast,
+ discard,
+ discardAll,
+ }),
+ [
+ $items,
+ $hasItems,
+ $isPending,
+ $progressData,
+ $selectedItem,
+ $selectedItemId,
+ $selectedItemIndex,
+ $selectedItemOutputImageDTO,
+ $itemCount,
+ selectNext,
+ selectPrev,
+ selectFirst,
+ selectLast,
+ discard,
+ discardAll,
+ ]
+ );
- return {children};
- }
-);
+ return {children};
+});
CanvasSessionContextProvider.displayName = 'CanvasSessionContextProvider';
export const useCanvasSessionContext = () => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
index 9d930b9ee1c..2161b3f1389 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
@@ -5,7 +5,7 @@ import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
-import { canvasSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionReset, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageNameToImageObject } from 'features/controlLayers/store/util';
@@ -19,6 +19,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
const ctx = useCanvasSessionContext();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
+ const canvasSessionId = useAppSelector(selectCanvasSessionId);
const bboxRect = useAppSelector(selectBboxRect);
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
@@ -41,14 +42,14 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
dispatch(canvasSessionReset());
- cancelQueueItemsByDestination.trigger(ctx.session.id, { withToast: false });
+ cancelQueueItemsByDestination.trigger(canvasSessionId, { withToast: false });
}, [
selectedItemImageDTO,
bboxRect,
dispatch,
selectedEntityIdentifier?.type,
cancelQueueItemsByDestination,
- ctx.session.id,
+ canvasSessionId,
]);
useHotkeys(
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
index 1a76f2cdde1..0c5f94206e2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
@@ -1,5 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useCancelQueueItemsByDestination } from 'features/queue/hooks/useCancelQueueItemsByDestination';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -9,11 +11,12 @@ export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisa
const ctx = useCanvasSessionContext();
const { t } = useTranslation();
const cancelQueueItemsByDestination = useCancelQueueItemsByDestination();
+ const canvasSessionId = useAppSelector(selectCanvasSessionId);
const discardAll = useCallback(() => {
ctx.discardAll();
- cancelQueueItemsByDestination.trigger(ctx.session.id, { withToast: false });
- }, [cancelQueueItemsByDestination, ctx]);
+ cancelQueueItemsByDestination.trigger(canvasSessionId, { withToast: false });
+ }, [cancelQueueItemsByDestination, ctx, canvasSessionId]);
return (
{
- if (selectedEntityIdentifier === null || isBusy || canvasRightPanelTab !== 'layers') {
+ if (selectedEntityIdentifier === null || isBusy || getFocusedRegion() !== 'layers') {
return;
}
dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier }));
- }, [canvasRightPanelTab, dispatch, isBusy, selectedEntityIdentifier]);
+ }, [dispatch, isBusy, selectedEntityIdentifier]);
useRegisteredHotkeys({
id: 'deleteSelected',
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts
index 9ca0e7772a6..6c55e949377 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts
@@ -16,7 +16,6 @@ import {
selectIsolatedLayerPreview,
selectIsolatedStagingPreview,
} from 'features/controlLayers/store/canvasSettingsSlice';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
buildSelectIsSelected,
getSelectIsTypeHidden,
@@ -283,7 +282,7 @@ export abstract class CanvasEntityAdapterBase {
- this.$isStaging.set(isStaging);
+ this.$isStaging.listen((isStaging, oldIsStaging) => {
if (isStaging && !oldIsStaging) {
this.$shouldShowStagedImage.set(true);
}
@@ -150,15 +147,17 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
initialize = () => {
this.log.debug('Initializing module');
this.render();
- this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
};
connectToSession = (
+ $items: Atom,
$selectedItemId: Atom,
- $progressData: ProgressDataMap,
- $isPending: Atom
+ $progressData: ProgressDataMap
) => {
- const cb = (selectedItemId: number | null, progressData: Record) => {
+ const imageSrcListener = (
+ selectedItemId: number | null,
+ progressData: Record
+ ) => {
if (!selectedItemId) {
this.$imageSrc.set(null);
return;
@@ -176,20 +175,30 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
this.$imageSrc.set(null);
}
};
+ const unsubImageSrc = effect([$selectedItemId, $progressData], imageSrcListener);
- // Run the effect & forcibly render once to initialize
- cb($selectedItemId.get(), $progressData.get());
- this.render();
+ const isPendingListener = (items: S['SessionQueueItem'][]) => {
+ this.$isPending.set(items.some((item) => item.status === 'pending' || item.status === 'in_progress'));
+ };
+ const unsubIsPending = effect([$items], isPendingListener);
- // Sync the $isPending flag with the computed
- const unsubIsPending = effect([$isPending], (isPending) => {
- this.$isPending.set(isPending);
- });
+ const isStagingListener = (items: S['SessionQueueItem'][]) => {
+ this.$isStaging.set(items.length > 0);
+ };
+ const unsubIsStaging = effect([$items], isStagingListener);
- const unsubImageSrc = effect([$selectedItemId, $progressData], cb);
+ // Run the effects & forcibly render once to initialize
+ isStagingListener($items.get());
+ isPendingListener($items.get());
+ imageSrcListener($selectedItemId.get(), $progressData.get());
+ this.render();
return () => {
+ this.$isStaging.set(false);
+ unsubIsStaging();
+ this.$isPending.set(false);
unsubIsPending();
+ this.$imageSrc.set(null);
unsubImageSrc();
};
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index aab222395cb..b00d8f100f6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -1,19 +1,21 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { EMPTY_ARRAY } from 'app/store/constants';
import type { PersistConfig, RootState } from 'app/store/store';
+import { useAppSelector } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
-import { canvasReset } from 'features/controlLayers/store/actions';
+import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { useMemo } from 'react';
import { queueApi } from 'services/api/endpoints/queue';
type CanvasStagingAreaState = {
- generateSessionId: string | null;
- canvasSessionId: string | null;
+ _version: 1;
+ canvasSessionId: string;
canvasDiscardedQueueItems: number[];
};
const INITIAL_STATE: CanvasStagingAreaState = {
- generateSessionId: null,
- canvasSessionId: null,
+ _version: 1,
+ canvasSessionId: getPrefixedId('canvas'),
canvasDiscardedQueueItems: [],
};
@@ -23,46 +25,38 @@ export const canvasSessionSlice = createSlice({
name: 'canvasSession',
initialState: getInitialState(),
reducers: {
- generateSessionIdChanged: (state, action: PayloadAction<{ id: string }>) => {
- const { id } = action.payload;
- state.generateSessionId = id;
- },
- generateSessionReset: (state) => {
- state.generateSessionId = null;
- },
canvasQueueItemDiscarded: (state, action: PayloadAction<{ itemId: number }>) => {
const { itemId } = action.payload;
if (!state.canvasDiscardedQueueItems.includes(itemId)) {
state.canvasDiscardedQueueItems.push(itemId);
}
},
- canvasSessionIdChanged: (state, action: PayloadAction<{ id: string }>) => {
- const { id } = action.payload;
- state.canvasSessionId = id;
- state.canvasDiscardedQueueItems = [];
- },
- canvasSessionReset: (state) => {
- state.canvasSessionId = null;
- state.canvasDiscardedQueueItems = [];
+ canvasSessionReset: {
+ reducer: (state, action: PayloadAction<{ canvasSessionId: string }>) => {
+ const { canvasSessionId } = action.payload;
+ state.canvasSessionId = canvasSessionId;
+ state.canvasDiscardedQueueItems = [];
+ },
+ prepare: () => {
+ return {
+ payload: {
+ canvasSessionId: getPrefixedId('canvas'),
+ },
+ };
+ },
},
},
- extraReducers(builder) {
- builder.addCase(canvasReset, (state) => {
- state.canvasSessionId = null;
- });
- },
});
-export const {
- generateSessionIdChanged,
- generateSessionReset,
- canvasSessionIdChanged,
- canvasSessionReset,
- canvasQueueItemDiscarded,
-} = canvasSessionSlice.actions;
+export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
+ if (!('_version' in state)) {
+ state._version = 1;
+ state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas');
+ }
+
return state;
};
@@ -74,13 +68,14 @@ export const canvasStagingAreaPersistConfig: PersistConfig s[canvasSessionSlice.name];
-
export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ canvasSessionId }) => canvasSessionId);
-export const selectGenerateSessionId = createSelector(
+
+const selectDiscardedItems = createSelector(
selectCanvasSessionSlice,
- ({ generateSessionId }) => generateSessionId
+ ({ canvasDiscardedQueueItems }) => canvasDiscardedQueueItems
);
-export const buildSelectSessionQueueItems = (sessionId: string) =>
+
+export const buildSelectCanvasQueueItems = (sessionId: string) =>
createSelector(
[queueApi.endpoints.listAllQueueItems.select({ destination: sessionId }), selectDiscardedItems],
({ data }, discardedItems) => {
@@ -93,21 +88,12 @@ export const buildSelectSessionQueueItems = (sessionId: string) =>
}
);
-export const selectIsStaging = (state: RootState) => {
- const sessionId = selectCanvasSessionId(state);
- if (!sessionId) {
- return false;
- }
- const { data } = queueApi.endpoints.listAllQueueItems.select({ destination: sessionId })(state);
- if (!data) {
- return false;
- }
- const discardedItems = selectDiscardedItems(state);
- return data.some(
- ({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id)
- );
+export const buildSelectIsStaging = (sessionId: string) =>
+ createSelector([buildSelectCanvasQueueItems(sessionId)], (queueItems) => {
+ return queueItems.length > 0;
+ });
+export const useCanvasIsStaging = () => {
+ const sessionId = useAppSelector(selectCanvasSessionId);
+ const selector = useMemo(() => buildSelectIsStaging(sessionId), [sessionId]);
+ return useAppSelector(selector);
};
-const selectDiscardedItems = createSelector(
- selectCanvasSessionSlice,
- ({ canvasDiscardedQueueItems }) => canvasDiscardedQueueItems
-);
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallAll.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallAll.ts
index 09cbbb338bb..96f728734bd 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallAll.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallAll.ts
@@ -1,5 +1,5 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
@@ -12,7 +12,7 @@ export const useRecallAll = (imageDTO: ImageDTO) => {
const store = useAppStore();
const tab = useAppSelector(selectActiveTab);
const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name);
- const isStaging = useAppSelector(selectIsStaging);
+ const isStaging = useCanvasIsStaging();
const clearStylePreset = useClearStylePresetWithToast();
const isEnabled = useMemo(() => {
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts
index cff4c036f92..030976fd9f4 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts
@@ -1,5 +1,5 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
@@ -8,7 +8,7 @@ import type { ImageDTO } from 'services/api/types';
export const useRecallDimensions = (imageDTO: ImageDTO) => {
const store = useAppStore();
const tab = useAppSelector(selectActiveTab);
- const isStaging = useAppSelector(selectIsStaging);
+ const isStaging = useCanvasIsStaging();
const isEnabled = useMemo(() => {
if (tab !== 'canvas' && tab !== 'generate') {
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts
index 9be1b6320e4..43116292e5d 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts
@@ -1,5 +1,5 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
@@ -11,7 +11,7 @@ import { useClearStylePresetWithToast } from './useClearStylePresetWithToast';
export const useRecallRemix = (imageDTO: ImageDTO) => {
const store = useAppStore();
const tab = useAppSelector(selectActiveTab);
- const isStaging = useAppSelector(selectIsStaging);
+ const isStaging = useCanvasIsStaging();
const clearStylePreset = useClearStylePresetWithToast();
const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name);
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
index 86a401b1991..e8bb6ff5ae0 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectSaveAllImagesToGallery } from 'features/controlLayers/store/canvasSettingsSlice';
+import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
selectImg2imgStrength,
selectMainModelConfig,
@@ -42,9 +43,8 @@ export const getBoardField = (state: RootState): BoardField | undefined => {
* - board
*/
export const selectCanvasOutputFields = (state: RootState) => {
- // Advanced session means working on canvas - images are not saved to gallery or added to a board.
- // Simple session means working in YOLO mode - images are saved to gallery & board.
const tab = selectActiveTab(state);
+ // This flag also has an effect on the canvas destination - see selectCanvasDestination below.
const saveAllImagesToGallery = selectSaveAllImagesToGallery(state);
// If we're on canvas and the save all images setting is enabled, save to gallery
@@ -59,6 +59,23 @@ export const selectCanvasOutputFields = (state: RootState) => {
};
};
+/**
+ * Select the destination to use for canvas queue items.
+ *
+ */
+export const selectCanvasDestination = (state: RootState) => {
+ // The canvas will stage images that have its session ID as the destination. When the user has enabled saving all
+ // images to gallery, we want to bypass the staging area. So we use 'canvas' as a generic destination. Images will
+ // go directly to the gallery.
+ //
+ // This flag also has an effect on the canvas output fields - see selectCanvasOutputFields above.
+ const saveAllImagesToGallery = selectSaveAllImagesToGallery(state);
+ if (saveAllImagesToGallery) {
+ return 'canvas';
+ }
+ return selectCanvasSessionId(state);
+};
+
/**
* Gets the prompts, modified for the active style preset.
*/
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
index c2c2cc30fb6..40145839085 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
@@ -2,7 +2,7 @@ import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
selectIsChatGPT4o,
selectIsFluxKontext,
@@ -26,7 +26,7 @@ export const BboxAspectRatioSelect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const id = useAppSelector(selectAspectRatioID);
- const isStaging = useAppSelector(selectIsStaging);
+ const isStaging = useCanvasIsStaging();
const isImagen3 = useAppSelector(selectIsImagen3);
const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
const isImagen4 = useAppSelector(selectIsImagen4);
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx
index a516cd27a96..54614419a57 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx
@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useAppDispatch } from 'app/store/storeHooks';
import { bboxDimensionsSwapped } from 'features/controlLayers/store/canvasSlice';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsDownUpBold } from 'react-icons/pi';
@@ -9,7 +9,7 @@ import { PiArrowsDownUpBold } from 'react-icons/pi';
export const BboxSwapDimensionsButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const isStaging = useAppSelector(selectIsStaging);
+ const isStaging = useCanvasIsStaging();
const onClick = useCallback(() => {
dispatch(bboxDimensionsSwapped());
}, [dispatch]);
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts b/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts
index b0c57b90f7b..6db1a3e1055 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts
@@ -1,9 +1,8 @@
-import { useAppSelector } from 'app/store/storeHooks';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
export const useIsBboxSizeLocked = () => {
- const isStaging = useAppSelector(selectIsStaging);
+ const isStaging = useCanvasIsStaging();
const isApiModel = useIsApiModel();
return isApiModel || isStaging;
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemDestination.ts b/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemDestination.ts
new file mode 100644
index 00000000000..5a06a8e9fb2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemDestination.ts
@@ -0,0 +1,11 @@
+import { useGetCurrentQueueItemQuery } from 'services/api/endpoints/queue';
+
+export const useCurrentQueueItemDestination = () => {
+ const { currentQueueItemDestination } = useGetCurrentQueueItemQuery(undefined, {
+ selectFromResult: ({ data }) => ({
+ currentQueueItemDestination: data?.destination ?? null,
+ }),
+ });
+
+ return currentQueueItemDestination;
+};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts b/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts
index daa3a82704a..0ce503ce475 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts
@@ -1,9 +1,9 @@
-import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
+import { useGetCurrentQueueItemQuery } from 'services/api/endpoints/queue';
export const useCurrentQueueItemId = () => {
- const { currentQueueItemId } = useGetQueueStatusQuery(undefined, {
+ const { currentQueueItemId } = useGetCurrentQueueItemQuery(undefined, {
selectFromResult: ({ data }) => ({
- currentQueueItemId: data?.queue.item_id ?? null,
+ currentQueueItemId: data?.item_id ?? null,
}),
});
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
index ffbecba7243..99c8ba7bfc2 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
@@ -7,8 +7,6 @@ import { extractMessageFromAssertionError } from 'common/util/extractMessageFrom
import { withResult, withResultAsync } from 'common/util/result';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
-import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { canvasSessionIdChanged, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
@@ -19,6 +17,7 @@ import { buildImagen4Graph } from 'features/nodes/util/graph/generation/buildIma
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph';
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
+import { selectCanvasDestination } from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderArg } from 'features/nodes/util/graph/types';
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { toast } from 'features/toast/toast';
@@ -37,11 +36,7 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep
const state = getState();
- let destination = selectCanvasSessionId(state);
- if (destination === null) {
- destination = getPrefixedId('canvas');
- dispatch(canvasSessionIdChanged({ id: destination }));
- }
+ const destination = selectCanvasDestination(state);
const buildGraphResult = await withResultAsync(async () => {
const model = state.params.model;
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
index ec6d024888a..fd5056ed41c 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
@@ -5,8 +5,6 @@ import type { AppStore } from 'app/store/store';
import { useAppStore } from 'app/store/storeHooks';
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
import { withResult, withResultAsync } from 'common/util/result';
-import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { generateSessionIdChanged, selectGenerateSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
@@ -36,11 +34,7 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => {
const state = getState();
- let destination = selectGenerateSessionId(state);
- if (destination === null) {
- destination = getPrefixedId('generate');
- dispatch(generateSessionIdChanged({ id: destination }));
- }
+ const destination = 'generate';
const buildGraphResult = await withResultAsync(async () => {
const model = state.params.model;
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts
index 668b9d44401..24c2f355107 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts
@@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react';
import { logger } from 'app/logging/logger';
import { useAppSelector } from 'app/store/storeHooks';
import { withResultAsync } from 'common/util/result';
+import { selectSaveAllImagesToGallery } from 'features/controlLayers/store/canvasSettingsSlice';
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows';
import { $isReadyToEnqueue } from 'features/queue/store/readiness';
@@ -26,6 +27,7 @@ export const useInvoke = () => {
const enqueueCanvas = useEnqueueCanvas();
const enqueueGenerate = useEnqueueGenerate();
const enqueueUpscaling = useEnqueueUpscaling();
+ const saveAllImagesToGallery = useAppSelector(selectSaveAllImagesToGallery);
const [_, { isLoading }] = useEnqueueBatchMutation({
...enqueueMutationFixedCacheKeyOptions,
@@ -62,7 +64,7 @@ export const useInvoke = () => {
const enqueueBack = useCallback(() => {
enqueue(false, false);
- if (tabName === 'generate' || tabName === 'upscaling') {
+ if (tabName === 'generate' || tabName === 'upscaling' || (tabName === 'canvas' && saveAllImagesToGallery)) {
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
} else if (tabName === 'workflows') {
// Only switch to viewer if the workflow editor is not currently active
@@ -73,11 +75,11 @@ export const useInvoke = () => {
} else if (tabName === 'canvas') {
navigationApi.focusPanel(tabName, WORKSPACE_PANEL_ID);
}
- }, [enqueue, tabName]);
+ }, [enqueue, saveAllImagesToGallery, tabName]);
const enqueueFront = useCallback(() => {
enqueue(true, false);
- if (tabName === 'generate' || tabName === 'upscaling') {
+ if (tabName === 'generate' || tabName === 'upscaling' || (tabName === 'canvas' && saveAllImagesToGallery)) {
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
} else if (tabName === 'workflows') {
// Only switch to viewer if the workflow editor is not currently active
@@ -88,7 +90,7 @@ export const useInvoke = () => {
} else if (tabName === 'canvas') {
navigationApi.focusPanel(tabName, WORKSPACE_PANEL_ID);
}
- }, [enqueue, tabName]);
+ }, [enqueue, saveAllImagesToGallery, tabName]);
return { enqueueBack, enqueueFront, isLoading, isDisabled: !isReady || isLocked, enqueue };
};
diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx
index 33954786961..de1993f053d 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx
@@ -17,7 +17,6 @@ import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasT
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
-import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
@@ -50,7 +49,6 @@ const canvasBgSx = {
export const CanvasWorkspacePanel = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
- const canvasId = useAppSelector(selectCanvasSessionId);
const renderMenu = useCallback(() => {
return ;
@@ -87,9 +85,9 @@ export const CanvasWorkspacePanel = memo(() => {
alignItems="flex-start"
>
{showHUD && }
+
-
@@ -103,13 +101,11 @@ export const CanvasWorkspacePanel = memo(() => {
)}
- {canvasId !== null && (
-
-
-
-
-
- )}
+
+
+
+
+
diff --git a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/DockviewTab.tsx
similarity index 86%
rename from invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
rename to invokeai/frontend/web/src/features/ui/layouts/DockviewTab.tsx
index 87021a4b917..61afb0fbd92 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/DockviewTab.tsx
@@ -7,7 +7,7 @@ import { memo, useCallback, useRef } from 'react';
import type { PanelParameters } from './auto-layout-context';
import { useHackOutDvTabDraggable } from './use-hack-out-dv-tab-draggable';
-export const TabWithoutCloseButton = memo((props: IDockviewPanelHeaderProps) => {
+export const DockviewTab = memo((props: IDockviewPanelHeaderProps) => {
const ref = useRef(null);
const setActive = useCallback(() => {
if (!props.api.isActive) {
@@ -31,4 +31,4 @@ export const TabWithoutCloseButton = memo((props: IDockviewPanelHeaderProps
);
});
-TabWithoutCloseButton.displayName = 'TabWithoutCloseButton';
+DockviewTab.displayName = 'DockviewTab';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasViewer.tsx b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasViewer.tsx
new file mode 100644
index 00000000000..490c012edde
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasViewer.tsx
@@ -0,0 +1,43 @@
+import { Flex, Text } from '@invoke-ai/ui-library';
+import { setFocusedRegion } from 'common/hooks/focus';
+import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
+import type { IDockviewPanelHeaderProps } from 'dockview';
+import { useCurrentQueueItemDestination } from 'features/queue/hooks/useCurrentQueueItemDestination';
+import ProgressBar from 'features/system/components/ProgressBar';
+import { memo, useCallback, useRef } from 'react';
+import { useIsGenerationInProgress } from 'services/api/endpoints/queue';
+
+import type { PanelParameters } from './auto-layout-context';
+import { useHackOutDvTabDraggable } from './use-hack-out-dv-tab-draggable';
+
+export const DockviewTabCanvasViewer = memo((props: IDockviewPanelHeaderProps) => {
+ const isGenerationInProgress = useIsGenerationInProgress();
+ const currentQueueItemDestination = useCurrentQueueItemDestination();
+
+ const ref = useRef(null);
+ const setActive = useCallback(() => {
+ if (!props.api.isActive) {
+ props.api.setActive();
+ }
+ }, [props.api]);
+
+ useCallbackOnDragEnter(setActive, ref, 300);
+
+ const onPointerDown = useCallback(() => {
+ setFocusedRegion(props.params.focusRegion);
+ }, [props.params.focusRegion]);
+
+ useHackOutDvTabDraggable(ref);
+
+ return (
+
+
+ {props.api.title ?? props.api.id}
+
+ {currentQueueItemDestination === 'canvas' && isGenerationInProgress && (
+
+ )}
+
+ );
+});
+DockviewTabCanvasViewer.displayName = 'DockviewTabCanvasViewer';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx
new file mode 100644
index 00000000000..3856f54c8a7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx
@@ -0,0 +1,46 @@
+import { Flex, Text } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { setFocusedRegion } from 'common/hooks/focus';
+import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
+import type { IDockviewPanelHeaderProps } from 'dockview';
+import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCurrentQueueItemDestination } from 'features/queue/hooks/useCurrentQueueItemDestination';
+import ProgressBar from 'features/system/components/ProgressBar';
+import { memo, useCallback, useRef } from 'react';
+import { useIsGenerationInProgress } from 'services/api/endpoints/queue';
+
+import type { PanelParameters } from './auto-layout-context';
+import { useHackOutDvTabDraggable } from './use-hack-out-dv-tab-draggable';
+
+export const DockviewTabCanvasWorkspace = memo((props: IDockviewPanelHeaderProps) => {
+ const isGenerationInProgress = useIsGenerationInProgress();
+ const canvasSessionId = useAppSelector(selectCanvasSessionId);
+ const currentQueueItemDestination = useCurrentQueueItemDestination();
+
+ const ref = useRef(null);
+ const setActive = useCallback(() => {
+ if (!props.api.isActive) {
+ props.api.setActive();
+ }
+ }, [props.api]);
+
+ useCallbackOnDragEnter(setActive, ref, 300);
+
+ const onPointerDown = useCallback(() => {
+ setFocusedRegion(props.params.focusRegion);
+ }, [props.params.focusRegion]);
+
+ useHackOutDvTabDraggable(ref);
+
+ return (
+
+
+ {props.api.title ?? props.api.id}
+
+ {currentQueueItemDestination === canvasSessionId && isGenerationInProgress && (
+
+ )}
+
+ );
+});
+DockviewTabCanvasWorkspace.displayName = 'DockviewTabCanvasWorkspace';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/TabWithLaunchpadIcon.tsx b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabLaunchpad.tsx
similarity index 93%
rename from invokeai/frontend/web/src/features/ui/layouts/TabWithLaunchpadIcon.tsx
rename to invokeai/frontend/web/src/features/ui/layouts/DockviewTabLaunchpad.tsx
index 0dc960fb302..cb2a68d344f 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/TabWithLaunchpadIcon.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabLaunchpad.tsx
@@ -27,7 +27,7 @@ const TAB_ICONS: Record = {
queue: PiQueueBold,
};
-export const TabWithLaunchpadIcon = memo((props: IDockviewPanelHeaderProps) => {
+export const DockviewTabLaunchpad = memo((props: IDockviewPanelHeaderProps) => {
const ref = useRef(null);
const activeTab = useAppSelector(selectActiveTab);
@@ -52,4 +52,4 @@ export const TabWithLaunchpadIcon = memo((props: IDockviewPanelHeaderProps) => {
);
});
-TabWithLaunchpadIcon.displayName = 'TabWithLaunchpadIcon';
+DockviewTabLaunchpad.displayName = 'DockviewTabLaunchpad';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabProgress.tsx b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabProgress.tsx
new file mode 100644
index 00000000000..b681365a515
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabProgress.tsx
@@ -0,0 +1,41 @@
+import { Flex, Text } from '@invoke-ai/ui-library';
+import { setFocusedRegion } from 'common/hooks/focus';
+import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
+import type { IDockviewPanelHeaderProps } from 'dockview';
+import ProgressBar from 'features/system/components/ProgressBar';
+import { memo, useCallback, useRef } from 'react';
+import { useIsGenerationInProgress } from 'services/api/endpoints/queue';
+
+import type { PanelParameters } from './auto-layout-context';
+import { useHackOutDvTabDraggable } from './use-hack-out-dv-tab-draggable';
+
+export const DockviewTabProgress = memo((props: IDockviewPanelHeaderProps) => {
+ const isGenerationInProgress = useIsGenerationInProgress();
+
+ const ref = useRef(null);
+ const setActive = useCallback(() => {
+ if (!props.api.isActive) {
+ props.api.setActive();
+ }
+ }, [props.api]);
+
+ useCallbackOnDragEnter(setActive, ref, 300);
+
+ const onPointerDown = useCallback(() => {
+ setFocusedRegion(props.params.focusRegion);
+ }, [props.params.focusRegion]);
+
+ useHackOutDvTabDraggable(ref);
+
+ return (
+
+
+ {props.api.title ?? props.api.id}
+
+ {isGenerationInProgress && (
+
+ )}
+
+ );
+});
+DockviewTabProgress.displayName = 'DockviewTabProgress';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx b/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx
index 822e5c32496..6b24dfef44d 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx
@@ -1,12 +1,11 @@
import { Flex } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo } from 'react';
export const StagingArea = memo(() => {
- const isStaging = useAppSelector(selectIsStaging);
+ const isStaging = useCanvasIsStaging();
if (!isStaging) {
return null;
diff --git a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButtonAndWithProgressIndicator.tsx b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButtonAndWithProgressIndicator.tsx
deleted file mode 100644
index 7433bb317f2..00000000000
--- a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButtonAndWithProgressIndicator.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Flex, Text } from '@invoke-ai/ui-library';
-import { setFocusedRegion } from 'common/hooks/focus';
-import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
-import type { IDockviewPanelHeaderProps } from 'dockview';
-import ProgressBar from 'features/system/components/ProgressBar';
-import { memo, useCallback, useRef } from 'react';
-import { useIsGenerationInProgress } from 'services/api/endpoints/queue';
-
-import type { PanelParameters } from './auto-layout-context';
-import { useHackOutDvTabDraggable } from './use-hack-out-dv-tab-draggable';
-
-export const TabWithoutCloseButtonAndWithProgressIndicator = memo(
- (props: IDockviewPanelHeaderProps) => {
- const isGenerationInProgress = useIsGenerationInProgress();
-
- const ref = useRef(null);
- const setActive = useCallback(() => {
- if (!props.api.isActive) {
- props.api.setActive();
- }
- }, [props.api]);
-
- useCallbackOnDragEnter(setActive, ref, 300);
-
- const onPointerDown = useCallback(() => {
- setFocusedRegion(props.params.focusRegion);
- }, [props.params.focusRegion]);
-
- useHackOutDvTabDraggable(ref);
-
- return (
-
-
- {props.api.title ?? props.api.id}
-
- {isGenerationInProgress && (
-
- )}
-
- );
- }
-);
-TabWithoutCloseButtonAndWithProgressIndicator.displayName = 'TabWithoutCloseButtonAndWithProgressIndicator';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx
index d8e4cd985e1..ff76901061c 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx
@@ -15,20 +15,24 @@ import type {
RootLayoutGridviewComponents,
} from 'features/ui/layouts/auto-layout-context';
import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context';
-import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import type { TabName } from 'features/ui/store/uiTypes';
import { dockviewTheme } from 'features/ui/styles/theme';
import { memo, useCallback, useEffect } from 'react';
import { CanvasTabLeftPanel } from './CanvasTabLeftPanel';
import { CanvasWorkspacePanel } from './CanvasWorkspacePanel';
+import { DockviewTabCanvasViewer } from './DockviewTabCanvasViewer';
+import { DockviewTabCanvasWorkspace } from './DockviewTabCanvasWorkspace';
+import { DockviewTabLaunchpad } from './DockviewTabLaunchpad';
import { navigationApi } from './navigation-api';
import { PanelHotkeysLogical } from './PanelHotkeysLogical';
import {
BOARD_PANEL_MIN_HEIGHT_PX,
BOARDS_PANEL_ID,
CANVAS_BOARD_PANEL_DEFAULT_HEIGHT_PX,
- DEFAULT_TAB_ID,
+ DOCKVIEW_TAB_CANVAS_VIEWER_ID,
+ DOCKVIEW_TAB_CANVAS_WORKSPACE_ID,
+ DOCKVIEW_TAB_LAUNCHPAD_ID,
GALLERY_PANEL_DEFAULT_HEIGHT_PX,
GALLERY_PANEL_ID,
GALLERY_PANEL_MIN_HEIGHT_PX,
@@ -42,18 +46,14 @@ import {
RIGHT_PANEL_ID,
RIGHT_PANEL_MIN_SIZE_PX,
SETTINGS_PANEL_ID,
- TAB_WITH_LAUNCHPAD_ICON_ID,
- TAB_WITH_PROGRESS_INDICATOR_ID,
VIEWER_PANEL_ID,
WORKSPACE_PANEL_ID,
} from './shared';
-import { TabWithLaunchpadIcon } from './TabWithLaunchpadIcon';
-import { TabWithoutCloseButtonAndWithProgressIndicator } from './TabWithoutCloseButtonAndWithProgressIndicator';
const tabComponents = {
- [DEFAULT_TAB_ID]: TabWithoutCloseButton,
- [TAB_WITH_PROGRESS_INDICATOR_ID]: TabWithoutCloseButtonAndWithProgressIndicator,
- [TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon,
+ [DOCKVIEW_TAB_LAUNCHPAD_ID]: DockviewTabLaunchpad,
+ [DOCKVIEW_TAB_CANVAS_VIEWER_ID]: DockviewTabCanvasViewer,
+ [DOCKVIEW_TAB_CANVAS_WORKSPACE_ID]: DockviewTabCanvasWorkspace,
};
const mainPanelComponents: AutoLayoutDockviewComponents = {
@@ -69,7 +69,7 @@ const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
- tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
+ tabComponent: DOCKVIEW_TAB_LAUNCHPAD_ID,
params: {
tab,
focusRegion: 'launchpad',
@@ -80,7 +80,7 @@ const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
id: WORKSPACE_PANEL_ID,
component: WORKSPACE_PANEL_ID,
title: 'Canvas',
- tabComponent: DEFAULT_TAB_ID,
+ tabComponent: DOCKVIEW_TAB_CANVAS_WORKSPACE_ID,
params: {
tab,
focusRegion: 'canvas',
@@ -95,7 +95,7 @@ const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
- tabComponent: DEFAULT_TAB_ID,
+ tabComponent: DOCKVIEW_TAB_CANVAS_VIEWER_ID,
params: {
tab,
focusRegion: 'viewer',
diff --git a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx
index cea63253f99..3c63ea64948 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx
@@ -14,11 +14,13 @@ import type {
RootLayoutGridviewComponents,
} from 'features/ui/layouts/auto-layout-context';
import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context';
-import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import type { TabName } from 'features/ui/store/uiTypes';
import { dockviewTheme } from 'features/ui/styles/theme';
import { memo, useCallback, useEffect } from 'react';
+import { DockviewTab } from './DockviewTab';
+import { DockviewTabLaunchpad } from './DockviewTabLaunchpad';
+import { DockviewTabProgress } from './DockviewTabProgress';
import { GenerateTabLeftPanel } from './GenerateTabLeftPanel';
import { navigationApi } from './navigation-api';
import { PanelHotkeysLogical } from './PanelHotkeysLogical';
@@ -26,7 +28,9 @@ import {
BOARD_PANEL_DEFAULT_HEIGHT_PX,
BOARD_PANEL_MIN_HEIGHT_PX,
BOARDS_PANEL_ID,
- DEFAULT_TAB_ID,
+ DOCKVIEW_TAB_ID,
+ DOCKVIEW_TAB_LAUNCHPAD_ID,
+ DOCKVIEW_TAB_PROGRESS_ID,
GALLERY_PANEL_DEFAULT_HEIGHT_PX,
GALLERY_PANEL_ID,
GALLERY_PANEL_MIN_HEIGHT_PX,
@@ -38,17 +42,13 @@ import {
RIGHT_PANEL_ID,
RIGHT_PANEL_MIN_SIZE_PX,
SETTINGS_PANEL_ID,
- TAB_WITH_LAUNCHPAD_ICON_ID,
- TAB_WITH_PROGRESS_INDICATOR_ID,
VIEWER_PANEL_ID,
} from './shared';
-import { TabWithLaunchpadIcon } from './TabWithLaunchpadIcon';
-import { TabWithoutCloseButtonAndWithProgressIndicator } from './TabWithoutCloseButtonAndWithProgressIndicator';
const tabComponents = {
- [DEFAULT_TAB_ID]: TabWithoutCloseButton,
- [TAB_WITH_PROGRESS_INDICATOR_ID]: TabWithoutCloseButtonAndWithProgressIndicator,
- [TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon,
+ [DOCKVIEW_TAB_ID]: DockviewTab,
+ [DOCKVIEW_TAB_PROGRESS_ID]: DockviewTabProgress,
+ [DOCKVIEW_TAB_LAUNCHPAD_ID]: DockviewTabLaunchpad,
};
const mainPanelComponents: AutoLayoutDockviewComponents = {
@@ -63,7 +63,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
- tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
+ tabComponent: DOCKVIEW_TAB_LAUNCHPAD_ID,
params: {
tab,
focusRegion: 'launchpad',
@@ -74,7 +74,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
- tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
+ tabComponent: DOCKVIEW_TAB_PROGRESS_ID,
params: {
tab,
focusRegion: 'viewer',
diff --git a/invokeai/frontend/web/src/features/ui/layouts/shared.ts b/invokeai/frontend/web/src/features/ui/layouts/shared.ts
index ec26851aab1..1d469948b50 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/shared.ts
+++ b/invokeai/frontend/web/src/features/ui/layouts/shared.ts
@@ -16,9 +16,11 @@ export const SETTINGS_PANEL_ID = 'settings';
export const MODELS_PANEL_ID = 'models';
export const QUEUE_PANEL_ID = 'queue';
-export const DEFAULT_TAB_ID = 'default-tab';
-export const TAB_WITH_PROGRESS_INDICATOR_ID = 'tab-with-progress-indicator';
-export const TAB_WITH_LAUNCHPAD_ICON_ID = 'tab-with-launchpad-icon';
+export const DOCKVIEW_TAB_ID = 'tab-default';
+export const DOCKVIEW_TAB_PROGRESS_ID = 'tab-progress';
+export const DOCKVIEW_TAB_LAUNCHPAD_ID = 'tab-launchpad';
+export const DOCKVIEW_TAB_CANVAS_VIEWER_ID = 'tab-canvas-viewer';
+export const DOCKVIEW_TAB_CANVAS_WORKSPACE_ID = 'tab-canvas-workspace';
export const LEFT_PANEL_MIN_SIZE_PX = 420;
export const RIGHT_PANEL_MIN_SIZE_PX = 420;
diff --git a/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx
index 0d095026b88..5dffd5172dc 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx
@@ -14,18 +14,22 @@ import type {
RootLayoutGridviewComponents,
} from 'features/ui/layouts/auto-layout-context';
import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context';
-import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
+import { DockviewTab } from 'features/ui/layouts/DockviewTab';
import type { TabName } from 'features/ui/store/uiTypes';
import { dockviewTheme } from 'features/ui/styles/theme';
import { memo, useCallback, useEffect } from 'react';
+import { DockviewTabLaunchpad } from './DockviewTabLaunchpad';
+import { DockviewTabProgress } from './DockviewTabProgress';
import { navigationApi } from './navigation-api';
import { PanelHotkeysLogical } from './PanelHotkeysLogical';
import {
BOARD_PANEL_DEFAULT_HEIGHT_PX,
BOARD_PANEL_MIN_HEIGHT_PX,
BOARDS_PANEL_ID,
- DEFAULT_TAB_ID,
+ DOCKVIEW_TAB_ID,
+ DOCKVIEW_TAB_LAUNCHPAD_ID,
+ DOCKVIEW_TAB_PROGRESS_ID,
GALLERY_PANEL_DEFAULT_HEIGHT_PX,
GALLERY_PANEL_ID,
GALLERY_PANEL_MIN_HEIGHT_PX,
@@ -37,18 +41,14 @@ import {
RIGHT_PANEL_ID,
RIGHT_PANEL_MIN_SIZE_PX,
SETTINGS_PANEL_ID,
- TAB_WITH_LAUNCHPAD_ICON_ID,
- TAB_WITH_PROGRESS_INDICATOR_ID,
VIEWER_PANEL_ID,
} from './shared';
-import { TabWithLaunchpadIcon } from './TabWithLaunchpadIcon';
-import { TabWithoutCloseButtonAndWithProgressIndicator } from './TabWithoutCloseButtonAndWithProgressIndicator';
import { UpscalingTabLeftPanel } from './UpscalingTabLeftPanel';
const tabComponents = {
- [DEFAULT_TAB_ID]: TabWithoutCloseButton,
- [TAB_WITH_PROGRESS_INDICATOR_ID]: TabWithoutCloseButtonAndWithProgressIndicator,
- [TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon,
+ [DOCKVIEW_TAB_ID]: DockviewTab,
+ [DOCKVIEW_TAB_PROGRESS_ID]: DockviewTabProgress,
+ [DOCKVIEW_TAB_LAUNCHPAD_ID]: DockviewTabLaunchpad,
};
const mainPanelComponents: AutoLayoutDockviewComponents = {
@@ -63,7 +63,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
- tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
+ tabComponent: DOCKVIEW_TAB_LAUNCHPAD_ID,
params: {
tab,
focusRegion: 'launchpad',
@@ -74,7 +74,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
- tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
+ tabComponent: DOCKVIEW_TAB_PROGRESS_ID,
params: {
tab,
focusRegion: 'viewer',
diff --git a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx
index 2490fd1fe61..7785ae836ff 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx
@@ -16,18 +16,22 @@ import type {
RootLayoutGridviewComponents,
} from 'features/ui/layouts/auto-layout-context';
import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context';
-import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
+import { DockviewTab } from 'features/ui/layouts/DockviewTab';
import type { TabName } from 'features/ui/store/uiTypes';
import { dockviewTheme } from 'features/ui/styles/theme';
import { memo, useCallback, useEffect } from 'react';
+import { DockviewTabLaunchpad } from './DockviewTabLaunchpad';
+import { DockviewTabProgress } from './DockviewTabProgress';
import { navigationApi } from './navigation-api';
import { PanelHotkeysLogical } from './PanelHotkeysLogical';
import {
BOARD_PANEL_DEFAULT_HEIGHT_PX,
BOARD_PANEL_MIN_HEIGHT_PX,
BOARDS_PANEL_ID,
- DEFAULT_TAB_ID,
+ DOCKVIEW_TAB_ID,
+ DOCKVIEW_TAB_LAUNCHPAD_ID,
+ DOCKVIEW_TAB_PROGRESS_ID,
GALLERY_PANEL_DEFAULT_HEIGHT_PX,
GALLERY_PANEL_ID,
GALLERY_PANEL_MIN_HEIGHT_PX,
@@ -39,18 +43,14 @@ import {
RIGHT_PANEL_ID,
RIGHT_PANEL_MIN_SIZE_PX,
SETTINGS_PANEL_ID,
- TAB_WITH_LAUNCHPAD_ICON_ID,
- TAB_WITH_PROGRESS_INDICATOR_ID,
VIEWER_PANEL_ID,
WORKSPACE_PANEL_ID,
} from './shared';
-import { TabWithLaunchpadIcon } from './TabWithLaunchpadIcon';
-import { TabWithoutCloseButtonAndWithProgressIndicator } from './TabWithoutCloseButtonAndWithProgressIndicator';
const tabComponents = {
- [DEFAULT_TAB_ID]: TabWithoutCloseButton,
- [TAB_WITH_PROGRESS_INDICATOR_ID]: TabWithoutCloseButtonAndWithProgressIndicator,
- [TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon,
+ [DOCKVIEW_TAB_ID]: DockviewTab,
+ [DOCKVIEW_TAB_PROGRESS_ID]: DockviewTabProgress,
+ [DOCKVIEW_TAB_LAUNCHPAD_ID]: DockviewTabLaunchpad,
};
const mainPanelComponents: AutoLayoutDockviewComponents = {
@@ -66,7 +66,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
- tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
+ tabComponent: DOCKVIEW_TAB_LAUNCHPAD_ID,
params: {
tab,
focusRegion: 'launchpad',
@@ -77,7 +77,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
id: WORKSPACE_PANEL_ID,
component: WORKSPACE_PANEL_ID,
title: 'Workflow Editor',
- tabComponent: DEFAULT_TAB_ID,
+ tabComponent: DOCKVIEW_TAB_ID,
params: {
tab,
focusRegion: 'workflows',
@@ -92,7 +92,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
- tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
+ tabComponent: DOCKVIEW_TAB_PROGRESS_ID,
params: {
tab,
focusRegion: 'viewer',
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
index 3990c28aa2b..81a138ee8a7 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
@@ -4,4 +4,3 @@ import { selectUiSlice } from 'features/ui/store/uiSlice';
export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab);
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
-export const selectActiveTabCanvasRightPanel = createSelector(selectUiSlice, (ui) => ui.activeTabCanvasRightPanel);
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
index 6a50e8fbb68..62f064c4744 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
@@ -12,9 +12,6 @@ export const uiSlice = createSlice({
setActiveTab: (state, action: PayloadAction) => {
state.activeTab = action.payload;
},
- activeTabCanvasRightPanelChanged: (state, action: PayloadAction) => {
- state.activeTabCanvasRightPanel = action.payload;
- },
setShouldShowImageDetails: (state, action: PayloadAction) => {
state.shouldShowImageDetails = action.payload;
},
@@ -73,7 +70,6 @@ export const uiSlice = createSlice({
export const {
setActiveTab,
- activeTabCanvasRightPanelChanged,
setShouldShowImageDetails,
setShouldShowProgressInViewer,
accordionStateChanged,
diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
index b150f8f4ab9..61b2ab02f7a 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
@@ -4,7 +4,6 @@ import { z } from 'zod/v4';
const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']);
export type TabName = z.infer;
-const zCanvasRightPanelTabName = z.enum(['layers', 'gallery']);
const zPartialDimensions = z.object({
width: z.number().optional(),
@@ -17,7 +16,6 @@ export type Serializable = z.infer;
const zUIState = z.object({
_version: z.literal(3).default(3),
activeTab: zTabName.default('generate'),
- activeTabCanvasRightPanel: zCanvasRightPanelTabName.default('gallery'),
shouldShowImageDetails: z.boolean().default(false),
shouldShowProgressInViewer: z.boolean().default(true),
accordions: z.record(z.string(), z.boolean()).default(() => ({})),
diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
index 671ddb8390a..612b4c9ce46 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
@@ -523,6 +523,7 @@ export const {
useListQueueItemsQuery,
useCancelQueueItemMutation,
useCancelQueueItemsByDestinationMutation,
+ useGetCurrentQueueItemQuery,
useDeleteQueueItemMutation,
useDeleteAllExceptCurrentMutation,