From 53a3dc52bc448e112b73128fe8dda3aeacf42a13 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 16 May 2025 12:00:39 +1000
Subject: [PATCH 001/210] feat(ui): viewer is a modal (wip)
---
.../src/app/components/GlobalHookIsolator.tsx | 8 +++
.../app/components/GlobalModalIsolator.tsx | 2 +
.../components/CanvasDropArea.tsx | 6 --
.../components/CanvasMainPanelContent.tsx | 2 -
.../components/CanvasRightPanel.tsx | 17 +----
.../NewSessionConfirmationAlertDialog.tsx | 9 +--
.../components/Tool/ToolBboxButton.tsx | 6 +-
.../components/Tool/ToolBrushButton.tsx | 6 +-
.../components/Tool/ToolBrushWidth.tsx | 10 ++-
.../components/Tool/ToolColorPickerButton.tsx | 6 +-
.../components/Tool/ToolEraserButton.tsx | 6 +-
.../components/Tool/ToolEraserWidth.tsx | 10 ++-
.../components/Tool/ToolFillColorPicker.tsx | 6 +-
.../components/Tool/ToolMoveButton.tsx | 6 +-
.../components/Tool/ToolRectButton.tsx | 6 +-
.../components/Tool/ToolViewButton.tsx | 6 +-
.../Toolbar/CanvasToolbarResetViewButton.tsx | 26 ++++----
.../hooks/useCanvasDeleteLayerHotkey.ts | 7 +-
.../hooks/useCanvasEntityQuickSwitchHotkey.ts | 5 +-
.../hooks/useCanvasResetLayerHotkey.ts | 6 +-
.../hooks/useCanvasUndoRedoHotkeys.tsx | 10 ++-
.../controlLayers/hooks/useEntityFilter.ts | 5 +-
.../hooks/useEntitySegmentAnything.ts | 5 +-
.../controlLayers/hooks/useEntityTransform.ts | 8 +--
.../web/src/features/dnd/DndImage.tsx | 4 ++
.../components/ImageViewer/ImageViewer.tsx | 65 ++++++++++++-------
.../components/ImageViewer/useImageViewer.ts | 3 +-
.../FloatingParametersPanelButtons.tsx | 4 +-
28 files changed, 114 insertions(+), 146 deletions(-)
diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
index 5dfcdcab5b..4be48fcd2c 100644
--- a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
@@ -10,9 +10,11 @@ import type { PartialAppConfig } from 'app/types/invokeai';
import { useFocusRegionWatcher } from 'common/hooks/focus';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
+import { toggleImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
import { useReadinessWatcher } from 'features/queue/store/readiness';
+import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { configChanged } from 'features/system/store/configSlice';
import { selectLanguage } from 'features/system/store/systemSelectors';
import i18n from 'i18n';
@@ -61,6 +63,12 @@ export const GlobalHookIsolator = memo(
useWorkflowBuilderWatcher();
useDynamicPromptsWatcher();
+ useRegisteredHotkeys({
+ id: 'toggleViewer',
+ category: 'viewer',
+ callback: toggleImageViewer,
+ });
+
return null;
}
);
diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
index da982d105f..ae83f0c5c2 100644
--- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
@@ -11,6 +11,7 @@ import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
+import { ImageViewerModal } from 'features/gallery/components/ImageViewer/ImageViewer';
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal';
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
@@ -58,6 +59,7 @@ export const GlobalModalIsolator = memo(() => {
+
>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx
index e988ecce68..adf21bd318 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx
@@ -2,7 +2,6 @@ import { Grid, GridItem } from '@invoke-ai/ui-library';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { newCanvasEntityFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -19,13 +18,8 @@ const addGlobalReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDn
export const CanvasDropArea = memo(() => {
const { t } = useTranslation();
- const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
- if (imageViewer.isOpen) {
- return null;
- }
-
return (
<>
{
-
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
index ac0c669005..c1bca93a26 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
@@ -11,7 +11,6 @@ import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
import type { DndTargetState } from 'features/dnd/types';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
-import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -32,11 +31,8 @@ export const CanvasRightPanel = memo(() => {
}, [activeTab]);
const onClickViewerToggleButton = useCallback(() => {
- if (activeTab !== 'gallery') {
- dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }
- imageViewer.toggle();
- }, [imageViewer, activeTab, dispatch]);
+ imageViewer.open();
+ }, [imageViewer]);
const onChangeTab = useCallback(
(index: number) => {
@@ -49,20 +45,13 @@ export const CanvasRightPanel = memo(() => {
[dispatch]
);
- useRegisteredHotkeys({
- id: 'toggleViewer',
- category: 'viewer',
- callback: imageViewer.toggle,
- dependencies: [imageViewer],
- });
-
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
index b365abb807..f57818964c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
@@ -3,7 +3,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import {
selectSystemShouldConfirmOnNewSession,
shouldConfirmOnNewSessionToggled,
@@ -17,15 +16,13 @@ const [useNewCanvasSessionDialog] = buildUseBoolean(false);
export const useNewGallerySession = () => {
const dispatch = useAppDispatch();
- const imageViewer = useImageViewer();
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
const newSessionDialog = useNewGallerySessionDialog();
const newGallerySessionImmediate = useCallback(() => {
dispatch(newGallerySessionRequested());
- imageViewer.open();
dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }, [dispatch, imageViewer]);
+ }, [dispatch]);
const newGallerySessionWithDialog = useCallback(() => {
if (shouldConfirmOnNewSession) {
@@ -40,15 +37,13 @@ export const useNewGallerySession = () => {
export const useNewCanvasSession = () => {
const dispatch = useAppDispatch();
- const imageViewer = useImageViewer();
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
const newSessionDialog = useNewCanvasSessionDialog();
const newCanvasSessionImmediate = useCallback(() => {
dispatch(newCanvasSessionRequested());
- imageViewer.close();
dispatch(activeTabCanvasRightPanelChanged('layers'));
- }, [dispatch, imageViewer]);
+ }, [dispatch]);
const newCanvasSessionWithDialog = useCallback(() => {
if (shouldConfirmOnNewSession) {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx
index 4796092faf..30f5eb0c38 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx
@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolBboxButton = memo(() => {
const { t } = useTranslation();
const selectBbox = useSelectTool('bbox');
const isSelected = useToolIsSelected('bbox');
- const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectBboxTool',
category: 'canvas',
callback: selectBbox,
- options: { enabled: !isSelected && !imageViewer.isOpen },
- dependencies: [selectBbox, isSelected, imageViewer.isOpen],
+ options: { enabled: !isSelected },
+ dependencies: [selectBbox, isSelected],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx
index d9afdb18f5..1f3f2a0044 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx
@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolBrushButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('brush');
const selectBrush = useSelectTool('brush');
- const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectBrushTool',
category: 'canvas',
callback: selectBrush,
- options: { enabled: !isSelected && !imageViewer.isOpen },
- dependencies: [isSelected, selectBrush, imageViewer.isOpen],
+ options: { enabled: !isSelected },
+ dependencies: [isSelected, selectBrush],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx
index a8dcb12c71..6d14493d50 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx
@@ -16,7 +16,6 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { selectCanvasSettingsSlice, settingsBrushWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { clamp } from 'lodash-es';
import type { KeyboardEvent } from 'react';
@@ -69,7 +68,6 @@ const sliderDefaultValue = mapRawValueToSliderValue(50);
export const ToolBrushWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
- const imageViewer = useImageViewer();
const isSelected = useToolIsSelected('brush');
const width = useAppSelector(selectBrushWidth);
const [localValue, setLocalValue] = useState(width);
@@ -133,15 +131,15 @@ export const ToolBrushWidth = memo(() => {
id: 'decrementToolWidth',
category: 'canvas',
callback: decrement,
- options: { enabled: isSelected && !imageViewer.isOpen },
- dependencies: [decrement, isSelected, imageViewer.isOpen],
+ options: { enabled: isSelected },
+ dependencies: [decrement, isSelected],
});
useRegisteredHotkeys({
id: 'incrementToolWidth',
category: 'canvas',
callback: increment,
- options: { enabled: isSelected && !imageViewer.isOpen },
- dependencies: [increment, isSelected, imageViewer.isOpen],
+ options: { enabled: isSelected },
+ dependencies: [increment, isSelected],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx
index 1f48b2ee72..a8c590163e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx
@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolColorPickerButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('colorPicker');
const selectColorPicker = useSelectTool('colorPicker');
- const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectColorPickerTool',
category: 'canvas',
callback: selectColorPicker,
- options: { enabled: !isSelected && !imageViewer.isOpen },
- dependencies: [selectColorPicker, isSelected, imageViewer.isOpen],
+ options: { enabled: !isSelected },
+ dependencies: [selectColorPicker, isSelected],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx
index 8257523367..f9de6cc4aa 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx
@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolEraserButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('eraser');
const selectEraser = useSelectTool('eraser');
- const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectEraserTool',
category: 'canvas',
callback: selectEraser,
- options: { enabled: !isSelected && !imageViewer.isOpen },
- dependencies: [isSelected, selectEraser, imageViewer.isOpen],
+ options: { enabled: !isSelected },
+ dependencies: [isSelected, selectEraser],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx
index 1bcdd69297..a0821c8776 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx
@@ -19,7 +19,6 @@ import {
selectCanvasSettingsSlice,
settingsEraserWidthChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { clamp } from 'lodash-es';
import type { KeyboardEvent } from 'react';
@@ -72,7 +71,6 @@ const sliderDefaultValue = mapRawValueToSliderValue(50);
export const ToolEraserWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
- const imageViewer = useImageViewer();
const isSelected = useToolIsSelected('eraser');
const width = useAppSelector(selectEraserWidth);
const [localValue, setLocalValue] = useState(width);
@@ -136,15 +134,15 @@ export const ToolEraserWidth = memo(() => {
id: 'decrementToolWidth',
category: 'canvas',
callback: decrement,
- options: { enabled: isSelected && !imageViewer.isOpen },
- dependencies: [decrement, isSelected, imageViewer.isOpen],
+ options: { enabled: isSelected },
+ dependencies: [decrement, isSelected],
});
useRegisteredHotkeys({
id: 'incrementToolWidth',
category: 'canvas',
callback: increment,
- options: { enabled: isSelected && !imageViewer.isOpen },
- dependencies: [increment, isSelected, imageViewer.isOpen],
+ options: { enabled: isSelected },
+ dependencies: [increment, isSelected],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx
index 7d1b4bdd0e..8b3de75e72 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx
@@ -14,7 +14,6 @@ import RgbaColorPicker from 'common/components/ColorPicker/RgbaColorPicker';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { selectCanvasSettingsSlice, settingsColorChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import type { RgbaColor } from 'features/controlLayers/store/types';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -31,14 +30,13 @@ export const ToolColorPicker = memo(() => {
},
[dispatch]
);
- const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'setFillToWhite',
category: 'canvas',
callback: () => dispatch(settingsColorChanged({ r: 255, g: 255, b: 255, a: 1 })),
- options: { preventDefault: true, enabled: !imageViewer.isOpen },
- dependencies: [dispatch, imageViewer.isOpen],
+ options: { preventDefault: true },
+ dependencies: [dispatch],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx
index 63cbbbce8f..cd842e64b6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx
@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolMoveButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('move');
const selectMove = useSelectTool('move');
- const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectMoveTool',
category: 'canvas',
callback: selectMove,
- options: { enabled: !isSelected && !imageViewer.isOpen },
- dependencies: [isSelected, selectMove, imageViewer.isOpen],
+ options: { enabled: !isSelected },
+ dependencies: [isSelected, selectMove],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx
index 6e9251c1ef..9302939088 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx
@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolRectButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('rect');
const selectRect = useSelectTool('rect');
- const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectRectTool',
category: 'canvas',
callback: selectRect,
- options: { enabled: !isSelected && !imageViewer.isOpen },
- dependencies: [isSelected, selectRect, imageViewer.isOpen],
+ options: { enabled: !isSelected },
+ dependencies: [isSelected, selectRect],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx
index f58e66b91c..f16c055fcf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx
@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolViewButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('view');
const selectView = useSelectTool('view');
- const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectViewTool',
category: 'canvas',
callback: selectView,
- options: { enabled: !isSelected && !imageViewer.isOpen },
- dependencies: [selectView, isSelected, imageViewer.isOpen],
+ options: { enabled: !isSelected },
+ dependencies: [selectView, isSelected],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx
index 88aae8a2ba..547575cff2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx
@@ -1,7 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,49 +10,48 @@ export const CanvasToolbarResetViewButton = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const isCanvasFocused = useIsRegionFocused('canvas');
- const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'fitLayersToCanvas',
category: 'canvas',
callback: canvasManager.stage.fitLayersToStage,
- options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
- dependencies: [isCanvasFocused, imageViewer.isOpen],
+ options: { enabled: isCanvasFocused, preventDefault: true },
+ dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'fitBboxToCanvas',
category: 'canvas',
callback: canvasManager.stage.fitBboxToStage,
- options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
- dependencies: [isCanvasFocused, imageViewer.isOpen],
+ options: { enabled: isCanvasFocused, preventDefault: true },
+ dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'setZoomTo100Percent',
category: 'canvas',
callback: () => canvasManager.stage.setScale(1),
- options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
- dependencies: [isCanvasFocused, imageViewer.isOpen],
+ options: { enabled: isCanvasFocused, preventDefault: true },
+ dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'setZoomTo200Percent',
category: 'canvas',
callback: () => canvasManager.stage.setScale(2),
- options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
- dependencies: [isCanvasFocused, imageViewer.isOpen],
+ options: { enabled: isCanvasFocused, preventDefault: true },
+ dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'setZoomTo400Percent',
category: 'canvas',
callback: () => canvasManager.stage.setScale(4),
- options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
- dependencies: [isCanvasFocused, imageViewer.isOpen],
+ options: { enabled: isCanvasFocused, preventDefault: true },
+ dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'setZoomTo800Percent',
category: 'canvas',
callback: () => canvasManager.stage.setScale(8),
- options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
- dependencies: [isCanvasFocused, imageViewer.isOpen],
+ options: { enabled: isCanvasFocused, preventDefault: true },
+ dependencies: [isCanvasFocused],
});
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts
index 98d9222bd7..11ac1b6897 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts
@@ -3,7 +3,6 @@ import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { selectActiveTab, selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
@@ -16,8 +15,6 @@ export function useCanvasDeleteLayerHotkey() {
const canvasRightPanelTab = useAppSelector(selectActiveTabCanvasRightPanel);
const appTab = useAppSelector(selectActiveTab);
- const imageViewer = useImageViewer();
-
const deleteSelectedLayer = useCallback(() => {
if (selectedEntityIdentifier === null) {
return;
@@ -34,7 +31,7 @@ export function useCanvasDeleteLayerHotkey() {
id: 'deleteSelected',
category: 'canvas',
callback: deleteSelectedLayer,
- options: { enabled: isDeleteEnabled && !imageViewer.isOpen },
- dependencies: [isDeleteEnabled, deleteSelectedLayer, imageViewer.isOpen],
+ options: { enabled: isDeleteEnabled },
+ dependencies: [isDeleteEnabled, deleteSelectedLayer],
});
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts
index 10b51c585f..f8ee66c289 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts
@@ -5,7 +5,6 @@ import {
selectSelectedEntityIdentifier,
} from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useCallback, useEffect, useState } from 'react';
@@ -15,7 +14,6 @@ export const useCanvasEntityQuickSwitchHotkey = () => {
const [current, setCurrent] = useState(null);
const selected = useAppSelector(selectSelectedEntityIdentifier);
const bookmarked = useAppSelector(selectBookmarkedEntityIdentifier);
- const imageViewer = useImageViewer();
// Update prev and current when selected entity changes
useEffect(() => {
@@ -49,7 +47,6 @@ export const useCanvasEntityQuickSwitchHotkey = () => {
id: 'quickSwitch',
category: 'canvas',
callback: onQuickSwitch,
- options: { enabled: !imageViewer.isOpen },
- dependencies: [onQuickSwitch, imageViewer.isOpen],
+ dependencies: [onQuickSwitch],
});
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts
index cfa648eb9f..b62d2d7dfd 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts
@@ -6,7 +6,6 @@ import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocke
import { entityReset } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isMaskEntityIdentifier } from 'features/controlLayers/store/types';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useCallback, useMemo } from 'react';
@@ -17,7 +16,6 @@ export function useCanvasResetLayerHotkey() {
const isBusy = useCanvasIsBusy();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isLocked = useEntityIsLocked(entityIdentifier);
- const imageViewer = useImageViewer();
const resetSelectedLayer = useCallback(() => {
if (entityIdentifier === null || adapter === null) {
@@ -36,7 +34,7 @@ export function useCanvasResetLayerHotkey() {
id: 'resetSelected',
category: 'canvas',
callback: resetSelectedLayer,
- options: { enabled: isResetAllowed && !isBusy && !isLocked && !imageViewer.isOpen },
- dependencies: [isResetAllowed, isBusy, isLocked, resetSelectedLayer, imageViewer.isOpen],
+ options: { enabled: isResetAllowed && !isBusy && !isLocked },
+ dependencies: [isResetAllowed, isBusy, isLocked, resetSelectedLayer],
});
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasUndoRedoHotkeys.tsx b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasUndoRedoHotkeys.tsx
index c5b1c03023..9e90a197e6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasUndoRedoHotkeys.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasUndoRedoHotkeys.tsx
@@ -3,7 +3,6 @@ import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { canvasRedo, canvasUndo } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasMayRedo, selectCanvasMayUndo } from 'features/controlLayers/store/selectors';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
@@ -12,7 +11,6 @@ export const useCanvasUndoRedoHotkeys = () => {
useAssertSingleton('useCanvasUndoRedo');
const dispatch = useDispatch();
const isBusy = useCanvasIsBusy();
- const imageViewer = useImageViewer();
const mayUndo = useAppSelector(selectCanvasMayUndo);
const handleUndo = useCallback(() => {
@@ -22,8 +20,8 @@ export const useCanvasUndoRedoHotkeys = () => {
id: 'undo',
category: 'canvas',
callback: handleUndo,
- options: { enabled: mayUndo && !isBusy && !imageViewer.isOpen, preventDefault: true },
- dependencies: [mayUndo, isBusy, handleUndo, imageViewer.isOpen],
+ options: { enabled: mayUndo && !isBusy, preventDefault: true },
+ dependencies: [mayUndo, isBusy, handleUndo],
});
const mayRedo = useAppSelector(selectCanvasMayRedo);
@@ -34,7 +32,7 @@ export const useCanvasUndoRedoHotkeys = () => {
id: 'redo',
category: 'canvas',
callback: handleRedo,
- options: { enabled: mayRedo && !isBusy && !imageViewer.isOpen, preventDefault: true },
- dependencies: [mayRedo, handleRedo, isBusy, imageViewer.isOpen],
+ options: { enabled: mayRedo && !isBusy, preventDefault: true },
+ dependencies: [mayRedo, handleRedo, isBusy],
});
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityFilter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityFilter.ts
index c86aa92dc2..e3da064b51 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityFilter.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityFilter.ts
@@ -5,13 +5,11 @@ import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty'
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCallback, useMemo } from 'react';
export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null) => {
const canvasManager = useCanvasManager();
const adapter = useEntityAdapterSafe(entityIdentifier);
- const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
@@ -52,9 +50,8 @@ export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null)
if (!adapter) {
return;
}
- imageViewer.close();
adapter.filterer.start();
- }, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
+ }, [isDisabled, entityIdentifier, canvasManager]);
return { isDisabled, start } as const;
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySegmentAnything.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySegmentAnything.ts
index 0893f02119..2959bb9b96 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySegmentAnything.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySegmentAnything.ts
@@ -5,13 +5,11 @@ import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty'
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isSegmentableEntityIdentifier } from 'features/controlLayers/store/types';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCallback, useMemo } from 'react';
export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifier | null) => {
const canvasManager = useCanvasManager();
const adapter = useEntityAdapterSafe(entityIdentifier);
- const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
@@ -52,9 +50,8 @@ export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifie
if (!adapter) {
return;
}
- imageViewer.close();
adapter.segmentAnything.start();
- }, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
+ }, [isDisabled, entityIdentifier, canvasManager]);
return { isDisabled, start } as const;
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTransform.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTransform.ts
index 5b7e3fbc74..624cb4395d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTransform.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTransform.ts
@@ -5,13 +5,11 @@ import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty'
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCallback, useMemo } from 'react';
export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | null) => {
const canvasManager = useCanvasManager();
const adapter = useEntityAdapterSafe(entityIdentifier);
- const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
@@ -52,9 +50,8 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
if (!adapter) {
return;
}
- imageViewer.close();
await adapter.transformer.startTransform();
- }, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
+ }, [isDisabled, entityIdentifier, canvasManager]);
const fitToBbox = useCallback(async () => {
if (isDisabled) {
@@ -70,11 +67,10 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
if (!adapter) {
return;
}
- imageViewer.close();
await adapter.transformer.startTransform({ silent: true });
adapter.transformer.fitToBboxContain();
await adapter.transformer.applyTransform();
- }, [canvasManager, entityIdentifier, imageViewer, isDisabled]);
+ }, [canvasManager, entityIdentifier, isDisabled]);
return { isDisabled, start, fitToBbox } as const;
};
diff --git a/invokeai/frontend/web/src/features/dnd/DndImage.tsx b/invokeai/frontend/web/src/features/dnd/DndImage.tsx
index 533bc2bd4c..5e7042cde7 100644
--- a/invokeai/frontend/web/src/features/dnd/DndImage.tsx
+++ b/invokeai/frontend/web/src/features/dnd/DndImage.tsx
@@ -8,6 +8,7 @@ import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreview
import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage';
import { firefoxDndFix } from 'features/dnd/util';
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
+import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo, useEffect, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
@@ -47,6 +48,9 @@ export const DndImage = memo(({ imageDTO, asThumbnail, ...rest }: DndImage.Props
getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name),
onDragStart: () => {
setIsDragging(true);
+ if ($imageViewer.get()) {
+ $imageViewer.set(false);
+ }
},
onDrop: () => {
setIsDragging(false);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
index 5f080d1ab6..4264196bca 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
@@ -1,4 +1,4 @@
-import { Box, IconButton, type SystemStyleObject } from '@invoke-ai/ui-library';
+import { Box, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
@@ -9,7 +9,7 @@ import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewe
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
import { selectHasImageToCompare } from 'features/gallery/store/gallerySelectors';
import type { ReactNode } from 'react';
-import { memo } from 'react';
+import { memo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
@@ -43,7 +43,7 @@ export const ImageViewer = memo(({ closeButton }: Props) => {
const [containerRef, containerDims] = useMeasure();
return (
-
+
{hasImageToCompare && }
{!hasImageToCompare && }
@@ -57,17 +57,50 @@ export const ImageViewer = memo(({ closeButton }: Props) => {
ImageViewer.displayName = 'ImageViewer';
-export const GatedImageViewer = memo(() => {
+const imageViewerContainerSx: SystemStyleObject = {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ transition: 'opacity 0.15s ease',
+ opacity: 1,
+ pointerEvents: 'auto',
+ '&[data-hidden="true"]': {
+ opacity: 0,
+ pointerEvents: 'none',
+ },
+ backdropFilter: 'blur(10px) brightness(70%)',
+};
+
+const imageViewerModalSx: SystemStyleObject = {
+ position: 'absolute',
+ bg: 'base.800',
+ borderRadius: 'base',
+ top: 16,
+ right: 16,
+ bottom: 16,
+ left: 16,
+};
+
+export const ImageViewerModal = memo(() => {
+ const ref = useRef(null);
const imageViewer = useImageViewer();
+ useOutsideClick({
+ ref,
+ handler: imageViewer.close,
+ });
- if (!imageViewer.isOpen) {
- return null;
- }
-
- return } />;
+ return (
+
+
+ } />
+
+
+ );
});
-GatedImageViewer.displayName = 'GatedImageViewer';
+ImageViewerModal.displayName = 'GatedImageViewer';
const ImageViewerCloseButton = memo(() => {
const { t } = useTranslation();
@@ -87,15 +120,3 @@ const ImageViewerCloseButton = memo(() => {
});
ImageViewerCloseButton.displayName = 'ImageViewerCloseButton';
-
-const GatedImageViewerCloseButton = memo(() => {
- const imageViewer = useImageViewer();
-
- if (!imageViewer.isOpen) {
- return null;
- }
-
- return ;
-});
-
-GatedImageViewerCloseButton.displayName = 'GatedImageViewerCloseButton';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts
index 854326450f..038c5ee607 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts
@@ -27,9 +27,10 @@ import type { ImageDTO } from 'services/api/types';
* TODO(psyche): Figure out a better way to do handle this...
*/
let didCloseImageViewer = false;
-const api = buildUseBoolean(true);
+const api = buildUseBoolean(false);
const useImageViewerState = api[0];
export const $imageViewer = api[1];
+export const toggleImageViewer = () => $imageViewer.set(!$imageViewer.get());
export const useImageViewer = () => {
const dispatch = useAppDispatch();
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
index a731d40ad9..61a118881e 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
@@ -2,7 +2,6 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
@@ -30,12 +29,11 @@ type Props = {
const FloatingSidePanelButtons = ({ togglePanel }: Props) => {
const { t } = useTranslation();
const tab = useAppSelector(selectActiveTab);
- const imageViewer = useImageViewer();
const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
return (
- {tab === 'canvas' && !imageViewer.isOpen && (
+ {tab === 'canvas' && (
From 341910739ec6fee3dbb2267d8ca98247f4d7f649 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 16 May 2025 12:24:22 +1000
Subject: [PATCH 002/210] chore(ui): bump @reduxjs/toolkit to latest
---
invokeai/frontend/web/package.json | 2 +-
invokeai/frontend/web/pnpm-lock.yaml | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index 3d1afd62ae..f7c2513edb 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -60,7 +60,7 @@
"@fontsource-variable/inter": "^5.2.5",
"@invoke-ai/ui-library": "^0.0.46",
"@nanostores/react": "^1.0.0",
- "@reduxjs/toolkit": "2.7.0",
+ "@reduxjs/toolkit": "2.8.2",
"@roarr/browser-log-writer": "^1.3.0",
"@xyflow/react": "^12.6.0",
"async-mutex": "^0.5.0",
diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml
index bb5722c934..79de0c48c1 100644
--- a/invokeai/frontend/web/pnpm-lock.yaml
+++ b/invokeai/frontend/web/pnpm-lock.yaml
@@ -30,8 +30,8 @@ dependencies:
specifier: ^1.0.0
version: 1.0.0(nanostores@1.0.1)(react@18.3.1)
'@reduxjs/toolkit':
- specifier: 2.7.0
- version: 2.7.0(react-redux@9.2.0)(react@18.3.1)
+ specifier: 2.8.2
+ version: 2.8.2(react-redux@9.2.0)(react@18.3.1)
'@roarr/browser-log-writer':
specifier: ^1.3.0
version: 1.3.0
@@ -2161,8 +2161,8 @@ packages:
- supports-color
dev: true
- /@reduxjs/toolkit@2.7.0(react-redux@9.2.0)(react@18.3.1):
- resolution: {integrity: sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==}
+ /@reduxjs/toolkit@2.8.2(react-redux@9.2.0)(react@18.3.1):
+ resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
From 668c475271dd07ba9571a68aacd1321b47e1d6dd Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 16 May 2025 12:39:18 +1000
Subject: [PATCH 003/210] feat(ui): default canvas tool is move
---
.../features/controlLayers/konva/CanvasTool/CanvasToolModule.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
index 8ef2f44b3a..762ae693d3 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
@@ -70,7 +70,7 @@ export class CanvasToolModule extends CanvasModuleBase {
/**
* The currently selected tool.
*/
- $tool = atom('brush');
+ $tool = atom('move');
/**
* A buffer for the currently selected tool. This is used to temporarily store the tool while the user is using any
* hold-to-activate tools, like the view or color picker tools.
From c9cd0a87be3f1876479651bdddae9d9bc00d05ea Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 21 May 2025 16:17:20 +1000
Subject: [PATCH 004/210] refactor(ui): image viewer & comparison
convolutedness
---
.../ImageViewer/CurrentImagePreview.tsx | 8 +--
.../ImageViewer/ImageComparison.tsx | 48 ++++++++------
.../ImageViewer/ImageComparisonSideBySide.tsx | 58 ++++++++---------
.../ImageViewer/ImageComparisonSlider.tsx | 1 +
.../components/ImageViewer/ImageViewer.tsx | 62 +++++++++----------
.../gallery/components/ImageViewer/common.ts | 6 ++
6 files changed, 89 insertions(+), 94 deletions(-)
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
index ea682485b2..5e26c69311 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
@@ -1,29 +1,23 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
import { CanvasAlertsSendingToCanvas } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
import { DndImage } from 'features/dnd/DndImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
-import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useCallback, useRef, useState } from 'react';
-import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { $hasProgressImage, $isProgressFromCanvas } from 'services/events/stores';
import { NoContentForViewer } from './NoContentForViewer';
import ProgressImage from './ProgressImage';
-const CurrentImagePreview = () => {
+const CurrentImagePreview = ({ imageDTO }: { imageDTO?: ImageDTO }) => {
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
- const imageName = useAppSelector(selectLastSelectedImageName);
-
- const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
// Show and hide the next/prev buttons on mouse move
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState(false);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx
index 6ebb4ab15c..4681a4bafb 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx
@@ -1,42 +1,50 @@
+import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
-import { IAINoContentFallback } from 'common/components/IAIImageFallback';
-import type { Dimensions } from 'features/controlLayers/store/types';
-import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common';
+import type { ComparisonProps } from 'features/gallery/components/ImageViewer/common';
+import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar';
+import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
import { ImageComparisonHover } from 'features/gallery/components/ImageViewer/ImageComparisonHover';
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
import { selectComparisonMode } from 'features/gallery/store/gallerySelectors';
import { memo } from 'react';
-import { useTranslation } from 'react-i18next';
-import { PiImagesBold } from 'react-icons/pi';
+import { useMeasure } from 'react-use';
+import type { Equals } from 'tsafe';
+import { assert } from 'tsafe';
-type Props = {
- containerDims: Dimensions;
-};
-
-export const ImageComparison = memo(({ containerDims }: Props) => {
- const { t } = useTranslation();
+export const ImageComparisonContent = memo(({ firstImage, secondImage, containerDims }: ComparisonProps) => {
const comparisonMode = useAppSelector(selectComparisonMode);
- const { firstImage, secondImage } = useAppSelector(selectComparisonImages);
-
- if (!firstImage || !secondImage) {
- // Should rarely/never happen - we don't render this component unless we have images to compare
- return ;
- }
if (comparisonMode === 'slider') {
- return ;
+ return ;
}
if (comparisonMode === 'side-by-side') {
return (
-
+
);
}
if (comparisonMode === 'hover') {
- return ;
+ return ;
}
+
+ assert>(false);
});
+ImageComparisonContent.displayName = 'ImageComparisonContent';
+
+export const ImageComparison = memo(({ firstImage, secondImage }: Omit) => {
+ const [containerRef, containerDims] = useMeasure();
+
+ return (
+
+
+
+
+
+
+
+ );
+});
ImageComparison.displayName = 'ImageComparison';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx
index 522cf3ddd0..a84e842dcc 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx
@@ -5,6 +5,7 @@ import { VerticalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
import { memo, useCallback, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
+import type { ImageDTO } from 'services/api/types';
export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: ComparisonProps) => {
const panelGroupRef = useRef(null);
@@ -25,42 +26,11 @@ export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Comp
autoSaveId="image-comparison-side-by-side"
>
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
@@ -69,3 +39,25 @@ export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Comp
});
ImageComparisonSideBySide.displayName = 'ImageComparisonSideBySide';
+
+const SideBySideImage = memo(({ imageDTO, type }: { imageDTO: ImageDTO; type: 'first' | 'second' }) => {
+ return (
+
+
+
+
+
+
+ );
+});
+SideBySideImage.displayName = 'SideBySideImage';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx
index 3bbcf6037f..35361ff23a 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx
@@ -21,6 +21,7 @@ const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`;
export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerDims }: ComparisonProps) => {
const comparisonFit = useAppSelector(selectComparisonFit);
+
// How far the handle is from the left - this will be a CSS calculation that takes into account the handle width
const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX);
// How wide the first image is
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
index 4264196bca..34d9329532 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
@@ -1,19 +1,18 @@
-import { Box, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library';
+import { Box, Flex, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library';
+import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector } from 'app/store/storeHooks';
-import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
-import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar';
+import { selectImageToCompare } from 'features/gallery/components/ImageViewer/common';
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
-import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
-import { selectHasImageToCompare } from 'features/gallery/store/gallerySelectors';
+import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
import type { ReactNode } from 'react';
import { memo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
-import { useMeasure } from 'react-use';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useImageViewer } from './useImageViewer';
@@ -37,22 +36,16 @@ const FOCUS_REGION_STYLES: SystemStyleObject = {
overflow: 'hidden',
};
-export const ImageViewer = memo(({ closeButton }: Props) => {
- useAssertSingleton('ImageViewer');
- const hasImageToCompare = useAppSelector(selectHasImageToCompare);
- const [containerRef, containerDims] = useMeasure();
+export const ImageViewer = memo(() => {
+ const lastSelectedImageName = useAppSelector(selectLastSelectedImageName);
+ const { data: lastSelectedImageDTO } = useGetImageDTOQuery(lastSelectedImageName ?? skipToken);
+ const comparisonImageDTO = useAppSelector(selectImageToCompare);
- return (
-
- {hasImageToCompare && }
- {!hasImageToCompare && }
-
- {!hasImageToCompare && }
- {hasImageToCompare && }
-
-
-
- );
+ if (lastSelectedImageDTO && comparisonImageDTO) {
+ return ;
+ }
+
+ return ;
});
ImageViewer.displayName = 'ImageViewer';
@@ -73,16 +66,6 @@ const imageViewerContainerSx: SystemStyleObject = {
backdropFilter: 'blur(10px) brightness(70%)',
};
-const imageViewerModalSx: SystemStyleObject = {
- position: 'absolute',
- bg: 'base.800',
- borderRadius: 'base',
- top: 16,
- right: 16,
- bottom: 16,
- left: 16,
-};
-
export const ImageViewerModal = memo(() => {
const ref = useRef(null);
const imageViewer = useImageViewer();
@@ -93,9 +76,20 @@ export const ImageViewerModal = memo(() => {
return (
-
- } />
-
+
+
+
+
);
});
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts
index ac3d7b172b..3170b08b16 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts
@@ -1,3 +1,4 @@
+import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import type { Dimensions } from 'features/controlLayers/store/types';
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
@@ -62,3 +63,8 @@ export const selectComparisonImages = createMemoizedSelector(selectGallerySlice,
const secondImage = gallerySlice.imageToCompare;
return { firstImage, secondImage };
});
+export const selectFirstImage = createSelector(
+ selectGallerySlice,
+ (gallerySlice) => gallerySlice.selection.slice(-1)[0] ?? null
+);
+export const selectImageToCompare = createSelector(selectGallerySlice, (gallerySlice) => gallerySlice.imageToCompare);
From 5f2f12f803eb0acf10d05d8dfae18ad1033511bb Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 21 May 2025 16:26:58 +1000
Subject: [PATCH 005/210] refactor(ui): org state in prep for new flow
---
.../frontend/web/src/common/util/zodUtils.ts | 10 ++
.../controlLayers/store/canvasSlice.ts | 54 +-------
.../features/controlLayers/store/selectors.ts | 79 +++++++-----
.../src/features/controlLayers/store/types.ts | 117 ++++++++++++------
.../store/dynamicPromptsSlice.ts | 3 +-
.../parameters/types/parameterSchemas.ts | 12 +-
6 files changed, 145 insertions(+), 130 deletions(-)
create mode 100644 invokeai/frontend/web/src/common/util/zodUtils.ts
diff --git a/invokeai/frontend/web/src/common/util/zodUtils.ts b/invokeai/frontend/web/src/common/util/zodUtils.ts
new file mode 100644
index 0000000000..10506736e1
--- /dev/null
+++ b/invokeai/frontend/web/src/common/util/zodUtils.ts
@@ -0,0 +1,10 @@
+import type { z } from 'zod';
+
+/**
+ * Helper to create a type guard from a zod schema. The type guard will infer the schema's TS type.
+ * @param schema The zod schema to create a type guard from.
+ * @returns A type guard function for the schema.
+ */
+export const buildZodTypeGuard = (schema: T) => {
+ return (val: unknown): val is z.infer => schema.safeParse(val).success;
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index 5fd71f8158..cbcd0dcb26 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -70,7 +70,9 @@ import type {
T2IAdapterConfig,
} from './types';
import {
+ DEFAULT_ASPECT_RATIO_CONFIG,
getEntityIdentifier,
+ getInitialState,
isChatGPT4oAspectRatioID,
isFluxKontextAspectRatioID,
isImagenAspectRatioID,
@@ -93,55 +95,9 @@ import {
initialT2IAdapter,
} from './util';
-/**
- * Gets a fresh canvas initial state with no references in memory to existing objects.
- */
-const getInitialState = (): CanvasState => {
- const initialInpaintMaskState = getInpaintMaskState(getPrefixedId('inpaint_mask'));
- const initialState: CanvasState = {
- _version: 3,
- selectedEntityIdentifier: getEntityIdentifier(initialInpaintMaskState),
- bookmarkedEntityIdentifier: getEntityIdentifier(initialInpaintMaskState),
- rasterLayers: {
- isHidden: false,
- entities: [],
- },
- controlLayers: {
- isHidden: false,
- entities: [],
- },
- inpaintMasks: {
- isHidden: false,
- entities: [initialInpaintMaskState],
- },
- regionalGuidance: {
- isHidden: false,
- entities: [],
- },
- referenceImages: { entities: [] },
- bbox: {
- rect: { x: 0, y: 0, width: 512, height: 512 },
- aspectRatio: {
- id: '1:1',
- value: 1,
- isLocked: false,
- },
- scaleMethod: 'auto',
- scaledSize: {
- width: 512,
- height: 512,
- },
- modelBase: 'sd-1',
- },
- };
- return initialState;
-};
-
-const initialState = getInitialState();
-
export const canvasSlice = createSlice({
name: 'canvas',
- initialState,
+ initialState: getInitialState(),
reducers: {
// undoable canvas state
//#region Raster layers
@@ -1409,7 +1365,7 @@ export const canvasSlice = createSlice({
state.bbox.rect.width = width;
state.bbox.rect.height = height;
} else {
- state.bbox.aspectRatio = deepClone(initialState.bbox.aspectRatio);
+ state.bbox.aspectRatio = deepClone(DEFAULT_ASPECT_RATIO_CONFIG);
state.bbox.rect.width = optimalDimension;
state.bbox.rect.height = optimalDimension;
}
@@ -1975,7 +1931,7 @@ const migrate = (state: any): any => {
export const canvasPersistConfig: PersistConfig = {
name: canvasSlice.name,
- initialState,
+ initialState: getInitialState(),
migrate,
persistDenylist: [],
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
index 32114001e3..fe37d1891b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
@@ -1,3 +1,4 @@
+import type { Selector } from '@reduxjs/toolkit';
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
@@ -23,6 +24,9 @@ import { assert } from 'tsafe';
*/
export const selectCanvasSlice = (state: RootState) => state.canvas.present;
+export const createCanvasSelector = (selector: Selector) =>
+ createSelector(selectCanvasSlice, selector);
+
/**
* Selects the total canvas entity count:
* - Regions
@@ -33,7 +37,7 @@ export const selectCanvasSlice = (state: RootState) => state.canvas.present;
*
* All entities are counted, regardless of their state.
*/
-const selectEntityCountAll = createSelector(selectCanvasSlice, (canvas) => {
+const selectEntityCountAll = createCanvasSelector((canvas) => {
return (
canvas.regionalGuidance.entities.length +
canvas.referenceImages.entities.length +
@@ -45,24 +49,29 @@ const selectEntityCountAll = createSelector(selectCanvasSlice, (canvas) => {
const isVisibleEntity = (entity: CanvasRenderableEntityState) => entity.isEnabled && entity.objects.length > 0;
-export const selectActiveRasterLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
- canvas.rasterLayers.entities.filter(isVisibleEntity)
+export const selectRasterLayerEntities = createCanvasSelector((canvas) => canvas.rasterLayers.entities);
+export const selectActiveRasterLayerEntities = createSelector(selectRasterLayerEntities, (entities) =>
+ entities.filter(isVisibleEntity)
);
-export const selectActiveControlLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
- canvas.controlLayers.entities.filter(isVisibleEntity)
+export const selectControlLayerEntities = createCanvasSelector((canvas) => canvas.controlLayers.entities);
+export const selectActiveControlLayerEntities = createSelector(selectControlLayerEntities, (entities) =>
+ entities.filter(isVisibleEntity)
);
-export const selectActiveInpaintMaskEntities = createSelector(selectCanvasSlice, (canvas) =>
- canvas.inpaintMasks.entities.filter(isVisibleEntity)
+export const selectInpaintMaskEntities = createCanvasSelector((canvas) => canvas.inpaintMasks.entities);
+export const selectActiveInpaintMaskEntities = createSelector(selectInpaintMaskEntities, (entities) =>
+ entities.filter(isVisibleEntity)
);
-export const selectActiveRegionalGuidanceEntities = createSelector(selectCanvasSlice, (canvas) =>
- canvas.regionalGuidance.entities.filter(isVisibleEntity)
+export const selectRegionalGuidanceEntities = createCanvasSelector((canvas) => canvas.regionalGuidance.entities);
+export const selectActiveRegionalGuidanceEntities = createSelector(selectRegionalGuidanceEntities, (entities) =>
+ entities.filter(isVisibleEntity)
);
-export const selectActiveReferenceImageEntities = createSelector(selectCanvasSlice, (canvas) =>
- canvas.referenceImages.entities.filter((e) => e.isEnabled)
+export const selectReferenceImageEntities = createCanvasSelector((canvas) => canvas.referenceImages.entities);
+export const selectActiveReferenceImageEntities = createSelector(selectReferenceImageEntities, (entities) =>
+ entities.filter((e) => e.isEnabled)
);
/**
@@ -192,14 +201,6 @@ export function selectEntityIdentifierBelowThisOne | undefined;
}
-export const selectRasterLayerEntities = createSelector(selectCanvasSlice, (canvas) => canvas.rasterLayers.entities);
-export const selectControlLayerEntities = createSelector(selectCanvasSlice, (canvas) => canvas.controlLayers.entities);
-export const selectInpaintMaskEntities = createSelector(selectCanvasSlice, (canvas) => canvas.inpaintMasks.entities);
-export const selectRegionalGuidanceEntities = createSelector(
- selectCanvasSlice,
- (canvas) => canvas.regionalGuidance.entities
-);
-
/**
* Selected an entity from the canvas slice. If the entity is not found, an error is thrown.
*
@@ -218,7 +219,7 @@ export function selectEntityOrThrow(
}
export const selectEntityExists = (entityIdentifier: T) => {
- return createSelector(selectCanvasSlice, (canvas) => Boolean(selectEntity(canvas, entityIdentifier)));
+ return createCanvasSelector((canvas) => Boolean(selectEntity(canvas, entityIdentifier)));
};
/**
@@ -299,7 +300,7 @@ export function selectRegionalGuidanceReferenceImage(
return entity.referenceImages.find(({ id }) => id === referenceImageId);
}
-export const selectBbox = createSelector(selectCanvasSlice, (canvas) => canvas.bbox);
+export const selectBbox = createCanvasSelector((canvas) => canvas.bbox);
export const selectSelectedEntityIdentifier = createSelector(
selectCanvasSlice,
@@ -331,10 +332,10 @@ export const selectSelectedEntityFill = createSelector(
}
);
-const selectRasterLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.rasterLayers.isHidden);
-const selectControlLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.controlLayers.isHidden);
-const selectInpaintMasksIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.inpaintMasks.isHidden);
-const selectRegionalGuidanceIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.regionalGuidance.isHidden);
+const selectRasterLayersIsHidden = createCanvasSelector((canvas) => canvas.rasterLayers.isHidden);
+const selectControlLayersIsHidden = createCanvasSelector((canvas) => canvas.controlLayers.isHidden);
+const selectInpaintMasksIsHidden = createCanvasSelector((canvas) => canvas.inpaintMasks.isHidden);
+const selectRegionalGuidanceIsHidden = createCanvasSelector((canvas) => canvas.regionalGuidance.isHidden);
/**
* Returns the hidden selector for the given entity type.
@@ -372,7 +373,7 @@ export const buildSelectIsSelected = (entityIdentifier: CanvasEntityIdentifier)
* Other entities are considered empty if they have no objects.
*/
export const buildSelectHasObjects = (entityIdentifier: CanvasEntityIdentifier) => {
- return createSelector(selectCanvasSlice, (canvas) => {
+ return createCanvasSelector((canvas) => {
const entity = selectEntity(canvas, entityIdentifier);
if (!entity) {
@@ -385,10 +386,10 @@ export const buildSelectHasObjects = (entityIdentifier: CanvasEntityIdentifier)
});
};
-export const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width);
-export const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height);
-export const selectAspectRatioID = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.id);
-export const selectAspectRatioValue = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.value);
+export const selectWidth = createCanvasSelector((canvas) => canvas.bbox.rect.width);
+export const selectHeight = createCanvasSelector((canvas) => canvas.bbox.rect.height);
+export const selectAspectRatioID = createCanvasSelector((canvas) => canvas.bbox.aspectRatio.id);
+export const selectAspectRatioValue = createCanvasSelector((canvas) => canvas.bbox.aspectRatio.value);
export const selectScaledSize = createSelector(selectBbox, (bbox) => bbox.scaledSize);
export const selectScaleMethod = createSelector(selectBbox, (bbox) => bbox.scaleMethod);
export const selectBboxRect = createSelector(selectBbox, (bbox) => bbox.rect);
@@ -407,3 +408,21 @@ export const selectCanvasMetadata = createSelector(
return { canvas_v2_metadata };
}
);
+
+export const selectIsSessionStarted = createCanvasSelector(({ isSessionStarted }) => isSessionStarted);
+export const selectIsCanvasEmpty = createCanvasSelector(
+ ({ controlLayers, inpaintMasks, rasterLayers, regionalGuidance }) => {
+ // Check it all manually - could use lodash isEqual, but this selector will be called very often!
+ // Also note - we do not care about ref images, as they are technically not part of canvas
+ return (
+ controlLayers.entities.length === 0 &&
+ controlLayers.isHidden === false &&
+ inpaintMasks.entities.length === 0 &&
+ inpaintMasks.isHidden === false &&
+ rasterLayers.entities.length === 0 &&
+ rasterLayers.isHidden === false &&
+ regionalGuidance.entities.length === 0 &&
+ regionalGuidance.isHidden === false
+ );
+ }
+);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 9d0f79b8a8..a4aee48086 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -1,3 +1,4 @@
+import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers';
import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common';
@@ -438,51 +439,87 @@ export const isFluxKontextAspectRatioID = (v: unknown): v is z.infer;
export const isAspectRatioID = (v: unknown): v is AspectRatioID => zAspectRatioID.safeParse(v).success;
+const zAspectRatioConfig = z.object({
+ id: zAspectRatioID,
+ value: z.number().gt(0),
+ isLocked: z.boolean(),
+});
+type AspectRatioConfig = z.infer;
+
+export const DEFAULT_ASPECT_RATIO_CONFIG: AspectRatioConfig = {
+ id: '1:1',
+ value: 1,
+ isLocked: false,
+};
+
+const zBboxState = z.object({
+ rect: z.object({
+ x: z.number().int(),
+ y: z.number().int(),
+ width: zParameterImageDimension,
+ height: zParameterImageDimension,
+ }),
+ aspectRatio: zAspectRatioConfig,
+ scaledSize: z.object({
+ width: zParameterImageDimension,
+ height: zParameterImageDimension,
+ }),
+ scaleMethod: zBoundingBoxScaleMethod,
+ modelBase: zMainModelBase,
+});
const zCanvasState = z.object({
- _version: z.literal(3),
- selectedEntityIdentifier: zCanvasEntityIdentifer.nullable(),
- bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable(),
- inpaintMasks: z.object({
- isHidden: z.boolean(),
- entities: z.array(zCanvasInpaintMaskState),
- }),
- rasterLayers: z.object({
- isHidden: z.boolean(),
- entities: z.array(zCanvasRasterLayerState),
- }),
- controlLayers: z.object({
- isHidden: z.boolean(),
- entities: z.array(zCanvasControlLayerState),
- }),
- regionalGuidance: z.object({
- isHidden: z.boolean(),
- entities: z.array(zCanvasRegionalGuidanceState),
- }),
- referenceImages: z.object({
- entities: z.array(zCanvasReferenceImageState),
- }),
- bbox: z.object({
- rect: z.object({
- x: z.number().int(),
- y: z.number().int(),
- width: zParameterImageDimension,
- height: zParameterImageDimension,
- }),
- aspectRatio: z.object({
- id: zAspectRatioID,
- value: z.number().gt(0),
- isLocked: z.boolean(),
- }),
- scaledSize: z.object({
- width: zParameterImageDimension,
- height: zParameterImageDimension,
- }),
- scaleMethod: zBoundingBoxScaleMethod,
- modelBase: zMainModelBase,
+ _version: z.literal(3).default(3),
+ isSessionStarted: z.boolean().default(false),
+ selectedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
+ bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
+ inpaintMasks: z
+ .object({
+ isHidden: z.boolean(),
+ entities: z.array(zCanvasInpaintMaskState),
+ })
+ .default({ isHidden: false, entities: [] }),
+ rasterLayers: z
+ .object({
+ isHidden: z.boolean(),
+ entities: z.array(zCanvasRasterLayerState),
+ })
+ .default({ isHidden: false, entities: [] }),
+ controlLayers: z
+ .object({
+ isHidden: z.boolean(),
+ entities: z.array(zCanvasControlLayerState),
+ })
+ .default({ isHidden: false, entities: [] }),
+ regionalGuidance: z
+ .object({
+ isHidden: z.boolean(),
+ entities: z.array(zCanvasRegionalGuidanceState),
+ })
+ .default({ isHidden: false, entities: [] }),
+ referenceImages: z
+ .object({
+ entities: z.array(zCanvasReferenceImageState),
+ })
+ .default({ entities: [] }),
+ bbox: zBboxState.default({
+ rect: { x: 0, y: 0, width: 512, height: 512 },
+ aspectRatio: DEFAULT_ASPECT_RATIO_CONFIG,
+ scaleMethod: 'auto',
+ scaledSize: {
+ width: 512,
+ height: 512,
+ },
+ modelBase: 'sd-1',
}),
});
export type CanvasState = z.infer;
+/**
+ * Gets a fresh canvas initial state with no references in memory to existing objects.
+ */
+const CANVAS_INITIAL_STATE = zCanvasState.parse({});
+export const getInitialState = () => deepClone(CANVAS_INITIAL_STATE);
+
export const zCanvasMetadata = z.object({
inpaintMasks: z.array(zCanvasInpaintMaskState),
rasterLayers: z.array(zCanvasRasterLayerState),
diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts
index 9f95039c4f..741c415f38 100644
--- a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts
+++ b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts
@@ -1,11 +1,12 @@
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
+import { buildZodTypeGuard } from 'common/util/zodUtils';
import { z } from 'zod';
const zSeedBehaviour = z.enum(['PER_ITERATION', 'PER_PROMPT']);
+export const isSeedBehaviour = buildZodTypeGuard(zSeedBehaviour);
export type SeedBehaviour = z.infer;
-export const isSeedBehaviour = (v: unknown): v is SeedBehaviour => zSeedBehaviour.safeParse(v).success;
export interface DynamicPromptsState {
_version: 1;
diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts
index 1076100947..acc02785b9 100644
--- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts
+++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts
@@ -1,5 +1,6 @@
import { NUMPY_RAND_MAX } from 'app/constants';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
+import { buildZodTypeGuard } from 'common/util/zodUtils';
import { zModelIdentifierField, zSchedulerField } from 'features/nodes/types/common';
import { z } from 'zod';
@@ -15,21 +16,12 @@ import { z } from 'zod';
* simply be the zod schema's safeParse function
*/
-/**
- * Helper to create a type guard from a zod schema. The type guard will infer the schema's TS type.
- * @param schema The zod schema to create a type guard from.
- * @returns A type guard function for the schema.
- */
-export const buildTypeGuard = (schema: T) => {
- return (val: unknown): val is z.infer => schema.safeParse(val).success;
-};
-
/**
* Helper to create a zod schema and a type guard from it.
* @param schema The zod schema to create a type guard from.
* @returns A tuple containing the zod schema and the type guard function.
*/
-const buildParameter = (schema: T) => [schema, buildTypeGuard(schema)] as const;
+export const buildParameter = (schema: T) => [schema, buildZodTypeGuard(schema)] as const;
// #region Positive prompt
export const [zParameterPositivePrompt, isParameterPositivePrompt] = buildParameter(z.string());
From 5c4cbc7fa22bc14429c62317ff38344353c8c54e Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 21 May 2025 16:38:07 +1000
Subject: [PATCH 006/210] refactor(ui): zod-ify params slice state
---
.../controlLayers/store/paramsSlice.ts | 189 ++++++++----------
.../src/features/controlLayers/store/types.ts | 2 +-
.../parameters/types/parameterSchemas.ts | 4 +-
3 files changed, 84 insertions(+), 111 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index 069e8fc366..72060f38ad 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -2,7 +2,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import type { RgbaColor } from 'features/controlLayers/store/types';
+import { type RgbaColor, zRgbaColor } from 'features/controlLayers/store/types';
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
import type {
ParameterCanvasCoherenceMode,
@@ -13,126 +13,99 @@ import type {
ParameterCLIPLEmbedModel,
ParameterControlLoRAModel,
ParameterGuidance,
- ParameterMaskBlurMethod,
ParameterModel,
- ParameterNegativePrompt,
- ParameterNegativeStylePromptSDXL,
- ParameterPositivePrompt,
- ParameterPositiveStylePromptSDXL,
ParameterPrecision,
ParameterScheduler,
ParameterSDXLRefinerModel,
- ParameterSeed,
- ParameterSteps,
- ParameterStrength,
ParameterT5EncoderModel,
ParameterVAEModel,
} from 'features/parameters/types/parameterSchemas';
+import {
+ zParameterCanvasCoherenceMode,
+ zParameterCFGRescaleMultiplier,
+ zParameterCFGScale,
+ zParameterCLIPEmbedModel,
+ zParameterCLIPGEmbedModel,
+ zParameterCLIPLEmbedModel,
+ zParameterControlLoRAModel,
+ zParameterGuidance,
+ zParameterMaskBlurMethod,
+ zParameterModel,
+ zParameterNegativePrompt,
+ zParameterNegativeStylePromptSDXL,
+ zParameterPositivePrompt,
+ zParameterPositiveStylePromptSDXL,
+ zParameterPrecision,
+ zParameterScheduler,
+ zParameterSDXLRefinerModel,
+ zParameterSeed,
+ zParameterSteps,
+ zParameterStrength,
+ zParameterT5EncoderModel,
+ zParameterVAEModel,
+} from 'features/parameters/types/parameterSchemas';
import { clamp } from 'lodash-es';
+import { z } from 'zod';
import { newSessionRequested } from './actions';
-export type ParamsState = {
- maskBlur: number;
- maskBlurMethod: ParameterMaskBlurMethod;
- canvasCoherenceMode: ParameterCanvasCoherenceMode;
- canvasCoherenceMinDenoise: ParameterStrength;
- canvasCoherenceEdgeSize: number;
- infillMethod: string;
- infillTileSize: number;
- infillPatchmatchDownscaleSize: number;
- infillColorValue: RgbaColor;
- cfgScale: ParameterCFGScale;
- cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
- guidance: ParameterGuidance;
- img2imgStrength: ParameterStrength;
- optimizedDenoisingEnabled: boolean;
- iterations: number;
- scheduler: ParameterScheduler;
- upscaleScheduler: ParameterScheduler;
- upscaleCfgScale: ParameterCFGScale;
- seed: ParameterSeed;
- shouldRandomizeSeed: boolean;
- steps: ParameterSteps;
- model: ParameterModel | null;
- vae: ParameterVAEModel | null;
- vaePrecision: ParameterPrecision;
- fluxVAE: ParameterVAEModel | null;
- seamlessXAxis: boolean;
- seamlessYAxis: boolean;
- clipSkip: number;
- shouldUseCpuNoise: boolean;
- positivePrompt: ParameterPositivePrompt;
- negativePrompt: ParameterNegativePrompt;
- positivePrompt2: ParameterPositiveStylePromptSDXL;
- negativePrompt2: ParameterNegativeStylePromptSDXL;
- shouldConcatPrompts: boolean;
- refinerModel: ParameterSDXLRefinerModel | null;
- refinerSteps: number;
- refinerCFGScale: number;
- refinerScheduler: ParameterScheduler;
- refinerPositiveAestheticScore: number;
- refinerNegativeAestheticScore: number;
- refinerStart: number;
- t5EncoderModel: ParameterT5EncoderModel | null;
- clipEmbedModel: ParameterCLIPEmbedModel | null;
- clipLEmbedModel: ParameterCLIPLEmbedModel | null;
- clipGEmbedModel: ParameterCLIPGEmbedModel | null;
- controlLora: ParameterControlLoRAModel | null;
-};
+const zParamsState = z.object({
+ maskBlur: z.number().default(16),
+ maskBlurMethod: zParameterMaskBlurMethod.default('box'),
+ canvasCoherenceMode: zParameterCanvasCoherenceMode.default('Gaussian Blur'),
+ canvasCoherenceMinDenoise: zParameterStrength.default(0),
+ canvasCoherenceEdgeSize: z.number().default(16),
+ infillMethod: z.string().default('lama'),
+ infillTileSize: z.number().default(32),
+ infillPatchmatchDownscaleSize: z.number().default(1),
+ infillColorValue: zRgbaColor.default({ r: 0, g: 0, b: 0, a: 1 }),
+ cfgScale: zParameterCFGScale.default(7.5),
+ cfgRescaleMultiplier: zParameterCFGRescaleMultiplier.default(0),
+ guidance: zParameterGuidance.default(4),
+ img2imgStrength: zParameterStrength.default(0.75),
+ optimizedDenoisingEnabled: z.boolean().default(true),
+ iterations: z.number().default(1),
+ scheduler: zParameterScheduler.default('dpmpp_3m_k'),
+ upscaleScheduler: zParameterScheduler.default('kdpm_2'),
+ upscaleCfgScale: zParameterCFGScale.default(2),
+ seed: zParameterSeed.default(0),
+ shouldRandomizeSeed: z.boolean().default(true),
+ steps: zParameterSteps.default(30),
+ model: zParameterModel.nullable().default(null),
+ vae: zParameterVAEModel.nullable().default(null),
+ vaePrecision: zParameterPrecision.default('fp32'),
+ fluxVAE: zParameterVAEModel.nullable().default(null),
+ seamlessXAxis: z.boolean().default(false),
+ seamlessYAxis: z.boolean().default(false),
+ clipSkip: z.number().default(0),
+ shouldUseCpuNoise: z.boolean().default(true),
+ positivePrompt: zParameterPositivePrompt.default(''),
+ negativePrompt: zParameterNegativePrompt.default(''),
+ positivePrompt2: zParameterPositiveStylePromptSDXL.default(''),
+ negativePrompt2: zParameterNegativeStylePromptSDXL.default(''),
+ shouldConcatPrompts: z.boolean().default(true),
+ refinerModel: zParameterSDXLRefinerModel.nullable().default(null),
+ refinerSteps: z.number().default(20),
+ refinerCFGScale: z.number().default(7.5),
+ refinerScheduler: zParameterScheduler.default('euler'),
+ refinerPositiveAestheticScore: z.number().default(6),
+ refinerNegativeAestheticScore: z.number().default(2.5),
+ refinerStart: z.number().default(0.8),
+ t5EncoderModel: zParameterT5EncoderModel.nullable().default(null),
+ clipEmbedModel: zParameterCLIPEmbedModel.nullable().default(null),
+ clipLEmbedModel: zParameterCLIPLEmbedModel.nullable().default(null),
+ clipGEmbedModel: zParameterCLIPGEmbedModel.nullable().default(null),
+ controlLora: zParameterControlLoRAModel.nullable().default(null),
+});
-const initialState: ParamsState = {
- maskBlur: 16,
- maskBlurMethod: 'box',
- canvasCoherenceMode: 'Gaussian Blur',
- canvasCoherenceMinDenoise: 0,
- canvasCoherenceEdgeSize: 16,
- infillMethod: 'lama',
- infillTileSize: 32,
- infillPatchmatchDownscaleSize: 1,
- infillColorValue: { r: 0, g: 0, b: 0, a: 1 },
- cfgScale: 7.5,
- cfgRescaleMultiplier: 0,
- guidance: 4,
- img2imgStrength: 0.75,
- optimizedDenoisingEnabled: true,
- iterations: 1,
- scheduler: 'dpmpp_3m_k',
- upscaleScheduler: 'kdpm_2',
- upscaleCfgScale: 2,
- seed: 0,
- shouldRandomizeSeed: true,
- steps: 30,
- model: null,
- vae: null,
- fluxVAE: null,
- vaePrecision: 'fp32',
- seamlessXAxis: false,
- seamlessYAxis: false,
- clipSkip: 0,
- shouldUseCpuNoise: true,
- positivePrompt: '',
- negativePrompt: '',
- positivePrompt2: '',
- negativePrompt2: '',
- shouldConcatPrompts: true,
- refinerModel: null,
- refinerSteps: 20,
- refinerCFGScale: 7.5,
- refinerScheduler: 'euler',
- refinerPositiveAestheticScore: 6,
- refinerNegativeAestheticScore: 2.5,
- refinerStart: 0.8,
- t5EncoderModel: null,
- clipEmbedModel: null,
- clipLEmbedModel: null,
- clipGEmbedModel: null,
- controlLora: null,
-};
+export type ParamsState = z.infer;
+
+const INITIAL_STATE = zParamsState.parse({});
+const getInitialState = () => deepClone(INITIAL_STATE);
export const paramsSlice = createSlice({
name: 'params',
- initialState,
+ initialState: getInitialState(),
reducers: {
setIterations: (state, action: PayloadAction) => {
state.iterations = action.payload;
@@ -300,7 +273,7 @@ export const paramsSlice = createSlice({
const resetState = (state: ParamsState): ParamsState => {
// When a new session is requested, we need to keep the current model selections, plus dependent state
// like VAE precision. Everything else gets reset to default.
- const newState = deepClone(initialState);
+ const newState = getInitialState();
newState.model = state.model;
newState.vae = state.vae;
newState.fluxVAE = state.fluxVAE;
@@ -366,7 +339,7 @@ const migrate = (state: any): any => {
export const paramsPersistConfig: PersistConfig = {
name: paramsSlice.name,
- initialState,
+ initialState: getInitialState(),
migrate,
persistDenylist: [],
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index a4aee48086..7040938ab2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -72,7 +72,7 @@ const zRgbColor = z.object({
b: z.number().int().min(0).max(255),
});
export type RgbColor = z.infer;
-const zRgbaColor = zRgbColor.extend({
+export const zRgbaColor = zRgbColor.extend({
a: z.number().min(0).max(1),
});
export type RgbaColor = z.infer;
diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts
index acc02785b9..3daf1f4137 100644
--- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts
+++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts
@@ -96,7 +96,7 @@ export type ParameterModel = z.infer;
// #endregion
// #region SDXL Refiner Model
-const zParameterSDXLRefinerModel = zModelIdentifierField;
+export const zParameterSDXLRefinerModel = zModelIdentifierField;
export type ParameterSDXLRefinerModel = z.infer;
// #endregion
@@ -188,7 +188,7 @@ export type ParameterSDXLRefinerStart = z.infer;
// #endregion
From a0b0c30be98238c3511fb097ae1dcab7f8993cec Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 21 May 2025 16:40:34 +1000
Subject: [PATCH 007/210] refactor(ui): move params state to big file of canvas
zod stuff
---
.../controlLayers/store/canvasSlice.ts | 10 +--
.../controlLayers/store/paramsSlice.ts | 89 ++-----------------
.../src/features/controlLayers/store/types.ts | 75 +++++++++++++++-
3 files changed, 84 insertions(+), 90 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index cbcd0dcb26..1f49abfa87 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -72,7 +72,7 @@ import type {
import {
DEFAULT_ASPECT_RATIO_CONFIG,
getEntityIdentifier,
- getInitialState,
+ getInitialCanvasState,
isChatGPT4oAspectRatioID,
isFluxKontextAspectRatioID,
isImagenAspectRatioID,
@@ -97,7 +97,7 @@ import {
export const canvasSlice = createSlice({
name: 'canvas',
- initialState: getInitialState(),
+ initialState: getInitialCanvasState(),
reducers: {
// undoable canvas state
//#region Raster layers
@@ -1745,7 +1745,7 @@ export const canvasSlice = createSlice({
},
allEntitiesDeleted: (state) => {
// Deleting all entities is equivalent to resetting the state for each entity type
- const initialState = getInitialState();
+ const initialState = getInitialCanvasState();
state.rasterLayers = initialState.rasterLayers;
state.controlLayers = initialState.controlLayers;
state.inpaintMasks = initialState.inpaintMasks;
@@ -1809,7 +1809,7 @@ export const canvasSlice = createSlice({
});
const resetState = (state: CanvasState) => {
- const newState = getInitialState();
+ const newState = getInitialCanvasState();
// We need to retain the optimal dimension across resets, as it is changed only when the model changes. Copy it
// from the old state, then recalculate the bbox size & scaled size.
@@ -1931,7 +1931,7 @@ const migrate = (state: any): any => {
export const canvasPersistConfig: PersistConfig = {
name: canvasSlice.name,
- initialState: getInitialState(),
+ initialState: getInitialCanvasState(),
migrate,
persistDenylist: [],
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index 72060f38ad..a6a7e8d98d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -1,8 +1,8 @@
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
-import { deepClone } from 'common/util/deepClone';
-import { type RgbaColor, zRgbaColor } from 'features/controlLayers/store/types';
+import type { ParamsState, RgbaColor } from 'features/controlLayers/store/types';
+import { getInitialParamsState } from 'features/controlLayers/store/types';
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
import type {
ParameterCanvasCoherenceMode,
@@ -20,92 +20,13 @@ import type {
ParameterT5EncoderModel,
ParameterVAEModel,
} from 'features/parameters/types/parameterSchemas';
-import {
- zParameterCanvasCoherenceMode,
- zParameterCFGRescaleMultiplier,
- zParameterCFGScale,
- zParameterCLIPEmbedModel,
- zParameterCLIPGEmbedModel,
- zParameterCLIPLEmbedModel,
- zParameterControlLoRAModel,
- zParameterGuidance,
- zParameterMaskBlurMethod,
- zParameterModel,
- zParameterNegativePrompt,
- zParameterNegativeStylePromptSDXL,
- zParameterPositivePrompt,
- zParameterPositiveStylePromptSDXL,
- zParameterPrecision,
- zParameterScheduler,
- zParameterSDXLRefinerModel,
- zParameterSeed,
- zParameterSteps,
- zParameterStrength,
- zParameterT5EncoderModel,
- zParameterVAEModel,
-} from 'features/parameters/types/parameterSchemas';
import { clamp } from 'lodash-es';
-import { z } from 'zod';
import { newSessionRequested } from './actions';
-const zParamsState = z.object({
- maskBlur: z.number().default(16),
- maskBlurMethod: zParameterMaskBlurMethod.default('box'),
- canvasCoherenceMode: zParameterCanvasCoherenceMode.default('Gaussian Blur'),
- canvasCoherenceMinDenoise: zParameterStrength.default(0),
- canvasCoherenceEdgeSize: z.number().default(16),
- infillMethod: z.string().default('lama'),
- infillTileSize: z.number().default(32),
- infillPatchmatchDownscaleSize: z.number().default(1),
- infillColorValue: zRgbaColor.default({ r: 0, g: 0, b: 0, a: 1 }),
- cfgScale: zParameterCFGScale.default(7.5),
- cfgRescaleMultiplier: zParameterCFGRescaleMultiplier.default(0),
- guidance: zParameterGuidance.default(4),
- img2imgStrength: zParameterStrength.default(0.75),
- optimizedDenoisingEnabled: z.boolean().default(true),
- iterations: z.number().default(1),
- scheduler: zParameterScheduler.default('dpmpp_3m_k'),
- upscaleScheduler: zParameterScheduler.default('kdpm_2'),
- upscaleCfgScale: zParameterCFGScale.default(2),
- seed: zParameterSeed.default(0),
- shouldRandomizeSeed: z.boolean().default(true),
- steps: zParameterSteps.default(30),
- model: zParameterModel.nullable().default(null),
- vae: zParameterVAEModel.nullable().default(null),
- vaePrecision: zParameterPrecision.default('fp32'),
- fluxVAE: zParameterVAEModel.nullable().default(null),
- seamlessXAxis: z.boolean().default(false),
- seamlessYAxis: z.boolean().default(false),
- clipSkip: z.number().default(0),
- shouldUseCpuNoise: z.boolean().default(true),
- positivePrompt: zParameterPositivePrompt.default(''),
- negativePrompt: zParameterNegativePrompt.default(''),
- positivePrompt2: zParameterPositiveStylePromptSDXL.default(''),
- negativePrompt2: zParameterNegativeStylePromptSDXL.default(''),
- shouldConcatPrompts: z.boolean().default(true),
- refinerModel: zParameterSDXLRefinerModel.nullable().default(null),
- refinerSteps: z.number().default(20),
- refinerCFGScale: z.number().default(7.5),
- refinerScheduler: zParameterScheduler.default('euler'),
- refinerPositiveAestheticScore: z.number().default(6),
- refinerNegativeAestheticScore: z.number().default(2.5),
- refinerStart: z.number().default(0.8),
- t5EncoderModel: zParameterT5EncoderModel.nullable().default(null),
- clipEmbedModel: zParameterCLIPEmbedModel.nullable().default(null),
- clipLEmbedModel: zParameterCLIPLEmbedModel.nullable().default(null),
- clipGEmbedModel: zParameterCLIPGEmbedModel.nullable().default(null),
- controlLora: zParameterControlLoRAModel.nullable().default(null),
-});
-
-export type ParamsState = z.infer;
-
-const INITIAL_STATE = zParamsState.parse({});
-const getInitialState = () => deepClone(INITIAL_STATE);
-
export const paramsSlice = createSlice({
name: 'params',
- initialState: getInitialState(),
+ initialState: getInitialParamsState(),
reducers: {
setIterations: (state, action: PayloadAction) => {
state.iterations = action.payload;
@@ -273,7 +194,7 @@ export const paramsSlice = createSlice({
const resetState = (state: ParamsState): ParamsState => {
// When a new session is requested, we need to keep the current model selections, plus dependent state
// like VAE precision. Everything else gets reset to default.
- const newState = getInitialState();
+ const newState = getInitialParamsState();
newState.model = state.model;
newState.vae = state.vae;
newState.fluxVAE = state.fluxVAE;
@@ -339,7 +260,7 @@ const migrate = (state: any): any => {
export const paramsPersistConfig: PersistConfig = {
name: paramsSlice.name,
- initialState: getInitialState(),
+ initialState: getInitialParamsState(),
migrate,
persistDenylist: [],
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 7040938ab2..fca9e1a6ce 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -4,9 +4,29 @@ import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchi
import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
import {
+ zParameterCanvasCoherenceMode,
+ zParameterCFGRescaleMultiplier,
+ zParameterCFGScale,
+ zParameterCLIPEmbedModel,
+ zParameterCLIPGEmbedModel,
+ zParameterCLIPLEmbedModel,
+ zParameterControlLoRAModel,
+ zParameterGuidance,
zParameterImageDimension,
+ zParameterMaskBlurMethod,
+ zParameterModel,
zParameterNegativePrompt,
+ zParameterNegativeStylePromptSDXL,
zParameterPositivePrompt,
+ zParameterPositiveStylePromptSDXL,
+ zParameterPrecision,
+ zParameterScheduler,
+ zParameterSDXLRefinerModel,
+ zParameterSeed,
+ zParameterSteps,
+ zParameterStrength,
+ zParameterT5EncoderModel,
+ zParameterVAEModel,
} from 'features/parameters/types/parameterSchemas';
import { getImageDTOSafe } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
@@ -467,6 +487,59 @@ const zBboxState = z.object({
scaleMethod: zBoundingBoxScaleMethod,
modelBase: zMainModelBase,
});
+
+const zParamsState = z.object({
+ maskBlur: z.number().default(16),
+ maskBlurMethod: zParameterMaskBlurMethod.default('box'),
+ canvasCoherenceMode: zParameterCanvasCoherenceMode.default('Gaussian Blur'),
+ canvasCoherenceMinDenoise: zParameterStrength.default(0),
+ canvasCoherenceEdgeSize: z.number().default(16),
+ infillMethod: z.string().default('lama'),
+ infillTileSize: z.number().default(32),
+ infillPatchmatchDownscaleSize: z.number().default(1),
+ infillColorValue: zRgbaColor.default({ r: 0, g: 0, b: 0, a: 1 }),
+ cfgScale: zParameterCFGScale.default(7.5),
+ cfgRescaleMultiplier: zParameterCFGRescaleMultiplier.default(0),
+ guidance: zParameterGuidance.default(4),
+ img2imgStrength: zParameterStrength.default(0.75),
+ optimizedDenoisingEnabled: z.boolean().default(true),
+ iterations: z.number().default(1),
+ scheduler: zParameterScheduler.default('dpmpp_3m_k'),
+ upscaleScheduler: zParameterScheduler.default('kdpm_2'),
+ upscaleCfgScale: zParameterCFGScale.default(2),
+ seed: zParameterSeed.default(0),
+ shouldRandomizeSeed: z.boolean().default(true),
+ steps: zParameterSteps.default(30),
+ model: zParameterModel.nullable().default(null),
+ vae: zParameterVAEModel.nullable().default(null),
+ vaePrecision: zParameterPrecision.default('fp32'),
+ fluxVAE: zParameterVAEModel.nullable().default(null),
+ seamlessXAxis: z.boolean().default(false),
+ seamlessYAxis: z.boolean().default(false),
+ clipSkip: z.number().default(0),
+ shouldUseCpuNoise: z.boolean().default(true),
+ positivePrompt: zParameterPositivePrompt.default(''),
+ negativePrompt: zParameterNegativePrompt.default(''),
+ positivePrompt2: zParameterPositiveStylePromptSDXL.default(''),
+ negativePrompt2: zParameterNegativeStylePromptSDXL.default(''),
+ shouldConcatPrompts: z.boolean().default(true),
+ refinerModel: zParameterSDXLRefinerModel.nullable().default(null),
+ refinerSteps: z.number().default(20),
+ refinerCFGScale: z.number().default(7.5),
+ refinerScheduler: zParameterScheduler.default('euler'),
+ refinerPositiveAestheticScore: z.number().default(6),
+ refinerNegativeAestheticScore: z.number().default(2.5),
+ refinerStart: z.number().default(0.8),
+ t5EncoderModel: zParameterT5EncoderModel.nullable().default(null),
+ clipEmbedModel: zParameterCLIPEmbedModel.nullable().default(null),
+ clipLEmbedModel: zParameterCLIPLEmbedModel.nullable().default(null),
+ clipGEmbedModel: zParameterCLIPGEmbedModel.nullable().default(null),
+ controlLora: zParameterControlLoRAModel.nullable().default(null),
+});
+export type ParamsState = z.infer;
+const INITIAL_PARAMS_STATE = zParamsState.parse({});
+export const getInitialParamsState = () => deepClone(INITIAL_PARAMS_STATE);
+
const zCanvasState = z.object({
_version: z.literal(3).default(3),
isSessionStarted: z.boolean().default(false),
@@ -518,7 +591,7 @@ export type CanvasState = z.infer;
* Gets a fresh canvas initial state with no references in memory to existing objects.
*/
const CANVAS_INITIAL_STATE = zCanvasState.parse({});
-export const getInitialState = () => deepClone(CANVAS_INITIAL_STATE);
+export const getInitialCanvasState = () => deepClone(CANVAS_INITIAL_STATE);
export const zCanvasMetadata = z.object({
inpaintMasks: z.array(zCanvasInpaintMaskState),
From 02e4a3aa82e8862d5be277cf53a50dd3b548801a Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 22 May 2025 13:54:57 +1000
Subject: [PATCH 008/210] refactor(ui): params state zodification
---
.../components/CanvasMainPanelContent.tsx | 30 ++++++++--
.../src/features/controlLayers/store/types.ts | 58 ++++++++-----------
.../util/getScaledBoundingBoxDimensions.ts | 18 +++---
.../nodes/util/graph/graphBuilderUtils.ts | 4 +-
.../parameters/util/optimalDimension.ts | 5 +-
.../web/src/features/queue/store/readiness.ts | 3 +-
6 files changed, 65 insertions(+), 53 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 9ea0026b59..4bc6262c41 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -26,6 +26,7 @@ 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 { selectIsSessionStarted } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
@@ -36,7 +37,7 @@ const FOCUS_REGION_STYLES: SystemStyleObject = {
height: 'full',
};
-const MenuContent = () => {
+const MenuContent = memo(() => {
return (
@@ -45,9 +46,31 @@ const MenuContent = () => {
);
-};
+});
+MenuContent.displayName = 'MenuContent';
export const CanvasMainPanelContent = memo(() => {
+ const isSessionStarted = useAppSelector(selectIsSessionStarted);
+
+ if (!isSessionStarted) {
+ return ;
+ }
+
+ return ;
+});
+
+CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
+
+const CanvasNoSession = memo(() => {
+ return (
+
+ FRESH CANVAS is fresh when: - No control layers - No inpaint masks - No regions - No Raster Layers
+
+ );
+});
+CanvasNoSession.displayName = 'CanvasNoSession';
+
+const CanvasActiveSession = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
@@ -134,5 +157,4 @@ export const CanvasMainPanelContent = memo(() => {
);
});
-
-CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
+CanvasActiveSession.displayName = 'ActiveCanvasContent';
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index fca9e1a6ce..09b540f951 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -540,48 +540,40 @@ export type ParamsState = z.infer;
const INITIAL_PARAMS_STATE = zParamsState.parse({});
export const getInitialParamsState = () => deepClone(INITIAL_PARAMS_STATE);
+const zInpaintMasks = z.object({
+ isHidden: z.boolean(),
+ entities: z.array(zCanvasInpaintMaskState),
+});
+const zRasterLayers = z.object({
+ isHidden: z.boolean(),
+ entities: z.array(zCanvasRasterLayerState),
+});
+const zControlLayers = z.object({
+ isHidden: z.boolean(),
+ entities: z.array(zCanvasControlLayerState),
+});
+const zRegionalGuidance = z.object({
+ isHidden: z.boolean(),
+ entities: z.array(zCanvasRegionalGuidanceState),
+});
+const zReferenceImages = z.object({
+ entities: z.array(zCanvasReferenceImageState),
+});
const zCanvasState = z.object({
_version: z.literal(3).default(3),
isSessionStarted: z.boolean().default(false),
selectedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
- inpaintMasks: z
- .object({
- isHidden: z.boolean(),
- entities: z.array(zCanvasInpaintMaskState),
- })
- .default({ isHidden: false, entities: [] }),
- rasterLayers: z
- .object({
- isHidden: z.boolean(),
- entities: z.array(zCanvasRasterLayerState),
- })
- .default({ isHidden: false, entities: [] }),
- controlLayers: z
- .object({
- isHidden: z.boolean(),
- entities: z.array(zCanvasControlLayerState),
- })
- .default({ isHidden: false, entities: [] }),
- regionalGuidance: z
- .object({
- isHidden: z.boolean(),
- entities: z.array(zCanvasRegionalGuidanceState),
- })
- .default({ isHidden: false, entities: [] }),
- referenceImages: z
- .object({
- entities: z.array(zCanvasReferenceImageState),
- })
- .default({ entities: [] }),
+ inpaintMasks: zInpaintMasks.default({ isHidden: false, entities: [] }),
+ rasterLayers: zRasterLayers.default({ isHidden: false, entities: [] }),
+ controlLayers: zControlLayers.default({ isHidden: false, entities: [] }),
+ regionalGuidance: zRegionalGuidance.default({ isHidden: false, entities: [] }),
+ referenceImages: zReferenceImages.default({ entities: [] }),
bbox: zBboxState.default({
rect: { x: 0, y: 0, width: 512, height: 512 },
aspectRatio: DEFAULT_ASPECT_RATIO_CONFIG,
scaleMethod: 'auto',
- scaledSize: {
- width: 512,
- height: 512,
- },
+ scaledSize: { width: 512, height: 512 },
modelBase: 'sd-1',
}),
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts
index 3b12790f2f..5f58e77545 100644
--- a/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts
@@ -1,6 +1,6 @@
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import type { Dimensions } from 'features/controlLayers/store/types';
-import type { MainModelBase } from 'features/nodes/types/common';
+import type { BaseModelType } from 'features/nodes/types/common';
import {
getGridSize,
getOptimalDimension,
@@ -11,16 +11,16 @@ import {
* Scales the bounding box dimensions to the optimal dimension. The optimal dimensions should be the trained dimension
* for the model. For example, 1024 for SDXL or 512 for SD1.5.
* @param dimensions The un-scaled bbox dimensions
- * @param modelBase The base model
+ * @param base The base model
*/
-export const getScaledBoundingBoxDimensions = (dimensions: Dimensions, modelBase: MainModelBase): Dimensions => {
+export const getScaledBoundingBoxDimensions = (dimensions: Dimensions, base?: BaseModelType): Dimensions => {
// Special cases: Return original if SDXL and in training dimensions
- if (modelBase === 'sdxl' && isInSDXLTrainingDimensions(dimensions.width, dimensions.height)) {
+ if (base === 'sdxl' && isInSDXLTrainingDimensions(dimensions.width, dimensions.height)) {
return { ...dimensions };
}
- const optimalDimension = getOptimalDimension(modelBase);
- const gridSize = getGridSize(modelBase);
+ const optimalDimension = getOptimalDimension(base);
+ const gridSize = getGridSize(base);
const width = roundToMultiple(dimensions.width, gridSize);
const height = roundToMultiple(dimensions.height, gridSize);
@@ -56,13 +56,13 @@ export const getScaledBoundingBoxDimensions = (dimensions: Dimensions, modelBase
* Calculate the new width and height that will fit the given aspect ratio, retaining the input area
* @param ratio The aspect ratio to calculate the new size for
* @param area The input area
- * @param modelBase The base model
+ * @param base The base model
* @returns The width and height that will fit the given aspect ratio, retaining the input area
*/
-export const calculateNewSize = (ratio: number, area: number, modelBase: MainModelBase): Dimensions => {
+export const calculateNewSize = (ratio: number, area: number, base?: BaseModelType): Dimensions => {
const exactWidth = Math.sqrt(area * ratio);
const exactHeight = exactWidth / ratio;
- const gridSize = getGridSize(modelBase);
+ const gridSize = getGridSize(base);
return {
width: roundToMultiple(exactWidth, gridSize),
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 c288f80bb7..3ebe337ed0 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
-import { type ParamsState, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
-import type { CanvasState } from 'features/controlLayers/store/types';
+import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import type { CanvasState, ParamsState } from 'features/controlLayers/store/types';
import type { BoardField } from 'features/nodes/types/common';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
diff --git a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts
index 90a5b41348..9041a87213 100644
--- a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts
+++ b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts
@@ -1,4 +1,3 @@
-import type { MainModelBase } from 'features/nodes/types/common';
import type { BaseModelType } from 'services/api/types';
/**
@@ -117,7 +116,7 @@ export const getIsSizeTooLarge = (width: number, height: number, optimalDimensio
* @param optimalDimension The optimal dimension
* @returns Whether the current width and height needs to be resized to the optimal dimension
*/
-export const getIsSizeOptimal = (width: number, height: number, modelBase: MainModelBase): boolean => {
- const optimalDimension = getOptimalDimension(modelBase);
+export const getIsSizeOptimal = (width: number, height: number, base?: BaseModelType): boolean => {
+ const optimalDimension = getOptimalDimension(base);
return !getIsSizeTooSmall(width, height, optimalDimension) && !getIsSizeTooLarge(width, height, optimalDimension);
};
diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts
index a91ccbfb19..849ed380e7 100644
--- a/invokeai/frontend/web/src/features/queue/store/readiness.ts
+++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts
@@ -8,10 +8,9 @@ import { useAppSelector } from 'app/store/storeHooks';
import type { AppConfig } from 'app/types/invokeai';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import type { ParamsState } from 'features/controlLayers/store/paramsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
-import type { CanvasState } from 'features/controlLayers/store/types';
+import type { CanvasState, ParamsState } from 'features/controlLayers/store/types';
import {
getControlLayerWarnings,
getGlobalReferenceImageWarnings,
From c4d1e78f59e1d18e0ca068255d00e5b4e45e8fd3 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 22 May 2025 14:18:02 +1000
Subject: [PATCH 009/210] fix(ui): circular import issue
---
.../components/CanvasMainPanelContent.tsx | 5 ++--
.../common/CanvasEntityHeaderWarnings.tsx | 2 +-
.../controlLayers/hooks/addLayerHooks.ts | 2 +-
.../controlLayers/hooks/saveCanvasHooks.ts | 2 +-
.../controlLayers/store/paramsSlice.ts | 23 +++++++++++++++++
.../graph/generation/buildChatGPT4oGraph.ts | 2 +-
.../util/graph/generation/buildFLUXGraph.ts | 2 +-
.../graph/generation/buildImagen3Graph.ts | 2 +-
.../util/graph/generation/buildSD1Graph.ts | 2 +-
.../util/graph/generation/buildSD3Graph.ts | 2 +-
.../util/graph/generation/buildSDXLGraph.ts | 2 +-
.../web/src/features/queue/store/readiness.ts | 3 +--
.../web/src/services/api/endpoints/models.ts | 25 ++-----------------
13 files changed, 38 insertions(+), 36 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 4bc6262c41..18c9f54489 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -26,7 +26,7 @@ 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 { selectIsSessionStarted } from 'features/controlLayers/store/selectors';
+import { selectIsCanvasEmpty, selectIsSessionStarted } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
@@ -51,8 +51,9 @@ MenuContent.displayName = 'MenuContent';
export const CanvasMainPanelContent = memo(() => {
const isSessionStarted = useAppSelector(selectIsSessionStarted);
+ const isCanvasEmpty = useAppSelector(selectIsCanvasEmpty);
- if (!isSessionStarted) {
+ if (!isSessionStarted && isCanvasEmpty) {
return ;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx
index ea61e9e30a..b3f0e21972 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx
@@ -18,7 +18,7 @@ import { upperFirst } from 'lodash-es';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiWarningBold } from 'react-icons/pi';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
index 7415678b71..dfc4780d37 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
@@ -37,9 +37,9 @@ import { zModelIdentifierField } from 'features/nodes/types/common';
import { useCallback } from 'react';
import {
modelConfigsAdapterSelectors,
- selectMainModelConfig,
selectModelConfigsQuery,
} from 'services/api/endpoints/models';
+import { selectMainModelConfig } from '../store/paramsSlice';
import type {
ControlLoRAModelConfig,
ControlNetModelConfig,
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
index 06f4a0328e..5eb153cfc7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
@@ -33,7 +33,7 @@ import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { serializeError } from 'serialize-error';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
+import { selectMainModelConfig } from '../store/paramsSlice';
import type { ImageDTO } from 'services/api/types';
import type { JsonObject } from 'type-fest';
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index a6a7e8d98d..dabddf3c13 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -21,6 +21,8 @@ import type {
ParameterVAEModel,
} from 'features/parameters/types/parameterSchemas';
import { clamp } from 'lodash-es';
+import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
+import { isNonRefinerMainModelConfig } from 'services/api/types';
import { newSessionRequested } from './actions';
@@ -336,3 +338,24 @@ export const selectRefinerNegativeAestheticScore = createParamsSelector(
export const selectRefinerScheduler = createParamsSelector((params) => params.refinerScheduler);
export const selectRefinerStart = createParamsSelector((params) => params.refinerStart);
export const selectRefinerSteps = createParamsSelector((params) => params.refinerSteps);
+
+export const selectMainModelConfig = createSelector(
+ selectModelConfigsQuery,
+ selectParamsSlice,
+ (modelConfigs, { model }) => {
+ if (!modelConfigs.data) {
+ return null;
+ }
+ if (!model) {
+ return null;
+ }
+ const modelConfig = modelConfigsAdapterSelectors.selectById(modelConfigs.data, model.key);
+ if (!modelConfig) {
+ return null;
+ }
+ if (!isNonRefinerMainModelConfig(modelConfig)) {
+ return null;
+ }
+ return modelConfig;
+ }
+);
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
index 0a93900d90..95f1a20cf3 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
@@ -15,7 +15,7 @@ import {
} from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
index 12379ced19..bff3803fb3 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
@@ -28,7 +28,7 @@ import {
UnsupportedGenerationModeError,
} from 'features/nodes/util/graph/types';
import { t } from 'i18next';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Invocation } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
index 7f573bdecf..7be80eb788 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
@@ -14,7 +14,7 @@ import {
} from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
index 8092c5ecd4..73875d57b8 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
@@ -24,7 +24,7 @@ import {
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Invocation } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
index 523ddb0e80..c0c864daaa 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
@@ -19,7 +19,7 @@ import {
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Invocation } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
index ee00ce320b..f6c167770e 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
@@ -24,7 +24,7 @@ import {
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Invocation } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts
index 849ed380e7..ebe96a21be 100644
--- a/invokeai/frontend/web/src/features/queue/store/readiness.ts
+++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts
@@ -8,7 +8,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import type { AppConfig } from 'app/types/invokeai';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectMainModelConfig,selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasState, ParamsState } from 'features/controlLayers/store/types';
import {
@@ -42,7 +42,6 @@ import i18n from 'i18next';
import { debounce, groupBy, upperFirst } from 'lodash-es';
import { atom, computed } from 'nanostores';
import { useEffect } from 'react';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
import type { MainModelConfig } from 'services/api/types';
import { $isConnected } from 'services/events/stores';
diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts
index aa78e8f739..405c794582 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/models.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts
@@ -1,7 +1,6 @@
import type { EntityState } from '@reduxjs/toolkit';
-import { createEntityAdapter, createSelector } from '@reduxjs/toolkit';
+import { createEntityAdapter } from '@reduxjs/toolkit';
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
-import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import queryString from 'query-string';
import type { operations, paths } from 'services/api/schema';
import type {
@@ -11,7 +10,6 @@ import type {
SetHFTokenArg,
SetHFTokenResponse,
} from 'services/api/types';
-import { isNonRefinerMainModelConfig } from 'services/api/types';
import type { Param0 } from 'tsafe';
import type { ApiTagDescription } from '..';
@@ -326,23 +324,4 @@ export const {
} = modelsApi;
export const selectModelConfigsQuery = modelsApi.endpoints.getModelConfigs.select();
-export const selectMainModelConfig = createSelector(
- selectModelConfigsQuery,
- selectParamsSlice,
- (modelConfigs, { model }) => {
- if (!modelConfigs.data) {
- return null;
- }
- if (!model) {
- return null;
- }
- const modelConfig = modelConfigsAdapterSelectors.selectById(modelConfigs.data, model.key);
- if (!modelConfig) {
- return null;
- }
- if (!isNonRefinerMainModelConfig(modelConfig)) {
- return null;
- }
- return modelConfig;
- }
-);
+
From cf2d67ef3d17decc4ebae83b2e4e5e74292f0412 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 22 May 2025 17:41:19 +1000
Subject: [PATCH 010/210] refactor(ui): canvas flow (wip)
---
.../listeners/enqueueRequestedLinear.ts | 6 +-
.../components/CanvasMainPanelContent.tsx | 83 ++++++++++---
.../controlLayers/store/canvasSlice.ts | 13 +-
.../store/canvasStagingAreaSlice.ts | 4 +
.../graph/generation/buildChatGPT4oGraph.ts | 11 +-
.../graph/generation/buildCogView4Graph.ts | 11 +-
.../util/graph/generation/buildFLUXGraph.ts | 102 +++++++++-------
.../graph/generation/buildImagen3Graph.ts | 10 +-
.../graph/generation/buildImagen4Graph.ts | 10 +-
.../util/graph/generation/buildSD1Graph.ts | 112 ++++++++++--------
.../util/graph/generation/buildSD3Graph.ts | 11 +-
.../util/graph/generation/buildSDXLGraph.ts | 111 +++++++++--------
.../graph/generation/getGenerationMode.ts | 9 ++
.../web/src/features/queue/store/readiness.ts | 14 +--
14 files changed, 315 insertions(+), 192 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/getGenerationMode.ts
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
index afd9a2fff2..8ed57f33ad 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
@@ -35,7 +35,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
const { prepend } = action.payload;
const manager = $canvasManager.get();
- assert(manager, 'No canvas manager');
+ // assert(manager, 'No canvas manager');
const model = state.params.model;
assert(model, 'No model found in state');
@@ -90,7 +90,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
const { g, seedFieldIdentifier, positivePromptFieldIdentifier } = buildGraphResult.value;
- const destination = state.canvasSettings.sendToCanvas ? 'canvas' : 'gallery';
+ // const destination = state.canvasSettings.sendToCanvas ? 'canvas' : 'gallery';
const prepareBatchResult = withResult(() =>
prepareLinearUIBatch({
@@ -100,7 +100,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
seedFieldIdentifier,
positivePromptFieldIdentifier,
origin: 'canvas',
- destination,
+ destination: 'canvas',
})
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 18c9f54489..2861a1db54 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,13 +1,6 @@
-import {
- ContextMenu,
- Flex,
- IconButton,
- Menu,
- MenuButton,
- MenuList,
- type SystemStyleObject,
-} from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Button, ContextMenu, Flex, IconButton, Image, Menu, MenuButton, MenuList, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
@@ -25,7 +18,13 @@ import { StagingAreaToolbar } from 'features/controlLayers/components/StagingAre
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { newCanvasSessionRequested } from 'features/controlLayers/store/actions';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
+import {
+ selectIsStaging,
+ selectSelectedImage,
+ selectStagedImages,
+} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectIsCanvasEmpty, selectIsSessionStarted } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
@@ -54,7 +53,11 @@ export const CanvasMainPanelContent = memo(() => {
const isCanvasEmpty = useAppSelector(selectIsCanvasEmpty);
if (!isSessionStarted && isCanvasEmpty) {
- return ;
+ return ;
+ }
+
+ if (isSessionStarted && isCanvasEmpty) {
+ return ;
}
return ;
@@ -62,14 +65,64 @@ export const CanvasMainPanelContent = memo(() => {
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
-const CanvasNoSession = memo(() => {
+const NoActiveSession = memo(() => {
+ const dispatch = useAppDispatch();
+ const newSesh = useCallback(() => {
+ dispatch(newCanvasSessionRequested());
+ }, [dispatch]);
return (
-
- FRESH CANVAS is fresh when: - No control layers - No inpaint masks - No regions - No Raster Layers
+
+
+ No Active Session
+
+
+
+ Generate with Starting Image
+ - New Canvas Session
+ - Dropped image as raster layer
+ - Bbox resized
+
+
+ Generate with Control Image
+ - New Canvas Session
+ - Dropped image as control layer
+ - Bbox resized
+
+
+ Edit Image
+ - New Canvas Session
+ - Dropped image as raster layer
+ - Bbox resized
+ - 1 Inpaint mask layer added
+
);
});
-CanvasNoSession.displayName = 'CanvasNoSession';
+NoActiveSession.displayName = 'NoActiveSession';
+
+const SimpleActiveSession = memo(() => {
+ const isStaging = useAppSelector(selectIsStaging);
+ const selectedImage = useAppSelector(selectSelectedImage);
+ const stagedImages = useAppSelector(selectStagedImages);
+ return (
+
+
+ Simple Session (staging view) {isStaging && 'STAGING'}
+
+ {selectedImage && }
+
+ {stagedImages.map(({ imageDTO }) => (
+
+ ))}
+
+
+ );
+});
+SimpleActiveSession.displayName = 'SimpleActiveSession';
const CanvasActiveSession = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index 1f49abfa87..d02aaed004 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -5,7 +5,11 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti
import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { canvasReset, newSessionRequested } from 'features/controlLayers/store/actions';
+import {
+ canvasReset,
+ newCanvasSessionRequested,
+ newGallerySessionRequested,
+} from 'features/controlLayers/store/actions';
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
import {
selectAllEntities,
@@ -1802,9 +1806,14 @@ export const canvasSlice = createSlice({
syncScaledSize(state);
}
});
- builder.addMatcher(newSessionRequested, (state) => {
+ builder.addCase(newGallerySessionRequested, (state) => {
return resetState(state);
});
+ builder.addCase(newCanvasSessionRequested, (state) => {
+ const newState = resetState(state);
+ newState.isSessionStarted = true;
+ return newState;
+ });
},
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index bf37b6e0d1..660ac1a680 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -97,6 +97,10 @@ export const selectSelectedImage = createSelector(
[selectCanvasStagingAreaSlice, selectStagedImageIndex],
(stagingArea, index) => stagingArea.stagedImages[index] ?? null
);
+export const selectStagedImages = createSelector(
+ selectCanvasStagingAreaSlice,
+ (stagingArea) => stagingArea.stagedImages
+);
export const selectImageCount = createSelector(
selectCanvasStagingAreaSlice,
(stagingArea) => stagingArea.stagedImages.length
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
index 95f1a20cf3..c53da0e1ec 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
@@ -3,10 +3,12 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { isChatGPT4oAspectRatioID, isChatGPT4oReferenceImageConfig } from 'features/controlLayers/store/types';
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
import { type ImageField, zModelIdentifierField } from 'features/nodes/types/common';
+import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
@@ -15,14 +17,16 @@ import {
} from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
-import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const log = logger('system');
-export const buildChatGPT4oGraph = async (state: RootState, manager: CanvasManager): Promise => {
- const generationMode = await manager.compositor.getGenerationMode();
+export const buildChatGPT4oGraph = async (
+ state: RootState,
+ manager?: CanvasManager | null
+): Promise => {
+ const generationMode = await getGenerationMode(manager);
if (generationMode !== 'txt2img' && generationMode !== 'img2img') {
throw new UnsupportedGenerationModeError(t('toast.chatGPT4oIncompatibleGenerationMode'));
@@ -91,6 +95,7 @@ export const buildChatGPT4oGraph = async (state: RootState, manager: CanvasManag
}
if (generationMode === 'img2img') {
+ assert(manager, 'Need manager to do img2img');
const adapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
const { image_name } = await manager.compositor.getCompositeImageDTO(adapters, bbox.rect, {
is_intermediate: true,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
index 4f19c6bd2e..b0b29774e9 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
@@ -12,6 +12,7 @@ import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChec
import { addOutpaint } from 'features/nodes/util/graph/generation/addOutpaint';
import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
+import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
@@ -27,8 +28,11 @@ import { assert } from 'tsafe';
const log = logger('system');
-export const buildCogView4Graph = async (state: RootState, manager: CanvasManager): Promise => {
- const generationMode = await manager.compositor.getGenerationMode();
+export const buildCogView4Graph = async (
+ state: RootState,
+ manager?: CanvasManager | null
+): Promise => {
+ const generationMode = await getGenerationMode(manager);
log.debug({ generationMode }, 'Building CogView4 graph');
const params = selectParamsSlice(state);
@@ -109,6 +113,7 @@ export const buildCogView4Graph = async (state: RootState, manager: CanvasManage
canvasOutput = addTextToImage({ g, l2i, originalSize, scaledSize });
g.upsertMetadata({ generation_mode: 'cogview4_txt2img' });
} else if (generationMode === 'img2img') {
+ assert(manager, 'Need manager to do img2img');
canvasOutput = await addImageToImage({
g,
manager,
@@ -124,6 +129,7 @@ export const buildCogView4Graph = async (state: RootState, manager: CanvasManage
});
g.upsertMetadata({ generation_mode: 'cogview4_img2img' });
} else if (generationMode === 'inpaint') {
+ assert(manager, 'Need manager to do inpaint');
canvasOutput = await addInpaint({
state,
g,
@@ -141,6 +147,7 @@ export const buildCogView4Graph = async (state: RootState, manager: CanvasManage
});
g.upsertMetadata({ generation_mode: 'cogview4_inpaint' });
} else if (generationMode === 'outpaint') {
+ assert(manager, 'Need manager to do outpaint');
canvasOutput = await addOutpaint({
state,
g,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
index bff3803fb3..36abfb6a4c 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
@@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
-import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addFLUXFill } from 'features/nodes/util/graph/generation/addFLUXFill';
import { addFLUXLoRAs } from 'features/nodes/util/graph/generation/addFLUXLoRAs';
@@ -15,6 +15,7 @@ import { addOutpaint } from 'features/nodes/util/graph/generation/addOutpaint';
import { addRegions } from 'features/nodes/util/graph/generation/addRegions';
import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
+import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
@@ -28,7 +29,6 @@ import {
UnsupportedGenerationModeError,
} from 'features/nodes/util/graph/types';
import { t } from 'i18next';
-import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Invocation } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
@@ -38,8 +38,8 @@ import { addIPAdapters } from './addIPAdapters';
const log = logger('system');
-export const buildFLUXGraph = async (state: RootState, manager: CanvasManager): Promise => {
- const generationMode = await manager.compositor.getGenerationMode();
+export const buildFLUXGraph = async (state: RootState, manager?: CanvasManager | null): Promise => {
+ const generationMode = await getGenerationMode(manager);
log.debug({ generationMode }, 'Building FLUX graph');
const params = selectParamsSlice(state);
@@ -171,6 +171,7 @@ export const buildFLUXGraph = async (state: RootState, manager: CanvasManager):
let canvasOutput: Invocation = l2i;
if (isFLUXFill) {
+ assert(manager, 'Need manager to do FLUX Fill');
canvasOutput = await addFLUXFill({
state,
g,
@@ -184,6 +185,7 @@ export const buildFLUXGraph = async (state: RootState, manager: CanvasManager):
canvasOutput = addTextToImage({ g, l2i, originalSize, scaledSize });
g.upsertMetadata({ generation_mode: 'flux_txt2img' });
} else if (generationMode === 'img2img') {
+ assert(manager, 'Need manager to do img2img');
canvasOutput = await addImageToImage({
g,
manager,
@@ -199,6 +201,7 @@ export const buildFLUXGraph = async (state: RootState, manager: CanvasManager):
});
g.upsertMetadata({ generation_mode: 'flux_img2img' });
} else if (generationMode === 'inpaint') {
+ assert(manager, 'Need manager to do inpaint');
canvasOutput = await addInpaint({
state,
g,
@@ -216,6 +219,7 @@ export const buildFLUXGraph = async (state: RootState, manager: CanvasManager):
});
g.upsertMetadata({ generation_mode: 'flux_inpaint' });
} else if (generationMode === 'outpaint') {
+ assert(manager, 'Need manager to do outpaint');
canvasOutput = await addOutpaint({
state,
g,
@@ -236,32 +240,34 @@ export const buildFLUXGraph = async (state: RootState, manager: CanvasManager):
assert>(false);
}
- const controlNetCollector = g.addNode({
- type: 'collect',
- id: getPrefixedId('control_net_collector'),
- });
- const controlNetResult = await addControlNets({
- manager,
- entities: canvas.controlLayers.entities,
- g,
- rect: canvas.bbox.rect,
- collector: controlNetCollector,
- model,
- });
- if (controlNetResult.addedControlNets > 0) {
- g.addEdge(controlNetCollector, 'collection', denoise, 'control');
- } else {
- g.deleteNode(controlNetCollector.id);
- }
+ if (manager) {
+ const controlNetCollector = g.addNode({
+ type: 'collect',
+ id: getPrefixedId('control_net_collector'),
+ });
+ const controlNetResult = await addControlNets({
+ manager,
+ entities: canvas.controlLayers.entities,
+ g,
+ rect: canvas.bbox.rect,
+ collector: controlNetCollector,
+ model,
+ });
+ if (controlNetResult.addedControlNets > 0) {
+ g.addEdge(controlNetCollector, 'collection', denoise, 'control');
+ } else {
+ g.deleteNode(controlNetCollector.id);
+ }
- await addControlLoRA({
- manager,
- entities: canvas.controlLayers.entities,
- g,
- rect: canvas.bbox.rect,
- denoise,
- model,
- });
+ await addControlLoRA({
+ manager,
+ entities: canvas.controlLayers.entities,
+ g,
+ rect: canvas.bbox.rect,
+ denoise,
+ model,
+ });
+ }
const ipAdapterCollect = g.addNode({
type: 'collect',
@@ -274,6 +280,8 @@ export const buildFLUXGraph = async (state: RootState, manager: CanvasManager):
model,
});
+ let totalIPAdaptersAdded = ipAdapterResult.addedIPAdapters;
+
const fluxReduxCollect = g.addNode({
type: 'collect',
id: getPrefixedId('ip_adapter_collector'),
@@ -284,31 +292,33 @@ export const buildFLUXGraph = async (state: RootState, manager: CanvasManager):
collector: fluxReduxCollect,
model,
});
+ let totalReduxesAdded = fluxReduxResult.addedFLUXReduxes;
- const regionsResult = await addRegions({
- manager,
- regions: canvas.regionalGuidance.entities,
- g,
- bbox: canvas.bbox.rect,
- model,
- posCond,
- negCond: null,
- posCondCollect,
- negCondCollect: null,
- ipAdapterCollect,
- fluxReduxCollect,
- });
+ if (manager) {
+ const regionsResult = await addRegions({
+ manager,
+ regions: canvas.regionalGuidance.entities,
+ g,
+ bbox: canvas.bbox.rect,
+ model,
+ posCond,
+ negCond: null,
+ posCondCollect,
+ negCondCollect: null,
+ ipAdapterCollect,
+ fluxReduxCollect,
+ });
+
+ totalIPAdaptersAdded += regionsResult.reduce((acc, r) => acc + r.addedIPAdapters, 0);
+ totalReduxesAdded += regionsResult.reduce((acc, r) => acc + r.addedFLUXReduxes, 0);
+ }
- const totalIPAdaptersAdded =
- ipAdapterResult.addedIPAdapters + regionsResult.reduce((acc, r) => acc + r.addedIPAdapters, 0);
if (totalIPAdaptersAdded > 0) {
g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter');
} else {
g.deleteNode(ipAdapterCollect.id);
}
- const totalReduxesAdded =
- fluxReduxResult.addedFLUXReduxes + regionsResult.reduce((acc, r) => acc + r.addedFLUXReduxes, 0);
if (totalReduxesAdded > 0) {
g.addEdge(fluxReduxCollect, 'collection', denoise, 'redux_conditioning');
} else {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
index 7be80eb788..6624aa73af 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
@@ -3,9 +3,11 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { isImagenAspectRatioID } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
@@ -14,14 +16,16 @@ import {
} from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
-import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const log = logger('system');
-export const buildImagen3Graph = async (state: RootState, manager: CanvasManager): Promise => {
- const generationMode = await manager.compositor.getGenerationMode();
+export const buildImagen3Graph = async (
+ state: RootState,
+ manager?: CanvasManager | null
+): Promise => {
+ const generationMode = await getGenerationMode(manager);
if (generationMode !== 'txt2img') {
throw new UnsupportedGenerationModeError(t('toast.imagenIncompatibleGenerationMode', { model: 'Imagen3' }));
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
index 71ea35ff01..698552be54 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
@@ -3,9 +3,11 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { isImagenAspectRatioID } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
+import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
@@ -14,14 +16,16 @@ import {
} from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
-import { selectMainModelConfig } from 'services/api/endpoints/models';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const log = logger('system');
-export const buildImagen4Graph = async (state: RootState, manager: CanvasManager): Promise => {
- const generationMode = await manager.compositor.getGenerationMode();
+export const buildImagen4Graph = async (
+ state: RootState,
+ manager?: CanvasManager | null
+): Promise => {
+ const generationMode = await getGenerationMode(manager);
if (generationMode !== 'txt2img') {
throw new UnsupportedGenerationModeError(t('toast.imagenIncompatibleGenerationMode', { model: 'Imagen4' }));
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
index 73875d57b8..0abdadfaba 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
@@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
-import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
@@ -16,6 +16,7 @@ import { addOutpaint } from 'features/nodes/util/graph/generation/addOutpaint';
import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
+import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
@@ -24,7 +25,6 @@ import {
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
-import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Invocation } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
@@ -33,8 +33,8 @@ import { addRegions } from './addRegions';
const log = logger('system');
-export const buildSD1Graph = async (state: RootState, manager: CanvasManager): Promise => {
- const generationMode = await manager.compositor.getGenerationMode();
+export const buildSD1Graph = async (state: RootState, manager?: CanvasManager | null): Promise => {
+ const generationMode = await getGenerationMode(manager);
log.debug({ generationMode }, 'Building SD1/SD2 graph');
const params = selectParamsSlice(state);
@@ -170,6 +170,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P
canvasOutput = addTextToImage({ g, l2i, originalSize, scaledSize });
g.upsertMetadata({ generation_mode: 'txt2img' });
} else if (generationMode === 'img2img') {
+ assert(manager, 'Need manager to do img2img');
canvasOutput = await addImageToImage({
g,
manager,
@@ -185,6 +186,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P
});
g.upsertMetadata({ generation_mode: 'img2img' });
} else if (generationMode === 'inpaint') {
+ assert(manager, 'Need manager to do inpaint');
canvasOutput = await addInpaint({
state,
g,
@@ -202,6 +204,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P
});
g.upsertMetadata({ generation_mode: 'inpaint' });
} else if (generationMode === 'outpaint') {
+ assert(manager, 'Need manager to do outpaint');
canvasOutput = await addOutpaint({
state,
g,
@@ -222,40 +225,42 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P
assert>(false);
}
- const controlNetCollector = g.addNode({
- type: 'collect',
- id: getPrefixedId('control_net_collector'),
- });
- const controlNetResult = await addControlNets({
- manager,
- entities: canvas.controlLayers.entities,
- g,
- rect: canvas.bbox.rect,
- collector: controlNetCollector,
- model,
- });
- if (controlNetResult.addedControlNets > 0) {
- g.addEdge(controlNetCollector, 'collection', denoise, 'control');
- } else {
- g.deleteNode(controlNetCollector.id);
- }
+ if (manager) {
+ const controlNetCollector = g.addNode({
+ type: 'collect',
+ id: getPrefixedId('control_net_collector'),
+ });
+ const controlNetResult = await addControlNets({
+ manager,
+ entities: canvas.controlLayers.entities,
+ g,
+ rect: canvas.bbox.rect,
+ collector: controlNetCollector,
+ model,
+ });
+ if (controlNetResult.addedControlNets > 0) {
+ g.addEdge(controlNetCollector, 'collection', denoise, 'control');
+ } else {
+ g.deleteNode(controlNetCollector.id);
+ }
- const t2iAdapterCollector = g.addNode({
- type: 'collect',
- id: getPrefixedId('t2i_adapter_collector'),
- });
- const t2iAdapterResult = await addT2IAdapters({
- manager,
- entities: canvas.controlLayers.entities,
- g,
- rect: canvas.bbox.rect,
- collector: t2iAdapterCollector,
- model,
- });
- if (t2iAdapterResult.addedT2IAdapters > 0) {
- g.addEdge(t2iAdapterCollector, 'collection', denoise, 't2i_adapter');
- } else {
- g.deleteNode(t2iAdapterCollector.id);
+ const t2iAdapterCollector = g.addNode({
+ type: 'collect',
+ id: getPrefixedId('t2i_adapter_collector'),
+ });
+ const t2iAdapterResult = await addT2IAdapters({
+ manager,
+ entities: canvas.controlLayers.entities,
+ g,
+ rect: canvas.bbox.rect,
+ collector: t2iAdapterCollector,
+ model,
+ });
+ if (t2iAdapterResult.addedT2IAdapters > 0) {
+ g.addEdge(t2iAdapterCollector, 'collection', denoise, 't2i_adapter');
+ } else {
+ g.deleteNode(t2iAdapterCollector.id);
+ }
}
const ipAdapterCollect = g.addNode({
@@ -268,23 +273,26 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P
collector: ipAdapterCollect,
model,
});
+ let totalIPAdaptersAdded = ipAdapterResult.addedIPAdapters;
- const regionsResult = await addRegions({
- manager,
- regions: canvas.regionalGuidance.entities,
- g,
- bbox: canvas.bbox.rect,
- model,
- posCond,
- negCond,
- posCondCollect,
- negCondCollect,
- ipAdapterCollect,
- fluxReduxCollect: null,
- });
+ if (manager) {
+ const regionsResult = await addRegions({
+ manager,
+ regions: canvas.regionalGuidance.entities,
+ g,
+ bbox: canvas.bbox.rect,
+ model,
+ posCond,
+ negCond,
+ posCondCollect,
+ negCondCollect,
+ ipAdapterCollect,
+ fluxReduxCollect: null,
+ });
+
+ totalIPAdaptersAdded += regionsResult.reduce((acc, r) => acc + r.addedIPAdapters, 0);
+ }
- const totalIPAdaptersAdded =
- ipAdapterResult.addedIPAdapters + regionsResult.reduce((acc, r) => acc + r.addedIPAdapters, 0);
if (totalIPAdaptersAdded > 0) {
g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter');
} else {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
index c0c864daaa..7843c6796f 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
@@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
-import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectMainModelConfig,selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
@@ -11,6 +11,7 @@ import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChec
import { addOutpaint } from 'features/nodes/util/graph/generation/addOutpaint';
import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
+import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
@@ -19,15 +20,14 @@ import {
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
-import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Invocation } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const log = logger('system');
-export const buildSD3Graph = async (state: RootState, manager: CanvasManager): Promise => {
- const generationMode = await manager.compositor.getGenerationMode();
+export const buildSD3Graph = async (state: RootState, manager?: CanvasManager | null): Promise => {
+ const generationMode = await getGenerationMode(manager);
log.debug({ generationMode }, 'Building SD3 graph');
const model = selectMainModelConfig(state);
@@ -134,6 +134,7 @@ export const buildSD3Graph = async (state: RootState, manager: CanvasManager): P
canvasOutput = addTextToImage({ g, l2i, originalSize, scaledSize });
g.upsertMetadata({ generation_mode: 'sd3_txt2img' });
} else if (generationMode === 'img2img') {
+ assert(manager, 'Need manager to do img2img');
canvasOutput = await addImageToImage({
g,
manager,
@@ -149,6 +150,7 @@ export const buildSD3Graph = async (state: RootState, manager: CanvasManager): P
});
g.upsertMetadata({ generation_mode: 'sd3_img2img' });
} else if (generationMode === 'inpaint') {
+ assert(manager, 'Need manager to do inpaint');
canvasOutput = await addInpaint({
state,
g,
@@ -166,6 +168,7 @@ export const buildSD3Graph = async (state: RootState, manager: CanvasManager): P
});
g.upsertMetadata({ generation_mode: 'sd3_inpaint' });
} else if (generationMode === 'outpaint') {
+ assert(manager, 'Need manager to do outpaint');
canvasOutput = await addOutpaint({
state,
g,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
index f6c167770e..8da0df7d3d 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
@@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
-import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
@@ -16,6 +16,7 @@ import { addSDXLRefiner } from 'features/nodes/util/graph/generation/addSDXLRefi
import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
+import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
@@ -24,7 +25,6 @@ import {
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
-import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Invocation } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
@@ -33,8 +33,8 @@ import { addRegions } from './addRegions';
const log = logger('system');
-export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): Promise => {
- const generationMode = await manager.compositor.getGenerationMode();
+export const buildSDXLGraph = async (state: RootState, manager?: CanvasManager | null): Promise => {
+ const generationMode = await getGenerationMode(manager);
log.debug({ generationMode }, 'Building SDXL graph');
const model = selectMainModelConfig(state);
@@ -177,6 +177,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager):
canvasOutput = addTextToImage({ g, l2i, originalSize, scaledSize });
g.upsertMetadata({ generation_mode: 'sdxl_txt2img' });
} else if (generationMode === 'img2img') {
+ assert(manager, 'Need manager to do img2img');
canvasOutput = await addImageToImage({
g,
manager,
@@ -192,6 +193,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager):
});
g.upsertMetadata({ generation_mode: 'sdxl_img2img' });
} else if (generationMode === 'inpaint') {
+ assert(manager, 'Need manager to do inpaint');
canvasOutput = await addInpaint({
state,
g,
@@ -209,6 +211,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager):
});
g.upsertMetadata({ generation_mode: 'sdxl_inpaint' });
} else if (generationMode === 'outpaint') {
+ assert(manager, 'Need manager to do outpaint');
canvasOutput = await addOutpaint({
state,
g,
@@ -229,40 +232,42 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager):
assert>(false);
}
- const controlNetCollector = g.addNode({
- type: 'collect',
- id: getPrefixedId('control_net_collector'),
- });
- const controlNetResult = await addControlNets({
- manager,
- entities: canvas.controlLayers.entities,
- g,
- rect: canvas.bbox.rect,
- collector: controlNetCollector,
- model,
- });
- if (controlNetResult.addedControlNets > 0) {
- g.addEdge(controlNetCollector, 'collection', denoise, 'control');
- } else {
- g.deleteNode(controlNetCollector.id);
- }
+ if (manager) {
+ const controlNetCollector = g.addNode({
+ type: 'collect',
+ id: getPrefixedId('control_net_collector'),
+ });
+ const controlNetResult = await addControlNets({
+ manager,
+ entities: canvas.controlLayers.entities,
+ g,
+ rect: canvas.bbox.rect,
+ collector: controlNetCollector,
+ model,
+ });
+ if (controlNetResult.addedControlNets > 0) {
+ g.addEdge(controlNetCollector, 'collection', denoise, 'control');
+ } else {
+ g.deleteNode(controlNetCollector.id);
+ }
- const t2iAdapterCollector = g.addNode({
- type: 'collect',
- id: getPrefixedId('t2i_adapter_collector'),
- });
- const t2iAdapterResult = await addT2IAdapters({
- manager,
- entities: canvas.controlLayers.entities,
- g,
- rect: canvas.bbox.rect,
- collector: t2iAdapterCollector,
- model,
- });
- if (t2iAdapterResult.addedT2IAdapters > 0) {
- g.addEdge(t2iAdapterCollector, 'collection', denoise, 't2i_adapter');
- } else {
- g.deleteNode(t2iAdapterCollector.id);
+ const t2iAdapterCollector = g.addNode({
+ type: 'collect',
+ id: getPrefixedId('t2i_adapter_collector'),
+ });
+ const t2iAdapterResult = await addT2IAdapters({
+ manager,
+ entities: canvas.controlLayers.entities,
+ g,
+ rect: canvas.bbox.rect,
+ collector: t2iAdapterCollector,
+ model,
+ });
+ if (t2iAdapterResult.addedT2IAdapters > 0) {
+ g.addEdge(t2iAdapterCollector, 'collection', denoise, 't2i_adapter');
+ } else {
+ g.deleteNode(t2iAdapterCollector.id);
+ }
}
const ipAdapterCollect = g.addNode({
@@ -275,23 +280,25 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager):
collector: ipAdapterCollect,
model,
});
+ let totalIPAdaptersAdded = ipAdapterResult.addedIPAdapters;
- const regionsResult = await addRegions({
- manager,
- regions: canvas.regionalGuidance.entities,
- g,
- bbox: canvas.bbox.rect,
- model,
- posCond,
- negCond,
- posCondCollect,
- negCondCollect,
- ipAdapterCollect,
- fluxReduxCollect: null,
- });
+ if (manager) {
+ const regionsResult = await addRegions({
+ manager,
+ regions: canvas.regionalGuidance.entities,
+ g,
+ bbox: canvas.bbox.rect,
+ model,
+ posCond,
+ negCond,
+ posCondCollect,
+ negCondCollect,
+ ipAdapterCollect,
+ fluxReduxCollect: null,
+ });
+ totalIPAdaptersAdded += regionsResult.reduce((acc, r) => acc + r.addedIPAdapters, 0);
+ }
- const totalIPAdaptersAdded =
- ipAdapterResult.addedIPAdapters + regionsResult.reduce((acc, r) => acc + r.addedIPAdapters, 0);
if (totalIPAdaptersAdded > 0) {
g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter');
} else {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/getGenerationMode.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/getGenerationMode.ts
new file mode 100644
index 0000000000..b458088d5a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/getGenerationMode.ts
@@ -0,0 +1,9 @@
+import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
+import type { GenerationMode } from 'features/controlLayers/store/types';
+
+export const getGenerationMode = async (manager?: CanvasManager | null): Promise => {
+ if (!manager) {
+ return 'txt2img';
+ }
+ return await manager.compositor.getGenerationMode();
+};
diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts
index ebe96a21be..f17542a4df 100644
--- a/invokeai/frontend/web/src/features/queue/store/readiness.ts
+++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts
@@ -2,13 +2,13 @@ import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppStore } from 'app/store/nanostores/store';
-import { $true } from 'app/store/nanostores/util';
+import { $false } from 'app/store/nanostores/util';
import type { AppDispatch, AppStore } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import type { AppConfig } from 'app/types/invokeai';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectMainModelConfig,selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasState, ParamsState } from 'features/controlLayers/store/types';
import {
@@ -145,11 +145,11 @@ export const useReadinessWatcher = () => {
const config = useAppSelector(selectConfigSlice);
const templates = useStore($templates);
const isConnected = useStore($isConnected);
- const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $true);
- const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $true);
- const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $true);
- const canvasIsSelectingObject = useStore(canvasManager?.stateApi.$isSegmenting ?? $true);
- const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $true);
+ const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $false);
+ const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $false);
+ const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $false);
+ const canvasIsSelectingObject = useStore(canvasManager?.stateApi.$isSegmenting ?? $false);
+ const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $false);
const isInPublishFlow = useStore($isInPublishFlow);
const { isChatGPT4oHighModelDisabled } = useIsModelDisabled();
From aa3b2106d46cdb834d6e7a796c8cbb5459312435 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 23 May 2025 13:43:31 +1000
Subject: [PATCH 011/210] refactor(ui): canvas flow (wip)
---
.../web/src/app/components/GlobalHookIsolator.tsx | 7 +++++++
.../components/CanvasMainPanelContent.tsx | 14 +++++++++-----
.../util/graph/generation/buildChatGPT4oGraph.ts | 8 ++++----
.../util/graph/generation/buildCogView4Graph.ts | 4 ++--
.../nodes/util/graph/generation/buildFLUXGraph.ts | 4 ++--
.../util/graph/generation/buildImagen3Graph.ts | 4 ++--
.../util/graph/generation/buildImagen4Graph.ts | 4 ++--
.../nodes/util/graph/generation/buildSD1Graph.ts | 4 ++--
.../nodes/util/graph/generation/buildSD3Graph.ts | 4 ++--
.../nodes/util/graph/generation/buildSDXLGraph.ts | 4 ++--
10 files changed, 34 insertions(+), 23 deletions(-)
diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
index 4be48fcd2c..ef45771119 100644
--- a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
@@ -21,8 +21,11 @@ import i18n from 'i18n';
import { size } from 'lodash-es';
import { memo, useEffect } from 'react';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
+import { useGetQueueCountsByDestinationQuery } from 'services/api/endpoints/queue';
import { useSocketIO } from 'services/events/useSocketIO';
+const queueCountArg = { destination: 'canvas' };
+
/**
* GlobalHookIsolator is a logical component that runs global hooks in an isolated component, so that they do not
* cause needless re-renders of any other components.
@@ -41,6 +44,10 @@ export const GlobalHookIsolator = memo(
useGetOpenAPISchemaQuery();
useSyncLoggingConfig();
+ // Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
+ // and/or in progress canvas sessions.
+ useGetQueueCountsByDestinationQuery(queueCountArg);
+
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 2861a1db54..551aabb8c6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -28,6 +28,7 @@ import {
import { selectIsCanvasEmpty, selectIsSessionStarted } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
+import { assert } from 'tsafe';
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
@@ -49,10 +50,10 @@ const MenuContent = memo(() => {
MenuContent.displayName = 'MenuContent';
export const CanvasMainPanelContent = memo(() => {
- const isSessionStarted = useAppSelector(selectIsSessionStarted);
const isCanvasEmpty = useAppSelector(selectIsCanvasEmpty);
+ const isSessionStarted = useAppSelector(selectIsSessionStarted);
- if (!isSessionStarted && isCanvasEmpty) {
+ if (!isSessionStarted) {
return ;
}
@@ -60,7 +61,11 @@ export const CanvasMainPanelContent = memo(() => {
return ;
}
- return ;
+ if (isSessionStarted && !isCanvasEmpty) {
+ return ;
+ }
+
+ assert(false);
});
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
@@ -103,7 +108,6 @@ const NoActiveSession = memo(() => {
);
});
NoActiveSession.displayName = 'NoActiveSession';
-
const SimpleActiveSession = memo(() => {
const isStaging = useAppSelector(selectIsStaging);
const selectedImage = useAppSelector(selectSelectedImage);
@@ -114,7 +118,7 @@ const SimpleActiveSession = memo(() => {
Simple Session (staging view) {isStaging && 'STAGING'}
{selectedImage && }
-
+
{stagedImages.map(({ imageDTO }) => (
))}
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
index c53da0e1ec..dbad701bc9 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
@@ -79,8 +79,8 @@ export const buildChatGPT4oGraph = async (
aspect_ratio: bbox.aspectRatio.id,
reference_images,
use_cache: false,
- is_intermediate,
- board,
+ is_intermediate: true,
+ board: undefined,
});
g.upsertMetadata({
positive_prompt: positivePrompt,
@@ -112,8 +112,8 @@ export const buildChatGPT4oGraph = async (
base_image: { image_name },
reference_images,
use_cache: false,
- is_intermediate,
- board,
+ is_intermediate: true,
+ board: undefined,
});
g.upsertMetadata({
positive_prompt: positivePrompt,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
index b0b29774e9..fb63235f9c 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
@@ -186,9 +186,9 @@ export const buildCogView4Graph = async (
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate,
+ is_intermediate: true,
use_cache: false,
- board,
+ board: undefined,
});
g.setMetadataReceivingNode(canvasOutput);
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
index 36abfb6a4c..136769b492 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
@@ -345,9 +345,9 @@ export const buildFLUXGraph = async (state: RootState, manager?: CanvasManager |
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate,
+ is_intermediate: true,
use_cache: false,
- board,
+ board: undefined,
});
g.setMetadataReceivingNode(canvasOutput);
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
index 6624aa73af..6f5497a64a 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
@@ -61,8 +61,8 @@ export const buildImagen3Graph = async (
enhance_prompt: true,
// When enhance_prompt is true, Imagen3 will return a new image every time, ignoring the seed.
use_cache: false,
- is_intermediate,
- board,
+ is_intermediate: true,
+ board: undefined,
});
g.upsertMetadata({
positive_prompt: positivePrompt,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
index 698552be54..b5e866312d 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
@@ -61,8 +61,8 @@ export const buildImagen4Graph = async (
enhance_prompt: true,
// When enhance_prompt is true, Imagen4 will return a new image every time, ignoring the seed.
use_cache: false,
- is_intermediate,
- board,
+ is_intermediate: true,
+ board: undefined,
});
g.upsertMetadata({
positive_prompt: positivePrompt,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
index 0abdadfaba..7f869f69f8 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
@@ -317,9 +317,9 @@ export const buildSD1Graph = async (state: RootState, manager?: CanvasManager |
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate,
+ is_intermediate: true,
use_cache: false,
- board,
+ board: undefined,
});
g.setMetadataReceivingNode(canvasOutput);
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
index 7843c6796f..ab1acf7806 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
@@ -207,9 +207,9 @@ export const buildSD3Graph = async (state: RootState, manager?: CanvasManager |
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate,
+ is_intermediate: true,
use_cache: false,
- board,
+ board: undefined,
});
g.setMetadataReceivingNode(canvasOutput);
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
index 8da0df7d3d..de19fe9151 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
@@ -323,9 +323,9 @@ export const buildSDXLGraph = async (state: RootState, manager?: CanvasManager |
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate,
+ is_intermediate: true,
use_cache: false,
- board,
+ board: undefined,
});
g.setMetadataReceivingNode(canvasOutput);
From c0428ee7efbdeb71a8ae098f1eef294f315bf98e Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 23 May 2025 19:07:26 +1000
Subject: [PATCH 012/210] refactor(ui): canvas flow (wip)
---
.../listeners/enqueueRequestedLinear.ts | 4 +
invokeai/frontend/web/src/app/store/store.ts | 4 +-
.../components/CanvasMainPanelContent.tsx | 220 ++++++++++++++++--
.../NewSessionConfirmationAlertDialog.tsx | 6 +-
.../konva/CanvasStagingAreaModule.ts | 2 +-
.../features/controlLayers/store/actions.ts | 6 +-
.../store/canvasSettingsSlice.ts | 6 +-
.../controlLayers/store/canvasSlice.ts | 9 +-
.../store/canvasStagingAreaSlice.ts | 85 ++++---
.../features/controlLayers/store/selectors.ts | 1 -
.../src/features/controlLayers/store/types.ts | 1 -
.../nodes/util/graph/graphBuilderUtils.ts | 6 +-
.../services/events/onInvocationComplete.tsx | 18 +-
.../web/src/services/events/stores.ts | 12 +
14 files changed, 297 insertions(+), 83 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
index 8ed57f33ad..c6b38c517a 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
@@ -5,6 +5,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
import { withResult, withResultAsync } from 'common/util/result';
import { parseify } from 'common/util/serialize';
+import { canvasSessionStarted, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
@@ -115,6 +116,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
try {
await req.unwrap();
+ if (!selectCanvasSessionType(state)) {
+ dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ }
log.debug(parseify({ batchConfig: prepareBatchResult.value }), 'Enqueued batch');
} catch (error) {
log.error({ error: serializeError(error as Error) }, 'Failed to enqueue batch');
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index c0eca358ab..a9f274c5cb 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -9,7 +9,7 @@ import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/contr
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
import {
canvasStagingAreaPersistConfig,
- canvasStagingAreaSlice,
+ canvasSessionSlice,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
@@ -65,7 +65,7 @@ const allReducers = {
[stylePresetSlice.name]: stylePresetSlice.reducer,
[paramsSlice.name]: paramsSlice.reducer,
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
- [canvasStagingAreaSlice.name]: canvasStagingAreaSlice.reducer,
+ [canvasSessionSlice.name]: canvasSessionSlice.reducer,
[lorasSlice.name]: lorasSlice.reducer,
[workflowLibrarySlice.name]: workflowLibrarySlice.reducer,
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 551aabb8c6..a4c554073c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,5 +1,6 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, ContextMenu, Flex, IconButton, Image, Menu, MenuButton, MenuList, Text } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
@@ -18,17 +19,32 @@ import { StagingAreaToolbar } from 'features/controlLayers/components/StagingAre
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { newCanvasSessionRequested } from 'features/controlLayers/store/actions';
+import { canvasReset, newAdvancedCanvasSessionRequested } from 'features/controlLayers/store/actions';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import {
+ selectCanvasSessionType,
selectIsStaging,
selectSelectedImage,
+ selectStagedImageIndex,
selectStagedImages,
+ stagingAreaImageSelected,
+ stagingAreaImageStaged,
+ stagingAreaNextStagedImageSelected,
+ stagingAreaPrevStagedImageSelected,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { selectIsCanvasEmpty, selectIsSessionStarted } from 'features/controlLayers/store/selectors';
-import { memo, useCallback } from 'react';
+import { isImageField, type ProgressImage } from 'features/nodes/types/common';
+import { isCanvasOutputEvent } from 'features/nodes/util/graph/graphBuilderUtils';
+import type { Atom } from 'nanostores';
+import { atom } from 'nanostores';
+import { memo, useCallback, useEffect, useState } from 'react';
+import { flushSync } from 'react-dom';
+import { useHotkeys } from 'react-hotkeys-hook';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
-import { assert } from 'tsafe';
+import { getImageDTOSafe } from 'services/api/endpoints/images';
+import type { ImageDTO, S } from 'services/api/types';
+import { $socket } from 'services/events/stores';
+import type { Equals } from 'tsafe';
+import { assert, objectEntries } from 'tsafe';
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
@@ -50,22 +66,21 @@ const MenuContent = memo(() => {
MenuContent.displayName = 'MenuContent';
export const CanvasMainPanelContent = memo(() => {
- const isCanvasEmpty = useAppSelector(selectIsCanvasEmpty);
- const isSessionStarted = useAppSelector(selectIsSessionStarted);
+ const sessionType = useAppSelector(selectCanvasSessionType);
- if (!isSessionStarted) {
+ if (sessionType === null) {
return ;
}
- if (isSessionStarted && isCanvasEmpty) {
+ if (sessionType === 'simple') {
return ;
}
- if (isSessionStarted && !isCanvasEmpty) {
+ if (sessionType === 'advanced') {
return ;
}
- assert(false);
+ assert>(false, 'Unexpected sessionType');
});
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
@@ -73,7 +88,7 @@ CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
const NoActiveSession = memo(() => {
const dispatch = useAppDispatch();
const newSesh = useCallback(() => {
- dispatch(newCanvasSessionRequested());
+ dispatch(newAdvancedCanvasSessionRequested());
}, [dispatch]);
return (
@@ -108,26 +123,189 @@ const NoActiveSession = memo(() => {
);
});
NoActiveSession.displayName = 'NoActiveSession';
+
+type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
+
const SimpleActiveSession = memo(() => {
+ const dispatch = useAppDispatch();
const isStaging = useAppSelector(selectIsStaging);
- const selectedImage = useAppSelector(selectSelectedImage);
- const stagedImages = useAppSelector(selectStagedImages);
+ const socket = useStore($socket);
+ const [$progressImage] = useState(() => atom(null));
+
+ useEffect(() => {
+ if (!socket) {
+ return;
+ }
+ const onInvocationProgress = (event: S['InvocationProgressEvent']) => {
+ if (!event) {
+ return;
+ }
+ if (event.origin !== 'canvas') {
+ return;
+ }
+ if (!event.image) {
+ return;
+ }
+ $progressImage.set({ sessionId: event.session_id, image: event.image });
+ };
+ const onInvocationComplete = async (event: S['InvocationCompleteEvent']) => {
+ const progressImage = $progressImage.get();
+ if (!progressImage) {
+ return;
+ }
+ if (progressImage.sessionId !== event.session_id) {
+ return;
+ }
+ if (!isCanvasOutputEvent(event)) {
+ return;
+ }
+ let imageDTO: ImageDTO | null = null;
+ for (const [_name, value] of objectEntries(event.result)) {
+ if (isImageField(value)) {
+ imageDTO = await getImageDTOSafe(value.image_name);
+ break;
+ }
+ }
+ if (!imageDTO) {
+ return;
+ }
+ flushSync(() => {
+ dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
+ });
+ $progressImage.set(null);
+ };
+
+ const onQueueItemStatusChanged = (event: S['QueueItemStatusChangedEvent']) => {
+ const progressImage = $progressImage.get();
+ if (!progressImage) {
+ return;
+ }
+ if (progressImage.sessionId !== event.session_id) {
+ return;
+ }
+ if (event.status !== 'canceled' && event.status !== 'failed') {
+ return;
+ }
+ $progressImage.set(null);
+ };
+ console.log('SUB session preview image listeners');
+ socket.on('invocation_progress', onInvocationProgress);
+ socket.on('invocation_complete', onInvocationComplete);
+ socket.on('queue_item_status_changed', onQueueItemStatusChanged);
+
+ return () => {
+ console.log('UNSUB session preview image listeners');
+ socket.off('invocation_progress', onInvocationProgress);
+ socket.off('invocation_complete', onInvocationComplete);
+ socket.off('queue_item_status_changed', onQueueItemStatusChanged);
+ };
+ }, [$progressImage, dispatch, socket]);
+
+ const onReset = useCallback(() => {
+ dispatch(canvasReset());
+ }, [dispatch]);
+
+ const selectNext = useCallback(() => {
+ dispatch(stagingAreaNextStagedImageSelected());
+ }, [dispatch]);
+
+ useHotkeys(['right'], selectNext, { preventDefault: true }, [selectNext]);
+
+ const selectPrev = useCallback(() => {
+ dispatch(stagingAreaPrevStagedImageSelected());
+ }, [dispatch]);
+
+ useHotkeys(['left'], selectPrev, { preventDefault: true }, [selectPrev]);
+
return (
-
- Simple Session (staging view) {isStaging && 'STAGING'}
-
- {selectedImage && }
-
- {stagedImages.map(({ imageDTO }) => (
-
- ))}
+
+
+ Simple Session (staging view) {isStaging && 'STAGING'}
+
+
+
+
);
});
SimpleActiveSession.displayName = 'SimpleActiveSession';
+const SelectedImage = memo(({ $progressImage }: { $progressImage: Atom }) => {
+ const progressImage = useStore($progressImage);
+ const selectedImage = useAppSelector(selectSelectedImage);
+
+ if (progressImage) {
+ return (
+
+
+
+ );
+ }
+
+ if (selectedImage) {
+ return (
+
+
+
+ );
+ }
+
+ return No images;
+});
+SelectedImage.displayName = 'SelectedImage';
+
+const SessionImages = memo(() => {
+ const stagedImages = useAppSelector(selectStagedImages);
+ return (
+
+ {stagedImages.map(({ imageDTO }, index) => (
+
+ ))}
+
+ );
+});
+SessionImages.displayName = 'SessionImages';
+
+const sx = {
+ '&[data-is-selected="false"]': {
+ opacity: 0.5,
+ },
+} satisfies SystemStyleObject;
+const SessionImage = memo(({ index, imageDTO }: { index: number; imageDTO: ImageDTO }) => {
+ const dispatch = useAppDispatch();
+ const selectedImageIndex = useAppSelector(selectStagedImageIndex);
+ const onClick = useCallback(() => {
+ dispatch(stagingAreaImageSelected({ index }));
+ }, [dispatch, index]);
+ return (
+
+ );
+});
+SessionImage.displayName = 'SessionImage';
+
const CanvasActiveSession = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
index f57818964c..0e341380ad 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
@@ -2,7 +2,7 @@ import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text }
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
-import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
+import { newAdvancedCanvasSessionRequested, newSimpleCanvasSessionRequested } from 'features/controlLayers/store/actions';
import {
selectSystemShouldConfirmOnNewSession,
shouldConfirmOnNewSessionToggled,
@@ -20,7 +20,7 @@ export const useNewGallerySession = () => {
const newSessionDialog = useNewGallerySessionDialog();
const newGallerySessionImmediate = useCallback(() => {
- dispatch(newGallerySessionRequested());
+ dispatch(newSimpleCanvasSessionRequested());
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
@@ -41,7 +41,7 @@ export const useNewCanvasSession = () => {
const newSessionDialog = useNewCanvasSessionDialog();
const newCanvasSessionImmediate = useCallback(() => {
- dispatch(newCanvasSessionRequested());
+ dispatch(newAdvancedCanvasSessionRequested());
dispatch(activeTabCanvasRightPanelChanged('layers'));
}, [dispatch]);
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
index 973ae61b33..a993ed7ef6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
@@ -78,7 +78,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
const { x, y } = this.manager.stateApi.getBbox().rect;
const shouldShowStagedImage = this.$shouldShowStagedImage.get();
- this.selectedImage = stagingArea.stagedImages[stagingArea.selectedStagedImageIndex] ?? null;
+ this.selectedImage = stagingArea.images[stagingArea.selectedImageIndex] ?? null;
this.konva.group.position({ x, y });
if (this.selectedImage) {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts
index f91ae31a1f..a63d19946d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts
@@ -2,6 +2,6 @@ import { createAction, isAnyOf } from '@reduxjs/toolkit';
// Needed to split this from canvasSlice.ts to avoid circular dependencies
export const canvasReset = createAction('canvas/canvasReset');
-export const newGallerySessionRequested = createAction('canvas/newGallerySessionRequested');
-export const newCanvasSessionRequested = createAction('canvas/newCanvasSessionRequested');
-export const newSessionRequested = isAnyOf(newGallerySessionRequested, newCanvasSessionRequested);
+export const newSimpleCanvasSessionRequested = createAction('canvas/newSimpleCanvasSessionRequested');
+export const newAdvancedCanvasSessionRequested = createAction('canvas/newAdvancedCanvasSessionRequested');
+export const newSessionRequested = isAnyOf(newSimpleCanvasSessionRequested, newAdvancedCanvasSessionRequested);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts
index cbd3dd2c01..6e8b3f0599 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts
@@ -1,7 +1,7 @@
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
-import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
+import { newAdvancedCanvasSessionRequested, newSimpleCanvasSessionRequested } from 'features/controlLayers/store/actions';
import type { RgbaColor } from 'features/controlLayers/store/types';
type CanvasSettingsState = {
@@ -158,10 +158,10 @@ export const canvasSettingsSlice = createSlice({
},
},
extraReducers(builder) {
- builder.addCase(newGallerySessionRequested, (state) => {
+ builder.addCase(newSimpleCanvasSessionRequested, (state) => {
state.sendToCanvas = false;
});
- builder.addCase(newCanvasSessionRequested, (state) => {
+ builder.addCase(newAdvancedCanvasSessionRequested, (state) => {
state.sendToCanvas = true;
});
},
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index d02aaed004..d73d0cd317 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -7,8 +7,8 @@ import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMul
import { getPrefixedId } from 'features/controlLayers/konva/util';
import {
canvasReset,
- newCanvasSessionRequested,
- newGallerySessionRequested,
+ newAdvancedCanvasSessionRequested,
+ newSimpleCanvasSessionRequested,
} from 'features/controlLayers/store/actions';
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
import {
@@ -1806,12 +1806,11 @@ export const canvasSlice = createSlice({
syncScaledSize(state);
}
});
- builder.addCase(newGallerySessionRequested, (state) => {
+ builder.addCase(newSimpleCanvasSessionRequested, (state) => {
return resetState(state);
});
- builder.addCase(newCanvasSessionRequested, (state) => {
+ builder.addCase(newAdvancedCanvasSessionRequested, (state) => {
const newState = resetState(state);
- newState.isSessionStarted = true;
return newState;
});
},
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index 660ac1a680..2dc441a801 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -1,51 +1,73 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { canvasReset } from 'features/controlLayers/store/actions';
+import {
+ canvasReset,
+ newAdvancedCanvasSessionRequested,
+ newSimpleCanvasSessionRequested,
+} from 'features/controlLayers/store/actions';
import type { StagingAreaImage } from 'features/controlLayers/store/types';
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
-import { newSessionRequested } from './actions';
-
type CanvasStagingAreaState = {
- stagedImages: StagingAreaImage[];
- selectedStagedImageIndex: number;
+ sessionType: 'simple' | 'advanced' | null;
+ images: StagingAreaImage[];
+ selectedImageIndex: number;
};
const initialState: CanvasStagingAreaState = {
- stagedImages: [],
- selectedStagedImageIndex: 0,
+ sessionType: null,
+ images: [],
+ selectedImageIndex: 0,
};
-export const canvasStagingAreaSlice = createSlice({
- name: 'canvasStagingArea',
+export const canvasSessionSlice = createSlice({
+ name: 'canvasSession',
initialState,
reducers: {
stagingAreaImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
const { stagingAreaImage } = action.payload;
- state.stagedImages.push(stagingAreaImage);
- state.selectedStagedImageIndex = state.stagedImages.length - 1;
+ state.images.push(stagingAreaImage);
+ state.selectedImageIndex = state.images.length - 1;
+ },
+ stagingAreaImageSelected: (state, action: PayloadAction<{ index: number }>) => {
+ const { index } = action.payload;
+ state.selectedImageIndex = index;
},
stagingAreaNextStagedImageSelected: (state) => {
- state.selectedStagedImageIndex = (state.selectedStagedImageIndex + 1) % state.stagedImages.length;
+ state.selectedImageIndex = (state.selectedImageIndex + 1) % state.images.length;
},
stagingAreaPrevStagedImageSelected: (state) => {
- state.selectedStagedImageIndex =
- (state.selectedStagedImageIndex - 1 + state.stagedImages.length) % state.stagedImages.length;
+ state.selectedImageIndex = (state.selectedImageIndex - 1 + state.images.length) % state.images.length;
},
stagingAreaStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => {
const { index } = action.payload;
- state.stagedImages.splice(index, 1);
- state.selectedStagedImageIndex = Math.min(state.selectedStagedImageIndex, state.stagedImages.length - 1);
+ state.images.splice(index, 1);
+ state.selectedImageIndex = Math.min(state.selectedImageIndex, state.images.length - 1);
},
stagingAreaReset: (state) => {
- state.stagedImages = [];
- state.selectedStagedImageIndex = 0;
+ state.images = [];
+ state.selectedImageIndex = 0;
+ },
+ canvasSessionStarted: (state, action: PayloadAction<{ sessionType: CanvasStagingAreaState['sessionType'] }>) => {
+ const { sessionType } = action.payload;
+ state.sessionType = sessionType;
+ state.images = [];
+ state.selectedImageIndex = 0;
},
},
extraReducers(builder) {
builder.addCase(canvasReset, () => deepClone(initialState));
- builder.addMatcher(newSessionRequested, () => deepClone(initialState));
+ builder.addCase(newSimpleCanvasSessionRequested, () => {
+ const state = deepClone(initialState);
+ state.sessionType === 'simple';
+ return state;
+ });
+ builder.addCase(newAdvancedCanvasSessionRequested, () => {
+ const state = deepClone(initialState);
+ state.sessionType === 'advanced';
+ return state;
+ });
},
});
@@ -53,9 +75,11 @@ export const {
stagingAreaImageStaged,
stagingAreaStagedImageDiscarded,
stagingAreaReset,
+ stagingAreaImageSelected,
stagingAreaNextStagedImageSelected,
stagingAreaPrevStagedImageSelected,
-} = canvasStagingAreaSlice.actions;
+ canvasSessionStarted,
+} = canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
@@ -63,13 +87,13 @@ const migrate = (state: any): any => {
};
export const canvasStagingAreaPersistConfig: PersistConfig = {
- name: canvasStagingAreaSlice.name,
+ name: canvasSessionSlice.name,
initialState,
migrate,
persistDenylist: [],
};
-export const selectCanvasStagingAreaSlice = (s: RootState) => s.canvasStagingArea;
+export const selectCanvasStagingAreaSlice = (s: RootState) => s[canvasSessionSlice.name];
/**
* Selects if we should be staging images. This is true if:
@@ -80,7 +104,7 @@ export const selectIsStaging = createSelector(
selectCanvasQueueCounts,
selectCanvasStagingAreaSlice,
({ data }, staging) => {
- if (staging.stagedImages.length > 0) {
+ if (staging.images.length > 0) {
return true;
}
if (!data) {
@@ -91,17 +115,18 @@ export const selectIsStaging = createSelector(
);
export const selectStagedImageIndex = createSelector(
selectCanvasStagingAreaSlice,
- (stagingArea) => stagingArea.selectedStagedImageIndex
+ (stagingArea) => stagingArea.selectedImageIndex
);
export const selectSelectedImage = createSelector(
[selectCanvasStagingAreaSlice, selectStagedImageIndex],
- (stagingArea, index) => stagingArea.stagedImages[index] ?? null
-);
-export const selectStagedImages = createSelector(
- selectCanvasStagingAreaSlice,
- (stagingArea) => stagingArea.stagedImages
+ (stagingArea, index) => stagingArea.images[index] ?? null
);
+export const selectStagedImages = createSelector(selectCanvasStagingAreaSlice, (stagingArea) => stagingArea.images);
export const selectImageCount = createSelector(
selectCanvasStagingAreaSlice,
- (stagingArea) => stagingArea.stagedImages.length
+ (stagingArea) => stagingArea.images.length
+);
+export const selectCanvasSessionType = createSelector(
+ selectCanvasStagingAreaSlice,
+ (canvasSession) => canvasSession.sessionType
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
index fe37d1891b..cffa517fd7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
@@ -409,7 +409,6 @@ export const selectCanvasMetadata = createSelector(
}
);
-export const selectIsSessionStarted = createCanvasSelector(({ isSessionStarted }) => isSessionStarted);
export const selectIsCanvasEmpty = createCanvasSelector(
({ controlLayers, inpaintMasks, rasterLayers, regionalGuidance }) => {
// Check it all manually - could use lodash isEqual, but this selector will be called very often!
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 09b540f951..165ad2c460 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -561,7 +561,6 @@ const zReferenceImages = z.object({
});
const zCanvasState = z.object({
_version: z.literal(3).default(3),
- isSessionStarted: z.boolean().default(false),
selectedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
inpaintMasks: zInpaintMasks.default({ isHidden: false, entities: [] }),
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 3ebe337ed0..1163c462ad 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -8,7 +8,7 @@ import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePreset
import { selectStylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
import { pick } from 'lodash-es';
import { selectListStylePresetsRequestState } from 'services/api/endpoints/stylePresets';
-import type { Invocation } from 'services/api/types';
+import type { Invocation, S } from 'services/api/types';
import { assert } from 'tsafe';
import type { MainModelLoaderNodes } from './types';
@@ -134,3 +134,7 @@ export const isMainModelWithoutUnet = (modelLoader: Invocation {
+ return data.invocation_source_id.split(':')[0] === CANVAS_OUTPUT_PREFIX;
+};
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index 95350f0fcc..2748dd1e6d 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -1,12 +1,10 @@
import { logger } from 'app/logging/logger';
import type { AppDispatch, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { stagingAreaImageStaged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { isImageField, isImageFieldCollection } from 'features/nodes/types/common';
import { zNodeStatus } from 'features/nodes/types/invocation';
-import { CANVAS_OUTPUT_PREFIX } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ApiTagDescription } from 'services/api';
import { boardsApi } from 'services/api/endpoints/boards';
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
@@ -19,10 +17,6 @@ import type { JsonObject } from 'type-fest';
const log = logger('events');
-const isCanvasOutputNode = (data: S['InvocationCompleteEvent']) => {
- return data.invocation_source_id.split(':')[0] === CANVAS_OUTPUT_PREFIX;
-};
-
const nodeTypeDenylist = ['load_image', 'image'];
export const buildOnInvocationComplete = (getState: () => RootState, dispatch: AppDispatch) => {
@@ -179,12 +173,12 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
if (data.destination === 'canvas') {
// TODO(psyche): Can/should we let canvas handle this itself?
- if (isCanvasOutputNode(data)) {
- if (data.result.type === 'image_output') {
- dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
- }
- addImagesToGallery(data, [imageDTO]);
- }
+ // if (isCanvasOutputEvent(data)) {
+ // if (data.result.type === 'image_output') {
+ // dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
+ // }
+ // addImagesToGallery(data, [imageDTO]);
+ // }
} else if (!imageDTO.is_intermediate) {
// Desintaion is gallery
addImagesToGallery(data, [imageDTO]);
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index b0b02e02c4..f46f3af079 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -9,6 +9,18 @@ export const $socketOptions = map>({});
export const $isConnected = atom(false);
export const $lastProgressEvent = atom(null);
export const $progressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
+export const $canvasProgressImage = computed($lastProgressEvent, (event) => {
+ if (!event) {
+ return null;
+ }
+ if (event.origin !== 'canvas') {
+ return null;
+ }
+ if (!event.image) {
+ return null;
+ }
+ return event.image;
+});
export const $hasProgressImage = computed($lastProgressEvent, (val) => Boolean(val?.image));
export const $isProgressFromCanvas = computed($lastProgressEvent, (val) => val?.destination === 'canvas');
export const $invocationProgressMessage = computed($lastProgressEvent, (val) => {
From ce5ae836898eff7ecb1cb35b45c46a0e8f8300cd Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 27 May 2025 16:20:10 +1000
Subject: [PATCH 013/210] refactor(ui): canvas flow (wip)
---
.../web/src/app/hooks/useStudioInitAction.ts | 10 +-
.../addCommitStagingAreaImageListener.ts | 4 +-
.../components/CanvasMainPanelContent.tsx | 5 +-
.../NewSessionConfirmationAlertDialog.tsx | 6 +-
.../konva/CanvasProgressImageModule.ts | 2 +
.../features/controlLayers/store/actions.ts | 5 +-
.../store/canvasSettingsSlice.ts | 20 --
.../controlLayers/store/canvasSlice.ts | 15 +-
.../store/canvasStagingAreaSlice.ts | 32 +--
.../controlLayers/store/lorasSlice.ts | 5 +-
.../controlLayers/store/paramsSlice.ts | 4 +-
.../ImageViewer/CurrentImagePreview.tsx | 5 +-
.../components/ImageViewer/ProgressImage.tsx | 5 +-
.../web/src/features/hrf/store/hrfSlice.ts | 4 +-
.../util/graph/generation/addFLUXFill.ts | 10 +-
.../nodes/util/graph/generation/addInpaint.ts | 10 +-
.../util/graph/generation/addOutpaint.ts | 11 +-
.../graph/generation/buildChatGPT4oGraph.ts | 11 +-
.../graph/generation/buildCogView4Graph.ts | 11 +-
.../util/graph/generation/buildFLUXGraph.ts | 11 +-
.../graph/generation/buildImagen3Graph.ts | 14 +-
.../graph/generation/buildImagen4Graph.ts | 15 +-
.../util/graph/generation/buildSD1Graph.ts | 11 +-
.../util/graph/generation/buildSD3Graph.ts | 13 +-
.../util/graph/generation/buildSDXLGraph.ts | 11 +-
.../InvokeButtonTooltip.tsx | 23 +-
.../queue/components/QueueControls.tsx | 11 -
.../queue/components/SendToToggle.tsx | 205 ------------------
.../stylePresets/store/stylePresetSlice.ts | 8 +-
.../web/src/features/ui/store/uiSlice.ts | 4 +-
.../services/events/onInvocationComplete.tsx | 22 +-
.../web/src/services/events/stores.ts | 27 ++-
32 files changed, 98 insertions(+), 452 deletions(-)
delete mode 100644 invokeai/frontend/web/src/features/queue/components/SendToToggle.tsx
diff --git a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
index f82949bb1e..34d2f8ea88 100644
--- a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
+++ b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
@@ -2,9 +2,8 @@ import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { withResultAsync } from 'common/util/result';
-import { canvasReset } from 'features/controlLayers/store/actions';
-import { settingsSendToCanvasChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
@@ -91,9 +90,8 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
const overrides: Partial = {
objects: [imageObject],
};
- store.dispatch(canvasReset());
+ store.dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
store.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
- store.dispatch(settingsSendToCanvasChanged(true));
store.dispatch(setActiveTab('canvas'));
store.dispatch(sentImageToCanvas());
$imageViewer.set(false);
@@ -164,15 +162,15 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
switch (destination) {
case 'generation':
// Go to the canvas tab, open the image viewer, and enable send-to-gallery mode
+ store.dispatch(canvasSessionStarted({ sessionType: 'simple' }));
store.dispatch(setActiveTab('canvas'));
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
- store.dispatch(settingsSendToCanvasChanged(false));
$imageViewer.set(true);
break;
case 'canvas':
// Go to the canvas tab, close the image viewer, and disable send-to-gallery mode
+ store.dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
store.dispatch(setActiveTab('canvas'));
- store.dispatch(settingsSendToCanvasChanged(true));
$imageViewer.set(false);
break;
case 'workflows':
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
index 11dd39d867..a83da52d21 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
@@ -1,7 +1,7 @@
import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
-import { canvasReset, newSessionRequested } from 'features/controlLayers/store/actions';
+import { canvasReset } from 'features/controlLayers/store/actions';
import { stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
@@ -9,7 +9,7 @@ import { queueApi } from 'services/api/endpoints/queue';
const log = logger('canvas');
-const matchCanvasOrStagingAreaReset = isAnyOf(stagingAreaReset, canvasReset, newSessionRequested);
+const matchCanvasOrStagingAreaReset = isAnyOf(stagingAreaReset, canvasReset);
export const addStagingListeners = (startAppListening: AppStartListening) => {
startAppListening({
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index a4c554073c..e17df9b425 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -19,9 +19,10 @@ import { StagingAreaToolbar } from 'features/controlLayers/components/StagingAre
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { canvasReset, newAdvancedCanvasSessionRequested } from 'features/controlLayers/store/actions';
+import { canvasReset } from 'features/controlLayers/store/actions';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import {
+ canvasSessionStarted,
selectCanvasSessionType,
selectIsStaging,
selectSelectedImage,
@@ -88,7 +89,7 @@ CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
const NoActiveSession = memo(() => {
const dispatch = useAppDispatch();
const newSesh = useCallback(() => {
- dispatch(newAdvancedCanvasSessionRequested());
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
}, [dispatch]);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
index 0e341380ad..fd65ae8095 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
@@ -2,7 +2,7 @@ import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text }
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
-import { newAdvancedCanvasSessionRequested, newSimpleCanvasSessionRequested } from 'features/controlLayers/store/actions';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
selectSystemShouldConfirmOnNewSession,
shouldConfirmOnNewSessionToggled,
@@ -20,7 +20,7 @@ export const useNewGallerySession = () => {
const newSessionDialog = useNewGallerySessionDialog();
const newGallerySessionImmediate = useCallback(() => {
- dispatch(newSimpleCanvasSessionRequested());
+ dispatch(canvasSessionStarted({ sessionType: 'simple' }));
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
@@ -41,7 +41,7 @@ export const useNewCanvasSession = () => {
const newSessionDialog = useNewCanvasSessionDialog();
const newCanvasSessionImmediate = useCallback(() => {
- dispatch(newAdvancedCanvasSessionRequested());
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
dispatch(activeTabCanvasRightPanelChanged('layers'));
}, [dispatch]);
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts
index 3dd469528c..c36933a6cf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts
@@ -85,6 +85,8 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
if (data.destination !== 'canvas') {
return;
}
+
+ // The staging area module handles _completed_ events. Only care about failed or canceled here.
if (data.status === 'failed' || data.status === 'canceled') {
this.$lastProgressEvent.set(null);
this.$hasActiveGeneration.set(false);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts
index a63d19946d..9e1d9734cd 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts
@@ -1,7 +1,4 @@
-import { createAction, isAnyOf } from '@reduxjs/toolkit';
+import { createAction } from '@reduxjs/toolkit';
// Needed to split this from canvasSlice.ts to avoid circular dependencies
export const canvasReset = createAction('canvas/canvasReset');
-export const newSimpleCanvasSessionRequested = createAction('canvas/newSimpleCanvasSessionRequested');
-export const newAdvancedCanvasSessionRequested = createAction('canvas/newAdvancedCanvasSessionRequested');
-export const newSessionRequested = isAnyOf(newSimpleCanvasSessionRequested, newAdvancedCanvasSessionRequested);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts
index 6e8b3f0599..ef9f86d189 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts
@@ -1,7 +1,6 @@
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
-import { newAdvancedCanvasSessionRequested, newSimpleCanvasSessionRequested } from 'features/controlLayers/store/actions';
import type { RgbaColor } from 'features/controlLayers/store/types';
type CanvasSettingsState = {
@@ -34,11 +33,6 @@ type CanvasSettingsState = {
* The color to use when drawing lines or filling shapes.
*/
color: RgbaColor;
- /**
- * Whether to send generated images to canvas staging area. When disabled, generated images will be sent directly to
- * the gallery.
- */
- sendToCanvas: boolean;
/**
* Whether to composite inpainted/outpainted regions back onto the source image when saving canvas generations.
*
@@ -89,7 +83,6 @@ const initialState: CanvasSettingsState = {
eraserWidth: 50,
invertScrollForToolWidth: false,
color: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
- sendToCanvas: false,
outputOnlyMaskedRegions: true,
autoProcess: true,
snapToGrid: true,
@@ -126,9 +119,6 @@ export const canvasSettingsSlice = createSlice({
settingsInvertScrollForToolWidthChanged: (state, action: PayloadAction) => {
state.invertScrollForToolWidth = action.payload;
},
- settingsSendToCanvasChanged: (state, action: PayloadAction) => {
- state.sendToCanvas = action.payload;
- },
settingsOutputOnlyMaskedRegionsToggled: (state) => {
state.outputOnlyMaskedRegions = !state.outputOnlyMaskedRegions;
},
@@ -157,14 +147,6 @@ export const canvasSettingsSlice = createSlice({
state.pressureSensitivity = !state.pressureSensitivity;
},
},
- extraReducers(builder) {
- builder.addCase(newSimpleCanvasSessionRequested, (state) => {
- state.sendToCanvas = false;
- });
- builder.addCase(newAdvancedCanvasSessionRequested, (state) => {
- state.sendToCanvas = true;
- });
- },
});
export const {
@@ -175,7 +157,6 @@ export const {
settingsEraserWidthChanged,
settingsColorChanged,
settingsInvertScrollForToolWidthChanged,
- settingsSendToCanvasChanged,
settingsOutputOnlyMaskedRegionsToggled,
settingsAutoProcessToggled,
settingsSnapToGridToggled,
@@ -212,7 +193,6 @@ export const selectBboxOverlay = createCanvasSettingsSelector((settings) => sett
export const selectShowHUD = createCanvasSettingsSelector((settings) => settings.showHUD);
export const selectAutoProcess = createCanvasSettingsSelector((settings) => settings.autoProcess);
export const selectSnapToGrid = createCanvasSettingsSelector((settings) => settings.snapToGrid);
-export const selectSendToCanvas = createCanvasSettingsSelector((canvasSettings) => canvasSettings.sendToCanvas);
export const selectShowProgressOnCanvas = createCanvasSettingsSelector(
(canvasSettings) => canvasSettings.showProgressOnCanvas
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index d73d0cd317..1b135b0ce4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -5,11 +5,8 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti
import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import {
- canvasReset,
- newAdvancedCanvasSessionRequested,
- newSimpleCanvasSessionRequested,
-} from 'features/controlLayers/store/actions';
+import { canvasReset } from 'features/controlLayers/store/actions';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
import {
selectAllEntities,
@@ -1806,13 +1803,7 @@ export const canvasSlice = createSlice({
syncScaledSize(state);
}
});
- builder.addCase(newSimpleCanvasSessionRequested, (state) => {
- return resetState(state);
- });
- builder.addCase(newAdvancedCanvasSessionRequested, (state) => {
- const newState = resetState(state);
- return newState;
- });
+ builder.addCase(canvasSessionStarted, (state) => resetState(state));
},
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index 2dc441a801..4e840dcc35 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -1,11 +1,7 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import {
- canvasReset,
- newAdvancedCanvasSessionRequested,
- newSimpleCanvasSessionRequested,
-} from 'features/controlLayers/store/actions';
+import { canvasReset } from 'features/controlLayers/store/actions';
import type { StagingAreaImage } from 'features/controlLayers/store/types';
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
@@ -15,15 +11,17 @@ type CanvasStagingAreaState = {
selectedImageIndex: number;
};
-const initialState: CanvasStagingAreaState = {
+const INITIAL_STATE: CanvasStagingAreaState = {
sessionType: null,
images: [],
selectedImageIndex: 0,
};
+const getInitialState = (): CanvasStagingAreaState => deepClone(INITIAL_STATE);
+
export const canvasSessionSlice = createSlice({
name: 'canvasSession',
- initialState,
+ initialState: getInitialState(),
reducers: {
stagingAreaImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
const { stagingAreaImage } = action.payload;
@@ -49,25 +47,15 @@ export const canvasSessionSlice = createSlice({
state.images = [];
state.selectedImageIndex = 0;
},
- canvasSessionStarted: (state, action: PayloadAction<{ sessionType: CanvasStagingAreaState['sessionType'] }>) => {
+ canvasSessionStarted: (_, action: PayloadAction<{ sessionType: CanvasStagingAreaState['sessionType'] }>) => {
const { sessionType } = action.payload;
+ const state = getInitialState();
state.sessionType = sessionType;
- state.images = [];
- state.selectedImageIndex = 0;
+ return state;
},
},
extraReducers(builder) {
- builder.addCase(canvasReset, () => deepClone(initialState));
- builder.addCase(newSimpleCanvasSessionRequested, () => {
- const state = deepClone(initialState);
- state.sessionType === 'simple';
- return state;
- });
- builder.addCase(newAdvancedCanvasSessionRequested, () => {
- const state = deepClone(initialState);
- state.sessionType === 'advanced';
- return state;
- });
+ builder.addCase(canvasReset, () => getInitialState());
},
});
@@ -88,7 +76,7 @@ const migrate = (state: any): any => {
export const canvasStagingAreaPersistConfig: PersistConfig = {
name: canvasSessionSlice.name,
- initialState,
+ initialState: getInitialState(),
migrate,
persistDenylist: [],
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts
index df94543507..e4d91cc2f2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts
@@ -1,13 +1,12 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { LoRA } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { LoRAModelConfig } from 'services/api/types';
import { v4 as uuidv4 } from 'uuid';
-import { newSessionRequested } from './actions';
-
type LoRAsState = {
loras: LoRA[];
};
@@ -65,7 +64,7 @@ export const lorasSlice = createSlice({
},
},
extraReducers(builder) {
- builder.addMatcher(newSessionRequested, () => {
+ builder.addCase(canvasSessionStarted, () => {
// When a new session is requested, clear all LoRAs
return deepClone(initialState);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index dabddf3c13..9fcf4a89f8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -1,6 +1,7 @@
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { ParamsState, RgbaColor } from 'features/controlLayers/store/types';
import { getInitialParamsState } from 'features/controlLayers/store/types';
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
@@ -24,7 +25,6 @@ import { clamp } from 'lodash-es';
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
import { isNonRefinerMainModelConfig } from 'services/api/types';
-import { newSessionRequested } from './actions';
export const paramsSlice = createSlice({
name: 'params',
@@ -189,7 +189,7 @@ export const paramsSlice = createSlice({
paramsReset: (state) => resetState(state),
},
extraReducers(builder) {
- builder.addMatcher(newSessionRequested, (state) => resetState(state));
+ builder.addCase(canvasSessionStarted, (state) => resetState(state));
},
});
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
index 5e26c69311..c856099900 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
@@ -11,7 +11,7 @@ import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useCallback, useRef, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
-import { $hasProgressImage, $isProgressFromCanvas } from 'services/events/stores';
+import { $hasProgressImage } from 'services/events/stores';
import { NoContentForViewer } from './NoContentForViewer';
import ProgressImage from './ProgressImage';
@@ -87,10 +87,9 @@ export default memo(CurrentImagePreview);
const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
const hasProgressImage = useStore($hasProgressImage);
- const isProgressFromCanvas = useStore($isProgressFromCanvas);
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
- if (hasProgressImage && !isProgressFromCanvas && shouldShowProgressInViewer) {
+ if (hasProgressImage && shouldShowProgressInViewer) {
return ;
}
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx
index d876ada415..f18c104fb2 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx
@@ -5,7 +5,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { memo, useMemo } from 'react';
-import { $isProgressFromCanvas, $progressImage } from 'services/events/stores';
+import { $progressImage } from 'services/events/stores';
const selectShouldAntialiasProgressImage = createSelector(
selectSystemSlice,
@@ -14,7 +14,6 @@ const selectShouldAntialiasProgressImage = createSelector(
const CurrentImagePreview = () => {
const progressImage = useStore($progressImage);
- const isProgressFromCanvas = useStore($isProgressFromCanvas);
const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
const sx = useMemo(
@@ -24,7 +23,7 @@ const CurrentImagePreview = () => {
[shouldAntialiasProgressImage]
);
- if (!progressImage || isProgressFromCanvas) {
+ if (!progressImage) {
return null;
}
diff --git a/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts b/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts
index a16a729077..c9499ad613 100644
--- a/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts
+++ b/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts
@@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { newSessionRequested } from 'features/controlLayers/store/actions';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { ParameterHRFMethod, ParameterStrength } from 'features/parameters/types/parameterSchemas';
interface HRFState {
@@ -34,7 +34,7 @@ export const hrfSlice = createSlice({
},
},
extraReducers(builder) {
- builder.addMatcher(newSessionRequested, () => {
+ builder.addCase(canvasSessionStarted, () => {
return deepClone(initialHRFState);
});
},
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXFill.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXFill.ts
index 0c2ad4a1ce..c839bfbf18 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXFill.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXFill.ts
@@ -119,9 +119,8 @@ export const addFLUXFill = async ({
});
g.addEdge(maskCombine, 'image', expandMask, 'mask');
- // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
- // to canvas but not outputting only masked regions
- if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
+ // Do the paste back if we are not outputting only masked regions
+ if (!canvasSettings.outputOnlyMaskedRegions) {
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
@@ -178,9 +177,8 @@ export const addFLUXFill = async ({
});
g.addEdge(maskCombine, 'image', expandMask, 'mask');
- // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
- // to canvas but not outputting only masked regions
- if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
+ // Do the paste back if we are not outputting only masked regions
+ if (!canvasSettings.outputOnlyMaskedRegions) {
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts
index f2fce326d1..82191385a0 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts
@@ -185,9 +185,8 @@ export const addInpaint = async ({
g.addEdge(expandMask, 'image', resizeMaskToOriginalSize, 'image');
// After denoising, resize the image and mask back to original size
- // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
- // to canvas but not outputting only masked regions
- if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
+ // Do the paste back if we are not outputting only masked regions
+ if (!canvasSettings.outputOnlyMaskedRegions) {
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
@@ -259,9 +258,8 @@ export const addInpaint = async ({
});
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
- // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
- // to canvas but not outputting only masked regions
- if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
+ // Do the paste back if we are not outputting only masked regions
+ if (!canvasSettings.outputOnlyMaskedRegions) {
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts
index 2c04288c9f..d02e6c6621 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts
@@ -207,9 +207,9 @@ export const addOutpaint = async ({
g.addEdge(l2i, 'image', resizeOutputImageToOriginalSize, 'image');
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
g.addEdge(expandMask, 'image', resizeOutputMaskToOriginalSize, 'image');
- // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
- // to canvas but not outputting only masked regions
- if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
+
+ // Do the paste back if we are not outputting only masked regions
+ if (!canvasSettings.outputOnlyMaskedRegions) {
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
@@ -295,9 +295,8 @@ export const addOutpaint = async ({
});
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
- // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
- // to canvas but not outputting only masked regions
- if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
+ // Do the paste back if we are not outputting only masked regions
+ if (!canvasSettings.outputOnlyMaskedRegions) {
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
index dbad701bc9..7c6b0fa76d 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
@@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { isChatGPT4oAspectRatioID, isChatGPT4oReferenceImageConfig } from 'features/controlLayers/store/types';
@@ -10,11 +9,7 @@ import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/va
import { type ImageField, zModelIdentifierField } from 'features/nodes/types/common';
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
-import {
- CANVAS_OUTPUT_PREFIX,
- getBoardField,
- selectPresetModifiedPrompts,
-} from 'features/nodes/util/graph/graphBuilderUtils';
+import { CANVAS_OUTPUT_PREFIX, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
import type { Equals } from 'tsafe';
@@ -37,7 +32,6 @@ export const buildChatGPT4oGraph = async (
const model = selectMainModelConfig(state);
const canvas = selectCanvasSlice(state);
- const canvasSettings = selectCanvasSettingsSlice(state);
const { bbox } = canvas;
const { positivePrompt } = selectPresetModifiedPrompts(state);
@@ -65,9 +59,6 @@ export const buildChatGPT4oGraph = async (
}
}
- const is_intermediate = canvasSettings.sendToCanvas;
- const board = canvasSettings.sendToCanvas ? undefined : getBoardField(state);
-
if (generationMode === 'txt2img') {
const g = new Graph(getPrefixedId('chatgpt_4o_txt2img_graph'));
const gptImage = g.addNode({
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
index fb63235f9c..7b94605d9f 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
@@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
@@ -16,7 +15,6 @@ import { getGenerationMode } from 'features/nodes/util/graph/generation/getGener
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
- getBoardField,
getSizes,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
@@ -36,7 +34,6 @@ export const buildCogView4Graph = async (
log.debug({ generationMode }, 'Building CogView4 graph');
const params = selectParamsSlice(state);
- const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
@@ -176,13 +173,7 @@ export const buildCogView4Graph = async (
canvasOutput = addWatermarker(g, canvasOutput);
}
- // This image will be staged, should not be saved to the gallery or added to a board.
- const is_intermediate = canvasSettings.sendToCanvas;
- const board = canvasSettings.sendToCanvas ? undefined : getBoardField(state);
-
- if (!canvasSettings.sendToCanvas) {
- g.upsertMetadata(selectCanvasMetadata(state));
- }
+ g.upsertMetadata(selectCanvasMetadata(state));
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
index 136769b492..e024f852ed 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
@@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addFLUXFill } from 'features/nodes/util/graph/generation/addFLUXFill';
@@ -19,7 +18,6 @@ import { getGenerationMode } from 'features/nodes/util/graph/generation/getGener
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
- getBoardField,
getSizes,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
@@ -43,7 +41,6 @@ export const buildFLUXGraph = async (state: RootState, manager?: CanvasManager |
log.debug({ generationMode }, 'Building FLUX graph');
const params = selectParamsSlice(state);
- const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
@@ -335,13 +332,7 @@ export const buildFLUXGraph = async (state: RootState, manager?: CanvasManager |
canvasOutput = addWatermarker(g, canvasOutput);
}
- // This image will be staged, should not be saved to the gallery or added to a board.
- const is_intermediate = canvasSettings.sendToCanvas;
- const board = canvasSettings.sendToCanvas ? undefined : getBoardField(state);
-
- if (!canvasSettings.sendToCanvas) {
- g.upsertMetadata(selectCanvasMetadata(state));
- }
+ g.upsertMetadata(selectCanvasMetadata(state));
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
index 6f5497a64a..ce1ecac591 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
@@ -2,18 +2,13 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
-import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
+import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { isImagenAspectRatioID } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
-import {
- CANVAS_OUTPUT_PREFIX,
- getBoardField,
- selectPresetModifiedPrompts,
-} from 'features/nodes/util/graph/graphBuilderUtils';
+import { CANVAS_OUTPUT_PREFIX, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
import type { Equals } from 'tsafe';
@@ -34,7 +29,6 @@ export const buildImagen3Graph = async (
log.debug({ generationMode }, 'Building Imagen3 graph');
const canvas = selectCanvasSlice(state);
- const canvasSettings = selectCanvasSettingsSlice(state);
const { bbox } = canvas;
const { positivePrompt, negativePrompt } = selectPresetModifiedPrompts(state);
@@ -45,9 +39,6 @@ export const buildImagen3Graph = async (
assert(isImagenAspectRatioID(bbox.aspectRatio.id), 'Imagen3 does not support this aspect ratio');
assert(positivePrompt.length > 0, 'Imagen3 requires positive prompt to have at least one character');
- const is_intermediate = canvasSettings.sendToCanvas;
- const board = canvasSettings.sendToCanvas ? undefined : getBoardField(state);
-
if (generationMode === 'txt2img') {
const g = new Graph(getPrefixedId('imagen3_txt2img_graph'));
const imagen3 = g.addNode({
@@ -70,6 +61,7 @@ export const buildImagen3Graph = async (
width: bbox.rect.width,
height: bbox.rect.height,
model: Graph.getModelMetadataField(model),
+ ...selectCanvasMetadata(state),
});
return {
g,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
index b5e866312d..ee499da7f8 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
@@ -2,18 +2,13 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
-import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
+import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { isImagenAspectRatioID } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
-import {
- CANVAS_OUTPUT_PREFIX,
- getBoardField,
- selectPresetModifiedPrompts,
-} from 'features/nodes/util/graph/graphBuilderUtils';
+import { CANVAS_OUTPUT_PREFIX, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
import type { Equals } from 'tsafe';
@@ -34,7 +29,6 @@ export const buildImagen4Graph = async (
log.debug({ generationMode }, 'Building Imagen4 graph');
const canvas = selectCanvasSlice(state);
- const canvasSettings = selectCanvasSettingsSlice(state);
const { bbox } = canvas;
const { positivePrompt, negativePrompt } = selectPresetModifiedPrompts(state);
@@ -45,9 +39,6 @@ export const buildImagen4Graph = async (
assert(isImagenAspectRatioID(bbox.aspectRatio.id), 'Imagen4 does not support this aspect ratio');
assert(positivePrompt.length > 0, 'Imagen4 requires positive prompt to have at least one character');
- const is_intermediate = canvasSettings.sendToCanvas;
- const board = canvasSettings.sendToCanvas ? undefined : getBoardField(state);
-
if (generationMode === 'txt2img') {
const g = new Graph(getPrefixedId('imagen4_txt2img_graph'));
const imagen4 = g.addNode({
@@ -70,7 +61,9 @@ export const buildImagen4Graph = async (
width: bbox.rect.width,
height: bbox.rect.height,
model: Graph.getModelMetadataField(model),
+ ...selectCanvasMetadata(state),
});
+
return {
g,
seedFieldIdentifier: { nodeId: imagen4.id, fieldName: 'seed' },
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
index 7f869f69f8..fa1a85778c 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
@@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
@@ -20,7 +19,6 @@ import { getGenerationMode } from 'features/nodes/util/graph/generation/getGener
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
- getBoardField,
getSizes,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
@@ -38,7 +36,6 @@ export const buildSD1Graph = async (state: RootState, manager?: CanvasManager |
log.debug({ generationMode }, 'Building SD1/SD2 graph');
const params = selectParamsSlice(state);
- const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
@@ -307,13 +304,7 @@ export const buildSD1Graph = async (state: RootState, manager?: CanvasManager |
canvasOutput = addWatermarker(g, canvasOutput);
}
- // This image will be staged, should not be saved to the gallery or added to a board.
- const is_intermediate = canvasSettings.sendToCanvas;
- const board = canvasSettings.sendToCanvas ? undefined : getBoardField(state);
-
- if (!canvasSettings.sendToCanvas) {
- g.upsertMetadata(selectCanvasMetadata(state));
- }
+ g.upsertMetadata(selectCanvasMetadata(state));
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
index ab1acf7806..f6ba0fae60 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
@@ -2,8 +2,7 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
-import { selectMainModelConfig,selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
@@ -15,7 +14,6 @@ import { getGenerationMode } from 'features/nodes/util/graph/generation/getGener
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
- getBoardField,
getSizes,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
@@ -35,7 +33,6 @@ export const buildSD3Graph = async (state: RootState, manager?: CanvasManager |
assert(model.base === 'sd-3');
const params = selectParamsSlice(state);
- const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
@@ -197,13 +194,7 @@ export const buildSD3Graph = async (state: RootState, manager?: CanvasManager |
canvasOutput = addWatermarker(g, canvasOutput);
}
- // This image will be staged, should not be saved to the gallery or added to a board.
- const is_intermediate = canvasSettings.sendToCanvas;
- const board = canvasSettings.sendToCanvas ? undefined : getBoardField(state);
-
- if (!canvasSettings.sendToCanvas) {
- g.upsertMetadata(selectCanvasMetadata(state));
- }
+ g.upsertMetadata(selectCanvasMetadata(state));
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
index de19fe9151..9aaf3960c7 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
@@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
@@ -20,7 +19,6 @@ import { getGenerationMode } from 'features/nodes/util/graph/generation/getGener
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
CANVAS_OUTPUT_PREFIX,
- getBoardField,
getSizes,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
@@ -42,7 +40,6 @@ export const buildSDXLGraph = async (state: RootState, manager?: CanvasManager |
assert(model.base === 'sdxl');
const params = selectParamsSlice(state);
- const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
@@ -313,13 +310,7 @@ export const buildSDXLGraph = async (state: RootState, manager?: CanvasManager |
canvasOutput = addWatermarker(g, canvasOutput);
}
- // This image will be staged, should not be saved to the gallery or added to a board.
- const is_intermediate = canvasSettings.sendToCanvas;
- const board = canvasSettings.sendToCanvas ? undefined : getBoardField(state);
-
- if (!canvasSettings.sendToCanvas) {
- g.upsertMetadata(selectCanvasMetadata(state));
- }
+ g.upsertMetadata(selectCanvasMetadata(state));
g.updateNode(canvasOutput, {
id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx
index bec8f2212a..68992a84f4 100644
--- a/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx
@@ -2,7 +2,6 @@ import type { TooltipProps } from '@invoke-ai/ui-library';
import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectIterations } from 'features/controlLayers/store/paramsSlice';
import { selectDynamicPromptsIsLoading } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
@@ -237,32 +236,14 @@ StyledDivider.displayName = 'StyledDivider';
const AddingToText = memo(() => {
const { t } = useTranslation();
- const sendToCanvas = useAppSelector(selectSendToCanvas);
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const autoAddBoardName = useBoardName(autoAddBoardId);
- const addingTo = useMemo(() => {
- if (sendToCanvas) {
- return t('controlLayers.stagingOnCanvas');
- }
- return t('parameters.invoke.addingImagesTo');
- }, [sendToCanvas, t]);
-
- const destination = useMemo(() => {
- if (sendToCanvas) {
- return t('queue.canvas');
- }
- if (autoAddBoardName) {
- return autoAddBoardName;
- }
- return t('boards.uncategorized');
- }, [autoAddBoardName, sendToCanvas, t]);
-
return (
- {addingTo}{' '}
+ {t('parameters.invoke.addingImagesTo')}{' '}
- {destination}
+ {autoAddBoardName || t('boards.uncategorized')}
);
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
index fc0172a117..d3f05cb30b 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
@@ -1,28 +1,17 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
-import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
import { QueueActionsMenuButton } from 'features/queue/components/QueueActionsMenuButton';
-import { SendToToggle } from 'features/queue/components/SendToToggle';
import ProgressBar from 'features/system/components/ProgressBar';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { InvokeButton } from './InvokeQueueBackButton';
const QueueControls = () => {
- const tab = useAppSelector(selectActiveTab);
-
return (
- {tab === 'canvas' && (
-
-
-
- )}
diff --git a/invokeai/frontend/web/src/features/queue/components/SendToToggle.tsx b/invokeai/frontend/web/src/features/queue/components/SendToToggle.tsx
deleted file mode 100644
index f4de748c91..0000000000
--- a/invokeai/frontend/web/src/features/queue/components/SendToToggle.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import {
- Box,
- Button,
- chakra,
- Flex,
- Icon,
- Popover,
- PopoverArrow,
- PopoverBody,
- PopoverContent,
- PopoverTrigger,
- Portal,
- Text,
- useCheckbox,
- useToken,
-} from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { selectSendToCanvas, settingsSendToCanvasChanged } from 'features/controlLayers/store/canvasSettingsSlice';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
-import { activeTabCanvasRightPanelChanged, setActiveTab } from 'features/ui/store/uiSlice';
-import type { ChangeEvent, PropsWithChildren } from 'react';
-import { memo, useCallback, useMemo } from 'react';
-import { Trans, useTranslation } from 'react-i18next';
-import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
-
-const getSx = (padding: string | number): SystemStyleObject => ({
- bg: 'base.700',
- w: '72px',
- cursor: 'pointer',
- '&[data-checked]': {
- '.thumb': {
- left: `calc(100% - ${padding})`,
- transform: 'translateX(-100%)',
- bg: 'invokeGreen.300',
- },
- '.unchecked-icon': {
- color: 'base.50',
- opacity: 0.4,
- },
- '.checked-icon': {
- color: 'base.900',
- opacity: 1,
- },
- },
- '&[data-disabled]': {
- bg: 'base.700',
- '.thumb': {
- bg: 'base.500',
- },
- '.unchecked-icon': {
- color: 'base.800',
- },
- '.checked-icon': {
- color: 'base.800',
- },
- },
- '.thumb': {
- transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out',
- left: padding,
- transform: 'translateX(0)',
- bg: 'invokeBlue.400',
- shadow: 'md',
- },
- '.unchecked-icon': {
- color: 'base.900',
- opacity: 1,
- },
- '.checked-icon': {
- color: 'base.50',
- opacity: 0.4,
- },
-});
-
-export const SendToToggle = memo(() => {
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const sendToCanvas = useAppSelector(selectSendToCanvas);
- const isStaging = useAppSelector(selectIsStaging);
-
- const gap = useToken('space', 1);
- const sx = useMemo(() => getSx(gap), [gap]);
-
- const onChange = useCallback(
- (e: ChangeEvent) => {
- dispatch(settingsSendToCanvasChanged(e.target.checked));
- },
- [dispatch]
- );
-
- const { getCheckboxProps, getInputProps, htmlProps } = useCheckbox({
- onChange,
- isChecked: sendToCanvas,
- isDisabled: isStaging,
- });
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-
-SendToToggle.displayName = 'SendToToggle';
-
-const TooltipContent = memo(() => {
- const { t } = useTranslation();
- const sendToCanvas = useAppSelector(selectSendToCanvas);
- const isStaging = useAppSelector(selectIsStaging);
-
- if (isStaging) {
- return (
-
- {t('controlLayers.sendingToCanvas')}
-
- }} />
-
-
- );
- }
-
- return (
-
-
- {sendToCanvas ? t('controlLayers.sendToCanvas') : t('controlLayers.sendToGallery')}
-
-
- {sendToCanvas ? t('controlLayers.sendToCanvasDesc') : t('controlLayers.sendToGalleryDesc')}
-
-
- );
-});
-
-TooltipContent.displayName = 'TooltipContent';
-
-const ActivateCanvasButton = memo((props: PropsWithChildren) => {
- const dispatch = useAppDispatch();
- const onClick = useCallback(() => {
- dispatch(setActiveTab('canvas'));
- dispatch(activeTabCanvasRightPanelChanged('layers'));
- $imageViewer.set(false);
- }, [dispatch]);
- return (
-
- );
-});
-
-ActivateCanvasButton.displayName = 'ActivateCanvasButton';
diff --git a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts
index a0e6eb4002..efee56c9fe 100644
--- a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts
+++ b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts
@@ -2,7 +2,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { newSessionRequested } from 'features/controlLayers/store/actions';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { atom } from 'nanostores';
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
@@ -29,6 +29,9 @@ export const stylePresetSlice = createSlice({
},
},
extraReducers(builder) {
+ builder.addCase(canvasSessionStarted, () => {
+ return deepClone(initialState);
+ });
builder.addMatcher(stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled, (state, action) => {
if (state.activeStylePresetId === null) {
return;
@@ -47,9 +50,6 @@ export const stylePresetSlice = createSlice({
state.activeStylePresetId = null;
}
});
- builder.addMatcher(newSessionRequested, () => {
- return deepClone(initialState);
- });
},
});
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
index eabcf5ae22..3c4bf462bc 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
@@ -1,7 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
-import { newSessionRequested } from 'features/controlLayers/store/actions';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { Dimensions } from 'features/controlLayers/store/types';
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
import { atom } from 'nanostores';
@@ -56,7 +56,7 @@ export const uiSlice = createSlice({
builder.addCase(workflowLoaded, (state) => {
state.activeTab = 'workflows';
});
- builder.addMatcher(newSessionRequested, (state) => {
+ builder.addCase(canvasSessionStarted, (state) => {
state.activeTab = 'canvas';
});
},
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index 2748dd1e6d..92ad0aa011 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -1,10 +1,12 @@
import { logger } from 'app/logging/logger';
import type { AppDispatch, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
+import { stagingAreaImageStaged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { isImageField, isImageFieldCollection } from 'features/nodes/types/common';
import { zNodeStatus } from 'features/nodes/types/invocation';
+import { isCanvasOutputEvent } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ApiTagDescription } from 'services/api';
import { boardsApi } from 'services/api/endpoints/boards';
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
@@ -163,26 +165,18 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
};
const handleOriginCanvas = async (data: S['InvocationCompleteEvent']) => {
- const imageDTOs = await getResultImageDTOs(data);
+ if (!isCanvasOutputEvent(data)) {
+ return;
+ }
// We expect only a single image in the canvas output
- const imageDTO = imageDTOs[0];
+ const imageDTO = (await getResultImageDTOs(data))[0];
+
if (!imageDTO) {
return;
}
- if (data.destination === 'canvas') {
- // TODO(psyche): Can/should we let canvas handle this itself?
- // if (isCanvasOutputEvent(data)) {
- // if (data.result.type === 'image_output') {
- // dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
- // }
- // addImagesToGallery(data, [imageDTO]);
- // }
- } else if (!imageDTO.is_intermediate) {
- // Desintaion is gallery
- addImagesToGallery(data, [imageDTO]);
- }
+ dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
};
const handleOriginOther = async (data: S['InvocationCompleteEvent']) => {
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index f46f3af079..d25a887c7d 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -8,21 +8,28 @@ export const $socket = atom(null);
export const $socketOptions = map>({});
export const $isConnected = atom(false);
export const $lastProgressEvent = atom(null);
-export const $progressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
-export const $canvasProgressImage = computed($lastProgressEvent, (event) => {
+$lastProgressEvent.subscribe((event) => {
if (!event) {
- return null;
+ return;
}
- if (event.origin !== 'canvas') {
- return null;
+ switch (event.destination) {
+ case 'workflows':
+ $lastWorkflowsProgressEvent.set(event);
+ break;
+ case 'upscaling':
+ $lastUpscalingProgressEvent.set(event);
+ break;
+ case 'canvas':
+ $lastCanvasProgressEvent.set(event);
+ break;
}
- if (!event.image) {
- return null;
- }
- return event.image;
});
+export const $lastCanvasProgressEvent = atom(null);
+export const $lastWorkflowsProgressEvent = atom(null);
+export const $lastUpscalingProgressEvent = atom(null);
+
+export const $progressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
export const $hasProgressImage = computed($lastProgressEvent, (val) => Boolean(val?.image));
-export const $isProgressFromCanvas = computed($lastProgressEvent, (val) => val?.destination === 'canvas');
export const $invocationProgressMessage = computed($lastProgressEvent, (val) => {
if (!val) {
return null;
From d985dfe82156f8174a62b8ddfe0f57d3fff995ce Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 27 May 2025 16:42:45 +1000
Subject: [PATCH 014/210] refactor(ui): canvas flow events (wip)
---
.../components/CanvasMainPanelContent.tsx | 69 +++----------------
.../controlLayers/store/paramsSlice.ts | 5 --
.../services/events/onInvocationComplete.tsx | 9 ++-
.../src/services/events/setEventListeners.tsx | 6 +-
.../web/src/services/events/stores.ts | 16 +++++
5 files changed, 38 insertions(+), 67 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index e17df9b425..e93587ef18 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -29,23 +29,17 @@ import {
selectStagedImageIndex,
selectStagedImages,
stagingAreaImageSelected,
- stagingAreaImageStaged,
stagingAreaNextStagedImageSelected,
stagingAreaPrevStagedImageSelected,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { isImageField, type ProgressImage } from 'features/nodes/types/common';
-import { isCanvasOutputEvent } from 'features/nodes/util/graph/graphBuilderUtils';
-import type { Atom } from 'nanostores';
-import { atom } from 'nanostores';
-import { memo, useCallback, useEffect, useState } from 'react';
-import { flushSync } from 'react-dom';
+import type { ProgressImage } from 'features/nodes/types/common';
+import { memo, useCallback, useEffect } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
-import { getImageDTOSafe } from 'services/api/endpoints/images';
import type { ImageDTO, S } from 'services/api/types';
-import { $socket } from 'services/events/stores';
+import { $lastCanvasProgressImage, $socket } from 'services/events/stores';
import type { Equals } from 'tsafe';
-import { assert, objectEntries } from 'tsafe';
+import { assert } from 'tsafe';
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
@@ -131,53 +125,14 @@ const SimpleActiveSession = memo(() => {
const dispatch = useAppDispatch();
const isStaging = useAppSelector(selectIsStaging);
const socket = useStore($socket);
- const [$progressImage] = useState(() => atom(null));
useEffect(() => {
if (!socket) {
return;
}
- const onInvocationProgress = (event: S['InvocationProgressEvent']) => {
- if (!event) {
- return;
- }
- if (event.origin !== 'canvas') {
- return;
- }
- if (!event.image) {
- return;
- }
- $progressImage.set({ sessionId: event.session_id, image: event.image });
- };
- const onInvocationComplete = async (event: S['InvocationCompleteEvent']) => {
- const progressImage = $progressImage.get();
- if (!progressImage) {
- return;
- }
- if (progressImage.sessionId !== event.session_id) {
- return;
- }
- if (!isCanvasOutputEvent(event)) {
- return;
- }
- let imageDTO: ImageDTO | null = null;
- for (const [_name, value] of objectEntries(event.result)) {
- if (isImageField(value)) {
- imageDTO = await getImageDTOSafe(value.image_name);
- break;
- }
- }
- if (!imageDTO) {
- return;
- }
- flushSync(() => {
- dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
- });
- $progressImage.set(null);
- };
const onQueueItemStatusChanged = (event: S['QueueItemStatusChangedEvent']) => {
- const progressImage = $progressImage.get();
+ const progressImage = $lastCanvasProgressImage.get();
if (!progressImage) {
return;
}
@@ -187,20 +142,16 @@ const SimpleActiveSession = memo(() => {
if (event.status !== 'canceled' && event.status !== 'failed') {
return;
}
- $progressImage.set(null);
+ $lastCanvasProgressImage.set(null);
};
console.log('SUB session preview image listeners');
- socket.on('invocation_progress', onInvocationProgress);
- socket.on('invocation_complete', onInvocationComplete);
socket.on('queue_item_status_changed', onQueueItemStatusChanged);
return () => {
console.log('UNSUB session preview image listeners');
- socket.off('invocation_progress', onInvocationProgress);
- socket.off('invocation_complete', onInvocationComplete);
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
};
- }, [$progressImage, dispatch, socket]);
+ }, [dispatch, socket]);
const onReset = useCallback(() => {
dispatch(canvasReset());
@@ -226,15 +177,15 @@ const SimpleActiveSession = memo(() => {
-
+
);
});
SimpleActiveSession.displayName = 'SimpleActiveSession';
-const SelectedImage = memo(({ $progressImage }: { $progressImage: Atom }) => {
- const progressImage = useStore($progressImage);
+const SelectedImage = memo(() => {
+ const progressImage = useStore($lastCanvasProgressImage);
const selectedImage = useAppSelector(selectSelectedImage);
if (progressImage) {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index 9fcf4a89f8..5fb816e709 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -1,7 +1,6 @@
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { ParamsState, RgbaColor } from 'features/controlLayers/store/types';
import { getInitialParamsState } from 'features/controlLayers/store/types';
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
@@ -25,7 +24,6 @@ import { clamp } from 'lodash-es';
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
import { isNonRefinerMainModelConfig } from 'services/api/types';
-
export const paramsSlice = createSlice({
name: 'params',
initialState: getInitialParamsState(),
@@ -188,9 +186,6 @@ export const paramsSlice = createSlice({
},
paramsReset: (state) => resetState(state),
},
- extraReducers(builder) {
- builder.addCase(canvasSessionStarted, (state) => resetState(state));
- },
});
const resetState = (state: ParamsState): ParamsState => {
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index 92ad0aa011..3c5f154950 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -7,12 +7,13 @@ import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks
import { isImageField, isImageFieldCollection } from 'features/nodes/types/common';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { isCanvasOutputEvent } from 'features/nodes/util/graph/graphBuilderUtils';
+import { flushSync } from 'react-dom';
import type { ApiTagDescription } from 'services/api';
import { boardsApi } from 'services/api/endpoints/boards';
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO, S } from 'services/api/types';
import { getCategories, getListImagesUrl } from 'services/api/util';
-import { $lastProgressEvent } from 'services/events/stores';
+import { $lastCanvasProgressImage, $lastProgressEvent } from 'services/events/stores';
import type { Param0 } from 'tsafe';
import { objectEntries } from 'tsafe';
import type { JsonObject } from 'type-fest';
@@ -176,7 +177,11 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
return;
}
- dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
+ flushSync(() => {
+ dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
+ });
+
+ $lastCanvasProgressImage.set(null);
};
const handleOriginOther = async (data: S['InvocationCompleteEvent']) => {
diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
index 15b5f545ed..f41e9b5da6 100644
--- a/invokeai/frontend/web/src/services/events/setEventListeners.tsx
+++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
@@ -30,7 +30,7 @@ import type { ClientToServerEvents, ServerToClientEvents } from 'services/events
import type { Socket } from 'socket.io-client';
import type { JsonObject } from 'type-fest';
-import { $lastProgressEvent } from './stores';
+import { $lastCanvasProgressEvent, $lastProgressEvent } from './stores';
const log = logger('events');
@@ -428,6 +428,10 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
// If the queue item is completed, failed, or cancelled, we want to clear the last progress event
$lastProgressEvent.set(null);
+ if (data.origin === 'canvas') {
+ $lastCanvasProgressEvent.set(null);
+ }
+
// When a validation run is completed, we want to clear the validation run batch ID & set the workflow as published
const validationRunData = $validationRunData.get();
if (!validationRunData || batch_status.batch_id !== validationRunData.batchId || status !== 'completed') {
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index d25a887c7d..b724f246c7 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -1,3 +1,4 @@
+import type { ProgressImage } from 'features/nodes/types/common';
import { round } from 'lodash-es';
import { atom, computed, map } from 'nanostores';
import type { S } from 'services/api/types';
@@ -15,18 +16,33 @@ $lastProgressEvent.subscribe((event) => {
switch (event.destination) {
case 'workflows':
$lastWorkflowsProgressEvent.set(event);
+ if (event.image) {
+ $lastWorkflowsProgressImage.set({ sessionId: event.session_id, image: event.image });
+ }
break;
case 'upscaling':
$lastUpscalingProgressEvent.set(event);
+ if (event.image) {
+ $lastUpscalingProgressImage.set({ sessionId: event.session_id, image: event.image });
+ }
break;
case 'canvas':
$lastCanvasProgressEvent.set(event);
+ if (event.image) {
+ $lastCanvasProgressImage.set({ sessionId: event.session_id, image: event.image });
+ }
break;
}
});
+
+type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
+
export const $lastCanvasProgressEvent = atom(null);
+export const $lastCanvasProgressImage = atom(null);
export const $lastWorkflowsProgressEvent = atom(null);
+export const $lastWorkflowsProgressImage = atom(null);
export const $lastUpscalingProgressEvent = atom(null);
+export const $lastUpscalingProgressImage = atom(null);
export const $progressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
export const $hasProgressImage = computed($lastProgressEvent, (val) => Boolean(val?.image));
From faeb5f0c3b2dca49c21992b3eb2005d02cfc0e39 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 29 May 2025 16:36:36 +1000
Subject: [PATCH 015/210] refactor(ui): canvas flow (wip)
---
invokeai/frontend/web/public/locales/en.json | 1 +
invokeai/frontend/web/src/app/store/store.ts | 2 +-
.../src/common/hooks/useImageUploadButton.tsx | 59 +++-
.../CanvasAlertsInvocationProgress.tsx | 4 +-
.../components/CanvasMainPanelContent.tsx | 223 +++++++++++++--
.../components/CanvasRightPanelStacked.tsx | 254 ++++++++++++++++++
.../IPAdapter/IPAdapterImagePreview.tsx | 4 +-
.../NewSessionConfirmationAlertDialog.tsx | 2 +-
...edModelLoadingInvocationProgressMessage.ts | 4 +-
.../src/features/controlLayers/store/types.ts | 3 +
invokeai/frontend/web/src/features/dnd/dnd.ts | 30 +++
.../components/GalleryPanelContent.tsx | 44 ++-
.../ImageViewer/CurrentImagePreview.tsx | 4 +-
.../components/ImageViewer/ProgressImage.tsx | 4 +-
.../web/src/features/imageActions/actions.ts | 42 ++-
.../inputs/ImageFieldInputComponent.tsx | 4 +-
.../UpscaleInitialImage.tsx | 4 +-
.../src/features/ui/components/AppContent.tsx | 8 +-
.../FloatingParametersPanelButtons.tsx | 4 +-
.../src/services/events/setEventListeners.tsx | 32 ++-
.../web/src/services/events/stores.ts | 35 +--
21 files changed, 658 insertions(+), 109 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index ac12910bec..0375f0f456 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -2018,6 +2018,7 @@
"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.",
"referenceImageEmptyState": "Upload an image, drag an image from the gallery onto this layer, or pull the bounding box into this layer to get started.",
+ "uploadOrDragAnImage": "Drag an image from the gallery or upload an image.",
"imageNoise": "Image Noise",
"denoiseLimit": "Denoise Limit",
"warnings": {
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index a9f274c5cb..db02a6090b 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -8,8 +8,8 @@ import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
import {
- canvasStagingAreaPersistConfig,
canvasSessionSlice,
+ canvasStagingAreaPersistConfig,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
index f4d88db6a0..93b3c50ce0 100644
--- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
+++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
@@ -1,11 +1,11 @@
-import type { IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
-import { IconButton } from '@invoke-ai/ui-library';
+import type { ButtonProps, IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
+import { Button, IconButton } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { useAppSelector } from 'app/store/storeHooks';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice';
import { toast } from 'features/toast/toast';
-import { useCallback } from 'react';
+import { memo, useCallback } from 'react';
import type { FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
@@ -163,32 +163,63 @@ const sx = {
},
} satisfies SystemStyleObject;
-export const UploadImageButton = ({
- isDisabled = false,
- onUpload,
- isError = false,
- ...rest
-}: {
+export const UploadImageIconButton = memo(
+ ({
+ isDisabled = false,
+ onUpload,
+ isError = false,
+ ...rest
+ }: {
+ onUpload?: (imageDTO: ImageDTO) => void;
+ isError?: boolean;
+ } & SetOptional) => {
+ const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload });
+ return (
+ <>
+ }
+ isLoading={uploadApi.request.isLoading}
+ {...rest}
+ {...uploadApi.getUploadButtonProps()}
+ />
+
+ >
+ );
+ }
+);
+UploadImageIconButton.displayName = 'UploadImageIconButton';
+
+type UploadImageButtonProps = {
onUpload?: (imageDTO: ImageDTO) => void;
isError?: boolean;
-} & SetOptional) => {
+} & ButtonProps;
+
+export const UploadImageButton = memo((props: UploadImageButtonProps) => {
+ const { children, isDisabled = false, onUpload, isError = false, ...rest } = props;
const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload });
return (
<>
- }
+ rightIcon={}
isLoading={uploadApi.request.isLoading}
{...rest}
{...uploadApi.getUploadButtonProps()}
- />
+ >
+ {children ?? 'Upload'}
+
>
);
-};
+});
+UploadImageButton.displayName = 'UploadImageButton';
export const UploadMultipleImageButton = ({
isDisabled = false,
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx
index 26a475469b..4e9dd5ec51 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx
@@ -6,11 +6,11 @@ import { selectIsLocal } from 'features/system/store/configSlice';
import { selectSystemShouldShowInvocationProgressDetail } from 'features/system/store/systemSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import { $invocationProgressMessage } from 'services/events/stores';
+import { $lastProgressMessage } from 'services/events/stores';
const CanvasAlertsInvocationProgressContentLocal = memo(() => {
const { t } = useTranslation();
- const invocationProgressMessage = useStore($invocationProgressMessage);
+ const invocationProgressMessage = useStore($lastProgressMessage);
if (!invocationProgressMessage) {
return null;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index e93587ef18..03f7844950 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,8 +1,21 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { Button, ContextMenu, Flex, IconButton, Image, Menu, MenuButton, MenuList, Text } from '@invoke-ai/ui-library';
+import {
+ Button,
+ ContextMenu,
+ Flex,
+ Heading,
+ IconButton,
+ Image,
+ Menu,
+ MenuButton,
+ MenuList,
+ Text,
+} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
+import { useAppStore } from 'app/store/nanostores/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
@@ -32,13 +45,17 @@ import {
stagingAreaNextStagedImageSelected,
stagingAreaPrevStagedImageSelected,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { newCanvasFromImage } from 'features/imageActions/actions';
import type { ProgressImage } from 'features/nodes/types/common';
-import { memo, useCallback, useEffect } from 'react';
+import { memo, useCallback, useEffect, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
-import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
+import { Trans, useTranslation } from 'react-i18next';
+import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
import type { ImageDTO, S } from 'services/api/types';
import { $lastCanvasProgressImage, $socket } from 'services/events/stores';
-import type { Equals } from 'tsafe';
+import type { Equals, Param0 } from 'tsafe';
import { assert } from 'tsafe';
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
@@ -80,45 +97,190 @@ export const CanvasMainPanelContent = memo(() => {
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
+const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({
+ type: 'raster_layer',
+ withResize: true,
+});
+const generateWithStartingImageAndInpaintMaskDndTargetData = newCanvasFromImageDndTarget.getData({
+ type: 'raster_layer',
+ withInpaintMask: true,
+});
+const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getData({
+ type: 'control_layer',
+ withResize: true,
+});
+
const NoActiveSession = memo(() => {
+ const { t } = useTranslation();
const dispatch = useAppDispatch();
const newSesh = useCallback(() => {
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
}, [dispatch]);
+
return (
-
- No Active Session
-
-
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx
new file mode 100644
index 0000000000..6ca1f26ed3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx
@@ -0,0 +1,254 @@
+import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
+import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
+import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
+import { Box, Tab } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
+import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
+import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
+import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
+import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
+import type { DndTargetState } from 'features/dnd/types';
+import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
+import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
+import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
+import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
+
+export const CanvasRightPanelStacked = memo(() => {
+ const { t } = useTranslation();
+ const activeTab = useAppSelector(selectActiveTabCanvasRightPanel);
+ const imageViewer = useImageViewer();
+ const dispatch = useAppDispatch();
+
+ const tabIndex = useMemo(() => {
+ if (activeTab === 'gallery') {
+ return 1;
+ } else {
+ return 0;
+ }
+ }, [activeTab]);
+
+ const onClickViewerToggleButton = useCallback(() => {
+ imageViewer.open();
+ }, [imageViewer]);
+
+ const onChangeTab = useCallback(
+ (index: number) => {
+ if (index === 0) {
+ dispatch(activeTabCanvasRightPanelChanged('layers'));
+ } else {
+ dispatch(activeTabCanvasRightPanelChanged('gallery'));
+ }
+ },
+ [dispatch]
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+CanvasRightPanelStacked.displayName = 'CanvasRightPanelStacked';
+
+const PanelTabs = memo(() => {
+ const { t } = useTranslation();
+ const store = useAppStore();
+ const activeEntityCount = useAppSelector(selectEntityCountActive);
+ const [layersTabDndState, setLayersTabDndState] = useState('idle');
+ const [galleryTabDndState, setGalleryTabDndState] = useState('idle');
+ const layersTabRef = useRef(null);
+ const galleryTabRef = useRef(null);
+ const timeoutRef = useRef(null);
+
+ const layersTabLabel = useMemo(() => {
+ if (activeEntityCount === 0) {
+ return t('controlLayers.layer_other');
+ }
+ return `${t('controlLayers.layer_other')} (${activeEntityCount})`;
+ }, [activeEntityCount, t]);
+
+ useEffect(() => {
+ if (!layersTabRef.current) {
+ return;
+ }
+
+ const getIsOnLayersTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'layers';
+
+ const onDragEnter = () => {
+ // If we are already on the layers tab, do nothing
+ if (getIsOnLayersTab()) {
+ return;
+ }
+
+ // Else set the state to active and switch to the layers tab after a timeout
+ setLayersTabDndState('over');
+ timeoutRef.current = window.setTimeout(() => {
+ timeoutRef.current = null;
+ store.dispatch(activeTabCanvasRightPanelChanged('layers'));
+ // When we switch tabs, the other tab should be pending
+ setLayersTabDndState('idle');
+ setGalleryTabDndState('potential');
+ }, 300);
+ };
+ const onDragLeave = () => {
+ // Set the state to idle or pending depending on the current tab
+ if (getIsOnLayersTab()) {
+ setLayersTabDndState('idle');
+ } else {
+ setLayersTabDndState('potential');
+ }
+ // Abort the tab switch if it hasn't happened yet
+ if (timeoutRef.current !== null) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
+ const onDragStart = () => {
+ // Set the state to pending when a drag starts
+ setLayersTabDndState('potential');
+ };
+ return combine(
+ dropTargetForElements({
+ element: layersTabRef.current,
+ onDragEnter,
+ onDragLeave,
+ }),
+ monitorForElements({
+ canMonitor: ({ source }) => {
+ if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
+ return false;
+ }
+ // Only monitor if we are not already on the gallery tab
+ return !getIsOnLayersTab();
+ },
+ onDragStart,
+ }),
+ dropTargetForExternal({
+ element: layersTabRef.current,
+ onDragEnter,
+ onDragLeave,
+ }),
+ monitorForExternal({
+ canMonitor: () => !getIsOnLayersTab(),
+ onDragStart,
+ })
+ );
+ }, [store]);
+
+ useEffect(() => {
+ if (!galleryTabRef.current) {
+ return;
+ }
+
+ const getIsOnGalleryTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'gallery';
+
+ const onDragEnter = () => {
+ // If we are already on the gallery tab, do nothing
+ if (getIsOnGalleryTab()) {
+ return;
+ }
+
+ // Else set the state to active and switch to the gallery tab after a timeout
+ setGalleryTabDndState('over');
+ timeoutRef.current = window.setTimeout(() => {
+ timeoutRef.current = null;
+ store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
+ // When we switch tabs, the other tab should be pending
+ setGalleryTabDndState('idle');
+ setLayersTabDndState('potential');
+ }, 300);
+ };
+
+ const onDragLeave = () => {
+ // Set the state to idle or pending depending on the current tab
+ if (getIsOnGalleryTab()) {
+ setGalleryTabDndState('idle');
+ } else {
+ setGalleryTabDndState('potential');
+ }
+ // Abort the tab switch if it hasn't happened yet
+ if (timeoutRef.current !== null) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
+
+ const onDragStart = () => {
+ // Set the state to pending when a drag starts
+ setGalleryTabDndState('potential');
+ };
+
+ return combine(
+ dropTargetForElements({
+ element: galleryTabRef.current,
+ onDragEnter,
+ onDragLeave,
+ }),
+ monitorForElements({
+ canMonitor: ({ source }) => {
+ if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
+ return false;
+ }
+ // Only monitor if we are not already on the gallery tab
+ return !getIsOnGalleryTab();
+ },
+ onDragStart,
+ }),
+ dropTargetForExternal({
+ element: galleryTabRef.current,
+ onDragEnter,
+ onDragLeave,
+ }),
+ monitorForExternal({
+ canMonitor: () => !getIsOnGalleryTab(),
+ onDragStart,
+ })
+ );
+ }, [store]);
+
+ useEffect(() => {
+ const onDrop = () => {
+ // Reset the dnd state when a drop happens
+ setGalleryTabDndState('idle');
+ setLayersTabDndState('idle');
+ };
+ const cleanup = combine(monitorForElements({ onDrop }), monitorForExternal({ onDrop }));
+
+ return () => {
+ cleanup();
+ if (timeoutRef.current !== null) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
+ }, []);
+
+ return (
+ <>
+
+
+ {layersTabLabel}
+
+
+
+
+
+ {t('gallery.gallery')}
+
+
+
+ >
+ );
+});
+
+PanelTabs.displayName = 'PanelTabs';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx
index 77c5ded10d..57d1a3f3f4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx
@@ -1,7 +1,7 @@
import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
-import { UploadImageButton } from 'common/hooks/useImageUploadButton';
+import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
import type { ImageWithDims } from 'features/controlLayers/store/types';
import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
@@ -51,7 +51,7 @@ export const IPAdapterImagePreview = memo(
return (
{!imageDTO && (
- {
const newSessionDialog = useNewGallerySessionDialog();
const newGallerySessionImmediate = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ dispatch(canvasSessionStarted({ sessionType: null }));
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useDeferredModelLoadingInvocationProgressMessage.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useDeferredModelLoadingInvocationProgressMessage.ts
index 5a458040df..a590562016 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useDeferredModelLoadingInvocationProgressMessage.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useDeferredModelLoadingInvocationProgressMessage.ts
@@ -1,11 +1,11 @@
import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { $invocationProgressMessage } from 'services/events/stores';
+import { $lastProgressMessage } from 'services/events/stores';
export const useDeferredModelLoadingInvocationProgressMessage = () => {
const { t } = useTranslation();
- const invocationProgressMessage = useStore($invocationProgressMessage);
+ const invocationProgressMessage = useStore($lastProgressMessage);
const [delayedMessage, setDelayedMessage] = useState(null);
useEffect(() => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 165ad2c460..b69cb9ec0c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -1,6 +1,7 @@
import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers';
+import type { ProgressImage} from 'features/nodes/types/common';
import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
import {
@@ -437,10 +438,12 @@ export type LoRA = {
};
export type StagingAreaImage = {
+ sessionId: string;
imageDTO: ImageDTO;
offsetX: number;
offsetY: number;
};
+export type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
export const zAspectRatioID = z.enum(['Free', '21:9', '9:21', '16:9', '3:2', '4:3', '1:1', '3:4', '2:3', '9:16']);
diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts
index 1764070e26..deacaa47ab 100644
--- a/invokeai/frontend/web/src/features/dnd/dnd.ts
+++ b/invokeai/frontend/web/src/features/dnd/dnd.ts
@@ -11,6 +11,7 @@ import type { BoardId } from 'features/gallery/store/types';
import {
addImagesToBoard,
createNewCanvasEntityFromImage,
+ newCanvasFromImage,
removeImagesFromBoard,
replaceCanvasEntityObjectsWithImage,
setComparisonImage,
@@ -343,7 +344,35 @@ export const newCanvasEntityFromImageDndTarget: DndTarget<
createNewCanvasEntityFromImage({ type, imageDTO, dispatch, getState });
},
};
+//#endregion
+//#region New Canvas from Image
+const _newCanvas = buildTypeAndKey('new-canvas-entity-from-image');
+type NewCanvasFromImageDndTargetData = DndData<
+ typeof _newCanvas.type,
+ typeof _newCanvas.key,
+ {
+ type: CanvasEntityType | 'regional_guidance_with_reference_image';
+ withResize?: boolean;
+ withInpaintMask?: boolean;
+ }
+>;
+export const newCanvasFromImageDndTarget: DndTarget = {
+ ..._newCanvas,
+ typeGuard: buildTypeGuard(_newCanvas.key),
+ getData: buildGetData(_newCanvas.key, _newCanvas.type),
+ isValid: ({ sourceData }) => {
+ if (!singleImageDndSource.typeGuard(sourceData)) {
+ return false;
+ }
+ return true;
+ },
+ handler: ({ sourceData, targetData, dispatch, getState }) => {
+ const { type, withResize } = targetData.payload;
+ const { imageDTO } = sourceData.payload;
+ newCanvasFromImage({ type, imageDTO, dispatch, getState, withResize });
+ },
+};
//#endregion
//#region Replace Canvas Entity Objects With Image
@@ -471,6 +500,7 @@ export const dndTargets = [
replaceCanvasEntityObjectsWithImageDndTarget,
addImageToBoardDndTarget,
removeImageFromBoardDndTarget,
+ newCanvasFromImageDndTarget,
// Single or Multiple Image
addImageToBoardDndTarget,
removeImageFromBoardDndTarget,
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx
index 658261adb8..ef28a923ac 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx
@@ -10,6 +10,9 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
+import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
+import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
@@ -43,6 +46,7 @@ const GalleryPanelContent = () => {
const dispatch = useAppDispatch();
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const imperativePanelGroupRef = useRef(null);
+ const sessionType = useAppSelector(selectCanvasSessionType);
const boardsListPanelOptions = useMemo(
() => ({
@@ -56,6 +60,30 @@ const GalleryPanelContent = () => {
);
const boardsListPanel = usePanel(boardsListPanelOptions);
+ const galleryPanelOptions = useMemo(
+ () => ({
+ id: 'gallery-panel',
+ minSizePx: 128,
+ defaultSizePx: 256,
+ imperativePanelGroupRef,
+ panelGroupDirection: 'vertical',
+ }),
+ []
+ );
+ const galleryPanel = usePanel(galleryPanelOptions);
+
+ const canvasLayersPanelOptions = useMemo(
+ () => ({
+ id: 'canvas-layers-panel',
+ minSizePx: 128,
+ defaultSizePx: 256,
+ imperativePanelGroupRef,
+ panelGroupDirection: 'vertical',
+ }),
+ []
+ );
+ const canvasLayersPanel = usePanel(canvasLayersPanelOptions);
+
const handleClickBoardSearch = useCallback(() => {
if (boardSearchText.length) {
dispatch(boardSearchTextChanged(''));
@@ -98,7 +126,7 @@ const GalleryPanelContent = () => {
-
+
@@ -109,10 +137,20 @@ const GalleryPanelContent = () => {
-
-
+
+
+ {sessionType === 'advanced' && (
+ <>
+
+
+
+
+
+
+ >
+ )}
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
index c856099900..29b6a46374 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
@@ -11,7 +11,7 @@ import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useCallback, useRef, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
-import { $hasProgressImage } from 'services/events/stores';
+import { $hasLastProgressImage } from 'services/events/stores';
import { NoContentForViewer } from './NoContentForViewer';
import ProgressImage from './ProgressImage';
@@ -86,7 +86,7 @@ const CurrentImagePreview = ({ imageDTO }: { imageDTO?: ImageDTO }) => {
export default memo(CurrentImagePreview);
const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
- const hasProgressImage = useStore($hasProgressImage);
+ const hasProgressImage = useStore($hasLastProgressImage);
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
if (hasProgressImage && shouldShowProgressInViewer) {
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx
index f18c104fb2..03453e4c56 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx
@@ -5,7 +5,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { memo, useMemo } from 'react';
-import { $progressImage } from 'services/events/stores';
+import { $lastProgressImage } from 'services/events/stores';
const selectShouldAntialiasProgressImage = createSelector(
selectSystemSlice,
@@ -13,7 +13,7 @@ const selectShouldAntialiasProgressImage = createSelector(
);
const CurrentImagePreview = () => {
- const progressImage = useStore($progressImage);
+ const progressImage = useStore($lastProgressImage);
const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
const sx = useMemo(
diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts
index 359899d494..99497ca928 100644
--- a/invokeai/frontend/web/src/features/imageActions/actions.ts
+++ b/invokeai/frontend/web/src/features/imageActions/actions.ts
@@ -3,9 +3,9 @@ import { deepClone } from 'common/util/deepClone';
import { selectDefaultIPAdapter, selectDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { canvasReset } from 'features/controlLayers/store/actions';
import {
bboxChangedFromCanvas,
+ canvasClearHistory,
controlLayerAdded,
entityRasterized,
inpaintMaskAdded,
@@ -15,6 +15,7 @@ import {
rgAdded,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasSlice';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
@@ -147,10 +148,11 @@ export const newCanvasFromImage = async (arg: {
imageDTO: ImageDTO;
type: CanvasEntityType | 'regional_guidance_with_reference_image';
withResize?: boolean;
+ withInpaintMask?: boolean;
dispatch: AppDispatch;
getState: () => RootState;
}) => {
- const { type, imageDTO, withResize = false, dispatch, getState } = arg;
+ const { type, imageDTO, withResize = false, withInpaintMask = false, dispatch, getState } = arg;
const state = getState();
const base = selectBboxModelBase(state);
@@ -192,10 +194,14 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasReset());
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
+ if (withInpaintMask) {
+ dispatch(inpaintMaskAdded({ isSelected: true }));
+ }
+ dispatch(canvasClearHistory());
break;
}
case 'control_layer': {
@@ -205,10 +211,14 @@ export const newCanvasFromImage = async (arg: {
controlAdapter: deepClone(initialControlNet),
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasReset());
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(controlLayerAdded({ overrides, isSelected: true }));
+ if (withInpaintMask) {
+ dispatch(inpaintMaskAdded({ isSelected: true }));
+ }
+ dispatch(canvasClearHistory());
break;
}
case 'inpaint_mask': {
@@ -217,10 +227,14 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasReset());
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
+ if (withInpaintMask) {
+ dispatch(inpaintMaskAdded({ isSelected: true }));
+ }
+ dispatch(canvasClearHistory());
break;
}
case 'regional_guidance': {
@@ -229,25 +243,37 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasReset());
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rgAdded({ overrides, isSelected: true }));
+ if (withInpaintMask) {
+ dispatch(inpaintMaskAdded({ isSelected: true }));
+ }
+ dispatch(canvasClearHistory());
break;
}
case 'reference_image': {
const ipAdapter = deepClone(selectDefaultRefImageConfig(getState()));
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
- dispatch(canvasReset());
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true }));
+ if (withInpaintMask) {
+ dispatch(inpaintMaskAdded({ isSelected: true }));
+ }
+ dispatch(canvasClearHistory());
break;
}
case 'regional_guidance_with_reference_image': {
const ipAdapter = deepClone(selectDefaultIPAdapter(getState()));
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }];
- dispatch(canvasReset());
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
+ if (withInpaintMask) {
+ dispatch(inpaintMaskAdded({ isSelected: true }));
+ }
+ dispatch(canvasClearHistory());
break;
}
default:
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx
index 3a493e852a..76778fde44 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx
@@ -2,7 +2,7 @@ import { Flex, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch } from 'app/store/storeHooks';
-import { UploadImageButton } from 'common/hooks/useImageUploadButton';
+import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
import type { SetNodeImageFieldImageDndTargetData } from 'features/dnd/dnd';
import { setNodeImageFieldImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
@@ -66,7 +66,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps
{!imageDTO && (
- {
return (
- {!imageDTO && }
+ {!imageDTO && }
{imageDTO && (
<>
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 665ac99db0..6704ab4d00 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -1,7 +1,7 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
-import { CanvasRightPanel } from 'features/controlLayers/components/CanvasRightPanel';
+import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
@@ -161,11 +161,9 @@ AppContent.displayName = 'AppContent';
const RightPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
+ const sessionType = useAppSelector(selectCanvasSessionType);
- if (tab === 'canvas') {
- return ;
- }
- if (tab === 'upscaling' || tab === 'workflows') {
+ if (tab === 'upscaling' || tab === 'workflows' || tab === 'canvas') {
return ;
}
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
index 61a118881e..ddb77c6918 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
@@ -2,6 +2,7 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
@@ -30,10 +31,11 @@ const FloatingSidePanelButtons = ({ togglePanel }: Props) => {
const { t } = useTranslation();
const tab = useAppSelector(selectActiveTab);
const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
+ const sessionType = useAppSelector(selectCanvasSessionType);
return (
- {tab === 'canvas' && (
+ {tab === 'canvas' && sessionType === 'advanced' && (
diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
index f41e9b5da6..5a6426fc2e 100644
--- a/invokeai/frontend/web/src/services/events/setEventListeners.tsx
+++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
@@ -30,7 +30,15 @@ import type { ClientToServerEvents, ServerToClientEvents } from 'services/events
import type { Socket } from 'socket.io-client';
import type { JsonObject } from 'type-fest';
-import { $lastCanvasProgressEvent, $lastProgressEvent } from './stores';
+import {
+ $lastCanvasProgressEvent,
+ $lastCanvasProgressImage,
+ $lastProgressEvent,
+ $lastUpscalingProgressEvent,
+ $lastUpscalingProgressImage,
+ $lastWorkflowsProgressEvent,
+ $lastWorkflowsProgressImage,
+} from './stores';
const log = logger('events');
@@ -92,7 +100,7 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
});
socket.on('invocation_progress', (data) => {
- const { invocation_source_id, invocation, image, origin, percentage, message } = data;
+ const { invocation_source_id, invocation, session_id, image, origin, percentage, message } = data;
let _message = 'Invocation progress';
if (message) {
@@ -107,7 +115,27 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
$lastProgressEvent.set(data);
+ if (origin === 'canvas') {
+ $lastCanvasProgressEvent.set(data);
+ if (image) {
+ $lastCanvasProgressImage.set({ sessionId: session_id, image });
+ }
+ }
+
+ if (origin === 'upscaling') {
+ $lastUpscalingProgressEvent.set(data);
+ if (image) {
+ $lastUpscalingProgressImage.set({ sessionId: session_id, image });
+ }
+ }
+
if (origin === 'workflows') {
+ $lastWorkflowsProgressEvent.set(data);
+
+ if (image) {
+ $lastWorkflowsProgressImage.set({ sessionId: session_id, image });
+ }
+
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
if (nes) {
nes.status = zNodeStatus.enum.IN_PROGRESS;
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index b724f246c7..63f020d52b 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -1,4 +1,4 @@
-import type { ProgressImage } from 'features/nodes/types/common';
+import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
import { round } from 'lodash-es';
import { atom, computed, map } from 'nanostores';
import type { S } from 'services/api/types';
@@ -9,33 +9,6 @@ export const $socket = atom(null);
export const $socketOptions = map>({});
export const $isConnected = atom(false);
export const $lastProgressEvent = atom(null);
-$lastProgressEvent.subscribe((event) => {
- if (!event) {
- return;
- }
- switch (event.destination) {
- case 'workflows':
- $lastWorkflowsProgressEvent.set(event);
- if (event.image) {
- $lastWorkflowsProgressImage.set({ sessionId: event.session_id, image: event.image });
- }
- break;
- case 'upscaling':
- $lastUpscalingProgressEvent.set(event);
- if (event.image) {
- $lastUpscalingProgressImage.set({ sessionId: event.session_id, image: event.image });
- }
- break;
- case 'canvas':
- $lastCanvasProgressEvent.set(event);
- if (event.image) {
- $lastCanvasProgressImage.set({ sessionId: event.session_id, image: event.image });
- }
- break;
- }
-});
-
-type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
export const $lastCanvasProgressEvent = atom(null);
export const $lastCanvasProgressImage = atom(null);
@@ -44,9 +17,9 @@ export const $lastWorkflowsProgressImage = atom(n
export const $lastUpscalingProgressEvent = atom(null);
export const $lastUpscalingProgressImage = atom(null);
-export const $progressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
-export const $hasProgressImage = computed($lastProgressEvent, (val) => Boolean(val?.image));
-export const $invocationProgressMessage = computed($lastProgressEvent, (val) => {
+export const $lastProgressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
+export const $hasLastProgressImage = computed($lastProgressEvent, (val) => Boolean(val?.image));
+export const $lastProgressMessage = computed($lastProgressEvent, (val) => {
if (!val) {
return null;
}
From 4dc3f1bcee2f1a075dabe88b46a23f1be94822b0 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 29 May 2025 20:24:22 +1000
Subject: [PATCH 016/210] refactor(ui): canvas flow (wip)
---
.../components/CanvasMainPanelContent.tsx | 76 ++++++-------------
.../src/services/events/setEventListeners.tsx | 3 +
2 files changed, 26 insertions(+), 53 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 03f7844950..6097b15ac1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -9,6 +9,7 @@ import {
Menu,
MenuButton,
MenuList,
+ Spacer,
Text,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
@@ -47,14 +48,14 @@ import {
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { DndImage } from 'features/dnd/DndImage';
import { newCanvasFromImage } from 'features/imageActions/actions';
-import type { ProgressImage } from 'features/nodes/types/common';
-import { memo, useCallback, useEffect, useMemo } from 'react';
+import { memo, useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Trans, useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
-import type { ImageDTO, S } from 'services/api/types';
-import { $lastCanvasProgressImage, $socket } from 'services/events/stores';
+import type { ImageDTO } from 'services/api/types';
+import { $lastCanvasProgressImage } from 'services/events/stores';
import type { Equals, Param0 } from 'tsafe';
import { assert } from 'tsafe';
@@ -281,39 +282,9 @@ const GenerateWithStartingImageAndInpaintMask = memo(() => {
});
GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
-type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
-
const SimpleActiveSession = memo(() => {
const dispatch = useAppDispatch();
const isStaging = useAppSelector(selectIsStaging);
- const socket = useStore($socket);
-
- useEffect(() => {
- if (!socket) {
- return;
- }
-
- const onQueueItemStatusChanged = (event: S['QueueItemStatusChangedEvent']) => {
- const progressImage = $lastCanvasProgressImage.get();
- if (!progressImage) {
- return;
- }
- if (progressImage.sessionId !== event.session_id) {
- return;
- }
- if (event.status !== 'canceled' && event.status !== 'failed') {
- return;
- }
- $lastCanvasProgressImage.set(null);
- };
- console.log('SUB session preview image listeners');
- socket.on('queue_item_status_changed', onQueueItemStatusChanged);
-
- return () => {
- console.log('UNSUB session preview image listeners');
- socket.off('queue_item_status_changed', onQueueItemStatusChanged);
- };
- }, [dispatch, socket]);
const onReset = useCallback(() => {
dispatch(canvasReset());
@@ -332,7 +303,7 @@ const SimpleActiveSession = memo(() => {
useHotkeys(['left'], selectPrev, { preventDefault: true }, [selectPrev]);
return (
-
+
Simple Session (staging view) {isStaging && 'STAGING'}
@@ -352,14 +323,13 @@ const SelectedImage = memo(() => {
if (progressImage) {
return (
-
+
);
@@ -367,18 +337,8 @@ const SelectedImage = memo(() => {
if (selectedImage) {
return (
-
- {
- console.log('onload');
- }}
- />
+
+
);
}
@@ -391,15 +351,26 @@ const SessionImages = memo(() => {
const stagedImages = useAppSelector(selectStagedImages);
return (
+
{stagedImages.map(({ imageDTO }, index) => (
))}
+
);
});
SessionImages.displayName = 'SessionImages';
const sx = {
+ objectFit: 'contain',
+ maxW: 'full',
+ maxH: 'full',
+ w: 'min-content',
+ borderRadius: 'base',
+ cursor: 'grab',
+ '&[data-is-dragging=true]': {
+ opacity: 0.3,
+ },
'&[data-is-selected="false"]': {
opacity: 0.5,
},
@@ -411,10 +382,9 @@ const SessionImage = memo(({ index, imageDTO }: { index: number; imageDTO: Image
dispatch(stagingAreaImageSelected({ index }));
}, [dispatch, index]);
return (
-
Date: Fri, 30 May 2025 19:25:50 +1000
Subject: [PATCH 017/210] wip
---
.../components/CanvasMainPanelContent.tsx | 73 ++++++++++++++++++-
.../web/src/features/dnd/DndImage.tsx | 9 ++-
.../ImageContextMenu/ImageContextMenu.tsx | 14 ++--
.../components/ImageGrid/GalleryImage.tsx | 9 ++-
4 files changed, 88 insertions(+), 17 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 6097b15ac1..af2a5db6c0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -50,7 +50,7 @@ import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { DndImage } from 'features/dnd/DndImage';
import { newCanvasFromImage } from 'features/imageActions/actions';
-import { memo, useCallback, useMemo } from 'react';
+import { memo, useCallback, useEffect, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Trans, useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
@@ -283,7 +283,9 @@ const GenerateWithStartingImageAndInpaintMask = memo(() => {
GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
const SimpleActiveSession = memo(() => {
- const dispatch = useAppDispatch();
+ const { getState, dispatch } = useAppStore();
+ const selectedImage = useAppSelector(selectSelectedImage);
+
const isStaging = useAppSelector(selectIsStaging);
const onReset = useCallback(() => {
@@ -302,13 +304,64 @@ const SimpleActiveSession = memo(() => {
useHotkeys(['left'], selectPrev, { preventDefault: true }, [selectPrev]);
+ const vary = useCallback(() => {
+ if (!selectedImage) {
+ return;
+ }
+ newCanvasFromImage({
+ imageDTO: selectedImage.imageDTO,
+ type: 'raster_layer',
+ withResize: true,
+ getState,
+ dispatch,
+ });
+ }, [dispatch, getState, selectedImage]);
+
+ const useAsControl = useCallback(() => {
+ if (!selectedImage) {
+ return;
+ }
+ newCanvasFromImage({
+ imageDTO: selectedImage.imageDTO,
+ type: 'control_layer',
+ withResize: true,
+ getState,
+ dispatch,
+ });
+ }, [dispatch, getState, selectedImage]);
+
+ const edit = useCallback(() => {
+ if (!selectedImage) {
+ return;
+ }
+ newCanvasFromImage({
+ imageDTO: selectedImage.imageDTO,
+ type: 'raster_layer',
+ withInpaintMask: true,
+ getState,
+ dispatch,
+ });
+ }, [dispatch, getState, selectedImage]);
+
return (
Simple Session (staging view) {isStaging && 'STAGING'}
- reset
+
+ Start Over
+
+
+
+ Vary
+
+
+ Use as Control
+
+
+ Edit
+
@@ -343,7 +396,11 @@ const SelectedImage = memo(() => {
);
}
- return No images;
+ return (
+
+ No images
+
+ );
});
SelectedImage.displayName = 'SelectedImage';
@@ -361,6 +418,8 @@ const SessionImages = memo(() => {
});
SessionImages.displayName = 'SessionImages';
+const getStagingImageId = (imageDTO: ImageDTO) => `staging-image-${imageDTO.image_name}`;
+
const sx = {
objectFit: 'contain',
maxW: 'full',
@@ -381,8 +440,14 @@ const SessionImage = memo(({ index, imageDTO }: { index: number; imageDTO: Image
const onClick = useCallback(() => {
dispatch(stagingAreaImageSelected({ index }));
}, [dispatch, index]);
+ useEffect(() => {
+ if (selectedImageIndex === index) {
+ document.getElementById(getStagingImageId(imageDTO))?.scrollIntoView();
+ }
+ }, [imageDTO, index, selectedImageIndex]);
return (
{
const store = useAppStore();
const [isDragging, setIsDragging] = useState(false);
- const [element, ref] = useState(null);
+ const ref = useRef(null);
const [dragPreviewState, setDragPreviewState] = useState(null);
useEffect(() => {
+ const element = ref.current;
if (!element) {
return;
}
@@ -66,9 +67,9 @@ export const DndImage = memo(({ imageDTO, asThumbnail, ...rest }: DndImage.Props
},
})
);
- }, [imageDTO, element, store]);
+ }, [imageDTO, store]);
- useImageContextMenu(imageDTO, element);
+ useImageContextMenu(imageDTO, ref);
return (
<>
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx
index 530269c5fb..b8676a2ad6 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx
@@ -7,6 +7,7 @@ import MultipleSelectionMenuItems from 'features/gallery/components/ImageContext
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
import { map } from 'nanostores';
+import type { RefObject } from 'react';
import { memo, useCallback, useEffect, useRef } from 'react';
import type { ImageDTO } from 'services/api/types';
@@ -43,7 +44,7 @@ const onClose = () => {
* Map of elements to image DTOs. This is used to determine which image DTO to show the context menu for, depending on
* the target of the context menu or long press event.
*/
-const elToImageMap = new Map();
+const elToImageMap = new Map();
/**
* Given a target node, find the first registered parent element that contains the target node and return the imageDTO
@@ -59,17 +60,20 @@ const getImageDTOFromMap = (target: Node): ImageDTO | undefined => {
* @param imageDTO The image DTO to register the context menu for.
* @param targetRef The ref of the target element that should trigger the context menu.
*/
-export const useImageContextMenu = (imageDTO: ImageDTO | undefined, targetRef: HTMLDivElement | null) => {
+export const useImageContextMenu = (imageDTO: ImageDTO | undefined, ref: RefObject) => {
useEffect(() => {
- if (!targetRef || !imageDTO) {
+ if (!imageDTO) {
+ return;
+ }
+ const el = ref.current;
+ if (!el) {
return;
}
- const el = targetRef;
elToImageMap.set(el, imageDTO);
return () => {
elToImageMap.delete(el);
};
- }, [imageDTO, targetRef]);
+ }, [imageDTO, ref]);
};
/**
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
index 5721307fba..91e425c1f6 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
@@ -19,7 +19,7 @@ import { SizedSkeletonLoader } from 'features/gallery/components/ImageGrid/Sized
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import type { MouseEventHandler } from 'react';
-import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react';
+import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
// This class name is used to calculate the number of images that fit in the gallery
@@ -89,7 +89,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
const [dragPreviewState, setDragPreviewState] = useState<
DndDragPreviewSingleImageState | DndDragPreviewMultipleImageState | null
>(null);
- const [element, ref] = useState(null);
+ const ref = useRef(null);
const dndId = useId();
const selectIsSelectedForCompare = useMemo(
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name === imageDTO.image_name),
@@ -111,6 +111,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
const isSelected = useAppSelector(selectIsSelected);
useEffect(() => {
+ const element = ref.current;
if (!element) {
return;
}
@@ -175,7 +176,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
},
})
);
- }, [imageDTO, element, store, dndId]);
+ }, [imageDTO, store, dndId]);
const [isHovered, setIsHovered] = useState(false);
@@ -211,7 +212,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO.image_name), [imageDTO.image_name]);
- useImageContextMenu(imageDTO, element);
+ useImageContextMenu(imageDTO, ref);
return (
<>
From 650809e50dd33459a62235544b19636b38110349 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 May 2025 21:13:38 +1000
Subject: [PATCH 018/210] feat(ui): images always added to gallery in simple
session
---
.../graph/generation/buildChatGPT4oGraph.ts | 12 +++-------
.../graph/generation/buildCogView4Graph.ts | 9 ++-----
.../util/graph/generation/buildFLUXGraph.ts | 9 ++-----
.../graph/generation/buildImagen3Graph.ts | 9 +++----
.../graph/generation/buildImagen4Graph.ts | 9 +++----
.../util/graph/generation/buildSD1Graph.ts | 9 ++-----
.../util/graph/generation/buildSD3Graph.ts | 9 ++-----
.../util/graph/generation/buildSDXLGraph.ts | 9 ++-----
.../nodes/util/graph/graphBuilderUtils.ts | 24 +++++++++++++++++++
9 files changed, 43 insertions(+), 56 deletions(-)
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
index 7c6b0fa76d..e6a4791587 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
@@ -9,7 +9,7 @@ import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/va
import { type ImageField, zModelIdentifierField } from 'features/nodes/types/common';
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
-import { CANVAS_OUTPUT_PREFIX, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
+import { selectCanvasOutputFields, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
import type { Equals } from 'tsafe';
@@ -64,14 +64,11 @@ export const buildChatGPT4oGraph = async (
const gptImage = g.addNode({
// @ts-expect-error: These nodes are not available in the OSS application
type: 'chatgpt_4o_generate_image',
- id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
model: zModelIdentifierField.parse(model),
positive_prompt: positivePrompt,
aspect_ratio: bbox.aspectRatio.id,
reference_images,
- use_cache: false,
- is_intermediate: true,
- board: undefined,
+ ...selectCanvasOutputFields(state),
});
g.upsertMetadata({
positive_prompt: positivePrompt,
@@ -96,15 +93,12 @@ export const buildChatGPT4oGraph = async (
const gptImage = g.addNode({
// @ts-expect-error: These nodes are not available in the OSS application
type: 'chatgpt_4o_edit_image',
- id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
model: zModelIdentifierField.parse(model),
positive_prompt: positivePrompt,
aspect_ratio: bbox.aspectRatio.id,
base_image: { image_name },
reference_images,
- use_cache: false,
- is_intermediate: true,
- board: undefined,
+ ...selectCanvasOutputFields(state),
});
g.upsertMetadata({
positive_prompt: positivePrompt,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
index 7b94605d9f..4e369bc3bf 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts
@@ -14,8 +14,8 @@ import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermar
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
- CANVAS_OUTPUT_PREFIX,
getSizes,
+ selectCanvasOutputFields,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
@@ -175,12 +175,7 @@ export const buildCogView4Graph = async (
g.upsertMetadata(selectCanvasMetadata(state));
- g.updateNode(canvasOutput, {
- id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate: true,
- use_cache: false,
- board: undefined,
- });
+ g.updateNode(canvasOutput, selectCanvasOutputFields(state));
g.setMetadataReceivingNode(canvasOutput);
return {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
index e024f852ed..2bbfe1d37e 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
@@ -17,8 +17,8 @@ import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermar
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
- CANVAS_OUTPUT_PREFIX,
getSizes,
+ selectCanvasOutputFields,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import {
@@ -334,12 +334,7 @@ export const buildFLUXGraph = async (state: RootState, manager?: CanvasManager |
g.upsertMetadata(selectCanvasMetadata(state));
- g.updateNode(canvasOutput, {
- id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate: true,
- use_cache: false,
- board: undefined,
- });
+ g.updateNode(canvasOutput, selectCanvasOutputFields(state));
g.setMetadataReceivingNode(canvasOutput);
return {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
index ce1ecac591..b200d6b915 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen3Graph.ts
@@ -8,7 +8,7 @@ import { isImagenAspectRatioID } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
-import { CANVAS_OUTPUT_PREFIX, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
+import { selectCanvasOutputFields, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
import type { Equals } from 'tsafe';
@@ -44,16 +44,13 @@ export const buildImagen3Graph = async (
const imagen3 = g.addNode({
// @ts-expect-error: These nodes are not available in the OSS application
type: 'google_imagen3_generate_image',
- id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
model: zModelIdentifierField.parse(model),
positive_prompt: positivePrompt,
negative_prompt: negativePrompt,
aspect_ratio: bbox.aspectRatio.id,
- enhance_prompt: true,
// When enhance_prompt is true, Imagen3 will return a new image every time, ignoring the seed.
- use_cache: false,
- is_intermediate: true,
- board: undefined,
+ enhance_prompt: true,
+ ...selectCanvasOutputFields(state),
});
g.upsertMetadata({
positive_prompt: positivePrompt,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
index ee499da7f8..5138041c9e 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImagen4Graph.ts
@@ -8,7 +8,7 @@ import { isImagenAspectRatioID } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
-import { CANVAS_OUTPUT_PREFIX, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
+import { selectCanvasOutputFields, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { type GraphBuilderReturn, UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { t } from 'i18next';
import type { Equals } from 'tsafe';
@@ -44,16 +44,13 @@ export const buildImagen4Graph = async (
const imagen4 = g.addNode({
// @ts-expect-error: These nodes are not available in the OSS application
type: 'google_imagen4_generate_image',
- id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
model: zModelIdentifierField.parse(model),
positive_prompt: positivePrompt,
negative_prompt: negativePrompt,
aspect_ratio: bbox.aspectRatio.id,
- enhance_prompt: true,
// When enhance_prompt is true, Imagen4 will return a new image every time, ignoring the seed.
- use_cache: false,
- is_intermediate: true,
- board: undefined,
+ enhance_prompt: true,
+ ...selectCanvasOutputFields(state),
});
g.upsertMetadata({
positive_prompt: positivePrompt,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
index fa1a85778c..0bf32670b4 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
@@ -18,8 +18,8 @@ import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermar
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
- CANVAS_OUTPUT_PREFIX,
getSizes,
+ selectCanvasOutputFields,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
@@ -306,12 +306,7 @@ export const buildSD1Graph = async (state: RootState, manager?: CanvasManager |
g.upsertMetadata(selectCanvasMetadata(state));
- g.updateNode(canvasOutput, {
- id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate: true,
- use_cache: false,
- board: undefined,
- });
+ g.updateNode(canvasOutput, selectCanvasOutputFields(state));
g.setMetadataReceivingNode(canvasOutput);
return {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
index f6ba0fae60..3230d7132c 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts
@@ -13,8 +13,8 @@ import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermar
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
- CANVAS_OUTPUT_PREFIX,
getSizes,
+ selectCanvasOutputFields,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
@@ -196,12 +196,7 @@ export const buildSD3Graph = async (state: RootState, manager?: CanvasManager |
g.upsertMetadata(selectCanvasMetadata(state));
- g.updateNode(canvasOutput, {
- id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate: true,
- use_cache: false,
- board: undefined,
- });
+ g.updateNode(canvasOutput, selectCanvasOutputFields(state));
g.setMetadataReceivingNode(canvasOutput);
return {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
index 9aaf3960c7..1960b6ab8b 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
@@ -18,8 +18,8 @@ import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermar
import { getGenerationMode } from 'features/nodes/util/graph/generation/getGenerationMode';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
- CANVAS_OUTPUT_PREFIX,
getSizes,
+ selectCanvasOutputFields,
selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
@@ -312,12 +312,7 @@ export const buildSDXLGraph = async (state: RootState, manager?: CanvasManager |
g.upsertMetadata(selectCanvasMetadata(state));
- g.updateNode(canvasOutput, {
- id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
- is_intermediate: true,
- use_cache: false,
- board: undefined,
- });
+ g.updateNode(canvasOutput, selectCanvasOutputFields(state));
g.setMetadataReceivingNode(canvasOutput);
return {
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 1163c462ad..dcce4be8b2 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -1,5 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
+import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import type { CanvasState, ParamsState } from 'features/controlLayers/store/types';
import type { BoardField } from 'features/nodes/types/common';
@@ -24,6 +26,28 @@ export const getBoardField = (state: RootState): BoardField | undefined => {
return { board_id: autoAddBoardId };
};
+/**
+ * Builds the common fields for canvas output:
+ * - id
+ * - use_cache
+ * - is_intermediate
+ * - 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 sessionType = selectCanvasSessionType(state);
+ const is_intermediate = sessionType === 'advanced';
+ const board = sessionType === 'advanced' ? undefined : getBoardField(state);
+
+ return {
+ is_intermediate,
+ board,
+ use_cache: false,
+ id: getPrefixedId(CANVAS_OUTPUT_PREFIX),
+ };
+};
+
/**
* Gets the prompts, modified for the active style preset.
*/
From b3f3020793718c8b2bba05003f772978911ada58 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 May 2025 21:29:43 +1000
Subject: [PATCH 019/210] fix(ui): ensure images are added to gallery in simple
sessions
---
.../src/services/events/onInvocationComplete.tsx | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index 3c5f154950..d191becfde 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -23,12 +23,17 @@ const log = logger('events');
const nodeTypeDenylist = ['load_image', 'image'];
export const buildOnInvocationComplete = (getState: () => RootState, dispatch: AppDispatch) => {
- const addImagesToGallery = (data: S['InvocationCompleteEvent'], imageDTOs: ImageDTO[]) => {
+ const addImagesToGallery = async (data: S['InvocationCompleteEvent']) => {
if (nodeTypeDenylist.includes(data.invocation.type)) {
log.trace(`Skipping denylisted node type (${data.invocation.type})`);
return;
}
+ const imageDTOs = await getResultImageDTOs(data);
+ if (imageDTOs.length === 0) {
+ return;
+ }
+
// For efficiency's sake, we want to minimize the number of dispatches and invalidations we do.
// We'll keep track of each change we need to make and do them all at once.
const boardTotalAdditions: Record = {};
@@ -161,8 +166,7 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
upsertExecutionState(nes.nodeId, nes);
}
- const imageDTOs = await getResultImageDTOs(data);
- addImagesToGallery(data, imageDTOs);
+ await addImagesToGallery(data);
};
const handleOriginCanvas = async (data: S['InvocationCompleteEvent']) => {
@@ -170,6 +174,8 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
return;
}
+ await addImagesToGallery(data);
+
// We expect only a single image in the canvas output
const imageDTO = (await getResultImageDTOs(data))[0];
@@ -185,8 +191,7 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
};
const handleOriginOther = async (data: S['InvocationCompleteEvent']) => {
- const imageDTOs = await getResultImageDTOs(data);
- addImagesToGallery(data, imageDTOs);
+ await addImagesToGallery(data);
};
return async (data: S['InvocationCompleteEvent']) => {
From 7a5fa25b48b368eead5217f75d85d61b91916110 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 May 2025 21:30:06 +1000
Subject: [PATCH 020/210] feat(ui): support bookmarking an entity when adding
it
---
.../controlLayers/store/canvasSlice.ts | 67 +++++++++++++++----
1 file changed, 55 insertions(+), 12 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index 1b135b0ce4..d3181c7e9c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -109,10 +109,11 @@ export const canvasSlice = createSlice({
id: string;
overrides?: Partial;
isSelected?: boolean;
+ isBookmarked?: boolean;
mergedEntitiesToDelete?: string[];
}>
) => {
- const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
+ const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [] } = action.payload;
const entityState = getRasterLayerState(id, overrides);
state.rasterLayers.entities.push(entityState);
@@ -123,13 +124,20 @@ export const canvasSlice = createSlice({
);
}
+ const entityIdentifier = getEntityIdentifier(entityState);
+
if (isSelected || mergedEntitiesToDelete.length > 0) {
- state.selectedEntityIdentifier = getEntityIdentifier(entityState);
+ state.selectedEntityIdentifier = entityIdentifier;
+ }
+
+ if (isBookmarked) {
+ state.bookmarkedEntityIdentifier = entityIdentifier;
}
},
prepare: (payload: {
overrides?: Partial;
isSelected?: boolean;
+ isBookmarked?: boolean;
mergedEntitiesToDelete?: string[];
}) => ({
payload: { ...payload, id: getPrefixedId('raster_layer') },
@@ -262,10 +270,11 @@ export const canvasSlice = createSlice({
id: string;
overrides?: Partial;
isSelected?: boolean;
+ isBookmarked?: boolean;
mergedEntitiesToDelete?: string[];
}>
) => {
- const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
+ const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [] } = action.payload;
const entityState = getControlLayerState(id, overrides);
@@ -276,14 +285,20 @@ export const canvasSlice = createSlice({
(entity) => !mergedEntitiesToDelete.includes(entity.id)
);
}
+ const entityIdentifier = getEntityIdentifier(entityState);
if (isSelected || mergedEntitiesToDelete.length > 0) {
- state.selectedEntityIdentifier = getEntityIdentifier(entityState);
+ state.selectedEntityIdentifier = entityIdentifier;
+ }
+
+ if (isBookmarked) {
+ state.bookmarkedEntityIdentifier = entityIdentifier;
}
},
prepare: (payload: {
overrides?: Partial;
isSelected?: boolean;
+ isBookmarked?: boolean;
mergedEntitiesToDelete?: string[];
}) => ({
payload: { ...payload, id: getPrefixedId('control_layer') },
@@ -549,18 +564,32 @@ export const canvasSlice = createSlice({
referenceImageAdded: {
reducer: (
state,
- action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }>
+ action: PayloadAction<{
+ id: string;
+ overrides?: Partial;
+ isSelected?: boolean;
+ isBookmarked?: boolean;
+ }>
) => {
- const { id, overrides, isSelected } = action.payload;
+ const { id, overrides, isSelected, isBookmarked } = action.payload;
const entityState = getReferenceImageState(id, overrides);
state.referenceImages.entities.push(entityState);
+ const entityIdentifier = getEntityIdentifier(entityState);
if (isSelected) {
- state.selectedEntityIdentifier = getEntityIdentifier(entityState);
+ state.selectedEntityIdentifier = entityIdentifier;
+ }
+
+ if (isBookmarked) {
+ state.bookmarkedEntityIdentifier = entityIdentifier;
}
},
- prepare: (payload?: { overrides?: Partial; isSelected?: boolean }) => ({
+ prepare: (payload?: {
+ overrides?: Partial;
+ isSelected?: boolean;
+ isBookmarked?: boolean;
+ }) => ({
payload: { ...payload, id: getPrefixedId('reference_image') },
}),
},
@@ -737,10 +766,11 @@ export const canvasSlice = createSlice({
id: string;
overrides?: Partial;
isSelected?: boolean;
+ isBookmarked?: boolean;
mergedEntitiesToDelete?: string[];
}>
) => {
- const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
+ const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [] } = action.payload;
const entityState = getRegionalGuidanceState(id, overrides);
@@ -751,14 +781,20 @@ export const canvasSlice = createSlice({
(entity) => !mergedEntitiesToDelete.includes(entity.id)
);
}
+ const entityIdentifier = getEntityIdentifier(entityState);
if (isSelected || mergedEntitiesToDelete.length > 0) {
- state.selectedEntityIdentifier = getEntityIdentifier(entityState);
+ state.selectedEntityIdentifier = entityIdentifier;
+ }
+
+ if (isBookmarked) {
+ state.bookmarkedEntityIdentifier = entityIdentifier;
}
},
prepare: (payload?: {
overrides?: Partial;
isSelected?: boolean;
+ isBookmarked?: boolean;
mergedEntitiesToDelete?: string[];
}) => ({
payload: { ...payload, id: getPrefixedId('regional_guidance') },
@@ -1038,10 +1074,11 @@ export const canvasSlice = createSlice({
id: string;
overrides?: Partial;
isSelected?: boolean;
+ isBookmarked?: boolean;
mergedEntitiesToDelete?: string[];
}>
) => {
- const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
+ const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [] } = action.payload;
const entityState = getInpaintMaskState(id, overrides);
@@ -1052,14 +1089,20 @@ export const canvasSlice = createSlice({
(entity) => !mergedEntitiesToDelete.includes(entity.id)
);
}
+ const entityIdentifier = getEntityIdentifier(entityState);
if (isSelected || mergedEntitiesToDelete.length > 0) {
- state.selectedEntityIdentifier = getEntityIdentifier(entityState);
+ state.selectedEntityIdentifier = entityIdentifier;
+ }
+
+ if (isBookmarked) {
+ state.bookmarkedEntityIdentifier = entityIdentifier;
}
},
prepare: (payload?: {
overrides?: Partial;
isSelected?: boolean;
+ isBookmarked?: boolean;
mergedEntitiesToDelete?: string[];
}) => ({
payload: { ...payload, id: getPrefixedId('inpaint_mask') },
From 2a92524546a380c6c3a81a77f3866bbb28bb005b Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 May 2025 21:30:24 +1000
Subject: [PATCH 021/210] feat(ui): bookmark new inpaint masks
---
.../web/src/features/imageActions/actions.ts | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts
index 99497ca928..d432433903 100644
--- a/invokeai/frontend/web/src/features/imageActions/actions.ts
+++ b/invokeai/frontend/web/src/features/imageActions/actions.ts
@@ -199,7 +199,7 @@ export const newCanvasFromImage = async (arg: {
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
if (withInpaintMask) {
- dispatch(inpaintMaskAdded({ isSelected: true }));
+ dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
}
dispatch(canvasClearHistory());
break;
@@ -216,7 +216,7 @@ export const newCanvasFromImage = async (arg: {
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(controlLayerAdded({ overrides, isSelected: true }));
if (withInpaintMask) {
- dispatch(inpaintMaskAdded({ isSelected: true }));
+ dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
}
dispatch(canvasClearHistory());
break;
@@ -232,7 +232,7 @@ export const newCanvasFromImage = async (arg: {
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
if (withInpaintMask) {
- dispatch(inpaintMaskAdded({ isSelected: true }));
+ dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
}
dispatch(canvasClearHistory());
break;
@@ -248,7 +248,7 @@ export const newCanvasFromImage = async (arg: {
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rgAdded({ overrides, isSelected: true }));
if (withInpaintMask) {
- dispatch(inpaintMaskAdded({ isSelected: true }));
+ dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
}
dispatch(canvasClearHistory());
break;
@@ -259,7 +259,7 @@ export const newCanvasFromImage = async (arg: {
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true }));
if (withInpaintMask) {
- dispatch(inpaintMaskAdded({ isSelected: true }));
+ dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
}
dispatch(canvasClearHistory());
break;
@@ -271,7 +271,7 @@ export const newCanvasFromImage = async (arg: {
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
if (withInpaintMask) {
- dispatch(inpaintMaskAdded({ isSelected: true }));
+ dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
}
dispatch(canvasClearHistory());
break;
From 57bfae6774282cb392c00142ab063717b4714957 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 May 2025 21:30:56 +1000
Subject: [PATCH 022/210] fix(ui): ensure all args are passed to handler when
creating new canvas from image
---
invokeai/frontend/web/src/features/dnd/dnd.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts
index deacaa47ab..89abd63a77 100644
--- a/invokeai/frontend/web/src/features/dnd/dnd.ts
+++ b/invokeai/frontend/web/src/features/dnd/dnd.ts
@@ -368,9 +368,8 @@ export const newCanvasFromImageDndTarget: DndTarget {
- const { type, withResize } = targetData.payload;
const { imageDTO } = sourceData.payload;
- newCanvasFromImage({ type, imageDTO, dispatch, getState, withResize });
+ newCanvasFromImage({ imageDTO, dispatch, getState, ...targetData.payload });
},
};
//#endregion
From 579318af703f203b2dac462140e53e4f3084dbe8 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 May 2025 21:31:06 +1000
Subject: [PATCH 023/210] fix(ui): remove unused sessionId field from type
---
.../frontend/web/src/features/controlLayers/store/types.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index b69cb9ec0c..02e1ff739c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -1,7 +1,7 @@
import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers';
-import type { ProgressImage} from 'features/nodes/types/common';
+import type { ProgressImage } from 'features/nodes/types/common';
import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
import {
@@ -438,7 +438,6 @@ export type LoRA = {
};
export type StagingAreaImage = {
- sessionId: string;
imageDTO: ImageDTO;
offsetX: number;
offsetY: number;
From 1446d3490b0291d2d05785c2bf3c6ef0e7c59d2b Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 May 2025 21:31:43 +1000
Subject: [PATCH 024/210] fix(ui): merge refs when forwardingin DndImage
---
.../web/src/features/dnd/DndImage.tsx | 125 +++++++++---------
.../ImageContextMenu/ImageContextMenu.tsx | 5 +-
2 files changed, 63 insertions(+), 67 deletions(-)
diff --git a/invokeai/frontend/web/src/features/dnd/DndImage.tsx b/invokeai/frontend/web/src/features/dnd/DndImage.tsx
index ded8ab249e..9009fe440f 100644
--- a/invokeai/frontend/web/src/features/dnd/DndImage.tsx
+++ b/invokeai/frontend/web/src/features/dnd/DndImage.tsx
@@ -9,7 +9,7 @@ import { createSingleImageDragPreview, setSingleImageDragPreview } from 'feature
import { firefoxDndFix } from 'features/dnd/util';
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
-import { memo, useEffect, useRef, useState } from 'react';
+import { forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
const sx = {
@@ -23,69 +23,68 @@ const sx = {
},
} satisfies SystemStyleObject;
-/* eslint-disable-next-line @typescript-eslint/no-namespace */
-export namespace DndImage {
- export interface Props extends ImageProps {
- imageDTO: ImageDTO;
- asThumbnail?: boolean;
- }
-}
+type Props = {
+ imageDTO: ImageDTO;
+ asThumbnail?: boolean;
+} & ImageProps;
-export const DndImage = memo(({ imageDTO, asThumbnail, ...rest }: DndImage.Props) => {
- const store = useAppStore();
- const [isDragging, setIsDragging] = useState(false);
- const ref = useRef(null);
- const [dragPreviewState, setDragPreviewState] = useState(null);
+export const DndImage = memo(
+ forwardRef(({ imageDTO, asThumbnail, ...rest }: Props, forwardedRef) => {
+ const store = useAppStore();
+ const [isDragging, setIsDragging] = useState(false);
+ const ref = useRef(null);
+ useImperativeHandle(forwardedRef, () => ref.current!, []);
+ const [dragPreviewState, setDragPreviewState] = useState(null);
- useEffect(() => {
- const element = ref.current;
- if (!element) {
- return;
- }
- return combine(
- firefoxDndFix(element),
- draggable({
- element,
- getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name),
- onDragStart: () => {
- setIsDragging(true);
- if ($imageViewer.get()) {
- $imageViewer.set(false);
- }
- },
- onDrop: () => {
- setIsDragging(false);
- },
- onGenerateDragPreview: (args) => {
- if (singleImageDndSource.typeGuard(args.source.data)) {
- setSingleImageDragPreview({
- singleImageDndData: args.source.data,
- onGenerateDragPreviewArgs: args,
- setDragPreviewState,
- });
- }
- },
- })
+ useEffect(() => {
+ const element = ref.current;
+ if (!element) {
+ return;
+ }
+ return combine(
+ firefoxDndFix(element),
+ draggable({
+ element,
+ getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name),
+ onDragStart: () => {
+ setIsDragging(true);
+ if ($imageViewer.get()) {
+ $imageViewer.set(false);
+ }
+ },
+ onDrop: () => {
+ setIsDragging(false);
+ },
+ onGenerateDragPreview: (args) => {
+ if (singleImageDndSource.typeGuard(args.source.data)) {
+ setSingleImageDragPreview({
+ singleImageDndData: args.source.data,
+ onGenerateDragPreviewArgs: args,
+ setDragPreviewState,
+ });
+ }
+ },
+ })
+ );
+ }, [forwardedRef, imageDTO, store]);
+
+ useImageContextMenu(imageDTO, ref);
+
+ return (
+ <>
+
+ {dragPreviewState?.type === 'single-image' ? createSingleImageDragPreview(dragPreviewState) : null}
+ >
);
- }, [imageDTO, store]);
-
- useImageContextMenu(imageDTO, ref);
-
- return (
- <>
-
- {dragPreviewState?.type === 'single-image' ? createSingleImageDragPreview(dragPreviewState) : null}
- >
- );
-});
-
+ })
+);
DndImage.displayName = 'DndImage';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx
index b8676a2ad6..d944d3dac0 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx
@@ -60,11 +60,8 @@ const getImageDTOFromMap = (target: Node): ImageDTO | undefined => {
* @param imageDTO The image DTO to register the context menu for.
* @param targetRef The ref of the target element that should trigger the context menu.
*/
-export const useImageContextMenu = (imageDTO: ImageDTO | undefined, ref: RefObject) => {
+export const useImageContextMenu = (imageDTO: ImageDTO, ref: RefObject) => {
useEffect(() => {
- if (!imageDTO) {
- return;
- }
const el = ref.current;
if (!el) {
return;
From eb45a457e98b48836805043052acf4b0a9d1e7e7 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 May 2025 21:32:44 +1000
Subject: [PATCH 025/210] fix(ui): ref goes undefined in GalleryImage
This appears to be a bug in Chakra UI v2 - use of a fallback component makes the ref passed to an image end up undefined. Had to remove the skeleton loader fallback component.
---
.../src/features/gallery/components/ImageGrid/GalleryImage.tsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
index 91e425c1f6..87ae975857 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
@@ -15,7 +15,6 @@ import { firefoxDndFix } from 'features/dnd/util';
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons';
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
-import { SizedSkeletonLoader } from 'features/gallery/components/ImageGrid/SizedSkeletonLoader';
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import type { MouseEventHandler } from 'react';
@@ -235,7 +234,6 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
}
w={imageDTO.width}
objectFit="contain"
maxW="full"
From a3851e0b0822fe94b5bb1e9c946130ab35b92e83 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 May 2025 21:57:42 +1000
Subject: [PATCH 026/210] refactor(ui): canvas flow (wip)
---
.../components/CanvasMainPanelContent.tsx | 161 +++++++++---------
1 file changed, 85 insertions(+), 76 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index af2a5db6c0..a70ed1e200 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,6 +1,7 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import {
Button,
+ ButtonGroup,
ContextMenu,
Flex,
Heading,
@@ -33,12 +34,10 @@ import { StagingAreaToolbar } from 'features/controlLayers/components/StagingAre
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { canvasReset } from 'features/controlLayers/store/actions';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import {
canvasSessionStarted,
selectCanvasSessionType,
- selectIsStaging,
selectSelectedImage,
selectStagedImageIndex,
selectStagedImages,
@@ -46,6 +45,7 @@ import {
stagingAreaNextStagedImageSelected,
stagingAreaPrevStagedImageSelected,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { DndImage } from 'features/dnd/DndImage';
@@ -286,10 +286,12 @@ const SimpleActiveSession = memo(() => {
const { getState, dispatch } = useAppStore();
const selectedImage = useAppSelector(selectSelectedImage);
- const isStaging = useAppSelector(selectIsStaging);
+ const startOver = useCallback(() => {
+ dispatch(canvasSessionStarted({ sessionType: null }));
+ }, [dispatch]);
- const onReset = useCallback(() => {
- dispatch(canvasReset());
+ const goAdvanced = useCallback(() => {
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
}, [dispatch]);
const selectNext = useCallback(() => {
@@ -304,96 +306,34 @@ const SimpleActiveSession = memo(() => {
useHotkeys(['left'], selectPrev, { preventDefault: true }, [selectPrev]);
- const vary = useCallback(() => {
- if (!selectedImage) {
- return;
- }
- newCanvasFromImage({
- imageDTO: selectedImage.imageDTO,
- type: 'raster_layer',
- withResize: true,
- getState,
- dispatch,
- });
- }, [dispatch, getState, selectedImage]);
-
- const useAsControl = useCallback(() => {
- if (!selectedImage) {
- return;
- }
- newCanvasFromImage({
- imageDTO: selectedImage.imageDTO,
- type: 'control_layer',
- withResize: true,
- getState,
- dispatch,
- });
- }, [dispatch, getState, selectedImage]);
-
- const edit = useCallback(() => {
- if (!selectedImage) {
- return;
- }
- newCanvasFromImage({
- imageDTO: selectedImage.imageDTO,
- type: 'raster_layer',
- withInpaintMask: true,
- getState,
- dispatch,
- });
- }, [dispatch, getState, selectedImage]);
-
return (
-
+
- Simple Session (staging view) {isStaging && 'STAGING'}
+ Generations from this Session
- Start Over
-
-
-
- Vary
-
-
- Use as Control
-
-
- Edit
+
+ Start Over
-
+
);
});
SimpleActiveSession.displayName = 'SimpleActiveSession';
-const SelectedImage = memo(() => {
+const SelectedImageOrProgressImage = memo(() => {
const progressImage = useStore($lastCanvasProgressImage);
const selectedImage = useAppSelector(selectSelectedImage);
if (progressImage) {
- return (
-
-
-
- );
+ return ;
}
if (selectedImage) {
- return (
-
-
-
- );
+ return ;
}
return (
@@ -402,12 +342,80 @@ const SelectedImage = memo(() => {
);
});
+SelectedImageOrProgressImage.displayName = 'SelectedImageOrProgressImage';
+
+const SelectedImage = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
+ const { getState, dispatch } = useAppStore();
+
+ const vary = useCallback(() => {
+ newCanvasFromImage({
+ imageDTO,
+ type: 'raster_layer',
+ withResize: true,
+ getState,
+ dispatch,
+ });
+ }, [dispatch, getState, imageDTO]);
+
+ const useAsControl = useCallback(() => {
+ newCanvasFromImage({
+ imageDTO,
+ type: 'control_layer',
+ withResize: true,
+ getState,
+ dispatch,
+ });
+ }, [dispatch, getState, imageDTO]);
+
+ const edit = useCallback(() => {
+ newCanvasFromImage({
+ imageDTO,
+ type: 'raster_layer',
+ withInpaintMask: true,
+ getState,
+ dispatch,
+ });
+ }, [dispatch, getState, imageDTO]);
+ return (
+
+
+
+
+
+ Vary
+
+
+ Use as Control
+
+
+ Edit
+
+
+
+
+ );
+});
SelectedImage.displayName = 'SelectedImage';
+const ProgressImage = memo(({ progressImage }: { progressImage: EphemeralProgressImage }) => {
+ return (
+
+
+
+ );
+});
+ProgressImage.displayName = 'ProgressImage';
+
const SessionImages = memo(() => {
const stagedImages = useAppSelector(selectStagedImages);
return (
-
+
{stagedImages.map(({ imageDTO }, index) => (
@@ -442,6 +450,7 @@ const SessionImage = memo(({ index, imageDTO }: { index: number; imageDTO: Image
}, [dispatch, index]);
useEffect(() => {
if (selectedImageIndex === index) {
+ // this doesn't work when the DndImage is in a popover... why
document.getElementById(getStagingImageId(imageDTO))?.scrollIntoView();
}
}, [imageDTO, index, selectedImageIndex]);
From 5e93f585300ca9db400f2bd60190d8ce5f332c80 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Sat, 31 May 2025 00:01:57 +1000
Subject: [PATCH 027/210] wip progress events
---
invokeai/frontend/web/src/app/store/store.ts | 2 +
.../components/CanvasMainPanelContent.tsx | 125 ++++++++++++++----
.../store/canvasStagingAreaSlice.ts | 30 ++++-
.../src/features/controlLayers/store/types.ts | 6 +
.../services/events/onInvocationComplete.tsx | 19 ++-
.../src/services/events/setEventListeners.tsx | 25 +++-
.../web/src/services/events/stores.ts | 27 +++-
7 files changed, 192 insertions(+), 42 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index db02a6090b..0a2f515334 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -3,6 +3,7 @@ import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/too
import { logger } from 'app/logging/logger';
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
+import { getDebugLoggerMiddleware } from 'app/store/middleware/debugLoggerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
@@ -175,6 +176,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
.concat(api.middleware)
.concat(dynamicMiddlewares)
.concat(authToastMiddleware)
+ .concat(getDebugLoggerMiddleware())
.prepend(listenerMiddleware.middleware),
enhancers: (getDefaultEnhancers) => {
const _enhancers = getDefaultEnhancers().concat(autoBatchEnhancer());
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index a70ed1e200..7c61000b89 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,5 +1,6 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import {
+ Box,
Button,
ButtonGroup,
ContextMenu,
@@ -40,12 +41,10 @@ import {
selectCanvasSessionType,
selectSelectedImage,
selectStagedImageIndex,
- selectStagedImages,
stagingAreaImageSelected,
stagingAreaNextStagedImageSelected,
stagingAreaPrevStagedImageSelected,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
-import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { DndImage } from 'features/dnd/DndImage';
@@ -55,7 +54,8 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { Trans, useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
-import { $lastCanvasProgressImage } from 'services/events/stores';
+import type { ProgressAndResult } from 'services/events/stores';
+import { $progressImages, useMapSelector } from 'services/events/stores';
import type { Equals, Param0 } from 'tsafe';
import { assert } from 'tsafe';
@@ -288,6 +288,7 @@ const SimpleActiveSession = memo(() => {
const startOver = useCallback(() => {
dispatch(canvasSessionStarted({ sessionType: null }));
+ $progressImages.set({});
}, [dispatch]);
const goAdvanced = useCallback(() => {
@@ -325,15 +326,10 @@ const SimpleActiveSession = memo(() => {
SimpleActiveSession.displayName = 'SimpleActiveSession';
const SelectedImageOrProgressImage = memo(() => {
- const progressImage = useStore($lastCanvasProgressImage);
const selectedImage = useAppSelector(selectSelectedImage);
- if (progressImage) {
- return ;
- }
-
if (selectedImage) {
- return ;
+ return ;
}
return (
@@ -397,36 +393,107 @@ const SelectedImage = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
});
SelectedImage.displayName = 'SelectedImage';
-const ProgressImage = memo(({ progressImage }: { progressImage: EphemeralProgressImage }) => {
+const FullSizeImage = memo(({ sessionId }: { sessionId: string }) => {
+ const _progressImage = useMapSelector(sessionId, $progressImages);
+
+ if (!_progressImage) {
+ return (
+
+ Pending
+
+ );
+ }
+
+ if (_progressImage.resultImage) {
+ return ;
+ }
+
+ if (_progressImage.progressImage) {
+ return (
+
+
+
+ );
+ }
+
return (
-
+ No progress yet
);
});
-ProgressImage.displayName = 'ProgressImage';
+FullSizeImage.displayName = 'FullSizeImage';
const SessionImages = memo(() => {
- const stagedImages = useAppSelector(selectStagedImages);
+ const progressImages = useStore($progressImages);
return (
- {stagedImages.map(({ imageDTO }, index) => (
-
- ))}
+ {Object.values(progressImages).map((data, index) => {
+ if (data.type === 'staged') {
+ return ;
+ } else {
+ return ;
+ }
+ })}
);
});
SessionImages.displayName = 'SessionImages';
-const getStagingImageId = (imageDTO: ImageDTO) => `staging-image-${imageDTO.image_name}`;
+const ProgressImagePreview = ({ index, data }: { index: number; data: ProgressAndResult }) => {
+ const dispatch = useAppDispatch();
+ const selectedImageIndex = useAppSelector(selectStagedImageIndex);
+ const onClick = useCallback(() => {
+ dispatch(stagingAreaImageSelected({ index }));
+ }, [dispatch, index]);
+
+ useEffect(() => {
+ if (selectedImageIndex === index) {
+ // this doesn't work when the DndImage is in a popover... why
+ document.getElementById(getStagingImageId(data.sessionId))?.scrollIntoView();
+ }
+ }, [data.sessionId, index, selectedImageIndex]);
+
+ if (data.resultImage) {
+ return (
+
+ );
+ }
+
+ if (data.progressImage) {
+ return (
+
+ );
+ }
+
+ return ;
+};
+
+const getStagingImageId = (session_id: string) => `staging-image-${session_id}`;
const sx = {
objectFit: 'contain',
@@ -442,7 +509,7 @@ const sx = {
opacity: 0.5,
},
} satisfies SystemStyleObject;
-const SessionImage = memo(({ index, imageDTO }: { index: number; imageDTO: ImageDTO }) => {
+const SessionImage = memo(({ index, data }: { index: number; data: ProgressAndResult }) => {
const dispatch = useAppDispatch();
const selectedImageIndex = useAppSelector(selectStagedImageIndex);
const onClick = useCallback(() => {
@@ -451,17 +518,19 @@ const SessionImage = memo(({ index, imageDTO }: { index: number; imageDTO: Image
useEffect(() => {
if (selectedImageIndex === index) {
// this doesn't work when the DndImage is in a popover... why
- document.getElementById(getStagingImageId(imageDTO))?.scrollIntoView();
+ document.getElementById(getStagingImageId(data.sessionId))?.scrollIntoView();
}
- }, [imageDTO, index, selectedImageIndex]);
+ }, [data.sessionId, index, selectedImageIndex]);
return (
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index 4e840dcc35..2703286e72 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -2,12 +2,12 @@ import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolki
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { canvasReset } from 'features/controlLayers/store/actions';
-import type { StagingAreaImage } from 'features/controlLayers/store/types';
+import type { StagingAreaImage, StagingAreaProgressImage } from 'features/controlLayers/store/types';
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
type CanvasStagingAreaState = {
sessionType: 'simple' | 'advanced' | null;
- images: StagingAreaImage[];
+ images: (StagingAreaImage | StagingAreaProgressImage)[];
selectedImageIndex: number;
};
@@ -25,8 +25,28 @@ export const canvasSessionSlice = createSlice({
reducers: {
stagingAreaImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
const { stagingAreaImage } = action.payload;
- state.images.push(stagingAreaImage);
- state.selectedImageIndex = state.images.length - 1;
+ let didReplace = false;
+ const newImages = [];
+ for (const i of state.images) {
+ if (i.sessionId === stagingAreaImage.sessionId) {
+ newImages.push(stagingAreaImage);
+ didReplace = true;
+ } else {
+ newImages.push(i);
+ }
+ }
+ if (!didReplace) {
+ newImages.push(stagingAreaImage);
+ }
+ state.images = newImages;
+ },
+ stagingAreaGenerationStarted: (state, action: PayloadAction<{ sessionId: string }>) => {
+ const { sessionId } = action.payload;
+ state.images.push({ type: 'progress', sessionId });
+ },
+ stagingAreaGenerationFinished: (state, action: PayloadAction<{ sessionId: string }>) => {
+ const { sessionId } = action.payload;
+ state.images = state.images.filter((data) => data.sessionId !== sessionId);
},
stagingAreaImageSelected: (state, action: PayloadAction<{ index: number }>) => {
const { index } = action.payload;
@@ -61,6 +81,8 @@ export const canvasSessionSlice = createSlice({
export const {
stagingAreaImageStaged,
+ stagingAreaGenerationStarted,
+ stagingAreaGenerationFinished,
stagingAreaStagedImageDiscarded,
stagingAreaReset,
stagingAreaImageSelected,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 02e1ff739c..070547f246 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -438,10 +438,16 @@ export type LoRA = {
};
export type StagingAreaImage = {
+ type: 'staged';
+ sessionId: string;
imageDTO: ImageDTO;
offsetX: number;
offsetY: number;
};
+export type StagingAreaProgressImage = {
+ type: 'progress';
+ sessionId: string;
+};
export type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
export const zAspectRatioID = z.enum(['Free', '21:9', '9:21', '16:9', '3:2', '4:3', '1:1', '3:4', '2:3', '9:16']);
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index d191becfde..bcd51ca191 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -13,7 +13,11 @@ import { boardsApi } from 'services/api/endpoints/boards';
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO, S } from 'services/api/types';
import { getCategories, getListImagesUrl } from 'services/api/util';
-import { $lastCanvasProgressImage, $lastProgressEvent } from 'services/events/stores';
+import {
+ $lastCanvasProgressImage,
+ $lastProgressEvent,
+ $progressImages,
+} from 'services/events/stores';
import type { Param0 } from 'tsafe';
import { objectEntries } from 'tsafe';
import type { JsonObject } from 'type-fest';
@@ -184,9 +188,20 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
}
flushSync(() => {
- dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
+ dispatch(
+ stagingAreaImageStaged({
+ stagingAreaImage: { type: 'staged', sessionId: data.session_id, imageDTO, offsetX: 0, offsetY: 0 },
+ })
+ );
});
+ const progressData = $progressImages.get()[data.session_id];
+ if (progressData) {
+ $progressImages.setKey(data.session_id, { ...progressData, isFinished: true, resultImage: imageDTO });
+ } else {
+ $progressImages.setKey(data.session_id, { sessionId: data.session_id, isFinished: true, resultImage: imageDTO });
+ }
+
$lastCanvasProgressImage.set(null);
};
diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
index febd966442..1a873f5d43 100644
--- a/invokeai/frontend/web/src/services/events/setEventListeners.tsx
+++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
@@ -8,6 +8,9 @@ import { $bulkDownloadId } from 'app/store/nanostores/bulkDownloadId';
import { $queueId } from 'app/store/nanostores/queueId';
import type { AppStore } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
+import {
+ stagingAreaGenerationStarted,
+} from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
$isInPublishFlow,
$outputNodeId,
@@ -38,6 +41,7 @@ import {
$lastUpscalingProgressImage,
$lastWorkflowsProgressEvent,
$lastWorkflowsProgressImage,
+ $progressImages,
} from './stores';
const log = logger('events');
@@ -115,6 +119,15 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
$lastProgressEvent.set(data);
+ if (data.image) {
+ const progressData = $progressImages.get()[session_id];
+ if (progressData) {
+ $progressImages.setKey(session_id, { ...progressData, progressImage: data.image });
+ } else {
+ $progressImages.setKey(session_id, { sessionId: session_id, isFinished: false, progressImage: data.image });
+ }
+ }
+
if (origin === 'canvas') {
$lastCanvasProgressEvent.set(data);
if (image) {
@@ -432,6 +445,10 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
clone.outputs = [];
$nodeExecutionStates.setKey(clone.nodeId, clone);
});
+ if (data.origin === 'canvas') {
+ store.dispatch(stagingAreaGenerationStarted({ sessionId: session_id }));
+ $progressImages.setKey(session_id, { sessionId: session_id, isFinished: false });
+ }
} else if (status === 'completed' || status === 'failed' || status === 'canceled') {
if (status === 'failed' && error_type) {
const isLocal = getState().config.isLocal ?? true;
@@ -455,13 +472,7 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
}
// If the queue item is completed, failed, or cancelled, we want to clear the last progress event
$lastProgressEvent.set(null);
-
- if (data.origin === 'canvas') {
- $lastCanvasProgressEvent.set(null);
- if (status === 'canceled' || status === 'failed') {
- $lastCanvasProgressImage.set(null);
- }
- }
+ $progressImages.setKey(session_id, undefined);
// When a validation run is completed, we want to clear the validation run batch ID & set the workflow as published
const validationRunData = $validationRunData.get();
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index 63f020d52b..e233e4fea1 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -1,7 +1,10 @@
import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
+import type { ProgressImage } from 'features/nodes/types/common';
import { round } from 'lodash-es';
+import type { MapStore } from 'nanostores';
import { atom, computed, map } from 'nanostores';
-import type { S } from 'services/api/types';
+import { useEffect, useState } from 'react';
+import type { ImageDTO, S } from 'services/api/types';
import type { AppSocket } from 'services/events/types';
import type { ManagerOptions, SocketOptions } from 'socket.io-client';
@@ -10,6 +13,28 @@ export const $socketOptions = map>({});
export const $isConnected = atom(false);
export const $lastProgressEvent = atom(null);
+export type ProgressAndResult = {
+ sessionId: string;
+ isFinished: boolean;
+ progressImage?: ProgressImage;
+ resultImage?: ImageDTO;
+};
+export const $progressImages = map({} as Record);
+
+export const useMapSelector = (id: string, map: MapStore>): T | undefined => {
+ const [value, setValue] = useState();
+ useEffect(() => {
+ const unsub = map.subscribe((data) => {
+ setValue(data[id]);
+ });
+ return () => {
+ unsub();
+ };
+ }, [id, map]);
+
+ return value;
+};
+
export const $lastCanvasProgressEvent = atom(null);
export const $lastCanvasProgressImage = atom(null);
export const $lastWorkflowsProgressEvent = atom(null);
From 8a78e37634e57baf514fefa8946612a4eda34bca Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 3 Jun 2025 13:45:01 +1000
Subject: [PATCH 028/210] feat: canvas flow rework (wip)
---
invokeai/app/api/routers/session_queue.py | 36 +-
.../session_queue/session_queue_base.py | 14 +-
.../session_queue/session_queue_common.py | 37 +-
.../session_queue/session_queue_sqlite.py | 67 ++-
invokeai/app/services/shared/graph.py | 17 +
invokeai/frontend/web/src/app/store/store.ts | 3 +-
.../components/CanvasMainPanelContent.tsx | 417 +++++++++++++++++-
.../nodes/util/graph/graphBuilderUtils.ts | 4 +-
.../QueueList/QueueItemComponent.tsx | 6 +-
.../components/QueueList/QueueItemDetail.tsx | 12 +-
.../queue/components/QueueList/QueueList.tsx | 10 +-
.../QueueList/QueueListComponent.tsx | 4 +-
.../QueueList/useDestinationText.ts | 4 +-
.../components/QueueList/useOriginText.ts | 4 +-
.../web/src/services/api/endpoints/queue.ts | 17 +-
.../frontend/web/src/services/api/schema.ts | 217 ++++-----
.../frontend/web/src/services/api/types.ts | 1 -
.../src/services/events/setEventListeners.tsx | 43 +-
18 files changed, 645 insertions(+), 268 deletions(-)
diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py
index fe0c31bce9..d21610fed3 100644
--- a/invokeai/app/api/routers/session_queue.py
+++ b/invokeai/app/api/routers/session_queue.py
@@ -20,7 +20,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
RetryItemsResult,
SessionQueueCountsByDestination,
SessionQueueItem,
- SessionQueueItemDTO,
SessionQueueStatus,
)
from invokeai.app.services.shared.pagination import CursorPaginatedResults
@@ -68,7 +67,7 @@ async def enqueue_batch(
"/{queue_id}/list",
operation_id="list_queue_items",
responses={
- 200: {"model": CursorPaginatedResults[SessionQueueItemDTO]},
+ 200: {"model": CursorPaginatedResults[SessionQueueItem]},
},
)
async def list_queue_items(
@@ -77,11 +76,38 @@ async def list_queue_items(
status: Optional[QUEUE_ITEM_STATUS] = Query(default=None, description="The status of items to fetch"),
cursor: Optional[int] = Query(default=None, description="The pagination cursor"),
priority: int = Query(default=0, description="The pagination cursor priority"),
-) -> CursorPaginatedResults[SessionQueueItemDTO]:
- """Gets all queue items (without graphs)"""
+ destination: Optional[str] = Query(default=None, description="The destination of queue items to fetch"),
+) -> CursorPaginatedResults[SessionQueueItem]:
+ """Gets cursor-paginated queue items"""
return ApiDependencies.invoker.services.session_queue.list_queue_items(
- queue_id=queue_id, limit=limit, status=status, cursor=cursor, priority=priority
+ queue_id=queue_id,
+ limit=limit,
+ status=status,
+ cursor=cursor,
+ priority=priority,
+ destination=destination,
+ )
+
+
+@session_queue_router.get(
+ "/{queue_id}/all",
+ operation_id="list_all_queue_items",
+ responses={
+ 200: {"model": list[SessionQueueItem]},
+ },
+)
+async def list_all_queue_items(
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ status: Optional[QUEUE_ITEM_STATUS] = Query(default=None, description="The status of items to fetch"),
+ destination: Optional[str] = Query(default=None, description="The destination of queue items to fetch"),
+) -> list[SessionQueueItem]:
+ """Gets all queue items"""
+
+ return ApiDependencies.invoker.services.session_queue.list_all_queue_items(
+ queue_id=queue_id,
+ status=status,
+ destination=destination,
)
diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py
index 39dac82e23..187fa2e815 100644
--- a/invokeai/app/services/session_queue/session_queue_base.py
+++ b/invokeai/app/services/session_queue/session_queue_base.py
@@ -17,7 +17,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
RetryItemsResult,
SessionQueueCountsByDestination,
SessionQueueItem,
- SessionQueueItemDTO,
SessionQueueStatus,
)
from invokeai.app.services.shared.graph import GraphExecutionState
@@ -127,10 +126,21 @@ class SessionQueueBase(ABC):
priority: int,
cursor: Optional[int] = None,
status: Optional[QUEUE_ITEM_STATUS] = None,
- ) -> CursorPaginatedResults[SessionQueueItemDTO]:
+ destination: Optional[str] = None,
+ ) -> CursorPaginatedResults[SessionQueueItem]:
"""Gets a page of session queue items"""
pass
+ @abstractmethod
+ def list_all_queue_items(
+ self,
+ queue_id: str,
+ status: Optional[QUEUE_ITEM_STATUS] = None,
+ destination: Optional[str] = None,
+ ) -> list[SessionQueueItem]:
+ """Gets all queue items that match the given parameters"""
+ pass
+
@abstractmethod
def get_queue_item(self, item_id: int) -> SessionQueueItem:
"""Gets a session queue item by ID"""
diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py
index fafb07b49f..1861110c97 100644
--- a/invokeai/app/services/session_queue/session_queue_common.py
+++ b/invokeai/app/services/session_queue/session_queue_common.py
@@ -208,7 +208,7 @@ class FieldIdentifier(BaseModel):
user_label: str | None = Field(description="The user label of the field, if any")
-class SessionQueueItemWithoutGraph(BaseModel):
+class SessionQueueItem(BaseModel):
"""Session queue item without the full graph. Used for serialization."""
item_id: int = Field(description="The identifier of the session queue item")
@@ -252,42 +252,7 @@ class SessionQueueItemWithoutGraph(BaseModel):
default=None,
description="The ID of the published workflow associated with this queue item",
)
- api_input_fields: Optional[list[FieldIdentifier]] = Field(
- default=None, description="The fields that were used as input to the API"
- )
- api_output_fields: Optional[list[FieldIdentifier]] = Field(
- default=None, description="The nodes that were used as output from the API"
- )
credits: Optional[float] = Field(default=None, description="The total credits used for this queue item")
-
- @classmethod
- def queue_item_dto_from_dict(cls, queue_item_dict: dict) -> "SessionQueueItemDTO":
- # must parse these manually
- queue_item_dict["field_values"] = get_field_values(queue_item_dict)
- return SessionQueueItemDTO(**queue_item_dict)
-
- model_config = ConfigDict(
- json_schema_extra={
- "required": [
- "item_id",
- "status",
- "batch_id",
- "queue_id",
- "session_id",
- "priority",
- "session_id",
- "created_at",
- "updated_at",
- ]
- }
- )
-
-
-class SessionQueueItemDTO(SessionQueueItemWithoutGraph):
- pass
-
-
-class SessionQueueItem(SessionQueueItemWithoutGraph):
session: GraphExecutionState = Field(description="The fully-populated session to be executed")
workflow: Optional[WorkflowWithoutID] = Field(
default=None, description="The workflow associated with this queue item"
diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py
index 3249441329..81239338ec 100644
--- a/invokeai/app/services/session_queue/session_queue_sqlite.py
+++ b/invokeai/app/services/session_queue/session_queue_sqlite.py
@@ -24,7 +24,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
RetryItemsResult,
SessionQueueCountsByDestination,
SessionQueueItem,
- SessionQueueItemDTO,
SessionQueueItemNotFoundError,
SessionQueueStatus,
ValueToInsertTuple,
@@ -540,26 +539,12 @@ class SqliteSessionQueue(SessionQueueBase):
priority: int,
cursor: Optional[int] = None,
status: Optional[QUEUE_ITEM_STATUS] = None,
- ) -> CursorPaginatedResults[SessionQueueItemDTO]:
+ destination: Optional[str] = None,
+ ) -> CursorPaginatedResults[SessionQueueItem]:
cursor_ = self._conn.cursor()
item_id = cursor
query = """--sql
- SELECT item_id,
- status,
- priority,
- field_values,
- error_type,
- error_message,
- error_traceback,
- created_at,
- updated_at,
- completed_at,
- started_at,
- session_id,
- batch_id,
- queue_id,
- origin,
- destination
+ SELECT *
FROM session_queue
WHERE queue_id = ?
"""
@@ -571,6 +556,12 @@ class SqliteSessionQueue(SessionQueueBase):
"""
params.append(status)
+ if destination is not None:
+ query += """---sql
+ AND destination = ?
+ """
+ params.append(destination)
+
if item_id is not None:
query += """--sql
AND (priority < ?) OR (priority = ? AND item_id > ?)
@@ -586,7 +577,7 @@ class SqliteSessionQueue(SessionQueueBase):
params.append(limit + 1)
cursor_.execute(query, params)
results = cast(list[sqlite3.Row], cursor_.fetchall())
- items = [SessionQueueItemDTO.queue_item_dto_from_dict(dict(result)) for result in results]
+ items = [SessionQueueItem.queue_item_from_dict(dict(result)) for result in results]
has_more = False
if len(items) > limit:
# remove the extra item
@@ -594,6 +585,44 @@ class SqliteSessionQueue(SessionQueueBase):
has_more = True
return CursorPaginatedResults(items=items, limit=limit, has_more=has_more)
+ def list_all_queue_items(
+ self,
+ queue_id: str,
+ status: Optional[QUEUE_ITEM_STATUS] = None,
+ destination: Optional[str] = None,
+ ) -> list[SessionQueueItem]:
+ """Gets all queue items that match the given parameters"""
+ cursor_ = self._conn.cursor()
+ query = """--sql
+ SELECT *
+ FROM session_queue
+ WHERE queue_id = ?
+ """
+ params: list[Union[str, int]] = [queue_id]
+
+ if status is not None:
+ query += """--sql
+ AND status = ?
+ """
+ params.append(status)
+
+ if destination is not None:
+ query += """---sql
+ AND destination = ?
+ """
+ params.append(destination)
+
+ query += """--sql
+ ORDER BY
+ priority DESC,
+ item_id ASC
+ ;
+ """
+ cursor_.execute(query, params)
+ results = cast(list[sqlite3.Row], cursor_.fetchall())
+ items = [SessionQueueItem.queue_item_from_dict(dict(result)) for result in results]
+ return items
+
def get_queue_status(self, queue_id: str) -> SessionQueueStatus:
cursor = self._conn.cursor()
cursor.execute(
diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py
index 9f22f2c6da..2706a0936a 100644
--- a/invokeai/app/services/shared/graph.py
+++ b/invokeai/app/services/shared/graph.py
@@ -7,6 +7,7 @@ from typing import Any, Optional, TypeVar, Union, get_args, get_origin, get_type
import networkx as nx
from pydantic import (
BaseModel,
+ ConfigDict,
GetCoreSchemaHandler,
GetJsonSchemaHandler,
ValidationError,
@@ -787,6 +788,22 @@ class GraphExecutionState(BaseModel):
default_factory=dict,
)
+ model_config = ConfigDict(
+ json_schema_extra={
+ "required": [
+ "id",
+ "graph",
+ "execution_graph",
+ "executed",
+ "executed_history",
+ "results",
+ "errors",
+ "prepared_source_mapping",
+ "source_prepared_mapping",
+ ]
+ }
+ )
+
@field_validator("graph")
def graph_is_valid(cls, v: Graph):
"""Validates that the graph is valid"""
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index 0a2f515334..a65416924d 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -3,7 +3,6 @@ import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/too
import { logger } from 'app/logging/logger';
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
-import { getDebugLoggerMiddleware } from 'app/store/middleware/debugLoggerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
@@ -176,7 +175,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
.concat(api.middleware)
.concat(dynamicMiddlewares)
.concat(authToastMiddleware)
- .concat(getDebugLoggerMiddleware())
+ // .concat(getDebugLoggerMiddleware())
.prepend(listenerMiddleware.middleware),
enhancers: (getDefaultEnhancers) => {
const _enhancers = getDefaultEnhancers().concat(autoBatchEnhancer());
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 7c61000b89..8f853f5d09 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,10 +1,14 @@
-import type { SystemStyleObject } from '@invoke-ai/ui-library';
+/* eslint-disable i18next/no-literal-string */
+import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
import {
Box,
Button,
ButtonGroup,
+ CircularProgress,
ContextMenu,
Flex,
+ FormControl,
+ FormLabel,
Heading,
IconButton,
Image,
@@ -12,9 +16,13 @@ import {
MenuButton,
MenuList,
Spacer,
+ Switch,
Text,
+ Tooltip,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppStore } from 'app/store/nanostores/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
@@ -49,15 +57,21 @@ import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { DndImage } from 'features/dnd/DndImage';
import { newCanvasFromImage } from 'features/imageActions/actions';
-import { memo, useCallback, useEffect, useMemo } from 'react';
+import type { ProgressImage } from 'features/nodes/types/common';
+import { isImageField } from 'features/nodes/types/common';
+import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
+import type { ChangeEvent } from 'react';
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Trans, useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
-import type { ImageDTO } from 'services/api/types';
+import { getImageDTOSafe, useGetImageDTOQuery } from 'services/api/endpoints/images';
+import { queueItemsAdapterSelectors, useListQueueItemsQuery } from 'services/api/endpoints/queue';
+import type { ImageDTO, S } from 'services/api/types';
import type { ProgressAndResult } from 'services/events/stores';
-import { $progressImages, useMapSelector } from 'services/events/stores';
+import { $progressImages, $socket, useMapSelector } from 'services/events/stores';
import type { Equals, Param0 } from 'tsafe';
-import { assert } from 'tsafe';
+import { assert, objectEntries } from 'tsafe';
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
@@ -311,20 +325,405 @@ const SimpleActiveSession = memo(() => {
- Generations from this Session
+ Generations
-
+
Start Over
-
-
+
);
});
SimpleActiveSession.displayName = 'SimpleActiveSession';
+const scrollIndicatorSx = {
+ opacity: 0,
+ '&[data-visible="true"]': {
+ opacity: 1,
+ },
+} satisfies SystemStyleObject;
+
+const StagingArea = memo(() => {
+ const [selectedItemId, setSelectedItemId] = useState(null);
+ const [autoSwitch, setAutoSwitch] = useState(true);
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
+ const [canScrollRight, setCanScrollRight] = useState(false);
+ const scrollableRef = useRef(null);
+ const { data } = useListQueueItemsQuery({ destination: 'canvas' });
+ const items = useMemo(() => {
+ if (!data) {
+ return EMPTY_ARRAY;
+ }
+ return queueItemsAdapterSelectors.selectAll(data);
+ }, [data]);
+ const selectedItem = useMemo(
+ () =>
+ data && selectedItemId !== null ? queueItemsAdapterSelectors.selectById(data, String(selectedItemId)) : null,
+ [data, selectedItemId]
+ );
+
+ useEffect(() => {
+ if (items.length === 0) {
+ setSelectedItemId(null);
+ return;
+ }
+ if (selectedItem === null && items.length > 0) {
+ setSelectedItemId(items[0]?.item_id ?? null);
+ return;
+ }
+ if (selectedItemId === null || items.find((item) => item.item_id === selectedItemId) === undefined) {
+ return;
+ }
+ document.getElementById(`queue-item-status-card-${selectedItemId}`)?.scrollIntoView();
+ }, [items, selectedItem, selectedItemId]);
+
+ useEffect(() => {
+ const el = scrollableRef.current;
+ if (!el) {
+ return;
+ }
+ const onScroll = () => {
+ const { scrollLeft, scrollWidth, clientWidth } = el;
+ setCanScrollLeft(scrollLeft > 0);
+ setCanScrollRight(scrollLeft + clientWidth < scrollWidth);
+ };
+ el.addEventListener('scroll', onScroll);
+ const observer = new ResizeObserver(onScroll);
+ observer.observe(el);
+ return () => {
+ el.removeEventListener('scroll', onScroll);
+ observer.disconnect();
+ };
+ }, []);
+
+ const onSelectItem = useCallback((item: S['SessionQueueItem']) => {
+ setSelectedItemId(item.item_id);
+ if (item.status !== 'in_progress') {
+ setAutoSwitch(false);
+ }
+ }, []);
+
+ const onNext = useCallback(() => {
+ if (selectedItemId === null) {
+ return;
+ }
+ const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
+ const nextIndex = (currentIndex + 1) % items.length;
+ const nextItem = items[nextIndex];
+ if (!nextItem) {
+ return;
+ }
+ setSelectedItemId(nextItem.item_id);
+ }, [items, selectedItemId]);
+ const onPrev = useCallback(() => {
+ if (selectedItemId === null) {
+ return;
+ }
+ const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
+ const prevItem = items[prevIndex];
+ if (!prevItem) {
+ return;
+ }
+ setSelectedItemId(prevItem.item_id);
+ }, [items, selectedItemId]);
+
+ useHotkeys('left', onPrev);
+ useHotkeys('right', onNext);
+
+ const socket = useStore($socket);
+ useEffect(() => {
+ if (!autoSwitch) {
+ return;
+ }
+
+ if (!socket) {
+ return;
+ }
+
+ const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
+ if (data.destination !== 'canvas') {
+ return;
+ }
+ if (data.status === 'in_progress') {
+ setSelectedItemId(data.item_id);
+ }
+ };
+
+ socket.on('queue_item_status_changed', onQueueItemStatusChanged);
+
+ return () => {
+ socket.off('queue_item_status_changed', onQueueItemStatusChanged);
+ };
+ }, [autoSwitch, socket]);
+
+ const onChangeAutoSwitch = useCallback((e: ChangeEvent) => {
+ setAutoSwitch(e.target.checked);
+ }, []);
+
+ return (
+
+
+ {selectedItem && }
+ {!selectedItem && No queued generations}
+
+
+ Auto-switch
+
+
+
+
+ {items.map((item, i) => (
+
+ ))}
+
+
+
+
+
+ );
+});
+StagingArea.displayName = 'StagingArea';
+
+const IMAGE_DTO_ERROR = Symbol('IMAGE_DTO_ERROR');
+
+const useOutputImageDTO = (item: S['SessionQueueItem']) => {
+ const [imageDTO, setImageDTO] = useState(null);
+ const syncImageDTO = useCallback(async (item: S['SessionQueueItem']) => {
+ const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
+ isCanvasOutputNodeId(nodeId)
+ )?.[1][0];
+ const output = nodeId ? item.session.results[nodeId] : undefined;
+
+ if (!output) {
+ return setImageDTO(null);
+ }
+
+ for (const [_name, value] of objectEntries(output)) {
+ if (isImageField(value)) {
+ const imageDTO = await getImageDTOSafe(value.image_name);
+ if (imageDTO) {
+ setImageDTO(imageDTO);
+ $progressImages.setKey(item.session_id, undefined);
+ return;
+ }
+ }
+ }
+
+ setImageDTO(IMAGE_DTO_ERROR);
+ }, []);
+ useEffect(() => {
+ syncImageDTO(item);
+ }, [item, syncImageDTO]);
+
+ return imageDTO;
+};
+
+const QueueItemStatusCard = memo(
+ ({
+ item,
+ isSelected,
+ number,
+ onSelectItem,
+ ...rest
+ }: {
+ item: S['SessionQueueItem'];
+ isSelected: boolean;
+ number?: number;
+ onSelectItem?: (item: S['SessionQueueItem']) => void;
+ } & FlexProps) => {
+ const onClick = useCallback(() => {
+ onSelectItem?.(item);
+ }, [item, onSelectItem]);
+ return (
+
+
+ {number !== undefined && {`#${number}`}}
+
+ );
+ }
+);
+QueueItemStatusCard.displayName = 'QueueItemStatusCard';
+
+const QueueItemStatusCardContent = memo(({ item }: { item: S['SessionQueueItem'] }) => {
+ const socket = useStore($socket);
+ const [progressEvent, setProgressEvent] = useState(null);
+ const [progressImage, setProgressImage] = useState(null);
+ useEffect(() => {
+ if (!socket) {
+ return;
+ }
+ const onProgress = (data: S['InvocationProgressEvent']) => {
+ if (data.session_id !== item.session_id) {
+ return;
+ }
+ setProgressEvent(data);
+ if (data.image) {
+ setProgressImage(data.image);
+ }
+ };
+ socket.on('invocation_progress', onProgress);
+
+ return () => {
+ socket.off('invocation_progress', onProgress);
+ };
+ }, [item.session_id, socket]);
+
+ const imageDTO = useOutputImageDTO(item);
+
+ if (item.status === 'pending') {
+ return (
+
+ Pending
+
+ );
+ }
+ if (item.status === 'canceled') {
+ return (
+
+ Canceled
+
+ );
+ }
+ if (item.status === 'failed') {
+ return (
+
+ Failed
+
+ );
+ }
+ if (item.status === 'in_progress' || !imageDTO) {
+ if (!progressImage) {
+ return (
+ <>
+
+ In Progress
+
+
+ >
+ );
+ }
+ return (
+ <>
+
+
+ >
+ );
+ }
+ if (item.status === 'completed' && imageDTO && imageDTO !== IMAGE_DTO_ERROR) {
+ return ;
+ }
+
+ if (item.status === 'completed') {
+ return (
+
+ Unable to get image
+
+ );
+ }
+ assert>(false);
+});
+QueueItemStatusCardContent.displayName = 'QueueItemStatusCardContent';
+
+const circleStyles: SystemStyleObject = {
+ circle: {
+ transitionProperty: 'none',
+ transitionDuration: '0s',
+ },
+ position: 'absolute',
+ top: 2,
+ right: 2,
+};
+
+const ProgressCircle = ({ data }: { data?: S['InvocationProgressEvent'] | null }) => {
+ return (
+
+
+
+ );
+};
+ProgressCircle.displayName = 'ProgressCircle';
+
+const QueueItemResultCard = memo(({ item }: { item: S['SessionQueueItem'] }) => {
+ const imageName = useMemo(() => {
+ const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
+ isCanvasOutputNodeId(nodeId)
+ )?.[1][0];
+ const output = nodeId ? item.session.results[nodeId] : undefined;
+ if (!output) {
+ return;
+ }
+
+ for (const [_name, value] of objectEntries(output)) {
+ if (isImageField(value)) {
+ return value.image_name;
+ }
+ }
+ }, [item]);
+
+ const { data: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
+
+ if (!imageDTO) {
+ return Unknown output type;
+ }
+
+ return ;
+});
+QueueItemResultCard.displayName = 'QueueItemResultCard';
+
const SelectedImageOrProgressImage = memo(() => {
const selectedImage = useAppSelector(selectSelectedImage);
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 dcce4be8b2..ce87203eeb 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -159,6 +159,8 @@ export const isMainModelWithoutUnet = (modelLoader: Invocation nodeId.split(':')[0] === CANVAS_OUTPUT_PREFIX;
+
export const isCanvasOutputEvent = (data: S['InvocationCompleteEvent']) => {
- return data.invocation_source_id.split(':')[0] === CANVAS_OUTPUT_PREFIX;
+ return isCanvasOutputNodeId(data.invocation_source_id);
};
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx
index 2b80dae71f..d645dcbfe5 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx
@@ -13,7 +13,7 @@ import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiXBold } from 'react-icons/pi';
import { useSelector } from 'react-redux';
-import type { SessionQueueItemDTO } from 'services/api/types';
+import type { S } from 'services/api/types';
import { COLUMN_WIDTHS } from './constants';
import QueueItemDetail from './QueueItemDetail';
@@ -23,7 +23,7 @@ const selectedStyles = { bg: 'base.700' };
type InnerItemProps = {
index: number;
- item: SessionQueueItemDTO;
+ item: S['SessionQueueItem'];
context: ListContext;
};
@@ -155,7 +155,7 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
-
+
);
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx
index 1068f51a14..a06e82ac5b 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx
@@ -12,24 +12,20 @@ import type { ReactNode } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiXBold } from 'react-icons/pi';
-import { useGetQueueItemQuery } from 'services/api/endpoints/queue';
-import type { SessionQueueItemDTO } from 'services/api/types';
+import type { S } from 'services/api/types';
type Props = {
- queueItemDTO: SessionQueueItemDTO;
+ queueItem: S['SessionQueueItem'];
};
-const QueueItemComponent = ({ queueItemDTO }: Props) => {
- const { session_id, batch_id, item_id, origin, destination } = queueItemDTO;
+const QueueItemComponent = ({ queueItem }: Props) => {
+ const { session_id, batch_id, item_id, origin, destination } = queueItem;
const { t } = useTranslation();
const isRetryEnabled = useFeatureStatus('retryQueueItem');
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled: isBatchCanceled } = useCancelBatch(batch_id);
-
const { cancelQueueItem, isLoading: isLoadingCancelQueueItem } = useCancelQueueItem(item_id);
const { retryQueueItem, isLoading: isLoadingRetryQueueItem } = useRetryQueueItem(item_id);
- const { data: queueItem } = useGetQueueItemQuery(item_id);
-
const originText = useOriginText(origin);
const destinationText = useDestinationText(destination);
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx
index c1af4436f2..06137aa409 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx
@@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next';
import type { Components, ItemContent } from 'react-virtuoso';
import { Virtuoso } from 'react-virtuoso';
import { queueItemsAdapterSelectors, useListQueueItemsQuery } from 'services/api/endpoints/queue';
-import type { SessionQueueItemDTO } from 'services/api/types';
+import type { S } from 'services/api/types';
import QueueItemComponent from './QueueItemComponent';
import QueueListComponent from './QueueListComponent';
@@ -24,13 +24,13 @@ import type { ListContext } from './types';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TableVirtuosoScrollerRef = (ref: HTMLElement | Window | null) => any;
-const computeItemKey = (index: number, item: SessionQueueItemDTO): number => item.item_id;
+const computeItemKey = (index: number, item: S['SessionQueueItem']): number => item.item_id;
-const components: Components = {
+const components: Components = {
List: QueueListComponent,
};
-const itemContent: ItemContent = (index, item, context) => (
+const itemContent: ItemContent = (index, item, context) => (
);
@@ -114,7 +114,7 @@ const QueueList = () => {
-
+
data={queueItems}
endReached={handleLoadMore}
scrollerRef={setScroller as TableVirtuosoScrollerRef}
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueListComponent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueListComponent.tsx
index f9ac99302e..932ef0d6d5 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueListComponent.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueListComponent.tsx
@@ -1,10 +1,10 @@
import { Flex, forwardRef, typedMemo } from '@invoke-ai/ui-library';
import type { Components } from 'react-virtuoso';
-import type { SessionQueueItemDTO } from 'services/api/types';
+import type { S } from 'services/api/types';
import type { ListContext } from './types';
-const QueueListComponent: Components['List'] = typedMemo(
+const QueueListComponent: Components['List'] = typedMemo(
forwardRef((props, ref) => {
return (
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/useDestinationText.ts b/invokeai/frontend/web/src/features/queue/components/QueueList/useDestinationText.ts
index e72d1d709c..ab8ea27de8 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueList/useDestinationText.ts
+++ b/invokeai/frontend/web/src/features/queue/components/QueueList/useDestinationText.ts
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
-import type { SessionQueueItemDTO } from 'services/api/types';
+import type { S } from 'services/api/types';
-export const useDestinationText = (destination: SessionQueueItemDTO['destination']) => {
+export const useDestinationText = (destination: S['SessionQueueItem']['destination']) => {
const { t } = useTranslation();
if (destination === 'canvas') {
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/useOriginText.ts b/invokeai/frontend/web/src/features/queue/components/QueueList/useOriginText.ts
index 9b3ac80272..deefbd08f4 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueList/useOriginText.ts
+++ b/invokeai/frontend/web/src/features/queue/components/QueueList/useOriginText.ts
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
-import type { SessionQueueItemDTO } from 'services/api/types';
+import type { S } from 'services/api/types';
-export const useOriginText = (origin: SessionQueueItemDTO['origin']) => {
+export const useOriginText = (origin: S['SessionQueueItem']['origin']) => {
const { t } = useTranslation();
if (origin === 'generation') {
diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
index 05906c2e56..fded3e8f09 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
@@ -5,9 +5,10 @@ import { $queueId } from 'app/store/nanostores/queueId';
import { listParamsReset } from 'features/queue/store/queueSlice';
import queryString from 'query-string';
import type { components, paths } from 'services/api/schema';
+import type { S } from 'services/api/types';
import type { ApiTagDescription } from '..';
-import { api, buildV1Url } from '..';
+import { api, buildV1Url, LIST_TAG } from '..';
/**
* Builds an endpoint URL for the queue router
@@ -35,7 +36,7 @@ export type SessionQueueItemStatus = NonNullable<
NonNullable['status']
>;
-export const queueItemsAdapter = createEntityAdapter({
+export const queueItemsAdapter = createEntityAdapter({
selectId: (queueItem) => String(queueItem.item_id),
sortComparer: (a, b) => {
// Sort by priority in descending order
@@ -388,10 +389,10 @@ export const queueApi = api.injectEndpoints({
invalidatesTags: ['CurrentSessionQueueItem', 'NextSessionQueueItem', 'QueueCountsByDestination'],
}),
listQueueItems: build.query<
- EntityState & {
+ EntityState & {
has_more: boolean;
},
- { cursor?: number; priority?: number } | undefined
+ { cursor?: number; priority?: number; destination?: string } | undefined
>({
query: (queryArgs) => ({
url: getListQueueItemsUrl(queryArgs),
@@ -400,20 +401,20 @@ export const queueApi = api.injectEndpoints({
serializeQueryArgs: () => {
return buildQueueUrl('list');
},
- transformResponse: (response: components['schemas']['CursorPaginatedResults_SessionQueueItemDTO_']) =>
- queueItemsAdapter.addMany(
+ transformResponse: (response: components['schemas']['CursorPaginatedResults_SessionQueueItem_']) =>
+ queueItemsAdapter.upsertMany(
queueItemsAdapter.getInitialState({
has_more: response.has_more,
}),
response.items
),
merge: (cache, response) => {
- queueItemsAdapter.addMany(cache, queueItemsAdapterSelectors.selectAll(response));
+ queueItemsAdapter.upsertMany(cache, queueItemsAdapterSelectors.selectAll(response));
cache.has_more = response.has_more;
},
forceRefetch: ({ currentArg, previousArg }) => currentArg !== previousArg,
keepUnusedDataFor: 60 * 5, // 5 minutes
- providesTags: ['FetchOnReconnect'],
+ providesTags: ['FetchOnReconnect', { type: 'SessionQueueItem', id: LIST_TAG }],
}),
getQueueCountsByDestination: build.query<
paths['/api/v1/queue/{queue_id}/counts_by_destination']['get']['responses']['200']['content']['application/json'],
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index 27c7c659e3..5d10535e66 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -1153,7 +1153,7 @@ export type paths = {
};
/**
* List Queue Items
- * @description Gets all queue items (without graphs)
+ * @description Gets cursor-paginated queue items
*/
get: operations["list_queue_items"];
put?: never;
@@ -1164,6 +1164,26 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/queue/{queue_id}/all": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List All Queue Items
+ * @description Gets all queue items
+ */
+ get: operations["list_all_queue_items"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/queue/{queue_id}/processor/resume": {
parameters: {
query?: never;
@@ -5715,8 +5735,8 @@ export type components = {
*/
type: "crop_latents";
};
- /** CursorPaginatedResults[SessionQueueItemDTO] */
- CursorPaginatedResults_SessionQueueItemDTO_: {
+ /** CursorPaginatedResults[SessionQueueItem] */
+ CursorPaginatedResults_SessionQueueItem_: {
/**
* Limit
* @description Limit of items to get
@@ -5731,7 +5751,7 @@ export type components = {
* Items
* @description Items
*/
- items: components["schemas"]["SessionQueueItemDTO"][];
+ items: components["schemas"]["SessionQueueItem"][];
};
/**
* OpenCV Inpaint
@@ -8742,47 +8762,47 @@ export type components = {
* Id
* @description The id of the execution state
*/
- id?: string;
+ id: string;
/** @description The graph being executed */
graph: components["schemas"]["Graph"];
/** @description The expanded graph of activated and executed nodes */
- execution_graph?: components["schemas"]["Graph"];
+ execution_graph: components["schemas"]["Graph"];
/**
* Executed
* @description The set of node ids that have been executed
*/
- executed?: string[];
+ executed: string[];
/**
* Executed History
* @description The list of node ids that have been executed, in order of execution
*/
- executed_history?: string[];
+ executed_history: string[];
/**
* Results
* @description The results of node executions
*/
- results?: {
+ results: {
[key: string]: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CogView4ConditioningOutput"] | components["schemas"]["CogView4ModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatGeneratorOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxControlLoRALoaderOutput"] | components["schemas"]["FluxControlNetOutput"] | components["schemas"]["FluxFillOutput"] | components["schemas"]["FluxLoRALoaderOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["FluxReduxOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageGeneratorOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImagePanelCoordinateOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerGeneratorOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsMetaOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MDControlListOutput"] | components["schemas"]["MDIPAdapterListOutput"] | components["schemas"]["MDT2IAdapterListOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["MetadataToLorasCollectionOutput"] | components["schemas"]["MetadataToModelOutput"] | components["schemas"]["MetadataToSDXLModelOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["SD3ConditioningOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["Sd3ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringGeneratorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"];
};
/**
* Errors
* @description Errors raised when executing nodes
*/
- errors?: {
+ errors: {
[key: string]: string;
};
/**
* Prepared Source Mapping
* @description The map of prepared nodes to original graph nodes
*/
- prepared_source_mapping?: {
+ prepared_source_mapping: {
[key: string]: string;
};
/**
* Source Prepared Mapping
* @description The map of original graph nodes to prepared nodes
*/
- source_prepared_mapping?: {
+ source_prepared_mapping: {
[key: string]: string[];
};
};
@@ -19117,7 +19137,10 @@ export type components = {
*/
total: number;
};
- /** SessionQueueItem */
+ /**
+ * SessionQueueItem
+ * @description Session queue item without the full graph. Used for serialization.
+ */
SessionQueueItem: {
/**
* Item Id
@@ -19218,16 +19241,6 @@ export type components = {
* @description The ID of the published workflow associated with this queue item
*/
published_workflow_id?: string | null;
- /**
- * Api Input Fields
- * @description The fields that were used as input to the API
- */
- api_input_fields?: components["schemas"]["FieldIdentifier"][] | null;
- /**
- * Api Output Fields
- * @description The nodes that were used as output from the API
- */
- api_output_fields?: components["schemas"]["FieldIdentifier"][] | null;
/**
* Credits
* @description The total credits used for this queue item
@@ -19238,123 +19251,6 @@ export type components = {
/** @description The workflow associated with this queue item */
workflow?: components["schemas"]["WorkflowWithoutID"] | null;
};
- /** SessionQueueItemDTO */
- SessionQueueItemDTO: {
- /**
- * Item Id
- * @description The identifier of the session queue item
- */
- item_id: number;
- /**
- * Status
- * @description The status of this queue item
- * @default pending
- * @enum {string}
- */
- status: "pending" | "in_progress" | "completed" | "failed" | "canceled";
- /**
- * Priority
- * @description The priority of this queue item
- * @default 0
- */
- priority: number;
- /**
- * Batch Id
- * @description The ID of the batch associated with this queue item
- */
- batch_id: string;
- /**
- * Origin
- * @description The origin of this queue item. This data is used by the frontend to determine how to handle results.
- */
- origin?: string | null;
- /**
- * Destination
- * @description The origin of this queue item. This data is used by the frontend to determine how to handle results
- */
- destination?: string | null;
- /**
- * Session Id
- * @description The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed.
- */
- session_id: string;
- /**
- * Error Type
- * @description The error type if this queue item errored
- */
- error_type?: string | null;
- /**
- * Error Message
- * @description The error message if this queue item errored
- */
- error_message?: string | null;
- /**
- * Error Traceback
- * @description The error traceback if this queue item errored
- */
- error_traceback?: string | null;
- /**
- * Created At
- * @description When this queue item was created
- */
- created_at: string;
- /**
- * Updated At
- * @description When this queue item was updated
- */
- updated_at: string;
- /**
- * Started At
- * @description When this queue item was started
- */
- started_at?: string | null;
- /**
- * Completed At
- * @description When this queue item was completed
- */
- completed_at?: string | null;
- /**
- * Queue Id
- * @description The id of the queue with which this item is associated
- */
- queue_id: string;
- /**
- * Field Values
- * @description The field values that were used for this queue item
- */
- field_values?: components["schemas"]["NodeFieldValue"][] | null;
- /**
- * Retried From Item Id
- * @description The item_id of the queue item that this item was retried from
- */
- retried_from_item_id?: number | null;
- /**
- * Is Api Validation Run
- * @description Whether this queue item is an API validation run.
- * @default false
- */
- is_api_validation_run?: boolean;
- /**
- * Published Workflow Id
- * @description The ID of the published workflow associated with this queue item
- */
- published_workflow_id?: string | null;
- /**
- * Api Input Fields
- * @description The fields that were used as input to the API
- */
- api_input_fields?: components["schemas"]["FieldIdentifier"][] | null;
- /**
- * Api Output Fields
- * @description The nodes that were used as output from the API
- */
- api_output_fields?: components["schemas"]["FieldIdentifier"][] | null;
- /**
- * Credits
- * @description The total credits used for this queue item
- */
- credits?: number | null;
- };
/** SessionQueueStatus */
SessionQueueStatus: {
/**
@@ -24476,6 +24372,8 @@ export interface operations {
cursor?: number | null;
/** @description The pagination cursor priority */
priority?: number;
+ /** @description The destination of queue items to fetch */
+ destination?: string | null;
};
header?: never;
path: {
@@ -24492,7 +24390,44 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CursorPaginatedResults_SessionQueueItemDTO_"];
+ "application/json": components["schemas"]["CursorPaginatedResults_SessionQueueItem_"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ list_all_queue_items: {
+ parameters: {
+ query?: {
+ /** @description The status of items to fetch */
+ status?: ("pending" | "in_progress" | "completed" | "failed" | "canceled") | null;
+ /** @description The destination of queue items to fetch */
+ destination?: string | null;
+ };
+ header?: never;
+ path: {
+ /** @description The queue id to perform this operation on */
+ queue_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SessionQueueItem"][];
};
};
/** @description Validation Error */
diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts
index 086ff3296f..5b7c234f1d 100644
--- a/invokeai/frontend/web/src/services/api/types.ts
+++ b/invokeai/frontend/web/src/services/api/types.ts
@@ -291,7 +291,6 @@ export type ModelInstallStatus = S['InstallStatus'];
export type Graph = S['Graph'];
export type NonNullableGraph = SetRequired;
export type Batch = S['Batch'];
-export type SessionQueueItemDTO = S['SessionQueueItemDTO'];
export type WorkflowRecordOrderBy = S['WorkflowRecordOrderBy'];
export type SQLiteDirection = S['SQLiteDirection'];
export type WorkflowRecordListItemWithThumbnailDTO = S['WorkflowRecordListItemWithThumbnailDTO'];
diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
index 1a873f5d43..357b86c7de 100644
--- a/invokeai/frontend/web/src/services/events/setEventListeners.tsx
+++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
@@ -8,9 +8,7 @@ import { $bulkDownloadId } from 'app/store/nanostores/bulkDownloadId';
import { $queueId } from 'app/store/nanostores/queueId';
import type { AppStore } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import {
- stagingAreaGenerationStarted,
-} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { stagingAreaGenerationStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
$isInPublishFlow,
$outputNodeId,
@@ -25,7 +23,7 @@ import { forEach, isNil, round } from 'lodash-es';
import type { ApiTagDescription } from 'services/api';
import { api, LIST_TAG } from 'services/api';
import { modelsApi } from 'services/api/endpoints/models';
-import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
+import { queueApi } from 'services/api/endpoints/queue';
import { workflowsApi } from 'services/api/endpoints/workflows';
import { buildOnInvocationComplete } from 'services/events/onInvocationComplete';
import { buildOnModelInstallError } from 'services/events/onModelInstallError';
@@ -383,24 +381,24 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
log.debug({ data }, `Queue item ${item_id} status updated: ${status}`);
- // Update this specific queue item in the list of queue items (this is the queue item DTO, without the session)
- dispatch(
- queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => {
- queueItemsAdapter.updateOne(draft, {
- id: String(item_id),
- changes: {
- status,
- started_at,
- updated_at: updated_at ?? undefined,
- completed_at: completed_at ?? undefined,
- error_type,
- error_message,
- error_traceback,
- credits,
- },
- });
- })
- );
+ // // Update this specific queue item in the list of queue items (this is the queue item DTO, without the session)
+ // dispatch(
+ // queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => {
+ // queueItemsAdapter.updateOne(draft, {
+ // id: String(item_id),
+ // changes: {
+ // status,
+ // started_at,
+ // updated_at: updated_at ?? undefined,
+ // completed_at: completed_at ?? undefined,
+ // error_type,
+ // error_message,
+ // error_traceback,
+ // credits,
+ // },
+ // });
+ // })
+ // );
// Optimistic update of the queue status. We prefer to do an optimistic update over tag invalidation due to the
// frequency of `queue_item_status_changed` events.
@@ -426,6 +424,7 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
'NextSessionQueueItem',
'InvocationCacheStatus',
{ type: 'SessionQueueItem', id: item_id },
+ { type: 'SessionQueueItem', id: LIST_TAG },
];
if (destination) {
tagsToInvalidate.push({ type: 'QueueCountsByDestination', id: destination });
From c9042e52d4981366627e7d297544eaf332f55ac3 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 3 Jun 2025 14:49:00 +1000
Subject: [PATCH 029/210] feat: canvas flow rework (wip)
---
invokeai/app/api/routers/session_queue.py | 2 +-
.../components/CanvasMainPanelContent.tsx | 15 +-
.../queue/components/QueueList/QueueList.tsx | 4 +-
.../web/src/services/api/endpoints/queue.ts | 272 +++++-------------
.../frontend/web/src/services/api/index.ts | 1 +
.../frontend/web/src/services/api/schema.ts | 2 +-
.../src/services/events/setEventListeners.tsx | 58 +---
7 files changed, 94 insertions(+), 260 deletions(-)
diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py
index d21610fed3..7d6b8ac93c 100644
--- a/invokeai/app/api/routers/session_queue.py
+++ b/invokeai/app/api/routers/session_queue.py
@@ -91,7 +91,7 @@ async def list_queue_items(
@session_queue_router.get(
- "/{queue_id}/all",
+ "/{queue_id}/list_all",
operation_id="list_all_queue_items",
responses={
200: {"model": list[SessionQueueItem]},
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 8f853f5d09..ba3f3c2a7f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -66,7 +66,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { Trans, useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
import { getImageDTOSafe, useGetImageDTOQuery } from 'services/api/endpoints/images';
-import { queueItemsAdapterSelectors, useListQueueItemsQuery } from 'services/api/endpoints/queue';
+import { useListAllQueueItemsQuery } from 'services/api/endpoints/queue';
import type { ImageDTO, S } from 'services/api/types';
import type { ProgressAndResult } from 'services/events/stores';
import { $progressImages, $socket, useMapSelector } from 'services/events/stores';
@@ -351,17 +351,12 @@ const StagingArea = memo(() => {
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const scrollableRef = useRef(null);
- const { data } = useListQueueItemsQuery({ destination: 'canvas' });
- const items = useMemo(() => {
- if (!data) {
- return EMPTY_ARRAY;
- }
- return queueItemsAdapterSelectors.selectAll(data);
- }, [data]);
+ const { data } = useListAllQueueItemsQuery({ destination: 'canvas' });
+ const items = useMemo(() => data?.filter(({ status }) => status !== 'canceled') ?? EMPTY_ARRAY, [data]);
const selectedItem = useMemo(
() =>
- data && selectedItemId !== null ? queueItemsAdapterSelectors.selectById(data, String(selectedItemId)) : null,
- [data, selectedItemId]
+ items.length > 0 && selectedItemId !== null ? items.find(({ item_id }) => item_id === selectedItemId) : null,
+ [items, selectedItemId]
);
useEffect(() => {
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx
index 06137aa409..e812324335 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx
@@ -13,7 +13,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { Components, ItemContent } from 'react-virtuoso';
import { Virtuoso } from 'react-virtuoso';
-import { queueItemsAdapterSelectors, useListQueueItemsQuery } from 'services/api/endpoints/queue';
+import { useListQueueItemsQuery } from 'services/api/endpoints/queue';
import type { S } from 'services/api/types';
import QueueItemComponent from './QueueItemComponent';
@@ -70,7 +70,7 @@ const QueueList = () => {
if (!listQueueItemsData) {
return [];
}
- return queueItemsAdapterSelectors.selectAll(listQueueItemsData);
+ return listQueueItemsData.items;
}, [listQueueItemsData]);
const handleLoadMore = useCallback(() => {
diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
index fded3e8f09..38f11b9928 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
@@ -1,14 +1,9 @@
-import type { EntityState, ThunkDispatch, UnknownAction } from '@reduxjs/toolkit';
-import { createEntityAdapter } from '@reduxjs/toolkit';
-import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
import { $queueId } from 'app/store/nanostores/queueId';
-import { listParamsReset } from 'features/queue/store/queueSlice';
import queryString from 'query-string';
import type { components, paths } from 'services/api/schema';
-import type { S } from 'services/api/types';
import type { ApiTagDescription } from '..';
-import { api, buildV1Url, LIST_TAG } from '..';
+import { api, buildV1Url, LIST_ALL_TAG, LIST_TAG } from '..';
/**
* Builds an endpoint URL for the queue router
@@ -36,30 +31,6 @@ export type SessionQueueItemStatus = NonNullable<
NonNullable['status']
>;
-export const queueItemsAdapter = createEntityAdapter({
- selectId: (queueItem) => String(queueItem.item_id),
- sortComparer: (a, b) => {
- // Sort by priority in descending order
- if (a.priority > b.priority) {
- return -1;
- }
- if (a.priority < b.priority) {
- return 1;
- }
-
- // If priority is the same, sort by id in ascending order
- if (a.item_id < b.item_id) {
- return -1;
- }
- if (a.item_id > b.item_id) {
- return 1;
- }
-
- return 0;
- },
-});
-export const queueItemsAdapterSelectors = queueItemsAdapter.getSelectors(undefined, getSelectorsOptions);
-
export const queueApi = api.injectEndpoints({
endpoints: (build) => ({
enqueueBatch: build.mutation<
@@ -71,58 +42,14 @@ export const queueApi = api.injectEndpoints({
body: arg,
method: 'POST',
}),
- invalidatesTags: ['CurrentSessionQueueItem', 'NextSessionQueueItem', 'QueueCountsByDestination'],
- onQueryStarted: async (arg, api) => {
- const { dispatch, queryFulfilled } = api;
- try {
- const { data } = await queryFulfilled;
- resetListQueryData(dispatch);
- /**
- * When a batch is enqueued, we need to update the queue status. While it might be templting to invalidate the
- * `SessionQueueStatus` tag here, this can introduce a race condition when the queue item executes quickly:
- *
- * - Enqueue via this query
- * - On success, we invalidate `SessionQueueStatus` tag - network request sent to server
- * - The server gets the queue status request and responds, but this takes some time... in the meantime:
- * - The new queue item starts executing, and we receive a socket queue item status changed event
- * - We optimistically update the queue status in the queue item status changed socket handler
- * - At this point, the queue status is correct
- * - Finally, we get the queue status from the tag invalidation request - but it's reporting the queue status
- * from _before_ the last queue event
- * - The queue status is now incorrect!
- *
- * Ok, what if we just never did optimistic updates and invalidated the tag in the queue event handlers instead?
- * It's much simpler that way, but it causes a lot of network requests - 3 per queue item, as it moves from
- * pending -> in_progress -> completed/failed/canceled.
- *
- * We can do a bit of extra work here, incrementing the pending and total counts in the queue status, and do
- * similar optimistic updates in the socket handler. Because this optimistic update runs immediately after the
- * enqueue network request, it should always occur _before_ the next queue event, so no race condition:
- *
- * - Enqueue batch via this query
- * - On success, optimistically update - this happens immediately on the HTTP OK - before the next queue event
- * - At this point, the queue status is correct
- * - A queue item status changes and we receive a socket event w/ updated status
- * - Update status optimistically in socket handler
- * - Queue status is still correct
- *
- * This problem occurs most commonly with canvas filters like Canny edge detection, which are single-node
- * graphs that execute very quickly. Image generation graphs take long enough to not trigger this race
- * condition - even when all nodes are cached on the server.
- */
- dispatch(
- queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => {
- if (!draft) {
- return;
- }
- draft.queue.pending += data.enqueued;
- draft.queue.total += data.enqueued;
- })
- );
- } catch {
- // no-op
- }
- },
+ invalidatesTags: [
+ 'SessionQueueStatus',
+ 'CurrentSessionQueueItem',
+ 'NextSessionQueueItem',
+ 'QueueCountsByDestination',
+ { type: 'SessionQueueItem', id: LIST_TAG },
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
+ ],
}),
resumeProcessor: build.mutation<
paths['/api/v1/queue/{queue_id}/processor/resume']['put']['responses']['200']['content']['application/json'],
@@ -152,16 +79,7 @@ export const queueApi = api.injectEndpoints({
url: buildQueueUrl('prune'),
method: 'PUT',
}),
- invalidatesTags: ['SessionQueueStatus', 'BatchStatus'],
- onQueryStarted: async (arg, api) => {
- const { dispatch, queryFulfilled } = api;
- try {
- await queryFulfilled;
- resetListQueryData(dispatch);
- } catch {
- // no-op
- }
- },
+ invalidatesTags: ['SessionQueueStatus', 'BatchStatus', { type: 'SessionQueueItem', id: LIST_TAG }],
}),
clearQueue: build.mutation<
paths['/api/v1/queue/{queue_id}/clear']['put']['responses']['200']['content']['application/json'],
@@ -178,16 +96,9 @@ export const queueApi = api.injectEndpoints({
'CurrentSessionQueueItem',
'NextSessionQueueItem',
'QueueCountsByDestination',
+ { type: 'SessionQueueItem', id: LIST_TAG },
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
],
- onQueryStarted: async (arg, api) => {
- const { dispatch, queryFulfilled } = api;
- try {
- await queryFulfilled;
- resetListQueryData(dispatch);
- } catch {
- // no-op
- }
- },
}),
getCurrentQueueItem: build.query<
paths['/api/v1/queue/{queue_id}/current']['get']['responses']['200']['content']['application/json'],
@@ -271,25 +182,6 @@ export const queueApi = api.injectEndpoints({
url: buildQueueUrl(`i/${item_id}/cancel`),
method: 'PUT',
}),
- onQueryStarted: async (item_id, { dispatch, queryFulfilled }) => {
- try {
- const { data } = await queryFulfilled;
- dispatch(
- queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => {
- queueItemsAdapter.updateOne(draft, {
- id: String(item_id),
- changes: {
- status: data.status,
- completed_at: data.completed_at,
- updated_at: data.updated_at,
- },
- });
- })
- );
- } catch {
- // no-op
- }
- },
invalidatesTags: (result) => {
if (!result) {
return [];
@@ -313,16 +205,19 @@ export const queueApi = api.injectEndpoints({
method: 'PUT',
body,
}),
- onQueryStarted: async (arg, api) => {
- const { dispatch, queryFulfilled } = api;
- try {
- await queryFulfilled;
- resetListQueryData(dispatch);
- } catch {
- // no-op
+ invalidatesTags: (result, error, { batch_ids }) => {
+ if (!result) {
+ return [];
}
+ return [
+ 'SessionQueueStatus',
+ 'BatchStatus',
+ 'QueueCountsByDestination',
+ { type: 'SessionQueueItem', id: LIST_TAG },
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
+ ...batch_ids.map((id) => ({ type: 'BatchStatus', id }) satisfies ApiTagDescription),
+ ];
},
- invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination'],
}),
cancelByBatchDestination: build.mutation<
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['responses']['200']['content']['application/json'],
@@ -333,20 +228,17 @@ export const queueApi = api.injectEndpoints({
method: 'PUT',
params,
}),
- onQueryStarted: async (arg, api) => {
- const { dispatch, queryFulfilled } = api;
- try {
- await queryFulfilled;
- resetListQueryData(dispatch);
- } catch {
- // no-op
- }
- },
invalidatesTags: (result, error, { destination }) => {
if (!result) {
return [];
}
- return ['SessionQueueStatus', 'BatchStatus', { type: 'QueueCountsByDestination', id: destination }];
+ return [
+ 'SessionQueueStatus',
+ 'BatchStatus',
+ { type: 'SessionQueueItem', id: LIST_TAG },
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
+ { type: 'QueueCountsByDestination', id: destination },
+ ];
},
}),
cancelAllExceptCurrent: build.mutation<
@@ -357,16 +249,7 @@ export const queueApi = api.injectEndpoints({
url: buildQueueUrl('cancel_all_except_current'),
method: 'PUT',
}),
- onQueryStarted: async (arg, api) => {
- const { dispatch, queryFulfilled } = api;
- try {
- await queryFulfilled;
- resetListQueryData(dispatch);
- } catch {
- // no-op
- }
- },
- invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination'],
+ invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination', 'SessionQueueItem'],
}),
retryItemsById: build.mutation<
paths['/api/v1/queue/{queue_id}/retry_items_by_id']['put']['responses']['200']['content']['application/json'],
@@ -377,44 +260,64 @@ export const queueApi = api.injectEndpoints({
method: 'PUT',
body,
}),
- onQueryStarted: async (arg, api) => {
- const { dispatch, queryFulfilled } = api;
- try {
- await queryFulfilled;
- resetListQueryData(dispatch);
- } catch {
- // no-op
+ invalidatesTags: (result, error, item_ids) => {
+ if (!result) {
+ return [];
}
+ return [
+ 'CurrentSessionQueueItem',
+ 'NextSessionQueueItem',
+ 'QueueCountsByDestination',
+ { type: 'SessionQueueItem', id: LIST_TAG },
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
+ ...item_ids.map((id) => ({ type: 'SessionQueueItem', id }) satisfies ApiTagDescription),
+ ];
},
- invalidatesTags: ['CurrentSessionQueueItem', 'NextSessionQueueItem', 'QueueCountsByDestination'],
}),
listQueueItems: build.query<
- EntityState & {
- has_more: boolean;
- },
+ components['schemas']['CursorPaginatedResults_SessionQueueItem_'],
{ cursor?: number; priority?: number; destination?: string } | undefined
>({
query: (queryArgs) => ({
url: getListQueueItemsUrl(queryArgs),
method: 'GET',
}),
- serializeQueryArgs: () => {
- return buildQueueUrl('list');
- },
- transformResponse: (response: components['schemas']['CursorPaginatedResults_SessionQueueItem_']) =>
- queueItemsAdapter.upsertMany(
- queueItemsAdapter.getInitialState({
- has_more: response.has_more,
- }),
- response.items
- ),
- merge: (cache, response) => {
- queueItemsAdapter.upsertMany(cache, queueItemsAdapterSelectors.selectAll(response));
- cache.has_more = response.has_more;
- },
- forceRefetch: ({ currentArg, previousArg }) => currentArg !== previousArg,
keepUnusedDataFor: 60 * 5, // 5 minutes
- providesTags: ['FetchOnReconnect', { type: 'SessionQueueItem', id: LIST_TAG }],
+ providesTags: (result, _error, _args) => {
+ if (!result) {
+ return [];
+ }
+ return [
+ 'FetchOnReconnect',
+ { type: 'SessionQueueItem', id: LIST_TAG },
+ ...result.items.map(({ item_id }) => ({ type: 'SessionQueueItem', id: item_id }) satisfies ApiTagDescription),
+ ];
+ },
+ }),
+ listAllQueueItems: build.query<
+ paths['/api/v1/queue/{queue_id}/list_all']['get']['responses']['200']['content']['application/json'],
+ paths['/api/v1/queue/{queue_id}/list_all']['get']['parameters']['query']
+ >({
+ query: (queryArgs) => {
+ const q = queryArgs
+ ? queryString.stringify(queryArgs, {
+ arrayFormat: 'none',
+ })
+ : undefined;
+
+ return q ? buildQueueUrl(`list_all?${q}`) : buildQueueUrl('list_all');
+ },
+ providesTags: (result, _error, _args) => {
+ if (!result) {
+ return [];
+ }
+ const tags: ApiTagDescription[] = [
+ 'FetchOnReconnect',
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
+ ...result.map(({ item_id }) => ({ type: 'SessionQueueItem', id: item_id }) satisfies ApiTagDescription),
+ ];
+ return tags;
+ },
}),
getQueueCountsByDestination: build.query<
paths['/api/v1/queue/{queue_id}/counts_by_destination']['get']['responses']['200']['content']['application/json'],
@@ -440,6 +343,7 @@ export const {
useGetQueueStatusQuery,
useGetQueueItemQuery,
useListQueueItemsQuery,
+ useListAllQueueItemsQuery,
useCancelQueueItemMutation,
useGetBatchStatusQuery,
useGetCurrentQueueItemQuery,
@@ -450,24 +354,6 @@ export const {
export const selectQueueStatus = queueApi.endpoints.getQueueStatus.select();
export const selectCanvasQueueCounts = queueApi.endpoints.getQueueCountsByDestination.select({ destination: 'canvas' });
-const resetListQueryData = (
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- dispatch: ThunkDispatch
-) => {
- dispatch(
- queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => {
- // remove all items from the list
- queueItemsAdapter.removeAll(draft);
- // reset the has_more flag
- draft.has_more = false;
- })
- );
- // set the list cursor and priority to undefined
- dispatch(listParamsReset());
- // we have to manually kick off another query to get the first page and re-initialize the list
- dispatch(queueApi.endpoints.listQueueItems.initiate(undefined));
-};
-
export const enqueueMutationFixedCacheKeyOptions = {
fixedCacheKey: 'enqueueBatch',
} as const;
diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts
index 7bc59202a4..123245fdfe 100644
--- a/invokeai/frontend/web/src/services/api/index.ts
+++ b/invokeai/frontend/web/src/services/api/index.ts
@@ -56,6 +56,7 @@ const tagTypes = [
] as const;
export type ApiTagDescription = TagDescription<(typeof tagTypes)[number]>;
export const LIST_TAG = 'LIST';
+export const LIST_ALL_TAG = 'LIST_ALL';
const dynamicBaseQuery: BaseQueryFn = (args, api, extraOptions) => {
const baseUrl = $baseUrl.get();
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index 5d10535e66..65f2398d53 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -1164,7 +1164,7 @@ export type paths = {
patch?: never;
trace?: never;
};
- "/api/v1/queue/{queue_id}/all": {
+ "/api/v1/queue/{queue_id}/list_all": {
parameters: {
query?: never;
header?: never;
diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
index 357b86c7de..512b2aa317 100644
--- a/invokeai/frontend/web/src/services/events/setEventListeners.tsx
+++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
@@ -21,7 +21,7 @@ import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { forEach, isNil, round } from 'lodash-es';
import type { ApiTagDescription } from 'services/api';
-import { api, LIST_TAG } from 'services/api';
+import { api, LIST_ALL_TAG, LIST_TAG } from 'services/api';
import { modelsApi } from 'services/api/endpoints/models';
import { queueApi } from 'services/api/endpoints/queue';
import { workflowsApi } from 'services/api/endpoints/workflows';
@@ -363,68 +363,20 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
socket.on('queue_item_status_changed', (data) => {
// we've got new status for the queue item, batch and queue
- const {
- item_id,
- session_id,
- status,
- started_at,
- updated_at,
- completed_at,
- batch_status,
- queue_status,
- error_type,
- error_message,
- error_traceback,
- destination,
- credits,
- } = data;
+ const { item_id, session_id, status, batch_status, error_type, error_message, destination } = data;
log.debug({ data }, `Queue item ${item_id} status updated: ${status}`);
- // // Update this specific queue item in the list of queue items (this is the queue item DTO, without the session)
- // dispatch(
- // queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => {
- // queueItemsAdapter.updateOne(draft, {
- // id: String(item_id),
- // changes: {
- // status,
- // started_at,
- // updated_at: updated_at ?? undefined,
- // completed_at: completed_at ?? undefined,
- // error_type,
- // error_message,
- // error_traceback,
- // credits,
- // },
- // });
- // })
- // );
-
- // Optimistic update of the queue status. We prefer to do an optimistic update over tag invalidation due to the
- // frequency of `queue_item_status_changed` events.
- dispatch(
- queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => {
- if (!draft) {
- return;
- }
- /**
- * Update the queue status - though the getQueueStatus query response contains the processor status (i.e. running
- * or paused), that data is not provided in the event we are handling. So we can only update `draft.queue` here.
- */
- Object.assign(draft.queue, queue_status);
- })
- );
-
- // Update the batch status
- dispatch(queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status));
-
// Invalidate caches for things we cannot easily update
const tagsToInvalidate: ApiTagDescription[] = [
+ 'SessionQueueStatus',
'CurrentSessionQueueItem',
'NextSessionQueueItem',
'InvocationCacheStatus',
{ type: 'SessionQueueItem', id: item_id },
{ type: 'SessionQueueItem', id: LIST_TAG },
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
+ { type: 'BatchStatus', id: batch_status.batch_id },
];
if (destination) {
tagsToInvalidate.push({ type: 'QueueCountsByDestination', id: destination });
From e80f0b2b43bc8b0f78554c8c1fc43d08d5e7a6ed Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 3 Jun 2025 19:12:07 +1000
Subject: [PATCH 030/210] fix(ui): unstable selector results in lora drop down
---
.../hooks/useRelatedGroupedModelCombobox.ts | 100 ++++++++++++------
.../src/common/hooks/useRelatedModelKeys.ts | 16 ++-
2 files changed, 82 insertions(+), 34 deletions(-)
diff --git a/invokeai/frontend/web/src/common/hooks/useRelatedGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useRelatedGroupedModelCombobox.ts
index af14f5460e..ba06451256 100644
--- a/invokeai/frontend/web/src/common/hooks/useRelatedGroupedModelCombobox.ts
+++ b/invokeai/frontend/web/src/common/hooks/useRelatedGroupedModelCombobox.ts
@@ -1,12 +1,18 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
+import { EMPTY_ARRAY } from 'app/store/constants';
+import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
+import { useAppSelector } from 'app/store/storeHooks';
import type { GroupBase } from 'chakra-react-select';
+import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
+import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import type { ModelIdentifierField } from 'features/nodes/types/common';
+import { uniq } from 'lodash-es';
+import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
+import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships';
import type { AnyModelConfig } from 'services/api/types';
import { useGroupedModelCombobox } from './useGroupedModelCombobox';
-import { useRelatedModelKeys } from './useRelatedModelKeys';
-import { useSelectedModelKeys } from './useSelectedModelKeys';
type UseRelatedGroupedModelComboboxArg = {
modelConfigs: T[];
@@ -29,6 +35,32 @@ type UseRelatedGroupedModelComboboxReturn = {
noOptionsMessage: () => string;
};
+const selectSelectedModelKeys = createMemoizedSelector(selectParamsSlice, selectLoRAsSlice, (params, loras) => {
+ const keys: string[] = [];
+ const main = params.model;
+ const vae = params.vae;
+ const refiner = params.refinerModel;
+ const controlnet = params.controlLora;
+
+ if (main) {
+ keys.push(main.key);
+ }
+ if (vae) {
+ keys.push(vae.key);
+ }
+ if (refiner) {
+ keys.push(refiner.key);
+ }
+ if (controlnet) {
+ keys.push(controlnet.key);
+ }
+ for (const { model } of loras.loras) {
+ keys.push(model.key);
+ }
+
+ return uniq(keys);
+});
+
export function useRelatedGroupedModelCombobox({
modelConfigs,
selectedModel,
@@ -39,9 +71,15 @@ export function useRelatedGroupedModelCombobox({
}: UseRelatedGroupedModelComboboxArg): UseRelatedGroupedModelComboboxReturn {
const { t } = useTranslation();
- const selectedKeys = useSelectedModelKeys();
-
- const relatedKeys = useRelatedModelKeys(selectedKeys);
+ const selectedKeys = useAppSelector(selectSelectedModelKeys);
+ const { relatedKeys } = useGetRelatedModelIdsBatchQuery(selectedKeys, {
+ selectFromResult: ({ data }) => {
+ if (!data) {
+ return { relatedKeys: EMPTY_ARRAY };
+ }
+ return { relatedKeys: data };
+ },
+ });
// Base grouped options
const base = useGroupedModelCombobox({
@@ -53,40 +91,42 @@ export function useRelatedGroupedModelCombobox({
groupByType,
});
- // If no related models selected, just return base
- if (relatedKeys.size === 0) {
- return base;
- }
+ const options = useMemo(() => {
+ if (relatedKeys.length === 0) {
+ return base.options;
+ }
- const relatedOptions: ComboboxOption[] = [];
- const updatedGroups: GroupBase[] = [];
+ const relatedOptions: ComboboxOption[] = [];
+ const updatedGroups: GroupBase[] = [];
- for (const group of base.options) {
- const remainingOptions: ComboboxOption[] = [];
+ for (const group of base.options) {
+ const remainingOptions: ComboboxOption[] = [];
- for (const option of group.options) {
- if (relatedKeys.has(option.value)) {
- relatedOptions.push({ ...option, label: `* ${option.label}` });
- } else {
- remainingOptions.push(option);
+ for (const option of group.options) {
+ if (relatedKeys.includes(option.value)) {
+ relatedOptions.push({ ...option, label: `* ${option.label}` });
+ } else {
+ remainingOptions.push(option);
+ }
+ }
+
+ if (remainingOptions.length > 0) {
+ updatedGroups.push({
+ label: group.label,
+ options: remainingOptions,
+ });
}
}
- if (remainingOptions.length > 0) {
- updatedGroups.push({
- label: group.label,
- options: remainingOptions,
- });
+ if (relatedOptions.length > 0) {
+ return [{ label: t('modelManager.relatedModels'), options: relatedOptions }, ...updatedGroups];
+ } else {
+ return updatedGroups;
}
- }
-
- const finalOptions: GroupBase[] =
- relatedOptions.length > 0
- ? [{ label: t('modelManager.relatedModels'), options: relatedOptions }, ...updatedGroups]
- : updatedGroups;
+ }, [base.options, relatedKeys, t]);
return {
...base,
- options: finalOptions,
+ options,
};
}
diff --git a/invokeai/frontend/web/src/common/hooks/useRelatedModelKeys.ts b/invokeai/frontend/web/src/common/hooks/useRelatedModelKeys.ts
index fc0711b969..9349c9b63d 100644
--- a/invokeai/frontend/web/src/common/hooks/useRelatedModelKeys.ts
+++ b/invokeai/frontend/web/src/common/hooks/useRelatedModelKeys.ts
@@ -1,14 +1,22 @@
+import { EMPTY_ARRAY } from 'app/store/constants';
import { useMemo } from 'react';
import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships';
+const options: Parameters[1] = {
+ selectFromResult: ({ data }) => {
+ if (!data) {
+ return { related: EMPTY_ARRAY };
+ }
+ return data;
+ },
+};
+
/**
* Fetches related model keys for a given set of selected model keys.
* Returns a Set for fast lookup.
*/
-export const useRelatedModelKeys = (selectedKeys: Set) => {
- const { data: related = [] } = useGetRelatedModelIdsBatchQuery([...selectedKeys], {
- skip: selectedKeys.size === 0,
- });
+export const useRelatedModelKeys = (selectedKeys: string[]) => {
+ const { related } = useGetRelatedModelIdsBatchQuery(selectedKeys, options);
return useMemo(() => new Set(related), [related]);
};
From 0e9b71801aa668129679ad88d25410d6221491e5 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 3 Jun 2025 19:12:14 +1000
Subject: [PATCH 031/210] feat: canvas flow rework (wip)
---
.../components/CanvasMainPanelContent.tsx | 617 ++++++------------
.../web/src/services/events/stores.ts | 62 +-
2 files changed, 271 insertions(+), 408 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index ba3f3c2a7f..3db7cf89af 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,5 +1,5 @@
/* eslint-disable i18next/no-literal-string */
-import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
import {
Box,
Button,
@@ -43,21 +43,13 @@ import { StagingAreaToolbar } from 'features/controlLayers/components/StagingAre
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { loadImage } from 'features/controlLayers/konva/util';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
-import {
- canvasSessionStarted,
- selectCanvasSessionType,
- selectSelectedImage,
- selectStagedImageIndex,
- stagingAreaImageSelected,
- stagingAreaNextStagedImageSelected,
- stagingAreaPrevStagedImageSelected,
-} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionStarted, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { DndImage } from 'features/dnd/DndImage';
import { newCanvasFromImage } from 'features/imageActions/actions';
-import type { ProgressImage } from 'features/nodes/types/common';
import { isImageField } from 'features/nodes/types/common';
import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ChangeEvent } from 'react';
@@ -65,11 +57,10 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Trans, useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
-import { getImageDTOSafe, useGetImageDTOQuery } from 'services/api/endpoints/images';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useListAllQueueItemsQuery } from 'services/api/endpoints/queue';
import type { ImageDTO, S } from 'services/api/types';
-import type { ProgressAndResult } from 'services/events/stores';
-import { $progressImages, $socket, useMapSelector } from 'services/events/stores';
+import { $socket, setProgress, useProgressData } from 'services/events/stores';
import type { Equals, Param0 } from 'tsafe';
import { assert, objectEntries } from 'tsafe';
@@ -100,7 +91,7 @@ export const CanvasMainPanelContent = memo(() => {
}
if (sessionType === 'simple') {
- return ;
+ return ;
}
if (sessionType === 'advanced') {
@@ -296,48 +287,6 @@ const GenerateWithStartingImageAndInpaintMask = memo(() => {
});
GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
-const SimpleActiveSession = memo(() => {
- const { getState, dispatch } = useAppStore();
- const selectedImage = useAppSelector(selectSelectedImage);
-
- const startOver = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: null }));
- $progressImages.set({});
- }, [dispatch]);
-
- const goAdvanced = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
- }, [dispatch]);
-
- const selectNext = useCallback(() => {
- dispatch(stagingAreaNextStagedImageSelected());
- }, [dispatch]);
-
- useHotkeys(['right'], selectNext, { preventDefault: true }, [selectNext]);
-
- const selectPrev = useCallback(() => {
- dispatch(stagingAreaPrevStagedImageSelected());
- }, [dispatch]);
-
- useHotkeys(['left'], selectPrev, { preventDefault: true }, [selectPrev]);
-
- return (
-
-
-
- Generations
-
-
-
- Start Over
-
-
-
-
- );
-});
-SimpleActiveSession.displayName = 'SimpleActiveSession';
-
const scrollIndicatorSx = {
opacity: 0,
'&[data-visible="true"]': {
@@ -346,6 +295,7 @@ const scrollIndicatorSx = {
} satisfies SystemStyleObject;
const StagingArea = memo(() => {
+ const dispatch = useAppDispatch();
const [selectedItemId, setSelectedItemId] = useState(null);
const [autoSwitch, setAutoSwitch] = useState(true);
const [canScrollLeft, setCanScrollLeft] = useState(false);
@@ -358,21 +308,15 @@ const StagingArea = memo(() => {
items.length > 0 && selectedItemId !== null ? items.find(({ item_id }) => item_id === selectedItemId) : null,
[items, selectedItemId]
);
+ const selectedItemIndex = useMemo(
+ () =>
+ items.length > 0 && selectedItemId !== null ? items.findIndex(({ item_id }) => item_id === selectedItemId) : null,
+ [items, selectedItemId]
+ );
- useEffect(() => {
- if (items.length === 0) {
- setSelectedItemId(null);
- return;
- }
- if (selectedItem === null && items.length > 0) {
- setSelectedItemId(items[0]?.item_id ?? null);
- return;
- }
- if (selectedItemId === null || items.find((item) => item.item_id === selectedItemId) === undefined) {
- return;
- }
- document.getElementById(`queue-item-status-card-${selectedItemId}`)?.scrollIntoView();
- }, [items, selectedItem, selectedItemId]);
+ const startOver = useCallback(() => {
+ dispatch(canvasSessionStarted({ sessionType: null }));
+ }, [dispatch]);
useEffect(() => {
const el = scrollableRef.current;
@@ -393,13 +337,31 @@ const StagingArea = memo(() => {
};
}, []);
- const onSelectItem = useCallback((item: S['SessionQueueItem']) => {
- setSelectedItemId(item.item_id);
- if (item.status !== 'in_progress') {
- setAutoSwitch(false);
+ const onSelectItemId = useCallback((item_id: number | null) => {
+ setSelectedItemId(item_id);
+ if (item_id !== null) {
+ document.getElementById(getCardId(item_id))?.scrollIntoView();
}
}, []);
+ const onChangeAutoSwitch = useCallback((autoSwitch: boolean) => {
+ setAutoSwitch(autoSwitch);
+ }, []);
+
+ useEffect(() => {
+ if (items.length === 0) {
+ onSelectItemId(null);
+ return;
+ }
+ if (selectedItem === null && items.length > 0) {
+ onSelectItemId(items[0]?.item_id ?? null);
+ return;
+ }
+ if (selectedItemId === null || items.find((item) => item.item_id === selectedItemId) === undefined) {
+ return;
+ }
+ }, [items, onSelectItemId, selectedItem, selectedItemId]);
+
const onNext = useCallback(() => {
if (selectedItemId === null) {
return;
@@ -410,8 +372,8 @@ const StagingArea = memo(() => {
if (!nextItem) {
return;
}
- setSelectedItemId(nextItem.item_id);
- }, [items, selectedItemId]);
+ onSelectItemId(nextItem.item_id);
+ }, [items, onSelectItemId, selectedItemId]);
const onPrev = useCallback(() => {
if (selectedItemId === null) {
return;
@@ -422,8 +384,8 @@ const StagingArea = memo(() => {
if (!prevItem) {
return;
}
- setSelectedItemId(prevItem.item_id);
- }, [items, selectedItemId]);
+ onSelectItemId(prevItem.item_id);
+ }, [items, onSelectItemId, selectedItemId]);
useHotkeys('left', onPrev);
useHotkeys('right', onNext);
@@ -443,7 +405,7 @@ const StagingArea = memo(() => {
return;
}
if (data.status === 'in_progress') {
- setSelectedItemId(data.item_id);
+ onSelectItemId(data.item_id);
}
};
@@ -452,35 +414,70 @@ const StagingArea = memo(() => {
return () => {
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
};
- }, [autoSwitch, socket]);
+ }, [autoSwitch, onSelectItemId, socket]);
- const onChangeAutoSwitch = useCallback((e: ChangeEvent) => {
+ const _onChangeAutoSwitch = useCallback((e: ChangeEvent) => {
setAutoSwitch(e.target.checked);
}, []);
+ useEffect(() => {
+ if (!socket) {
+ return;
+ }
+ const onProgress = (data: S['InvocationProgressEvent']) => {
+ if (data.destination !== 'canvas') {
+ return;
+ }
+ setProgress(data);
+ };
+ socket.on('invocation_progress', onProgress);
+
+ return () => {
+ socket.off('invocation_progress', onProgress);
+ };
+ }, [socket]);
+
return (
-
-
- {selectedItem && }
- {!selectedItem && No queued generations}
+
+
+
+ Generations
+
+
+
+ Start Over
+
-
- Auto-switch
-
-
-
-
+
+
+ {selectedItem && selectedItemIndex !== null && (
+
+ )}
+ {!selectedItem && No queued generations}
+
+
+ Auto-switch
+
+
+
+
+
{items.map((item, i) => (
-
))}
@@ -514,103 +511,110 @@ const StagingArea = memo(() => {
});
StagingArea.displayName = 'StagingArea';
-const IMAGE_DTO_ERROR = Symbol('IMAGE_DTO_ERROR');
-
-const useOutputImageDTO = (item: S['SessionQueueItem']) => {
- const [imageDTO, setImageDTO] = useState(null);
- const syncImageDTO = useCallback(async (item: S['SessionQueueItem']) => {
- const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
- isCanvasOutputNodeId(nodeId)
- )?.[1][0];
- const output = nodeId ? item.session.results[nodeId] : undefined;
-
- if (!output) {
- return setImageDTO(null);
- }
-
- for (const [_name, value] of objectEntries(output)) {
- if (isImageField(value)) {
- const imageDTO = await getImageDTOSafe(value.image_name);
- if (imageDTO) {
- setImageDTO(imageDTO);
- $progressImages.setKey(item.session_id, undefined);
- return;
- }
- }
- }
-
- setImageDTO(IMAGE_DTO_ERROR);
- }, []);
- useEffect(() => {
- syncImageDTO(item);
- }, [item, syncImageDTO]);
-
- return imageDTO;
+const queueItemStatusCardMiniSx = {
+ cursor: 'pointer',
+ pos: 'relative',
+ borderWidth: 1,
+ borderRadius: 'base',
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ aspectRatio: '1/1',
+ maxH: 'full',
+ maxW: 'full',
+ '&[data-selected="true"]': {
+ borderColor: 'invokeBlue.300',
+ },
+ '&[data-size="mini"]': {
+ flexShrink: 0,
+ },
};
-const QueueItemStatusCard = memo(
- ({
- item,
- isSelected,
- number,
- onSelectItem,
- ...rest
- }: {
- item: S['SessionQueueItem'];
- isSelected: boolean;
- number?: number;
- onSelectItem?: (item: S['SessionQueueItem']) => void;
- } & FlexProps) => {
+const getCardId = (item_id: number) => `queue-item-status-card-${item_id}`;
+
+type QueueItemStatusCardMiniProps = {
+ item: S['SessionQueueItem'];
+ isSelected: boolean;
+ number: number;
+ onSelectItemId: (item_id: number) => void;
+ onChangeAutoSwitch: (autoSwitch: boolean) => void;
+ size: 'mini' | 'full';
+};
+
+const QueueItemCard = memo(
+ ({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch, size }: QueueItemStatusCardMiniProps) => {
+ const [isImageLoaded, setIsImageLoaded] = useState(false);
+
+ const outputImageName = useMemo(() => {
+ const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
+ isCanvasOutputNodeId(nodeId)
+ )?.[1][0];
+ const output = nodeId ? item.session.results[nodeId] : undefined;
+
+ if (!output) {
+ return null;
+ }
+
+ for (const [_name, value] of objectEntries(output)) {
+ if (isImageField(value)) {
+ return value.image_name;
+ }
+ }
+
+ return null;
+ }, [item.session.results, item.session.source_prepared_mapping]);
+
+ const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
+
+ useEffect(() => {
+ if (imageDTO) {
+ loadImage(imageDTO.thumbnail_url, true).then(() => {
+ setIsImageLoaded(true);
+ });
+ }
+ }, [imageDTO, item.session_id]);
+
const onClick = useCallback(() => {
- onSelectItem?.(item);
- }, [item, onSelectItem]);
+ onSelectItemId(item.item_id);
+ }, [item.item_id, onSelectItemId]);
+
+ const onDoubleClick = useCallback(() => {
+ onChangeAutoSwitch(item.status === 'in_progress');
+ }, [item.status, onChangeAutoSwitch]);
+
+ if (imageDTO && isImageLoaded) {
+ return (
+
+
+ {`#${number}`}
+ {size === 'full' && (
+
+
+
+ )}
+
+ );
+ }
+
return (
-
- {number !== undefined && {`#${number}`}}
+
+ {`#${number}`}
);
}
);
-QueueItemStatusCard.displayName = 'QueueItemStatusCard';
+QueueItemCard.displayName = 'QueueItemStatusCard';
-const QueueItemStatusCardContent = memo(({ item }: { item: S['SessionQueueItem'] }) => {
- const socket = useStore($socket);
- const [progressEvent, setProgressEvent] = useState(null);
- const [progressImage, setProgressImage] = useState(null);
- useEffect(() => {
- if (!socket) {
- return;
- }
- const onProgress = (data: S['InvocationProgressEvent']) => {
- if (data.session_id !== item.session_id) {
- return;
- }
- setProgressEvent(data);
- if (data.image) {
- setProgressImage(data.image);
- }
- };
- socket.on('invocation_progress', onProgress);
-
- return () => {
- socket.off('invocation_progress', onProgress);
- };
- }, [item.session_id, socket]);
-
- const imageDTO = useOutputImageDTO(item);
+const InProgressContent = memo(({ item }: { item: S['SessionQueueItem'] }) => {
+ const { progressEvent, progressImage } = useProgressData(item.session_id);
if (item.status === 'pending') {
return (
@@ -633,17 +637,8 @@ const QueueItemStatusCardContent = memo(({ item }: { item: S['SessionQueueItem']
);
}
- if (item.status === 'in_progress' || !imageDTO) {
- if (!progressImage) {
- return (
- <>
-
- In Progress
-
-
- >
- );
- }
+
+ if (progressImage) {
return (
<>
@@ -651,8 +646,16 @@ const QueueItemStatusCardContent = memo(({ item }: { item: S['SessionQueueItem']
>
);
}
- if (item.status === 'completed' && imageDTO && imageDTO !== IMAGE_DTO_ERROR) {
- return ;
+
+ if (item.status === 'in_progress') {
+ return (
+ <>
+
+ In Progress
+
+
+ >
+ );
}
if (item.status === 'completed') {
@@ -664,7 +667,7 @@ const QueueItemStatusCardContent = memo(({ item }: { item: S['SessionQueueItem']
}
assert>(false);
});
-QueueItemStatusCardContent.displayName = 'QueueItemStatusCardContent';
+InProgressContent.displayName = 'InProgressContent';
const circleStyles: SystemStyleObject = {
circle: {
@@ -676,7 +679,7 @@ const circleStyles: SystemStyleObject = {
right: 2,
};
-const ProgressCircle = ({ data }: { data?: S['InvocationProgressEvent'] | null }) => {
+const ProgressCircle = memo(({ data }: { data?: S['InvocationProgressEvent'] | null }) => {
return (
);
-};
+});
ProgressCircle.displayName = 'ProgressCircle';
-const QueueItemResultCard = memo(({ item }: { item: S['SessionQueueItem'] }) => {
- const imageName = useMemo(() => {
- const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
- isCanvasOutputNodeId(nodeId)
- )?.[1][0];
- const output = nodeId ? item.session.results[nodeId] : undefined;
- if (!output) {
- return;
- }
-
- for (const [_name, value] of objectEntries(output)) {
- if (isImageField(value)) {
- return value.image_name;
- }
- }
- }, [item]);
-
- const { data: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
-
- if (!imageDTO) {
- return Unknown output type;
- }
-
- return ;
-});
-QueueItemResultCard.displayName = 'QueueItemResultCard';
-
-const SelectedImageOrProgressImage = memo(() => {
- const selectedImage = useAppSelector(selectSelectedImage);
-
- if (selectedImage) {
- return ;
- }
-
- return (
-
- No images
-
- );
-});
-SelectedImageOrProgressImage.displayName = 'SelectedImageOrProgressImage';
-
-const SelectedImage = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
+const ImageActions = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
const { getState, dispatch } = useAppStore();
const vary = useCallback(() => {
@@ -767,168 +728,20 @@ const SelectedImage = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
});
}, [dispatch, getState, imageDTO]);
return (
-
-
-
-
-
- Vary
-
-
- Use as Control
-
-
- Edit
-
-
-
-
+
+
+ Vary
+
+
+ Use as Control
+
+
+ Edit
+
+
);
});
-SelectedImage.displayName = 'SelectedImage';
-
-const FullSizeImage = memo(({ sessionId }: { sessionId: string }) => {
- const _progressImage = useMapSelector(sessionId, $progressImages);
-
- if (!_progressImage) {
- return (
-
- Pending
-
- );
- }
-
- if (_progressImage.resultImage) {
- return ;
- }
-
- if (_progressImage.progressImage) {
- return (
-
-
-
- );
- }
-
- return (
-
- No progress yet
-
- );
-});
-FullSizeImage.displayName = 'FullSizeImage';
-
-const SessionImages = memo(() => {
- const progressImages = useStore($progressImages);
- return (
-
-
- {Object.values(progressImages).map((data, index) => {
- if (data.type === 'staged') {
- return ;
- } else {
- return ;
- }
- })}
-
-
- );
-});
-SessionImages.displayName = 'SessionImages';
-
-const ProgressImagePreview = ({ index, data }: { index: number; data: ProgressAndResult }) => {
- const dispatch = useAppDispatch();
- const selectedImageIndex = useAppSelector(selectStagedImageIndex);
- const onClick = useCallback(() => {
- dispatch(stagingAreaImageSelected({ index }));
- }, [dispatch, index]);
-
- useEffect(() => {
- if (selectedImageIndex === index) {
- // this doesn't work when the DndImage is in a popover... why
- document.getElementById(getStagingImageId(data.sessionId))?.scrollIntoView();
- }
- }, [data.sessionId, index, selectedImageIndex]);
-
- if (data.resultImage) {
- return (
-
- );
- }
-
- if (data.progressImage) {
- return (
-
- );
- }
-
- return ;
-};
-
-const getStagingImageId = (session_id: string) => `staging-image-${session_id}`;
-
-const sx = {
- objectFit: 'contain',
- maxW: 'full',
- maxH: 'full',
- w: 'min-content',
- borderRadius: 'base',
- cursor: 'grab',
- '&[data-is-dragging=true]': {
- opacity: 0.3,
- },
- '&[data-is-selected="false"]': {
- opacity: 0.5,
- },
-} satisfies SystemStyleObject;
-const SessionImage = memo(({ index, data }: { index: number; data: ProgressAndResult }) => {
- const dispatch = useAppDispatch();
- const selectedImageIndex = useAppSelector(selectStagedImageIndex);
- const onClick = useCallback(() => {
- dispatch(stagingAreaImageSelected({ index }));
- }, [dispatch, index]);
- useEffect(() => {
- if (selectedImageIndex === index) {
- // this doesn't work when the DndImage is in a popover... why
- document.getElementById(getStagingImageId(data.sessionId))?.scrollIntoView();
- }
- }, [data.sessionId, index, selectedImageIndex]);
- return (
-
- );
-});
-SessionImage.displayName = 'SessionImage';
+ImageActions.displayName = 'ImageActions';
const CanvasActiveSession = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index e233e4fea1..f380019a5b 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -1,7 +1,6 @@
import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
import type { ProgressImage } from 'features/nodes/types/common';
import { round } from 'lodash-es';
-import type { MapStore } from 'nanostores';
import { atom, computed, map } from 'nanostores';
import { useEffect, useState } from 'react';
import type { ImageDTO, S } from 'services/api/types';
@@ -21,20 +20,71 @@ export type ProgressAndResult = {
};
export const $progressImages = map({} as Record);
-export const useMapSelector = (id: string, map: MapStore>): T | undefined => {
- const [value, setValue] = useState();
+type ProgressData = {
+ sessionId: string;
+ progressEvent: S['InvocationProgressEvent'] | null;
+ progressImage: ProgressImage | null;
+};
+
+export const $progressData = atom>({});
+
+export const useProgressData = (sessionId: string): ProgressData => {
+ const [value, setValue] = useState({ sessionId, progressEvent: null, progressImage: null });
useEffect(() => {
- const unsub = map.subscribe((data) => {
- setValue(data[id]);
+ const unsub = $progressData.subscribe((data) => {
+ const progressData = data[sessionId];
+ if (!progressData) {
+ return;
+ }
+ setValue(progressData);
});
return () => {
unsub();
};
- }, [id, map]);
+ }, [sessionId]);
return value;
};
+export const setProgress = (data: S['InvocationProgressEvent']) => {
+ const progressData = $progressData.get();
+ const current = progressData[data.session_id];
+ if (current) {
+ const next = { ...current };
+ next.progressEvent = data;
+ if (data.image) {
+ next.progressImage = data.image;
+ }
+ $progressData.set({
+ ...progressData,
+ [data.session_id]: next,
+ });
+ } else {
+ $progressData.set({
+ ...progressData,
+ [data.session_id]: {
+ sessionId: data.session_id,
+ progressEvent: data,
+ progressImage: data.image ?? null,
+ },
+ });
+ }
+};
+
+export const clearProgressImage = (sessionId: string) => {
+ const progressData = $progressData.get();
+ const current = progressData[sessionId];
+ if (!current) {
+ return;
+ }
+ const next = { ...current };
+ next.progressImage = null;
+ $progressData.set({
+ ...progressData,
+ [sessionId]: next,
+ });
+};
+
export const $lastCanvasProgressEvent = atom(null);
export const $lastCanvasProgressImage = atom(null);
export const $lastWorkflowsProgressEvent = atom(null);
From ad736bc19073b505162820ae310a0c2f30a7924e Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 3 Jun 2025 20:54:19 +1000
Subject: [PATCH 032/210] feat: canvas flow rework (wip)
---
.../listeners/enqueueRequestedLinear.ts | 14 +-
.../components/CanvasMainPanelContent.tsx | 210 ++++++++++++++----
.../NewSessionConfirmationAlertDialog.tsx | 2 +-
.../store/canvasStagingAreaSlice.ts | 30 ++-
.../util/graph/buildLinearBatchConfig.ts | 2 +-
.../services/events/onInvocationComplete.tsx | 42 ++--
.../src/services/events/setEventListeners.tsx | 7 +-
.../web/src/services/events/stores.ts | 55 ++++-
8 files changed, 266 insertions(+), 96 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
index c6b38c517a..79413b94a0 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
@@ -5,7 +5,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
import { withResult, withResultAsync } from 'common/util/result';
import { parseify } from 'common/util/serialize';
-import { canvasSessionStarted, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionStarted, selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
@@ -32,6 +32,11 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
actionCreator: enqueueRequestedCanvas,
effect: async (action, { getState, dispatch }) => {
log.debug('Enqueue requested');
+
+ if (!selectCanvasSession(getState())) {
+ dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ }
+
const state = getState();
const { prepend } = action.payload;
@@ -91,7 +96,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
const { g, seedFieldIdentifier, positivePromptFieldIdentifier } = buildGraphResult.value;
- // const destination = state.canvasSettings.sendToCanvas ? 'canvas' : 'gallery';
+ const destination = state.canvasSession.session?.id ?? 'canvas';
const prepareBatchResult = withResult(() =>
prepareLinearUIBatch({
@@ -101,7 +106,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
seedFieldIdentifier,
positivePromptFieldIdentifier,
origin: 'canvas',
- destination: 'canvas',
+ destination,
})
);
@@ -116,9 +121,6 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
try {
await req.unwrap();
- if (!selectCanvasSessionType(state)) {
- dispatch(canvasSessionStarted({ sessionType: 'simple' }));
- }
log.debug(parseify({ batchConfig: prepareBatchResult.value }), 'Enqueued batch');
} catch (error) {
log.error({ error: serializeError(error as Error) }, 'Failed to enqueue batch');
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 3db7cf89af..0bf7d226ce 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -45,22 +45,25 @@ import { Transform } from 'features/controlLayers/components/Transform/Transform
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { loadImage } from 'features/controlLayers/konva/util';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
-import { canvasSessionStarted, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionStarted, selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { DndImage } from 'features/dnd/DndImage';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { isImageField } from 'features/nodes/types/common';
import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
+import { round } from 'lodash-es';
+import { atom, type WritableAtom } from 'nanostores';
import type { ChangeEvent } from 'react';
-import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Trans, useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useListAllQueueItemsQuery } from 'services/api/endpoints/queue';
import type { ImageDTO, S } from 'services/api/types';
-import { $socket, setProgress, useProgressData } from 'services/events/stores';
+import type { ProgressData } from 'services/events/stores';
+import { $socket, clearProgressEvent, setProgress, useHasProgressImage, useProgressData } from 'services/events/stores';
import type { Equals, Param0 } from 'tsafe';
import { assert, objectEntries } from 'tsafe';
@@ -84,25 +87,45 @@ const MenuContent = memo(() => {
MenuContent.displayName = 'MenuContent';
export const CanvasMainPanelContent = memo(() => {
- const sessionType = useAppSelector(selectCanvasSessionType);
+ const session = useAppSelector(selectCanvasSession);
- if (sessionType === null) {
+ if (session === null) {
return ;
}
- if (sessionType === 'simple') {
- return ;
+ if (session.type === 'simple') {
+ return ;
}
- if (sessionType === 'advanced') {
+ if (session.type === 'advanced') {
return ;
}
- assert>(false, 'Unexpected sessionType');
+ assert>(false, 'Unexpected session');
});
-
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
+const StagingAreaWrapper = memo(({ id }: { id: string }) => {
+ const ctx = useMemo(
+ () =>
+ ({
+ session: {
+ type: 'simple',
+ id,
+ },
+ $progressData: atom>({}),
+ }) as const,
+ [id]
+ );
+
+ return (
+
+
+
+ );
+});
+StagingAreaWrapper.displayName = 'StagingAreaWrapper';
+
const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({
type: 'raster_layer',
withResize: true,
@@ -116,6 +139,8 @@ const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getDat
withResize: true,
});
+const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))';
+
const NoActiveSession = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -294,14 +319,36 @@ const scrollIndicatorSx = {
},
} satisfies SystemStyleObject;
+type StagingContextValue = {
+ session:
+ | {
+ type: 'simple';
+ id: string;
+ }
+ | {
+ type: 'advanced';
+ id: string;
+ };
+ $progressData: WritableAtom>;
+};
+
+const StagingContext = createContext(null);
+
+const useStagingContext = () => {
+ const ctx = useContext(StagingContext);
+ assert(ctx !== null, 'use in stg prov');
+ return ctx;
+};
+
const StagingArea = memo(() => {
+ const ctx = useStagingContext();
const dispatch = useAppDispatch();
const [selectedItemId, setSelectedItemId] = useState(null);
const [autoSwitch, setAutoSwitch] = useState(true);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const scrollableRef = useRef(null);
- const { data } = useListAllQueueItemsQuery({ destination: 'canvas' });
+ const { data } = useListAllQueueItemsQuery({ destination: ctx.session.id });
const items = useMemo(() => data?.filter(({ status }) => status !== 'canceled') ?? EMPTY_ARRAY, [data]);
const selectedItem = useMemo(
() =>
@@ -315,7 +362,7 @@ const StagingArea = memo(() => {
);
const startOver = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: null }));
+ dispatch(canvasSessionStarted({ sessionType: 'simple' }));
}, [dispatch]);
useEffect(() => {
@@ -353,13 +400,10 @@ const StagingArea = memo(() => {
onSelectItemId(null);
return;
}
- if (selectedItem === null && items.length > 0) {
+ if (selectedItemId === null && items.length > 0) {
onSelectItemId(items[0]?.item_id ?? null);
return;
}
- if (selectedItemId === null || items.find((item) => item.item_id === selectedItemId) === undefined) {
- return;
- }
}, [items, onSelectItemId, selectedItem, selectedItemId]);
const onNext = useCallback(() => {
@@ -387,26 +431,42 @@ const StagingArea = memo(() => {
onSelectItemId(prevItem.item_id);
}, [items, onSelectItemId, selectedItemId]);
- useHotkeys('left', onPrev);
- useHotkeys('right', onNext);
+ const onFirst = useCallback(() => {
+ const first = items.at(0);
+ if (!first) {
+ return;
+ }
+ onSelectItemId(first.item_id);
+ }, [items, onSelectItemId]);
+ const onLast = useCallback(() => {
+ const last = items.at(-1);
+ if (!last) {
+ return;
+ }
+ onSelectItemId(last.item_id);
+ }, [items, onSelectItemId]);
+
+ useHotkeys('left', onPrev, { preventDefault: true });
+ useHotkeys('right', onNext, { preventDefault: true });
+ useHotkeys('meta+left', onFirst, { preventDefault: true });
+ useHotkeys('meta+right', onLast, { preventDefault: true });
const socket = useStore($socket);
useEffect(() => {
- if (!autoSwitch) {
- return;
- }
-
if (!socket) {
return;
}
const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
- if (data.destination !== 'canvas') {
+ if (data.destination !== ctx.session.id) {
return;
}
- if (data.status === 'in_progress') {
+ if (data.status === 'in_progress' && autoSwitch) {
onSelectItemId(data.item_id);
}
+ if (data.status === 'completed' || data.status === 'canceled' || data.status === 'failed') {
+ clearProgressEvent(ctx.$progressData, data.session_id);
+ }
};
socket.on('queue_item_status_changed', onQueueItemStatusChanged);
@@ -414,7 +474,7 @@ const StagingArea = memo(() => {
return () => {
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
};
- }, [autoSwitch, onSelectItemId, socket]);
+ }, [autoSwitch, ctx.$progressData, ctx.session.id, onSelectItemId, socket]);
const _onChangeAutoSwitch = useCallback((e: ChangeEvent) => {
setAutoSwitch(e.target.checked);
@@ -425,17 +485,17 @@ const StagingArea = memo(() => {
return;
}
const onProgress = (data: S['InvocationProgressEvent']) => {
- if (data.destination !== 'canvas') {
+ if (data.destination !== ctx.session.id) {
return;
}
- setProgress(data);
+ setProgress(ctx.$progressData, data);
};
socket.on('invocation_progress', onProgress);
return () => {
socket.off('invocation_progress', onProgress);
};
- }, [socket]);
+ }, [ctx.$progressData, ctx.session.id, socket]);
return (
@@ -467,8 +527,8 @@ const StagingArea = memo(() => {
-
-
+
+
{items.map((item, i) => (
{
});
StagingArea.displayName = 'StagingArea';
-const queueItemStatusCardMiniSx = {
+const queueItemCardSx = {
cursor: 'pointer',
pos: 'relative',
borderWidth: 1,
@@ -528,6 +588,9 @@ const queueItemStatusCardMiniSx = {
'&[data-size="mini"]': {
flexShrink: 0,
},
+ '&[data-size="full"]&[data-has-progress-image="false"]': {
+ w: 1024,
+ },
};
const getCardId = (item_id: number) => `queue-item-status-card-${item_id}`;
@@ -543,7 +606,9 @@ type QueueItemStatusCardMiniProps = {
const QueueItemCard = memo(
({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch, size }: QueueItemStatusCardMiniProps) => {
+ const ctx = useStagingContext();
const [isImageLoaded, setIsImageLoaded] = useState(false);
+ const hasProgressImage = useHasProgressImage(ctx.$progressData, item.session_id);
const outputImageName = useMemo(() => {
const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
@@ -566,13 +631,23 @@ const QueueItemCard = memo(
const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
- useEffect(() => {
- if (imageDTO) {
- loadImage(imageDTO.thumbnail_url, true).then(() => {
- setIsImageLoaded(true);
- });
+ const syncIsReady = useCallback(async () => {
+ if (!imageDTO) {
+ setIsImageLoaded(false);
+ return;
}
- }, [imageDTO, item.session_id]);
+ try {
+ const _ = await loadImage(size === 'mini' ? imageDTO.thumbnail_url : imageDTO.image_url, true);
+ setIsImageLoaded(true);
+ return;
+ } catch {
+ setIsImageLoaded(false);
+ }
+ }, [imageDTO, size]);
+
+ useEffect(() => {
+ syncIsReady();
+ }, [syncIsReady]);
const onClick = useCallback(() => {
onSelectItemId(item.item_id);
@@ -584,9 +659,16 @@ const QueueItemCard = memo(
if (imageDTO && isImageLoaded) {
return (
-
-
- {`#${number}`}
+
+
+ {`#${number}`}
{size === 'full' && (
@@ -599,40 +681,72 @@ const QueueItemCard = memo(
return (
- {`#${number}`}
+ {`#${number}`}
+ {size === 'full' && }
);
}
);
QueueItemCard.displayName = 'QueueItemStatusCard';
+const getMessage = (data: S['InvocationProgressEvent']) => {
+ let message = data.message;
+ if (data.percentage) {
+ message += ` (${round(data.percentage * 100)}%)`;
+ }
+ return message;
+};
+
+const ProgressMessage = memo(({ session_id }: { session_id: string }) => {
+ const { $progressData } = useStagingContext();
+ const { progressEvent } = useProgressData($progressData, session_id);
+ if (!progressEvent) {
+ return null;
+ }
+ return (
+
+ {getMessage(progressEvent)}
+
+ );
+});
+ProgressMessage.displayName = 'ProgressMessage';
+
const InProgressContent = memo(({ item }: { item: S['SessionQueueItem'] }) => {
- const { progressEvent, progressImage } = useProgressData(item.session_id);
+ const { $progressData } = useStagingContext();
+ const { progressEvent, progressImage } = useProgressData($progressData, item.session_id);
if (item.status === 'pending') {
return (
-
+
Pending
);
}
if (item.status === 'canceled') {
return (
-
+
Canceled
);
}
if (item.status === 'failed') {
return (
-
+
Failed
);
@@ -650,7 +764,7 @@ const InProgressContent = memo(({ item }: { item: S['SessionQueueItem'] }) => {
if (item.status === 'in_progress') {
return (
<>
-
+
In Progress
@@ -660,7 +774,7 @@ const InProgressContent = memo(({ item }: { item: S['SessionQueueItem'] }) => {
if (item.status === 'completed') {
return (
-
+
Unable to get image
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
index ef25dba0dc..fd65ae8095 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
@@ -20,7 +20,7 @@ export const useNewGallerySession = () => {
const newSessionDialog = useNewGallerySessionDialog();
const newGallerySessionImmediate = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: null }));
+ dispatch(canvasSessionStarted({ sessionType: 'simple' }));
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index 2703286e72..bf035be31c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -1,17 +1,20 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
+import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
import type { StagingAreaImage, StagingAreaProgressImage } from 'features/controlLayers/store/types';
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
type CanvasStagingAreaState = {
+ session: { type: 'simple'; id: string } | { type: 'advanced'; id: string } | null;
sessionType: 'simple' | 'advanced' | null;
images: (StagingAreaImage | StagingAreaProgressImage)[];
selectedImageIndex: number;
};
const INITIAL_STATE: CanvasStagingAreaState = {
+ session: null,
sessionType: null,
images: [],
selectedImageIndex: 0,
@@ -23,6 +26,10 @@ export const canvasSessionSlice = createSlice({
name: 'canvasSession',
initialState: getInitialState(),
reducers: {
+ sessionChanged: (state, action: PayloadAction<{ session: CanvasStagingAreaState['session'] }>) => {
+ const { session } = action.payload;
+ state.session = session;
+ },
stagingAreaImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
const { stagingAreaImage } = action.payload;
let didReplace = false;
@@ -67,11 +74,19 @@ export const canvasSessionSlice = createSlice({
state.images = [];
state.selectedImageIndex = 0;
},
- canvasSessionStarted: (_, action: PayloadAction<{ sessionType: CanvasStagingAreaState['sessionType'] }>) => {
- const { sessionType } = action.payload;
- const state = getInitialState();
- state.sessionType = sessionType;
- return state;
+ canvasSessionStarted: {
+ reducer: (state, action: PayloadAction<{ session: CanvasStagingAreaState['session'] }>) => {
+ const { session } = action.payload;
+ state.session = session;
+ },
+ prepare: (payload: { sessionType: 'simple' | 'advanced' }) => ({
+ payload: {
+ session: {
+ type: payload.sessionType,
+ id: getPrefixedId(`canvas:${payload.sessionType}`),
+ },
+ },
+ }),
},
},
extraReducers(builder) {
@@ -80,6 +95,7 @@ export const canvasSessionSlice = createSlice({
});
export const {
+ sessionChanged,
stagingAreaImageStaged,
stagingAreaGenerationStarted,
stagingAreaGenerationFinished,
@@ -140,3 +156,7 @@ export const selectCanvasSessionType = createSelector(
selectCanvasStagingAreaSlice,
(canvasSession) => canvasSession.sessionType
);
+export const selectCanvasSession = createSelector(
+ selectCanvasStagingAreaSlice,
+ (canvasSession) => canvasSession.session
+);
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts
index 461dde930a..0daf113873 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts
@@ -34,7 +34,7 @@ export const prepareLinearUIBatch = (arg: {
seedFieldIdentifier?: FieldIdentifier;
positivePromptFieldIdentifier: FieldIdentifier;
origin: 'canvas' | 'workflows' | 'upscaling';
- destination: 'canvas' | 'gallery';
+ destination: string;
}): EnqueueBatchArg => {
const { state, g, prepend, seedFieldIdentifier, positivePromptFieldIdentifier, origin, destination } = arg;
const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params;
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index bcd51ca191..e26a2d05d7 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -1,22 +1,18 @@
import { logger } from 'app/logging/logger';
import type { AppDispatch, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { stagingAreaImageStaged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { isImageField, isImageFieldCollection } from 'features/nodes/types/common';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { isCanvasOutputEvent } from 'features/nodes/util/graph/graphBuilderUtils';
-import { flushSync } from 'react-dom';
import type { ApiTagDescription } from 'services/api';
import { boardsApi } from 'services/api/endpoints/boards';
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO, S } from 'services/api/types';
import { getCategories, getListImagesUrl } from 'services/api/util';
import {
- $lastCanvasProgressImage,
$lastProgressEvent,
- $progressImages,
} from 'services/events/stores';
import type { Param0 } from 'tsafe';
import { objectEntries } from 'tsafe';
@@ -180,29 +176,29 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
await addImagesToGallery(data);
- // We expect only a single image in the canvas output
- const imageDTO = (await getResultImageDTOs(data))[0];
+ // // We expect only a single image in the canvas output
+ // const imageDTO = (await getResultImageDTOs(data))[0];
- if (!imageDTO) {
- return;
- }
+ // if (!imageDTO) {
+ // return;
+ // }
- flushSync(() => {
- dispatch(
- stagingAreaImageStaged({
- stagingAreaImage: { type: 'staged', sessionId: data.session_id, imageDTO, offsetX: 0, offsetY: 0 },
- })
- );
- });
+ // flushSync(() => {
+ // dispatch(
+ // stagingAreaImageStaged({
+ // stagingAreaImage: { type: 'staged', sessionId: data.session_id, imageDTO, offsetX: 0, offsetY: 0 },
+ // })
+ // );
+ // });
- const progressData = $progressImages.get()[data.session_id];
- if (progressData) {
- $progressImages.setKey(data.session_id, { ...progressData, isFinished: true, resultImage: imageDTO });
- } else {
- $progressImages.setKey(data.session_id, { sessionId: data.session_id, isFinished: true, resultImage: imageDTO });
- }
+ // const progressData = $progressImages.get()[data.session_id];
+ // if (progressData) {
+ // $progressImages.setKey(data.session_id, { ...progressData, isFinished: true, resultImage: imageDTO });
+ // } else {
+ // $progressImages.setKey(data.session_id, { sessionId: data.session_id, isFinished: true, resultImage: imageDTO });
+ // }
- $lastCanvasProgressImage.set(null);
+ // $lastCanvasProgressImage.set(null);
};
const handleOriginOther = async (data: S['InvocationCompleteEvent']) => {
diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
index 512b2aa317..72ec0feb95 100644
--- a/invokeai/frontend/web/src/services/events/setEventListeners.tsx
+++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
@@ -8,7 +8,6 @@ import { $bulkDownloadId } from 'app/store/nanostores/bulkDownloadId';
import { $queueId } from 'app/store/nanostores/queueId';
import type { AppStore } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { stagingAreaGenerationStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
$isInPublishFlow,
$outputNodeId,
@@ -397,8 +396,8 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
$nodeExecutionStates.setKey(clone.nodeId, clone);
});
if (data.origin === 'canvas') {
- store.dispatch(stagingAreaGenerationStarted({ sessionId: session_id }));
- $progressImages.setKey(session_id, { sessionId: session_id, isFinished: false });
+ // store.dispatch(stagingAreaGenerationStarted({ sessionId: session_id }));
+ // $progressImages.setKey(session_id, { sessionId: session_id, isFinished: false });
}
} else if (status === 'completed' || status === 'failed' || status === 'canceled') {
if (status === 'failed' && error_type) {
@@ -423,7 +422,7 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
}
// If the queue item is completed, failed, or cancelled, we want to clear the last progress event
$lastProgressEvent.set(null);
- $progressImages.setKey(session_id, undefined);
+ // $progressImages.setKey(session_id, undefined);
// When a validation run is completed, we want to clear the validation run batch ID & set the workflow as published
const validationRunData = $validationRunData.get();
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index f380019a5b..b6c8305bfe 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -1,6 +1,7 @@
import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
import type { ProgressImage } from 'features/nodes/types/common';
import { round } from 'lodash-es';
+import type { WritableAtom } from 'nanostores';
import { atom, computed, map } from 'nanostores';
import { useEffect, useState } from 'react';
import type { ImageDTO, S } from 'services/api/types';
@@ -20,16 +21,19 @@ export type ProgressAndResult = {
};
export const $progressImages = map({} as Record);
-type ProgressData = {
+export type ProgressData = {
sessionId: string;
progressEvent: S['InvocationProgressEvent'] | null;
progressImage: ProgressImage | null;
};
-export const $progressData = atom>({});
-
-export const useProgressData = (sessionId: string): ProgressData => {
- const [value, setValue] = useState({ sessionId, progressEvent: null, progressImage: null });
+export const useProgressData = (
+ $progressData: WritableAtom>,
+ sessionId: string
+): ProgressData => {
+ const [value, setValue] = useState(() => {
+ return $progressData.get()[sessionId] ?? { sessionId, progressEvent: null, progressImage: null };
+ });
useEffect(() => {
const unsub = $progressData.subscribe((data) => {
const progressData = data[sessionId];
@@ -41,12 +45,33 @@ export const useProgressData = (sessionId: string): ProgressData => {
return () => {
unsub();
};
- }, [sessionId]);
+ }, [$progressData, sessionId]);
return value;
};
-export const setProgress = (data: S['InvocationProgressEvent']) => {
+export const useHasProgressImage = (
+ $progressData: WritableAtom>,
+ sessionId: string
+): boolean => {
+ const [value, setValue] = useState(false);
+ useEffect(() => {
+ const unsub = $progressData.subscribe((data) => {
+ const progressData = data[sessionId];
+ setValue(Boolean(progressData?.progressImage));
+ });
+ return () => {
+ unsub();
+ };
+ }, [$progressData, sessionId]);
+
+ return value;
+};
+
+export const setProgress = (
+ $progressData: WritableAtom>,
+ data: S['InvocationProgressEvent']
+) => {
const progressData = $progressData.get();
const current = progressData[data.session_id];
if (current) {
@@ -71,7 +96,21 @@ export const setProgress = (data: S['InvocationProgressEvent']) => {
}
};
-export const clearProgressImage = (sessionId: string) => {
+export const clearProgressEvent = ($progressData: WritableAtom>, sessionId: string) => {
+ const progressData = $progressData.get();
+ const current = progressData[sessionId];
+ if (!current) {
+ return;
+ }
+ const next = { ...current };
+ next.progressEvent = null;
+ $progressData.set({
+ ...progressData,
+ [sessionId]: next,
+ });
+};
+
+export const clearProgressImage = ($progressData: WritableAtom>, sessionId: string) => {
const progressData = $progressData.get();
const current = progressData[sessionId];
if (!current) {
From 5dbc2a74a2140e81103353c3dcab66f6e8a28cff Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 3 Jun 2025 21:52:38 +1000
Subject: [PATCH 033/210] feat: canvas flow rework (wip)
---
.../components/CanvasMainPanelContent.tsx | 134 +++++++++---------
1 file changed, 68 insertions(+), 66 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 0bf7d226ce..17e23ff26f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,5 +1,5 @@
/* eslint-disable i18next/no-literal-string */
-import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import type { ButtonGroupProps, SystemStyleObject, TextProps } from '@invoke-ai/ui-library';
import {
Box,
Button,
@@ -26,6 +26,7 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppStore } from 'app/store/nanostores/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
+import { IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
@@ -63,7 +64,7 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useListAllQueueItemsQuery } from 'services/api/endpoints/queue';
import type { ImageDTO, S } from 'services/api/types';
import type { ProgressData } from 'services/events/stores';
-import { $socket, clearProgressEvent, setProgress, useHasProgressImage, useProgressData } from 'services/events/stores';
+import { $socket, clearProgressImage, setProgress, useHasProgressImage, useProgressData } from 'services/events/stores';
import type { Equals, Param0 } from 'tsafe';
import { assert, objectEntries } from 'tsafe';
@@ -464,9 +465,6 @@ const StagingArea = memo(() => {
if (data.status === 'in_progress' && autoSwitch) {
onSelectItemId(data.item_id);
}
- if (data.status === 'completed' || data.status === 'canceled' || data.status === 'failed') {
- clearProgressEvent(ctx.$progressData, data.session_id);
- }
};
socket.on('queue_item_status_changed', onQueueItemStatusChanged);
@@ -512,6 +510,7 @@ const StagingArea = memo(() => {
{selectedItem && selectedItemIndex !== null && (
{
{items.map((item, i) => (
`queue-item-status-card-${item_id}`;
@@ -607,7 +606,6 @@ type QueueItemStatusCardMiniProps = {
const QueueItemCard = memo(
({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch, size }: QueueItemStatusCardMiniProps) => {
const ctx = useStagingContext();
- const [isImageLoaded, setIsImageLoaded] = useState(false);
const hasProgressImage = useHasProgressImage(ctx.$progressData, item.session_id);
const outputImageName = useMemo(() => {
@@ -631,24 +629,6 @@ const QueueItemCard = memo(
const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
- const syncIsReady = useCallback(async () => {
- if (!imageDTO) {
- setIsImageLoaded(false);
- return;
- }
- try {
- const _ = await loadImage(size === 'mini' ? imageDTO.thumbnail_url : imageDTO.image_url, true);
- setIsImageLoaded(true);
- return;
- } catch {
- setIsImageLoaded(false);
- }
- }, [imageDTO, size]);
-
- useEffect(() => {
- syncIsReady();
- }, [syncIsReady]);
-
const onClick = useCallback(() => {
onSelectItemId(item.item_id);
}, [item.item_id, onSelectItemId]);
@@ -657,22 +637,38 @@ const QueueItemCard = memo(
onChangeAutoSwitch(item.status === 'in_progress');
}, [item.status, onChangeAutoSwitch]);
- if (imageDTO && isImageLoaded) {
+ const syncIsReady = useCallback(async () => {
+ if (!imageDTO) {
+ return;
+ }
+ try {
+ await loadImage(imageDTO.image_url, true);
+ clearProgressImage(ctx.$progressData, item.session_id);
+ return;
+ } catch {
+ // noop
+ }
+ }, [ctx.$progressData, imageDTO, item.session_id]);
+
+ useEffect(() => {
+ syncIsReady();
+ }, [syncIsReady]);
+
+ if (imageDTO && !hasProgressImage) {
return (
-
- {`#${number}`}
+
+ {size === 'mini' && }
{size === 'full' && (
-
-
-
+ <>
+
+
+ >
)}
);
@@ -689,15 +685,13 @@ const QueueItemCard = memo(
onDoubleClick={onDoubleClick}
>
- {`#${number}`}
- {size === 'full' && }
+ {size === 'mini' && }
+ {size === 'full' && (
+ <>
+
+
+ >
+ )}
);
}
@@ -712,14 +706,19 @@ const getMessage = (data: S['InvocationProgressEvent']) => {
return message;
};
-const ProgressMessage = memo(({ session_id }: { session_id: string }) => {
+const ItemNumber = memo(({ number, ...rest }: { number: number } & TextProps) => {
+ return {`#${number}`};
+});
+ItemNumber.displayName = 'ItemNumber';
+
+const ProgressMessage = memo(({ session_id, ...rest }: { session_id: string } & TextProps) => {
const { $progressData } = useStagingContext();
const { progressEvent } = useProgressData($progressData, session_id);
if (!progressEvent) {
return null;
}
return (
-
+
{getMessage(progressEvent)}
);
@@ -755,7 +754,14 @@ const InProgressContent = memo(({ item }: { item: S['SessionQueueItem'] }) => {
if (progressImage) {
return (
<>
-
+
>
);
@@ -773,11 +779,7 @@ const InProgressContent = memo(({ item }: { item: S['SessionQueueItem'] }) => {
}
if (item.status === 'completed') {
- return (
-
- Unable to get image
-
- );
+ return ;
}
assert>(false);
});
@@ -809,7 +811,7 @@ const ProgressCircle = memo(({ data }: { data?: S['InvocationProgressEvent'] | n
});
ProgressCircle.displayName = 'ProgressCircle';
-const ImageActions = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
+const ImageActions = memo(({ imageDTO, ...rest }: { imageDTO: ImageDTO } & ButtonGroupProps) => {
const { getState, dispatch } = useAppStore();
const vary = useCallback(() => {
@@ -842,7 +844,7 @@ const ImageActions = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
});
}, [dispatch, getState, imageDTO]);
return (
-
+
Vary
From e16414b452680cfd26c80bac53ec57810806b5a7 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 11:44:37 +1000
Subject: [PATCH 034/210] tidy(ui): app layout components
---
.../components/CanvasRightPanel.tsx | 4 +-
.../components/CanvasRightPanelStacked.tsx | 4 +-
.../Boards/BoardsList/BoardsList.tsx | 7 +-
.../Boards/BoardsList/BoardsListWrapper.tsx | 7 +-
.../Boards/BoardsList/BoardsSearch.tsx | 7 +-
.../Boards/BoardsListSortControls.tsx | 7 +-
.../Boards/BoardsSettingsPopover.tsx | 7 +-
.../components/BoardsListPanelContent.tsx | 25 +++
.../features/gallery/components/Gallery.tsx | 9 +-
.../components/GalleryPanelContent.tsx | 159 ------------------
.../GallerySettingsPopover.tsx | 7 +-
.../gallery/components/GalleryTopBar.tsx | 67 ++++++++
.../components/GalleryUploadButton.tsx | 10 +-
.../src/features/ui/components/AppContent.tsx | 60 +++----
.../ui/components/FloatingGalleryButton.tsx | 28 ---
...ttons.tsx => FloatingLeftPanelButtons.tsx} | 87 +++++-----
.../components/FloatingRightPanelButtons.tsx | 28 +++
.../ui/components/LeftPanelContent.tsx | 25 +++
.../ui/components/MainPanelContent.tsx | 33 ++++
.../ui/components/RightPanelContent.tsx | 94 +++++++++++
20 files changed, 379 insertions(+), 296 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
delete mode 100644 invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
delete mode 100644 invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx
rename invokeai/frontend/web/src/features/ui/components/{FloatingParametersPanelButtons.tsx => FloatingLeftPanelButtons.tsx} (71%)
create mode 100644 invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
index c1bca93a26..4e1b3d2c69 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
@@ -9,7 +9,7 @@ import { selectEntityCountActive } from 'features/controlLayers/store/selectors'
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
import type { DndTargetState } from 'features/dnd/types';
-import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
+import RightPanelContent from 'features/gallery/components/GalleryTopBar';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
@@ -61,7 +61,7 @@ export const CanvasRightPanel = memo(() => {
-
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx
index 6ca1f26ed3..4806728f27 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx
@@ -9,7 +9,7 @@ import { selectEntityCountActive } from 'features/controlLayers/store/selectors'
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
import type { DndTargetState } from 'features/dnd/types';
-import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
+import RightPanelContent from 'features/gallery/components/GalleryTopBar';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
@@ -49,7 +49,7 @@ export const CanvasRightPanelStacked = memo(() => {
return (
-
+
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx
index fbe21e2218..3b48882b5c 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx
@@ -8,7 +8,7 @@ import {
selectSelectedBoardId,
} from 'features/gallery/store/gallerySelectors';
import { selectAllowPrivateBoards } from 'features/system/store/configSelectors';
-import { useMemo } from 'react';
+import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold } from 'react-icons/pi';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
@@ -21,7 +21,7 @@ type Props = {
isPrivate: boolean;
};
-export const BoardsList = ({ isPrivate }: Props) => {
+export const BoardsList = memo(({ isPrivate }: Props) => {
const { t } = useTranslation();
const selectedBoardId = useAppSelector(selectSelectedBoardId);
const boardSearchText = useAppSelector(selectBoardSearchText);
@@ -118,4 +118,5 @@ export const BoardsList = ({ isPrivate }: Props) => {
);
-};
+});
+BoardsList.displayName = 'BoardsList';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsListWrapper.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsListWrapper.tsx
index ca944e8981..4b6c403020 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsListWrapper.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsListWrapper.tsx
@@ -17,7 +17,7 @@ const overlayScrollbarsStyles: CSSProperties = {
width: '100%',
};
-const BoardsListWrapper = () => {
+export const BoardsListWrapper = memo(() => {
const allowPrivateBoards = useAppSelector(selectAllowPrivateBoards);
const [os, osRef] = useState(null);
useEffect(() => {
@@ -54,5 +54,6 @@ const BoardsListWrapper = () => {
);
-};
-export default memo(BoardsListWrapper);
+});
+
+BoardsListWrapper.displayName = 'BoardsListWrapper';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
index f2ea242240..5c578a5539 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
@@ -7,7 +7,7 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
-const BoardsSearch = () => {
+export const BoardsSearch = memo(() => {
const dispatch = useAppDispatch();
const boardSearchText = useAppSelector(selectBoardSearchText);
const { t } = useTranslation();
@@ -62,6 +62,5 @@ const BoardsSearch = () => {
)}
);
-};
-
-export default memo(BoardsSearch);
+});
+BoardsSearch.displayName = 'BoardsSearch';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsListSortControls.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsListSortControls.tsx
index 47514b0b60..e4ed79189b 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsListSortControls.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsListSortControls.tsx
@@ -3,7 +3,7 @@ import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectBoardsListOrderBy, selectBoardsListOrderDir } from 'features/gallery/store/gallerySelectors';
import { boardsListOrderByChanged, boardsListOrderDirChanged } from 'features/gallery/store/gallerySlice';
-import { useCallback, useMemo } from 'react';
+import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
@@ -15,7 +15,7 @@ const zDirection = z.enum(['ASC', 'DESC']);
type Direction = z.infer;
const isDirection = (v: unknown): v is Direction => zDirection.safeParse(v).success;
-export const BoardsListSortControls = () => {
+export const BoardsListSortControls = memo(() => {
const { t } = useTranslation();
const orderBy = useAppSelector(selectBoardsListOrderBy);
@@ -83,4 +83,5 @@ export const BoardsListSortControls = () => {
);
-};
+});
+BoardsListSortControls.displayName = 'BoardsListSortControls';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsSettingsPopover.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsSettingsPopover.tsx
index 9fa890bd6b..ac338a66a1 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsSettingsPopover.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsSettingsPopover.tsx
@@ -17,7 +17,7 @@ import { PiGearSixFill } from 'react-icons/pi';
import { BoardsListSortControls } from './BoardsListSortControls';
-const BoardsSettingsPopover = () => {
+export const BoardsSettingsPopover = memo(() => {
const { t } = useTranslation();
return (
@@ -48,6 +48,5 @@ const BoardsSettingsPopover = () => {
);
-};
-
-export default memo(BoardsSettingsPopover);
+});
+BoardsSettingsPopover.displayName = 'BoardsSettingsPopover';
diff --git a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
new file mode 100644
index 0000000000..b456cbaaa7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
@@ -0,0 +1,25 @@
+import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
+import { Box, Collapse, Divider, Flex } from '@invoke-ai/ui-library';
+import { BoardsListWrapper } from 'features/gallery/components/Boards/BoardsList/BoardsListWrapper';
+import { BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
+import type { CSSProperties } from 'react';
+import { memo } from 'react';
+
+const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
+
+export const BoardsListPanelContent = memo(
+ ({ boardSearchDisclosure }: { boardSearchDisclosure: UseDisclosureReturn }) => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
+BoardsListPanelContent.displayName = 'BoardsListPanelContent';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
index 7836b99c68..785bc53af8 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
@@ -18,12 +18,12 @@ import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useG
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import type { CSSProperties } from 'react';
-import { useCallback } from 'react';
+import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiMagnifyingGlassBold } from 'react-icons/pi';
import { useBoardName } from 'services/api/hooks/useBoardName';
-import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
+import { GallerySettingsPopover } from './GallerySettingsPopover/GallerySettingsPopover';
import { GalleryUploadButton } from './GalleryUploadButton';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
import { GalleryPagination } from './ImageGrid/GalleryPagination';
@@ -46,7 +46,7 @@ const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '10
const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView);
const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
-export const Gallery = () => {
+export const Gallery = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const galleryView = useAppSelector(selectGalleryView);
@@ -116,4 +116,5 @@ export const Gallery = () => {
);
-};
+});
+Gallery.displayName = 'Gallery';
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx
deleted file mode 100644
index ef28a923ac..0000000000
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import {
- Box,
- Button,
- Collapse,
- Divider,
- Flex,
- IconButton,
- type SystemStyleObject,
- useDisclosure,
-} from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
-import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
-import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
-import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
-import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
-import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
-import { usePanel, type UsePanelOptions } from 'features/ui/hooks/usePanel';
-import type { CSSProperties } from 'react';
-import { memo, useCallback, useMemo, useRef } from 'react';
-import { useTranslation } from 'react-i18next';
-import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
-import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
-import { Panel, PanelGroup } from 'react-resizable-panels';
-
-import BoardsListWrapper from './Boards/BoardsList/BoardsListWrapper';
-import BoardsSearch from './Boards/BoardsList/BoardsSearch';
-import BoardsSettingsPopover from './Boards/BoardsSettingsPopover';
-import { Gallery } from './Gallery';
-
-const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
-
-const FOCUS_REGION_STYLES: SystemStyleObject = {
- width: 'full',
- height: 'full',
- position: 'relative',
- flexDirection: 'column',
- display: 'flex',
-};
-
-const GalleryPanelContent = () => {
- const { t } = useTranslation();
- const boardSearchText = useAppSelector(selectBoardSearchText);
- const dispatch = useAppDispatch();
- const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
- const imperativePanelGroupRef = useRef(null);
- const sessionType = useAppSelector(selectCanvasSessionType);
-
- const boardsListPanelOptions = useMemo(
- () => ({
- id: 'boards-list-panel',
- minSizePx: 128,
- defaultSizePx: 256,
- imperativePanelGroupRef,
- panelGroupDirection: 'vertical',
- }),
- []
- );
- const boardsListPanel = usePanel(boardsListPanelOptions);
-
- const galleryPanelOptions = useMemo(
- () => ({
- id: 'gallery-panel',
- minSizePx: 128,
- defaultSizePx: 256,
- imperativePanelGroupRef,
- panelGroupDirection: 'vertical',
- }),
- []
- );
- const galleryPanel = usePanel(galleryPanelOptions);
-
- const canvasLayersPanelOptions = useMemo(
- () => ({
- id: 'canvas-layers-panel',
- minSizePx: 128,
- defaultSizePx: 256,
- imperativePanelGroupRef,
- panelGroupDirection: 'vertical',
- }),
- []
- );
- const canvasLayersPanel = usePanel(canvasLayersPanelOptions);
-
- const handleClickBoardSearch = useCallback(() => {
- if (boardSearchText.length) {
- dispatch(boardSearchTextChanged(''));
- }
- boardSearchDisclosure.onToggle();
- boardsListPanel.expand();
- }, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
-
- return (
-
-
-
- : }
- >
- {boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
-
-
-
-
-
-
-
- }
- colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {sessionType === 'advanced' && (
- <>
-
-
-
-
-
-
- >
- )}
-
-
- );
-};
-
-export default memo(GalleryPanelContent);
diff --git a/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover/GallerySettingsPopover.tsx b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover/GallerySettingsPopover.tsx
index a5d0861486..7ca1f23c58 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover/GallerySettingsPopover.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover/GallerySettingsPopover.tsx
@@ -8,7 +8,7 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiGearSixFill } from 'react-icons/pi';
-const GallerySettingsPopover = () => {
+export const GallerySettingsPopover = memo(() => {
const { t } = useTranslation();
return (
@@ -37,6 +37,5 @@ const GallerySettingsPopover = () => {
);
-};
-
-export default memo(GallerySettingsPopover);
+});
+GallerySettingsPopover.displayName = 'GallerySettingsPopover';
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
new file mode 100644
index 0000000000..e84e733c58
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
@@ -0,0 +1,67 @@
+import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
+import { Button, Flex, IconButton } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { BoardsSettingsPopover } from 'features/gallery/components/Boards/BoardsSettingsPopover';
+import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
+import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
+import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
+import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
+
+export const GalleryTopBar = memo(
+ ({
+ boardsListPanel,
+ boardSearchDisclosure,
+ }: {
+ boardsListPanel: UsePanelReturn;
+ boardSearchDisclosure: UseDisclosureReturn;
+ }) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const boardSearchText = useAppSelector(selectBoardSearchText);
+
+ const onClickBoardSearch = useCallback(() => {
+ if (boardSearchText.length) {
+ dispatch(boardSearchTextChanged(''));
+ }
+ boardSearchDisclosure.onToggle();
+ boardsListPanel.expand();
+ }, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
+
+ return (
+
+
+ : }
+ >
+ {boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
+
+
+
+
+
+
+
+ }
+ colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
+ />
+
+
+ );
+ }
+);
+GalleryTopBar.displayName = 'GalleryTopBar';
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryUploadButton.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryUploadButton.tsx
index ca6cc78051..af3b980947 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryUploadButton.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryUploadButton.tsx
@@ -1,10 +1,13 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { t } from 'i18next';
+import { memo } from 'react';
import { PiUploadBold } from 'react-icons/pi';
-export const GalleryUploadButton = () => {
- const uploadApi = useImageUploadButton({ allowMultiple: true });
+const UPLOAD_OPTIONS: Parameters[0] = { allowMultiple: true };
+
+export const GalleryUploadButton = memo(() => {
+ const uploadApi = useImageUploadButton(UPLOAD_OPTIONS);
return (
<>
{
>
);
-};
+});
+GalleryUploadButton.displayName = 'GalleryUploadButton';
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 6704ab4d00..83f220e4a6 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -1,16 +1,15 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
-import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
-import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
import QueueControls from 'features/queue/components/QueueControls';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
-import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
-import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
+import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
+import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
+import { RightPanelContent } from 'features/ui/components/RightPanelContent';
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
import QueueTab from 'features/ui/components/tabs/QueueTab';
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
@@ -30,6 +29,8 @@ import type { CSSProperties } from 'react';
import { memo, useMemo, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
+import type { Equals } from 'tsafe';
+import { assert } from 'tsafe';
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
import { VerticalResizeHandle } from './tabs/ResizeHandle';
@@ -128,26 +129,21 @@ export const AppContent = memo(() => {
>
{withLeftPanel && (
<>
-
-
-
-
-
-
-
+
+
>
)}
- {withLeftPanel && }
- {withRightPanel && }
+ {withLeftPanel && }
+ {withRightPanel && }
{withRightPanel && (
<>
-
+
>
@@ -156,35 +152,21 @@ export const AppContent = memo(() => {
);
});
-
AppContent.displayName = 'AppContent';
-const RightPanelContent = memo(() => {
- const tab = useAppSelector(selectActiveTab);
- const sessionType = useAppSelector(selectCanvasSessionType);
-
- if (tab === 'upscaling' || tab === 'workflows' || tab === 'canvas') {
- return ;
- }
-
- return null;
-});
-RightPanelContent.displayName = 'RightPanelContent';
-
const LeftPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
- if (tab === 'canvas') {
- return ;
- }
- if (tab === 'upscaling') {
- return ;
- }
- if (tab === 'workflows') {
- return ;
- }
-
- return null;
+ return (
+
+
+
+ {tab === 'canvas' && }
+ {tab === 'upscaling' && }
+ {tab === 'workflows' && }
+
+
+ );
});
LeftPanelContent.displayName = 'LeftPanelContent';
@@ -207,6 +189,6 @@ const MainPanelContent = memo(() => {
return ;
}
- return null;
+ assert>(false);
});
MainPanelContent.displayName = 'MainPanelContent';
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx
deleted file mode 100644
index 20c38b2d55..0000000000
--- a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
-import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
-import { memo } from 'react';
-import { useTranslation } from 'react-i18next';
-import { PiImagesSquareBold } from 'react-icons/pi';
-
-type Props = {
- panelApi: UsePanelReturn;
-};
-
-const FloatingGalleryButton = (props: Props) => {
- const { t } = useTranslation();
-
- return (
-
-
- }
- h={48}
- />
-
-
- );
-};
-
-export default memo(FloatingGalleryButton);
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
similarity index 71%
rename from invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
rename to invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
index ddb77c6918..bbd10d868c 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
@@ -2,15 +2,14 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke';
-import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
-import { memo, useMemo } from 'react';
+import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiCircleNotchBold,
@@ -23,60 +22,48 @@ import {
} from 'react-icons/pi';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
-type Props = {
- togglePanel: () => void;
-};
-
-const FloatingSidePanelButtons = ({ togglePanel }: Props) => {
- const { t } = useTranslation();
+export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) => {
const tab = useAppSelector(selectActiveTab);
- const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
- const sessionType = useAppSelector(selectCanvasSessionType);
+ const session = useAppSelector(selectCanvasSession);
return (
- {tab === 'canvas' && sessionType === 'advanced' && (
+ {tab === 'canvas' && session?.type === 'advanced' && (
)}
-
- }
- flexGrow={1}
- />
-
+
- {/* Show the cancel all except current button instead of cancel and clear all when it is disabled */}
- {isCancelAndClearAllEnabled && }
- {!isCancelAndClearAllEnabled && }
+
);
-};
+});
-export default memo(FloatingSidePanelButtons);
+FloatingLeftPanelButtons.displayName = 'FloatingLeftPanelButtons';
+
+const ToggleLeftPanelButton = memo((props: { onToggle: () => void }) => {
+ const { t } = useTranslation();
+ return (
+
+ }
+ flexGrow={1}
+ />
+
+ );
+});
+ToggleLeftPanelButton.displayName = 'ToggleLeftPanelButton';
const InvokeIconButton = memo(() => {
const { t } = useTranslation();
const queue = useInvoke();
const shift = useShiftModifier();
- const { data: queueStatus } = useGetQueueStatusQuery();
-
- const queueButtonIcon = useMemo(() => {
- const isProcessing = (queueStatus?.queue.in_progress ?? 0) > 0;
- if (!queue.isDisabled && isProcessing) {
- return ;
- }
- if (shift) {
- return ;
- }
- return ;
- }, [queue.isDisabled, queueStatus?.queue.in_progress, shift]);
return (
@@ -85,7 +72,7 @@ const InvokeIconButton = memo(() => {
onClick={shift ? queue.enqueueFront : queue.enqueueBack}
isLoading={queue.isLoading}
isDisabled={queue.isDisabled}
- icon={queueButtonIcon}
+ icon={}
colorScheme="invokeYellow"
flexGrow={1}
/>
@@ -94,6 +81,30 @@ const InvokeIconButton = memo(() => {
});
InvokeIconButton.displayName = 'InvokeIconButton';
+const InvokeIconButtonIcon = memo(() => {
+ const shift = useShiftModifier();
+ const queue = useInvoke();
+ const { isProcessing } = useGetQueueStatusQuery(undefined, {
+ selectFromResult: ({ data }) => {
+ if (!data) {
+ return { isProcessing: false };
+ }
+ return { isProcessing: data.queue.in_progress > 0 };
+ },
+ });
+
+ if (!queue.isDisabled && isProcessing) {
+ return ;
+ }
+
+ if (shift) {
+ return ;
+ }
+
+ return ;
+});
+InvokeIconButtonIcon.displayName = 'InvokeIconButtonIcon';
+
const CancelCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx
new file mode 100644
index 0000000000..3c33bca265
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx
@@ -0,0 +1,28 @@
+import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiImagesSquareBold } from 'react-icons/pi';
+
+export const FloatingRightPanelButtons = memo((props: { onToggle: () => void }) => {
+ return (
+
+
+
+ );
+});
+FloatingRightPanelButtons.displayName = 'FloatingRightPanelButtons';
+
+const ToggleRightPanelButton = memo((props: { onToggle: () => void }) => {
+ const { t } = useTranslation();
+ return (
+
+ }
+ h={48}
+ />
+
+ );
+});
+ToggleRightPanelButton.displayName = 'ToggleRightPanelButton';
diff --git a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
new file mode 100644
index 0000000000..873e803186
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
@@ -0,0 +1,25 @@
+import { Box, Flex } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
+import QueueControls from 'features/queue/components/QueueControls';
+import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
+import { memo } from 'react';
+
+import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
+
+const LeftPanelContent = memo(() => {
+ const tab = useAppSelector(selectActiveTab);
+
+ return (
+
+
+
+ {tab === 'canvas' && }
+ {tab === 'upscaling' && }
+ {tab === 'workflows' && }
+
+
+ );
+});
+LeftPanelContent.displayName = 'LeftPanelContent';
diff --git a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
new file mode 100644
index 0000000000..58ac5a4558
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
@@ -0,0 +1,33 @@
+import { useAppSelector } from 'app/store/storeHooks';
+import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
+import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
+import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
+import QueueTab from 'features/ui/components/tabs/QueueTab';
+import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
+import { memo } from 'react';
+import type { Equals } from 'tsafe';
+import { assert } from 'tsafe';
+
+const MainPanelContent = memo(() => {
+ const tab = useAppSelector(selectActiveTab);
+
+ if (tab === 'canvas') {
+ return ;
+ }
+ if (tab === 'upscaling') {
+ return ;
+ }
+ if (tab === 'workflows') {
+ return ;
+ }
+ if (tab === 'models') {
+ return ;
+ }
+ if (tab === 'queue') {
+ return ;
+ }
+
+ assert>(false);
+});
+MainPanelContent.displayName = 'MainPanelContent';
diff --git a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
new file mode 100644
index 0000000000..049e52f901
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
@@ -0,0 +1,94 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { useDisclosure } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
+import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
+import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
+import { Gallery } from 'features/gallery/components/Gallery';
+import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
+import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
+import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
+import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
+import { usePanel } from 'features/ui/hooks/usePanel';
+import { memo, useMemo, useRef } from 'react';
+import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
+import { Panel, PanelGroup } from 'react-resizable-panels';
+
+const FOCUS_REGION_STYLES: SystemStyleObject = {
+ width: 'full',
+ height: 'full',
+ position: 'relative',
+ flexDirection: 'column',
+ display: 'flex',
+};
+
+export const RightPanelContent = memo(() => {
+ const boardSearchText = useAppSelector(selectBoardSearchText);
+ const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
+ const imperativePanelGroupRef = useRef(null);
+ const session = useAppSelector(selectCanvasSession);
+
+ const boardsListPanelOptions = useMemo(
+ () => ({
+ id: 'boards-list-panel',
+ minSizePx: 128,
+ defaultSizePx: 256,
+ imperativePanelGroupRef,
+ panelGroupDirection: 'vertical',
+ }),
+ []
+ );
+ const boardsListPanel = usePanel(boardsListPanelOptions);
+
+ const galleryPanelOptions = useMemo(
+ () => ({
+ id: 'gallery-panel',
+ minSizePx: 128,
+ defaultSizePx: 256,
+ imperativePanelGroupRef,
+ panelGroupDirection: 'vertical',
+ }),
+ []
+ );
+ const galleryPanel = usePanel(galleryPanelOptions);
+
+ const canvasLayersPanelOptions = useMemo(
+ () => ({
+ id: 'canvas-layers-panel',
+ minSizePx: 128,
+ defaultSizePx: 256,
+ imperativePanelGroupRef,
+ panelGroupDirection: 'vertical',
+ }),
+ []
+ );
+ const canvasLayersPanel = usePanel(canvasLayersPanelOptions);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {session?.type === 'advanced' && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+});
+RightPanelContent.displayName = 'RightPanelContent';
From 0af20b03e58992f0e2c6315a37c2f82f3207d274 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 12:32:43 +1000
Subject: [PATCH 035/210] feat(api): remove status from list all queue items
query
---
invokeai/app/api/routers/session_queue.py | 2 --
invokeai/app/services/session_queue/session_queue_base.py | 1 -
.../app/services/session_queue/session_queue_sqlite.py | 7 -------
3 files changed, 10 deletions(-)
diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py
index 7d6b8ac93c..660d09728c 100644
--- a/invokeai/app/api/routers/session_queue.py
+++ b/invokeai/app/api/routers/session_queue.py
@@ -99,14 +99,12 @@ async def list_queue_items(
)
async def list_all_queue_items(
queue_id: str = Path(description="The queue id to perform this operation on"),
- status: Optional[QUEUE_ITEM_STATUS] = Query(default=None, description="The status of items to fetch"),
destination: Optional[str] = Query(default=None, description="The destination of queue items to fetch"),
) -> list[SessionQueueItem]:
"""Gets all queue items"""
return ApiDependencies.invoker.services.session_queue.list_all_queue_items(
queue_id=queue_id,
- status=status,
destination=destination,
)
diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py
index 187fa2e815..a2aa844742 100644
--- a/invokeai/app/services/session_queue/session_queue_base.py
+++ b/invokeai/app/services/session_queue/session_queue_base.py
@@ -135,7 +135,6 @@ class SessionQueueBase(ABC):
def list_all_queue_items(
self,
queue_id: str,
- status: Optional[QUEUE_ITEM_STATUS] = None,
destination: Optional[str] = None,
) -> list[SessionQueueItem]:
"""Gets all queue items that match the given parameters"""
diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py
index 81239338ec..4ba16fb85a 100644
--- a/invokeai/app/services/session_queue/session_queue_sqlite.py
+++ b/invokeai/app/services/session_queue/session_queue_sqlite.py
@@ -588,7 +588,6 @@ class SqliteSessionQueue(SessionQueueBase):
def list_all_queue_items(
self,
queue_id: str,
- status: Optional[QUEUE_ITEM_STATUS] = None,
destination: Optional[str] = None,
) -> list[SessionQueueItem]:
"""Gets all queue items that match the given parameters"""
@@ -600,12 +599,6 @@ class SqliteSessionQueue(SessionQueueBase):
"""
params: list[Union[str, int]] = [queue_id]
- if status is not None:
- query += """--sql
- AND status = ?
- """
- params.append(status)
-
if destination is not None:
query += """---sql
AND destination = ?
From 84f70942e7188bcdd7b9c37cbc61e789c0272c97 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 12:32:54 +1000
Subject: [PATCH 036/210] chore(ui): typegen
---
invokeai/frontend/web/src/services/api/schema.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index 65f2398d53..d44246571e 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -24407,8 +24407,6 @@ export interface operations {
list_all_queue_items: {
parameters: {
query?: {
- /** @description The status of items to fetch */
- status?: ("pending" | "in_progress" | "completed" | "failed" | "canceled") | null;
/** @description The destination of queue items to fetch */
destination?: string | null;
};
From db4220fb205030e2347c206461d341e1d0db64fb Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 12:33:00 +1000
Subject: [PATCH 037/210] feat: canvas flow rework (wip)
---
.../components/CanvasMainPanelContent.tsx | 473 ++++++++++--------
1 file changed, 262 insertions(+), 211 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 17e23ff26f..6692ee1c9e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -313,13 +313,31 @@ const GenerateWithStartingImageAndInpaintMask = memo(() => {
});
GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
-const scrollIndicatorSx = {
+const scrollIndicatorBaseSx = {
opacity: 0,
+ position: 'absolute',
+ w: 16,
+ h: 'full',
+ transitionProperty: 'opacity',
+ transitionDuration: '0.3s',
+ pointerEvents: 'none',
'&[data-visible="true"]': {
opacity: 1,
},
} satisfies SystemStyleObject;
+const scrollIndicatorLeftSx = {
+ ...scrollIndicatorBaseSx,
+ left: 0,
+ bg: 'linear-gradient(to right, var(--invoke-colors-base-900), transparent)',
+} satisfies SystemStyleObject;
+
+const scrollIndicatorRightSx = {
+ ...scrollIndicatorBaseSx,
+ right: 0,
+ bg: 'linear-gradient(to left, var(--invoke-colors-base-900), transparent)',
+} satisfies SystemStyleObject;
+
type StagingContextValue = {
session:
| {
@@ -341,72 +359,11 @@ const useStagingContext = () => {
return ctx;
};
-const StagingArea = memo(() => {
- const ctx = useStagingContext();
- const dispatch = useAppDispatch();
- const [selectedItemId, setSelectedItemId] = useState(null);
- const [autoSwitch, setAutoSwitch] = useState(true);
- const [canScrollLeft, setCanScrollLeft] = useState(false);
- const [canScrollRight, setCanScrollRight] = useState(false);
- const scrollableRef = useRef(null);
- const { data } = useListAllQueueItemsQuery({ destination: ctx.session.id });
- const items = useMemo(() => data?.filter(({ status }) => status !== 'canceled') ?? EMPTY_ARRAY, [data]);
- const selectedItem = useMemo(
- () =>
- items.length > 0 && selectedItemId !== null ? items.find(({ item_id }) => item_id === selectedItemId) : null,
- [items, selectedItemId]
- );
- const selectedItemIndex = useMemo(
- () =>
- items.length > 0 && selectedItemId !== null ? items.findIndex(({ item_id }) => item_id === selectedItemId) : null,
- [items, selectedItemId]
- );
-
- const startOver = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'simple' }));
- }, [dispatch]);
-
- useEffect(() => {
- const el = scrollableRef.current;
- if (!el) {
- return;
- }
- const onScroll = () => {
- const { scrollLeft, scrollWidth, clientWidth } = el;
- setCanScrollLeft(scrollLeft > 0);
- setCanScrollRight(scrollLeft + clientWidth < scrollWidth);
- };
- el.addEventListener('scroll', onScroll);
- const observer = new ResizeObserver(onScroll);
- observer.observe(el);
- return () => {
- el.removeEventListener('scroll', onScroll);
- observer.disconnect();
- };
- }, []);
-
- const onSelectItemId = useCallback((item_id: number | null) => {
- setSelectedItemId(item_id);
- if (item_id !== null) {
- document.getElementById(getCardId(item_id))?.scrollIntoView();
- }
- }, []);
-
- const onChangeAutoSwitch = useCallback((autoSwitch: boolean) => {
- setAutoSwitch(autoSwitch);
- }, []);
-
- useEffect(() => {
- if (items.length === 0) {
- onSelectItemId(null);
- return;
- }
- if (selectedItemId === null && items.length > 0) {
- onSelectItemId(items[0]?.item_id ?? null);
- return;
- }
- }, [items, onSelectItemId, selectedItem, selectedItemId]);
-
+const useStagingAreaKeyboardNav = (
+ items: S['SessionQueueItem'][],
+ selectedItemId: number | null,
+ onSelectItemId: (item_id: number) => void
+) => {
const onNext = useCallback(() => {
if (selectedItemId === null) {
return;
@@ -451,6 +408,78 @@ const StagingArea = memo(() => {
useHotkeys('right', onNext, { preventDefault: true });
useHotkeys('meta+left', onFirst, { preventDefault: true });
useHotkeys('meta+right', onLast, { preventDefault: true });
+};
+
+const LIST_ALL_OPTIONS = {
+ selectFromResult: ({ data }) => {
+ if (!data) {
+ return { items: EMPTY_ARRAY };
+ }
+ return { items: data.filter(({ status }) => status !== 'canceled') };
+ },
+} satisfies Parameters[1];
+
+const StagingArea = memo(() => {
+ const ctx = useStagingContext();
+ const [selectedItemId, setSelectedItemId] = useState(null);
+ const [autoSwitch, setAutoSwitch] = useState(true);
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
+ const [canScrollRight, setCanScrollRight] = useState(false);
+ const scrollableRef = useRef(null);
+ const { items } = useListAllQueueItemsQuery({ destination: ctx.session.id }, LIST_ALL_OPTIONS);
+ const selectedItem = useMemo(
+ () =>
+ items.length > 0 && selectedItemId !== null ? items.find(({ item_id }) => item_id === selectedItemId) : null,
+ [items, selectedItemId]
+ );
+ const selectedItemIndex = useMemo(
+ () =>
+ items.length > 0 && selectedItemId !== null ? items.findIndex(({ item_id }) => item_id === selectedItemId) : null,
+ [items, selectedItemId]
+ );
+
+ useEffect(() => {
+ const el = scrollableRef.current;
+ if (!el) {
+ return;
+ }
+ const onScroll = () => {
+ const { scrollLeft, scrollWidth, clientWidth } = el;
+ setCanScrollLeft(scrollLeft > 0);
+ setCanScrollRight(scrollLeft + clientWidth < scrollWidth);
+ };
+ el.addEventListener('scroll', onScroll);
+ const observer = new ResizeObserver(onScroll);
+ observer.observe(el);
+ return () => {
+ el.removeEventListener('scroll', onScroll);
+ observer.disconnect();
+ };
+ }, []);
+
+ const onSelectItemId = useCallback((item_id: number | null) => {
+ setSelectedItemId(item_id);
+ if (item_id !== null) {
+ document.getElementById(getCardId(item_id))?.scrollIntoView();
+ }
+ }, []);
+
+ useStagingAreaKeyboardNav(items, selectedItemId, onSelectItemId);
+
+ const onChangeAutoSwitch = useCallback((autoSwitch: boolean) => {
+ setAutoSwitch(autoSwitch);
+ }, []);
+
+ useEffect(() => {
+ if (items.length === 0) {
+ onSelectItemId(null);
+ return;
+ }
+ if (selectedItemId === null && items.length > 0) {
+ onSelectItemId(items[0]?.item_id ?? null);
+ return;
+ }
+ }, [items, onSelectItemId, selectedItem, selectedItemId]);
const socket = useStore($socket);
useEffect(() => {
@@ -497,29 +526,17 @@ const StagingArea = memo(() => {
return (
-
-
- Generations
-
-
-
- Start Over
-
-
+
{selectedItem && selectedItemIndex !== null && (
-
)}
- {!selectedItem && No queued generations}
+ {!selectedItem && No generation selected}
Auto-switch
@@ -529,48 +546,46 @@ const StagingArea = memo(() => {
{items.map((item, i) => (
-
))}
-
-
+
+
);
});
StagingArea.displayName = 'StagingArea';
-const queueItemCardSx = {
+const StagingAreaHeader = memo(() => {
+ const dispatch = useAppDispatch();
+
+ const startOver = useCallback(() => {
+ dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ }, [dispatch]);
+
+ return (
+
+
+ Generations
+
+
+
+ Start Over
+
+
+ );
+});
+StagingAreaHeader.displayName = 'StagingAreaHeader';
+
+const miniQueueItemSx = {
cursor: 'pointer',
pos: 'relative',
alignItems: 'center',
@@ -581,122 +596,154 @@ const queueItemCardSx = {
maxW: 'full',
minW: 0,
minH: 0,
- '&[data-size="mini"]': {
- borderWidth: 1,
- borderRadius: 'base',
- '&[data-selected="true"]': {
- borderColor: 'invokeBlue.300',
- },
- aspectRatio: '1/1',
- flexShrink: 0,
+ borderWidth: 1,
+ borderRadius: 'base',
+ '&[data-selected="true"]': {
+ borderColor: 'invokeBlue.300',
},
+ aspectRatio: '1/1',
+ flexShrink: 0,
};
const getCardId = (item_id: number) => `queue-item-status-card-${item_id}`;
-type QueueItemStatusCardMiniProps = {
- item: S['SessionQueueItem'];
- isSelected: boolean;
- number: number;
- onSelectItemId: (item_id: number) => void;
- onChangeAutoSwitch: (autoSwitch: boolean) => void;
- size: 'mini' | 'full';
-};
+const useOutputImageDTO = (item: S['SessionQueueItem']) => {
+ const ctx = useStagingContext();
-const QueueItemCard = memo(
- ({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch, size }: QueueItemStatusCardMiniProps) => {
- const ctx = useStagingContext();
- const hasProgressImage = useHasProgressImage(ctx.$progressData, item.session_id);
-
- const outputImageName = useMemo(() => {
- const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
- isCanvasOutputNodeId(nodeId)
- )?.[1][0];
- const output = nodeId ? item.session.results[nodeId] : undefined;
-
- if (!output) {
- return null;
- }
-
- for (const [_name, value] of objectEntries(output)) {
- if (isImageField(value)) {
- return value.image_name;
- }
- }
+ const outputImageName = useMemo(() => {
+ const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
+ isCanvasOutputNodeId(nodeId)
+ )?.[1][0];
+ const output = nodeId ? item.session.results[nodeId] : undefined;
+ if (!output) {
return null;
- }, [item.session.results, item.session.source_prepared_mapping]);
+ }
- const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
-
- const onClick = useCallback(() => {
- onSelectItemId(item.item_id);
- }, [item.item_id, onSelectItemId]);
-
- const onDoubleClick = useCallback(() => {
- onChangeAutoSwitch(item.status === 'in_progress');
- }, [item.status, onChangeAutoSwitch]);
-
- const syncIsReady = useCallback(async () => {
- if (!imageDTO) {
- return;
+ for (const [_name, value] of objectEntries(output)) {
+ if (isImageField(value)) {
+ return value.image_name;
}
+ }
+
+ return null;
+ }, [item.session.results, item.session.source_prepared_mapping]);
+
+ const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
+
+ const preloadOutputImageAndClearProgress = useCallback(
+ async (imageDTO: ImageDTO) => {
try {
await loadImage(imageDTO.image_url, true);
clearProgressImage(ctx.$progressData, item.session_id);
return;
} catch {
- // noop
+ // noop - but should we do something? means image failed to load...
}
- }, [ctx.$progressData, imageDTO, item.session_id]);
+ },
+ [ctx.$progressData, item.session_id]
+ );
- useEffect(() => {
- syncIsReady();
- }, [syncIsReady]);
-
- if (imageDTO && !hasProgressImage) {
- return (
-
-
- {size === 'mini' && }
- {size === 'full' && (
- <>
-
-
- >
- )}
-
- );
+ useEffect(() => {
+ if (!imageDTO) {
+ return;
}
+ if (!ctx.$progressData.get()[item.session_id]?.progressImage) {
+ return;
+ }
+ preloadOutputImageAndClearProgress(imageDTO);
+ }, [ctx.$progressData, imageDTO, item.session_id, preloadOutputImageAndClearProgress]);
+ return imageDTO;
+};
+
+type MiniQueueItemProps = {
+ item: S['SessionQueueItem'];
+ number: number;
+ isSelected: boolean;
+ onSelectItemId: (item_id: number) => void;
+ onChangeAutoSwitch: (autoSwitch: boolean) => void;
+};
+
+const MiniQueueItem = memo(({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch }: MiniQueueItemProps) => {
+ const ctx = useStagingContext();
+ const hasProgressImage = useHasProgressImage(ctx.$progressData, item.session_id);
+ const imageDTO = useOutputImageDTO(item);
+
+ const onClick = useCallback(() => {
+ onSelectItemId(item.item_id);
+ }, [item.item_id, onSelectItemId]);
+
+ const onDoubleClick = useCallback(() => {
+ onChangeAutoSwitch(item.status === 'in_progress');
+ }, [item.status, onChangeAutoSwitch]);
+
+ if (imageDTO && !hasProgressImage) {
return (
-
-
- {size === 'mini' && }
- {size === 'full' && (
- <>
-
-
- >
- )}
+
+
+
);
}
-);
-QueueItemCard.displayName = 'QueueItemStatusCard';
+
+ return (
+
+
+
+
+ );
+});
+MiniQueueItem.displayName = 'MiniQueueItem';
+
+const fullSizeQueueItemSx = {
+ cursor: 'pointer',
+ pos: 'relative',
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ h: 'full',
+ maxH: 'full',
+ maxW: 'full',
+ minW: 0,
+ minH: 0,
+};
+
+type FullSizeQueueItemProps = {
+ item: S['SessionQueueItem'];
+ number: number;
+};
+
+const FullSizeQueueItem = memo(({ item, number }: FullSizeQueueItemProps) => {
+ const ctx = useStagingContext();
+ const hasProgressImage = useHasProgressImage(ctx.$progressData, item.session_id);
+ const imageDTO = useOutputImageDTO(item);
+
+ if (imageDTO && !hasProgressImage) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+});
+FullSizeQueueItem.displayName = 'FullSizeQueueItem';
const getMessage = (data: S['InvocationProgressEvent']) => {
let message = data.message;
@@ -859,6 +906,18 @@ const ImageActions = memo(({ imageDTO, ...rest }: { imageDTO: ImageDTO } & Butto
});
ImageActions.displayName = 'ImageActions';
+const canvasBgSx = {
+ position: 'relative',
+ w: 'full',
+ h: 'full',
+ borderRadius: 'base',
+ overflow: 'hidden',
+ bg: 'base.900',
+ '&[data-dynamic-grid="true"]': {
+ bg: 'base.850',
+ },
+};
+
const CanvasActiveSession = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
@@ -886,15 +945,7 @@ const CanvasActiveSession = memo(() => {
renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
-
+
Date: Wed, 4 Jun 2025 14:24:03 +1000
Subject: [PATCH 038/210] feat: canvas flow rework (wip)
---
.../OverlayScrollbars/ScrollableContent.tsx | 5 +-
.../components/CanvasMainPanelContent.tsx | 492 +++++++++---------
2 files changed, 244 insertions(+), 253 deletions(-)
diff --git a/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx b/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx
index d61a5e498c..5da75b10c6 100644
--- a/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx
+++ b/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx
@@ -11,13 +11,14 @@ import { memo, useEffect, useMemo, useState } from 'react';
type Props = PropsWithChildren & {
maxHeight?: ChakraProps['maxHeight'];
+ maxWidth?: ChakraProps['maxWidth'];
overflowX?: 'hidden' | 'scroll';
overflowY?: 'hidden' | 'scroll';
};
const styles: CSSProperties = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };
-const ScrollableContent = ({ children, maxHeight, overflowX = 'hidden', overflowY = 'scroll' }: Props) => {
+const ScrollableContent = ({ children, maxHeight, maxWidth, overflowX = 'hidden', overflowY = 'scroll' }: Props) => {
const overlayscrollbarsOptions = useMemo(
() => getOverlayScrollbarsParams({ overflowX, overflowY }).options,
[overflowX, overflowY]
@@ -44,7 +45,7 @@ const ScrollableContent = ({ children, maxHeight, overflowX = 'hidden', overflow
}, [os]);
return (
-
+
{children}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 6692ee1c9e..de6bab9717 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,11 +1,17 @@
/* eslint-disable i18next/no-literal-string */
-import type { ButtonGroupProps, SystemStyleObject, TextProps } from '@invoke-ai/ui-library';
+import type {
+ ButtonGroupProps,
+ CircularProgressProps,
+ ImageProps,
+ SystemStyleObject,
+ TextProps,
+} from '@invoke-ai/ui-library';
import {
- Box,
Button,
ButtonGroup,
CircularProgress,
ContextMenu,
+ Divider,
Flex,
FormControl,
FormLabel,
@@ -26,7 +32,7 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppStore } from 'app/store/nanostores/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
-import { IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
+import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
@@ -44,7 +50,6 @@ import { StagingAreaToolbar } from 'features/controlLayers/components/StagingAre
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { loadImage } from 'features/controlLayers/konva/util';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasSessionStarted, selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
@@ -56,7 +61,7 @@ import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtil
import { round } from 'lodash-es';
import { atom, type WritableAtom } from 'nanostores';
import type { ChangeEvent } from 'react';
-import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Trans, useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
@@ -64,7 +69,7 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useListAllQueueItemsQuery } from 'services/api/endpoints/queue';
import type { ImageDTO, S } from 'services/api/types';
import type { ProgressData } from 'services/events/stores';
-import { $socket, clearProgressImage, setProgress, useHasProgressImage, useProgressData } from 'services/events/stores';
+import { $socket, setProgress, useProgressData } from 'services/events/stores';
import type { Equals, Param0 } from 'tsafe';
import { assert, objectEntries } from 'tsafe';
@@ -313,31 +318,6 @@ const GenerateWithStartingImageAndInpaintMask = memo(() => {
});
GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
-const scrollIndicatorBaseSx = {
- opacity: 0,
- position: 'absolute',
- w: 16,
- h: 'full',
- transitionProperty: 'opacity',
- transitionDuration: '0.3s',
- pointerEvents: 'none',
- '&[data-visible="true"]': {
- opacity: 1,
- },
-} satisfies SystemStyleObject;
-
-const scrollIndicatorLeftSx = {
- ...scrollIndicatorBaseSx,
- left: 0,
- bg: 'linear-gradient(to right, var(--invoke-colors-base-900), transparent)',
-} satisfies SystemStyleObject;
-
-const scrollIndicatorRightSx = {
- ...scrollIndicatorBaseSx,
- right: 0,
- bg: 'linear-gradient(to left, var(--invoke-colors-base-900), transparent)',
-} satisfies SystemStyleObject;
-
type StagingContextValue = {
session:
| {
@@ -423,39 +403,25 @@ const StagingArea = memo(() => {
const ctx = useStagingContext();
const [selectedItemId, setSelectedItemId] = useState(null);
const [autoSwitch, setAutoSwitch] = useState(true);
- const [canScrollLeft, setCanScrollLeft] = useState(false);
- const [canScrollRight, setCanScrollRight] = useState(false);
- const scrollableRef = useRef(null);
const { items } = useListAllQueueItemsQuery({ destination: ctx.session.id }, LIST_ALL_OPTIONS);
- const selectedItem = useMemo(
- () =>
- items.length > 0 && selectedItemId !== null ? items.find(({ item_id }) => item_id === selectedItemId) : null,
- [items, selectedItemId]
- );
- const selectedItemIndex = useMemo(
- () =>
- items.length > 0 && selectedItemId !== null ? items.findIndex(({ item_id }) => item_id === selectedItemId) : null,
- [items, selectedItemId]
- );
-
- useEffect(() => {
- const el = scrollableRef.current;
- if (!el) {
- return;
+ const selectedItem = useMemo(() => {
+ if (items.length === 0) {
+ return null;
}
- const onScroll = () => {
- const { scrollLeft, scrollWidth, clientWidth } = el;
- setCanScrollLeft(scrollLeft > 0);
- setCanScrollRight(scrollLeft + clientWidth < scrollWidth);
- };
- el.addEventListener('scroll', onScroll);
- const observer = new ResizeObserver(onScroll);
- observer.observe(el);
- return () => {
- el.removeEventListener('scroll', onScroll);
- observer.disconnect();
- };
- }, []);
+ if (selectedItemId === null) {
+ return null;
+ }
+ return items.find(({ item_id }) => item_id === selectedItemId) ?? null;
+ }, [items, selectedItemId]);
+ const selectedItemIndex = useMemo(() => {
+ if (items.length === 0) {
+ return null;
+ }
+ if (selectedItemId === null) {
+ return null;
+ }
+ return items.findIndex(({ item_id }) => item_id === selectedItemId) ?? null;
+ }, [items, selectedItemId]);
const onSelectItemId = useCallback((item_id: number | null) => {
setSelectedItemId(item_id);
@@ -466,10 +432,6 @@ const StagingArea = memo(() => {
useStagingAreaKeyboardNav(items, selectedItemId, onSelectItemId);
- const onChangeAutoSwitch = useCallback((autoSwitch: boolean) => {
- setAutoSwitch(autoSwitch);
- }, []);
-
useEffect(() => {
if (items.length === 0) {
onSelectItemId(null);
@@ -503,10 +465,6 @@ const StagingArea = memo(() => {
};
}, [autoSwitch, ctx.$progressData, ctx.session.id, onSelectItemId, socket]);
- const _onChangeAutoSwitch = useCallback((e: ChangeEvent) => {
- setAutoSwitch(e.target.checked);
- }, []);
-
useEffect(() => {
if (!socket) {
return;
@@ -526,9 +484,47 @@ const StagingArea = memo(() => {
return (
-
-
-
+
+
+ {items.length > 0 && (
+
+ )}
+ {items.length === 0 && (
+
+ No generations
+
+ )}
+
+ );
+});
+StagingArea.displayName = 'StagingArea';
+
+const StagingAreaContent = memo(
+ ({
+ items,
+ selectedItem,
+ selectedItemId,
+ selectedItemIndex,
+ onChangeAutoSwitch,
+ onSelectItemId,
+ }: {
+ items: S['SessionQueueItem'][];
+ selectedItem: S['SessionQueueItem'] | null;
+ selectedItemId: number | null;
+ selectedItemIndex: number | null;
+ onChangeAutoSwitch: (autoSwitch: boolean) => void;
+ onSelectItemId: (itemId: number) => void;
+ }) => {
+ return (
+ <>
+
{selectedItem && selectedItemIndex !== null && (
{
)}
{!selectedItem && No generation selected}
-
- Auto-switch
-
-
-
-
-
- {items.map((item, i) => (
-
- ))}
+
+
+
+
+ {items.map((item, i) => (
+
+ ))}
+
+
-
-
+ >
+ );
+ }
+);
+StagingAreaContent.displayName = 'StagingAreaContent';
+
+const StagingAreaHeader = memo(
+ ({ autoSwitch, setAutoSwitch }: { autoSwitch: boolean; setAutoSwitch: (autoSwitch: boolean) => void }) => {
+ const dispatch = useAppDispatch();
+
+ const startOver = useCallback(() => {
+ dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ }, [dispatch]);
+
+ const onChangeAutoSwitch = useCallback(
+ (e: ChangeEvent) => {
+ setAutoSwitch(e.target.checked);
+ },
+ [setAutoSwitch]
+ );
+
+ return (
+
+
+ Generations
+
+
+
+ Auto-switch
+
+
+
+ Start Over
+
-
- );
-});
-StagingArea.displayName = 'StagingArea';
-
-const StagingAreaHeader = memo(() => {
- const dispatch = useAppDispatch();
-
- const startOver = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'simple' }));
- }, [dispatch]);
-
- return (
-
-
- Generations
-
-
-
- Start Over
-
-
- );
-});
+ );
+ }
+);
StagingAreaHeader.displayName = 'StagingAreaHeader';
const miniQueueItemSx = {
cursor: 'pointer',
+ userSelect: 'none',
pos: 'relative',
alignItems: 'center',
justifyContent: 'center',
@@ -603,57 +610,34 @@ const miniQueueItemSx = {
},
aspectRatio: '1/1',
flexShrink: 0,
-};
+} satisfies SystemStyleObject;
const getCardId = (item_id: number) => `queue-item-status-card-${item_id}`;
-const useOutputImageDTO = (item: S['SessionQueueItem']) => {
- const ctx = useStagingContext();
-
- const outputImageName = useMemo(() => {
- const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
- isCanvasOutputNodeId(nodeId)
- )?.[1][0];
- const output = nodeId ? item.session.results[nodeId] : undefined;
-
- if (!output) {
- return null;
- }
-
- for (const [_name, value] of objectEntries(output)) {
- if (isImageField(value)) {
- return value.image_name;
- }
- }
+const getOutputImageName = (item: S['SessionQueueItem']) => {
+ const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
+ isCanvasOutputNodeId(nodeId)
+ )?.[1][0];
+ const output = nodeId ? item.session.results[nodeId] : undefined;
+ if (!output) {
return null;
- }, [item.session.results, item.session.source_prepared_mapping]);
+ }
+
+ for (const [_name, value] of objectEntries(output)) {
+ if (isImageField(value)) {
+ return value.image_name;
+ }
+ }
+
+ return null;
+};
+
+const useOutputImageDTO = (item: S['SessionQueueItem']) => {
+ const outputImageName = useMemo(() => getOutputImageName(item), [item]);
const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
- const preloadOutputImageAndClearProgress = useCallback(
- async (imageDTO: ImageDTO) => {
- try {
- await loadImage(imageDTO.image_url, true);
- clearProgressImage(ctx.$progressData, item.session_id);
- return;
- } catch {
- // noop - but should we do something? means image failed to load...
- }
- },
- [ctx.$progressData, item.session_id]
- );
-
- useEffect(() => {
- if (!imageDTO) {
- return;
- }
- if (!ctx.$progressData.get()[item.session_id]?.progressImage) {
- return;
- }
- preloadOutputImageAndClearProgress(imageDTO);
- }, [ctx.$progressData, imageDTO, item.session_id, preloadOutputImageAndClearProgress]);
-
return imageDTO;
};
@@ -666,8 +650,7 @@ type MiniQueueItemProps = {
};
const MiniQueueItem = memo(({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch }: MiniQueueItemProps) => {
- const ctx = useStagingContext();
- const hasProgressImage = useHasProgressImage(ctx.$progressData, item.session_id);
+ const [imageLoaded, setImageLoaded] = useState(false);
const imageDTO = useOutputImageDTO(item);
const onClick = useCallback(() => {
@@ -678,14 +661,9 @@ const MiniQueueItem = memo(({ item, isSelected, number, onSelectItemId, onChange
onChangeAutoSwitch(item.status === 'in_progress');
}, [item.status, onChangeAutoSwitch]);
- if (imageDTO && !hasProgressImage) {
- return (
-
-
-
-
- );
- }
+ const onLoad = useCallback(() => {
+ setImageLoaded(true);
+ }, []);
return (
-
+
+ {imageDTO && }
+ {!imageLoaded && }
+
);
});
@@ -704,16 +685,14 @@ MiniQueueItem.displayName = 'MiniQueueItem';
const fullSizeQueueItemSx = {
cursor: 'pointer',
+ userSelect: 'none',
pos: 'relative',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
h: 'full',
- maxH: 'full',
- maxW: 'full',
- minW: 0,
- minH: 0,
-};
+ w: 'full',
+} satisfies SystemStyleObject;
type FullSizeQueueItemProps = {
item: S['SessionQueueItem'];
@@ -721,30 +700,48 @@ type FullSizeQueueItemProps = {
};
const FullSizeQueueItem = memo(({ item, number }: FullSizeQueueItemProps) => {
- const ctx = useStagingContext();
- const hasProgressImage = useHasProgressImage(ctx.$progressData, item.session_id);
const imageDTO = useOutputImageDTO(item);
+ const [imageLoaded, setImageLoaded] = useState(false);
- if (imageDTO && !hasProgressImage) {
- return (
-
-
-
-
-
- );
- }
+ const onLoad = useCallback(() => {
+ setImageLoaded(true);
+ }, []);
return (
-
+
+ {imageDTO && }
+ {!imageLoaded && }
-
+
+
);
});
FullSizeQueueItem.displayName = 'FullSizeQueueItem';
+const ProgressImage = memo(({ session_id, ...rest }: { session_id: string } & ImageProps) => {
+ const { $progressData } = useStagingContext();
+ const { progressImage } = useProgressData($progressData, session_id);
+
+ if (!progressImage) {
+ return null;
+ }
+
+ return (
+
+ );
+});
+ProgressImage.displayName = 'ProgressImage';
+
const getMessage = (data: S['InvocationProgressEvent']) => {
let message = data.message;
if (data.percentage) {
@@ -758,79 +755,58 @@ const ItemNumber = memo(({ number, ...rest }: { number: number } & TextProps) =>
});
ItemNumber.displayName = 'ItemNumber';
-const ProgressMessage = memo(({ session_id, ...rest }: { session_id: string } & TextProps) => {
- const { $progressData } = useStagingContext();
- const { progressEvent } = useProgressData($progressData, session_id);
- if (!progressEvent) {
- return null;
+const ProgressMessage = memo(
+ ({ session_id, status, ...rest }: { session_id: string; status: S['SessionQueueItem']['status'] } & TextProps) => {
+ const { $progressData } = useStagingContext();
+ const { progressEvent } = useProgressData($progressData, session_id);
+
+ if (status === 'completed' || status === 'failed' || status === 'canceled') {
+ return null;
+ }
+
+ return (
+
+ {progressEvent ? getMessage(progressEvent) : 'Waiting to start...'}
+
+ );
}
- return (
-
- {getMessage(progressEvent)}
-
- );
-});
+);
ProgressMessage.displayName = 'ProgressMessage';
-const InProgressContent = memo(({ item }: { item: S['SessionQueueItem'] }) => {
- const { $progressData } = useStagingContext();
- const { progressEvent, progressImage } = useProgressData($progressData, item.session_id);
-
- if (item.status === 'pending') {
+const ProgressLabel = memo(({ status, ...rest }: { status: S['SessionQueueItem']['status'] } & TextProps) => {
+ if (status === 'pending') {
return (
-
+
Pending
);
}
- if (item.status === 'canceled') {
+ if (status === 'canceled') {
return (
-
+
Canceled
);
}
- if (item.status === 'failed') {
+ if (status === 'failed') {
return (
-
+
Failed
);
}
- if (progressImage) {
+ if (status === 'in_progress') {
return (
- <>
-
-
- >
+
+ In Progress
+
);
}
- if (item.status === 'in_progress') {
- return (
- <>
-
- In Progress
-
-
- >
- );
- }
-
- if (item.status === 'completed') {
- return ;
- }
- assert>(false);
+ return null;
});
-InProgressContent.displayName = 'InProgressContent';
+ProgressLabel.displayName = 'ProgressLabel';
const circleStyles: SystemStyleObject = {
circle: {
@@ -842,20 +818,34 @@ const circleStyles: SystemStyleObject = {
right: 2,
};
-const ProgressCircle = memo(({ data }: { data?: S['InvocationProgressEvent'] | null }) => {
- return (
-
-
-
- );
-});
+const ProgressCircle = memo(
+ ({
+ session_id,
+ status,
+ ...rest
+ }: { session_id: string; status: S['SessionQueueItem']['status'] } & CircularProgressProps) => {
+ const { $progressData } = useStagingContext();
+ const { progressEvent } = useProgressData($progressData, session_id);
+
+ if (status !== 'in_progress') {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ }
+);
ProgressCircle.displayName = 'ProgressCircle';
const ImageActions = memo(({ imageDTO, ...rest }: { imageDTO: ImageDTO } & ButtonGroupProps) => {
From cd136194ad07533f617d2711d8f7307f9a818b77 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 14:30:02 +1000
Subject: [PATCH 039/210] fix(ui): prevent drag of progress images
---
.../features/controlLayers/components/CanvasMainPanelContent.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index de6bab9717..b0ba682988 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -733,6 +733,7 @@ const ProgressImage = memo(({ session_id, ...rest }: { session_id: string } & Im
objectFit="contain"
maxH="full"
maxW="full"
+ draggable={false}
src={progressImage.dataURL}
width={progressImage.width}
height={progressImage.height}
From 985cd8272bcf2a8ae640c1dc4d88aaed5306b07b Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 16:30:01 +1000
Subject: [PATCH 040/210] tidy(ui): component organization
---
.../AdvancedSession/AdvancedSession.tsx | 130 +++
.../components/CanvasMainPanelContent.tsx | 986 +-----------------
.../NoSession/GenerateWithControlImage.tsx | 68 ++
.../NoSession/GenerateWithStartingImage.tsx | 65 ++
...enerateWithStartingImageAndInpaintMask.tsx | 65 ++
.../components/NoSession/NoSession.tsx | 32 +
.../components/SimpleSession/ImageActions.tsx | 55 +
.../QueueItemCircularProgress.tsx | 46 +
.../SimpleSession/QueueItemNumber.tsx | 9 +
.../SimpleSession/QueueItemPreviewFull.tsx | 62 ++
.../SimpleSession/QueueItemPreviewMini.tsx | 79 ++
.../SimpleSession/QueueItemProgressImage.tsx | 28 +
.../QueueItemProgressMessage.tsx | 34 +
.../SimpleSession/QueueItemStatusLabel.tsx | 42 +
.../SimpleSession/SimpleSession.tsx | 22 +
.../components/SimpleSession/StagingArea.tsx | 130 +++
.../SimpleSession/StagingAreaContent.tsx | 58 ++
.../SimpleSession/StagingAreaHeader.tsx | 40 +
.../components/SimpleSession/context.tsx | 135 +++
.../components/SimpleSession/shared.ts | 51 +
.../SimpleSession/use-staging-keyboard-nav.ts | 54 +
.../store/canvasStagingAreaSlice.ts | 12 +-
22 files changed, 1227 insertions(+), 976 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithControlImage.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImage.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemNumber.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingArea.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
new file mode 100644
index 0000000000..73f50ca36f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
@@ -0,0 +1,130 @@
+/* eslint-disable i18next/no-literal-string */
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { ContextMenu, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
+import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
+import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
+import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
+import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
+import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
+import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
+import { Filter } from 'features/controlLayers/components/Filters/Filter';
+import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
+import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
+import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
+import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
+import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
+import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
+import { Transform } from 'features/controlLayers/components/Transform/Transform';
+import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
+import type { AdvancedSessionIdentifier } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useCallback } from 'react';
+import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
+
+const FOCUS_REGION_STYLES: SystemStyleObject = {
+ width: 'full',
+ height: 'full',
+};
+
+const MenuContent = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+MenuContent.displayName = 'MenuContent';
+
+const canvasBgSx = {
+ position: 'relative',
+ w: 'full',
+ h: 'full',
+ borderRadius: 'base',
+ overflow: 'hidden',
+ bg: 'base.900',
+ '&[data-dynamic-grid="true"]': {
+ bg: 'base.850',
+ },
+};
+
+export const AdvancedSession = memo((_props: { session: AdvancedSessionIdentifier }) => {
+ const dynamicGrid = useAppSelector(selectDynamicGrid);
+ const showHUD = useAppSelector(selectShowHUD);
+
+ const renderMenu = useCallback(() => {
+ return ;
+ }, []);
+
+ return (
+
+
+
+
+
+ renderMenu={renderMenu} withLongPress={false}>
+ {(ref) => (
+
+
+
+
+ {showHUD && }
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+AdvancedSession.displayName = 'AdvancedSession';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index b0ba682988..98b7bf3a5b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,991 +1,27 @@
-/* eslint-disable i18next/no-literal-string */
-import type {
- ButtonGroupProps,
- CircularProgressProps,
- ImageProps,
- SystemStyleObject,
- TextProps,
-} from '@invoke-ai/ui-library';
-import {
- Button,
- ButtonGroup,
- CircularProgress,
- ContextMenu,
- Divider,
- Flex,
- FormControl,
- FormLabel,
- Heading,
- IconButton,
- Image,
- Menu,
- MenuButton,
- MenuList,
- Spacer,
- Switch,
- Text,
- Tooltip,
-} from '@invoke-ai/ui-library';
-import { useStore } from '@nanostores/react';
-import { skipToken } from '@reduxjs/toolkit/query';
-import { EMPTY_ARRAY } from 'app/store/constants';
-import { useAppStore } from 'app/store/nanostores/store';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
-import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
-import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
-import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
-import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
-import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
-import { CanvasBusySpinner } from 'features/controlLayers/components/CanvasBusySpinner';
-import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
-import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
-import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
-import { Filter } from 'features/controlLayers/components/Filters/Filter';
-import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
-import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
-import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
-import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
-import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
-import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
-import { Transform } from 'features/controlLayers/components/Transform/Transform';
-import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
-import { canvasSessionStarted, selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
-import { DndDropTarget } from 'features/dnd/DndDropTarget';
-import { DndImage } from 'features/dnd/DndImage';
-import { newCanvasFromImage } from 'features/imageActions/actions';
-import { isImageField } from 'features/nodes/types/common';
-import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
-import { round } from 'lodash-es';
-import { atom, type WritableAtom } from 'nanostores';
-import type { ChangeEvent } from 'react';
-import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
-import { useHotkeys } from 'react-hotkeys-hook';
-import { Trans, useTranslation } from 'react-i18next';
-import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
-import { useGetImageDTOQuery } from 'services/api/endpoints/images';
-import { useListAllQueueItemsQuery } from 'services/api/endpoints/queue';
-import type { ImageDTO, S } from 'services/api/types';
-import type { ProgressData } from 'services/events/stores';
-import { $socket, setProgress, useProgressData } from 'services/events/stores';
-import type { Equals, Param0 } from 'tsafe';
-import { assert, objectEntries } from 'tsafe';
-
-import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
-
-const FOCUS_REGION_STYLES: SystemStyleObject = {
- width: 'full',
- height: 'full',
-};
-
-const MenuContent = memo(() => {
- return (
-
-
-
-
-
-
- );
-});
-MenuContent.displayName = 'MenuContent';
+import { useAppSelector } from 'app/store/storeHooks';
+import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
+import { NoSession } from 'features/controlLayers/components/NoSession/NoSession';
+import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
+import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo } from 'react';
+import type { Equals } from 'tsafe';
+import { assert } from 'tsafe';
export const CanvasMainPanelContent = memo(() => {
const session = useAppSelector(selectCanvasSession);
if (session === null) {
- return ;
+ return ;
}
if (session.type === 'simple') {
- return ;
+ return ;
}
if (session.type === 'advanced') {
- return ;
+ return ;
}
assert>(false, 'Unexpected session');
});
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
-
-const StagingAreaWrapper = memo(({ id }: { id: string }) => {
- const ctx = useMemo(
- () =>
- ({
- session: {
- type: 'simple',
- id,
- },
- $progressData: atom>({}),
- }) as const,
- [id]
- );
-
- return (
-
-
-
- );
-});
-StagingAreaWrapper.displayName = 'StagingAreaWrapper';
-
-const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({
- type: 'raster_layer',
- withResize: true,
-});
-const generateWithStartingImageAndInpaintMaskDndTargetData = newCanvasFromImageDndTarget.getData({
- type: 'raster_layer',
- withInpaintMask: true,
-});
-const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getData({
- type: 'control_layer',
- withResize: true,
-});
-
-const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))';
-
-const NoActiveSession = memo(() => {
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const newSesh = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
- }, [dispatch]);
-
- return (
-
- Get Started with Invoke
-
- Start a new Canvas Session
-
- or
-
-
-
-
-
-
- );
-});
-NoActiveSession.displayName = 'NoActiveSession';
-
-const GenerateWithStartingImage = memo(() => {
- const { t } = useTranslation();
- const { getState, dispatch } = useAppStore();
- const useImageUploadButtonOptions = useMemo>(
- () => ({
- onUpload: (imageDTO: ImageDTO) => {
- newCanvasFromImage({ imageDTO, type: 'raster_layer', withResize: true, getState, dispatch });
- },
- allowMultiple: false,
- }),
- [dispatch, getState]
- );
- const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
- const components = useMemo(
- () => ({
- UploadButton: (
- }
- />
- ),
- }),
- [uploadApi]
- );
-
- return (
-
-
- Generate with a Starting Image
-
- Regenerate the starting image using the model (Image to Image).
-
-
-
-
-
-
- );
-});
-GenerateWithStartingImage.displayName = 'GenerateWithStartingImage';
-
-const GenerateWithControlImage = memo(() => {
- const { t } = useTranslation();
- const { getState, dispatch } = useAppStore();
- const useImageUploadButtonOptions = useMemo>(
- () => ({
- onUpload: (imageDTO: ImageDTO) => {
- newCanvasFromImage({ imageDTO, type: 'control_layer', withResize: true, getState, dispatch });
- },
- allowMultiple: false,
- }),
- [dispatch, getState]
- );
- const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
- const components = useMemo(
- () => ({
- UploadButton: (
- }
- />
- ),
- }),
- [uploadApi]
- );
-
- return (
-
-
- Generate with a Control Image
-
-
- Generate a new image using the control image to guide the structure and composition (Text to Image with
- Control).
-
-
-
-
-
-
-
- );
-});
-GenerateWithControlImage.displayName = 'GenerateWithControlImage';
-
-const GenerateWithStartingImageAndInpaintMask = memo(() => {
- const { t } = useTranslation();
- const { getState, dispatch } = useAppStore();
- const useImageUploadButtonOptions = useMemo>(
- () => ({
- onUpload: (imageDTO: ImageDTO) => {
- newCanvasFromImage({ imageDTO, type: 'raster_layer', withInpaintMask: true, getState, dispatch });
- },
- allowMultiple: false,
- }),
- [dispatch, getState]
- );
- const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
- const components = useMemo(
- () => ({
- UploadButton: (
- }
- />
- ),
- }),
- [uploadApi]
- );
-
- return (
-
-
- Edit Image
-
- Edit the image by regenerating parts of it (Inpaint).
-
-
-
-
-
-
- );
-});
-GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
-
-type StagingContextValue = {
- session:
- | {
- type: 'simple';
- id: string;
- }
- | {
- type: 'advanced';
- id: string;
- };
- $progressData: WritableAtom>;
-};
-
-const StagingContext = createContext(null);
-
-const useStagingContext = () => {
- const ctx = useContext(StagingContext);
- assert(ctx !== null, 'use in stg prov');
- return ctx;
-};
-
-const useStagingAreaKeyboardNav = (
- items: S['SessionQueueItem'][],
- selectedItemId: number | null,
- onSelectItemId: (item_id: number) => void
-) => {
- const onNext = useCallback(() => {
- if (selectedItemId === null) {
- return;
- }
- const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
- const nextIndex = (currentIndex + 1) % items.length;
- const nextItem = items[nextIndex];
- if (!nextItem) {
- return;
- }
- onSelectItemId(nextItem.item_id);
- }, [items, onSelectItemId, selectedItemId]);
- const onPrev = useCallback(() => {
- if (selectedItemId === null) {
- return;
- }
- const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
- const prevIndex = (currentIndex - 1 + items.length) % items.length;
- const prevItem = items[prevIndex];
- if (!prevItem) {
- return;
- }
- onSelectItemId(prevItem.item_id);
- }, [items, onSelectItemId, selectedItemId]);
-
- const onFirst = useCallback(() => {
- const first = items.at(0);
- if (!first) {
- return;
- }
- onSelectItemId(first.item_id);
- }, [items, onSelectItemId]);
- const onLast = useCallback(() => {
- const last = items.at(-1);
- if (!last) {
- return;
- }
- onSelectItemId(last.item_id);
- }, [items, onSelectItemId]);
-
- useHotkeys('left', onPrev, { preventDefault: true });
- useHotkeys('right', onNext, { preventDefault: true });
- useHotkeys('meta+left', onFirst, { preventDefault: true });
- useHotkeys('meta+right', onLast, { preventDefault: true });
-};
-
-const LIST_ALL_OPTIONS = {
- selectFromResult: ({ data }) => {
- if (!data) {
- return { items: EMPTY_ARRAY };
- }
- return { items: data.filter(({ status }) => status !== 'canceled') };
- },
-} satisfies Parameters[1];
-
-const StagingArea = memo(() => {
- const ctx = useStagingContext();
- const [selectedItemId, setSelectedItemId] = useState(null);
- const [autoSwitch, setAutoSwitch] = useState(true);
- const { items } = useListAllQueueItemsQuery({ destination: ctx.session.id }, LIST_ALL_OPTIONS);
- const selectedItem = useMemo(() => {
- if (items.length === 0) {
- return null;
- }
- if (selectedItemId === null) {
- return null;
- }
- return items.find(({ item_id }) => item_id === selectedItemId) ?? null;
- }, [items, selectedItemId]);
- const selectedItemIndex = useMemo(() => {
- if (items.length === 0) {
- return null;
- }
- if (selectedItemId === null) {
- return null;
- }
- return items.findIndex(({ item_id }) => item_id === selectedItemId) ?? null;
- }, [items, selectedItemId]);
-
- const onSelectItemId = useCallback((item_id: number | null) => {
- setSelectedItemId(item_id);
- if (item_id !== null) {
- document.getElementById(getCardId(item_id))?.scrollIntoView();
- }
- }, []);
-
- useStagingAreaKeyboardNav(items, selectedItemId, onSelectItemId);
-
- useEffect(() => {
- if (items.length === 0) {
- onSelectItemId(null);
- return;
- }
- if (selectedItemId === null && items.length > 0) {
- onSelectItemId(items[0]?.item_id ?? null);
- return;
- }
- }, [items, onSelectItemId, selectedItem, selectedItemId]);
-
- const socket = useStore($socket);
- useEffect(() => {
- if (!socket) {
- return;
- }
-
- const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
- if (data.destination !== ctx.session.id) {
- return;
- }
- if (data.status === 'in_progress' && autoSwitch) {
- onSelectItemId(data.item_id);
- }
- };
-
- socket.on('queue_item_status_changed', onQueueItemStatusChanged);
-
- return () => {
- socket.off('queue_item_status_changed', onQueueItemStatusChanged);
- };
- }, [autoSwitch, ctx.$progressData, ctx.session.id, onSelectItemId, socket]);
-
- useEffect(() => {
- if (!socket) {
- return;
- }
- const onProgress = (data: S['InvocationProgressEvent']) => {
- if (data.destination !== ctx.session.id) {
- return;
- }
- setProgress(ctx.$progressData, data);
- };
- socket.on('invocation_progress', onProgress);
-
- return () => {
- socket.off('invocation_progress', onProgress);
- };
- }, [ctx.$progressData, ctx.session.id, socket]);
-
- return (
-
-
-
- {items.length > 0 && (
-
- )}
- {items.length === 0 && (
-
- No generations
-
- )}
-
- );
-});
-StagingArea.displayName = 'StagingArea';
-
-const StagingAreaContent = memo(
- ({
- items,
- selectedItem,
- selectedItemId,
- selectedItemIndex,
- onChangeAutoSwitch,
- onSelectItemId,
- }: {
- items: S['SessionQueueItem'][];
- selectedItem: S['SessionQueueItem'] | null;
- selectedItemId: number | null;
- selectedItemIndex: number | null;
- onChangeAutoSwitch: (autoSwitch: boolean) => void;
- onSelectItemId: (itemId: number) => void;
- }) => {
- return (
- <>
-
- {selectedItem && selectedItemIndex !== null && (
-
- )}
- {!selectedItem && No generation selected}
-
-
-
-
-
- {items.map((item, i) => (
-
- ))}
-
-
-
- >
- );
- }
-);
-StagingAreaContent.displayName = 'StagingAreaContent';
-
-const StagingAreaHeader = memo(
- ({ autoSwitch, setAutoSwitch }: { autoSwitch: boolean; setAutoSwitch: (autoSwitch: boolean) => void }) => {
- const dispatch = useAppDispatch();
-
- const startOver = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'simple' }));
- }, [dispatch]);
-
- const onChangeAutoSwitch = useCallback(
- (e: ChangeEvent) => {
- setAutoSwitch(e.target.checked);
- },
- [setAutoSwitch]
- );
-
- return (
-
-
- Generations
-
-
-
- Auto-switch
-
-
-
- Start Over
-
-
- );
- }
-);
-StagingAreaHeader.displayName = 'StagingAreaHeader';
-
-const miniQueueItemSx = {
- cursor: 'pointer',
- userSelect: 'none',
- pos: 'relative',
- alignItems: 'center',
- justifyContent: 'center',
- overflow: 'hidden',
- h: 'full',
- maxH: 'full',
- maxW: 'full',
- minW: 0,
- minH: 0,
- borderWidth: 1,
- borderRadius: 'base',
- '&[data-selected="true"]': {
- borderColor: 'invokeBlue.300',
- },
- aspectRatio: '1/1',
- flexShrink: 0,
-} satisfies SystemStyleObject;
-
-const getCardId = (item_id: number) => `queue-item-status-card-${item_id}`;
-
-const getOutputImageName = (item: S['SessionQueueItem']) => {
- const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
- isCanvasOutputNodeId(nodeId)
- )?.[1][0];
- const output = nodeId ? item.session.results[nodeId] : undefined;
-
- if (!output) {
- return null;
- }
-
- for (const [_name, value] of objectEntries(output)) {
- if (isImageField(value)) {
- return value.image_name;
- }
- }
-
- return null;
-};
-
-const useOutputImageDTO = (item: S['SessionQueueItem']) => {
- const outputImageName = useMemo(() => getOutputImageName(item), [item]);
-
- const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
-
- return imageDTO;
-};
-
-type MiniQueueItemProps = {
- item: S['SessionQueueItem'];
- number: number;
- isSelected: boolean;
- onSelectItemId: (item_id: number) => void;
- onChangeAutoSwitch: (autoSwitch: boolean) => void;
-};
-
-const MiniQueueItem = memo(({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch }: MiniQueueItemProps) => {
- const [imageLoaded, setImageLoaded] = useState(false);
- const imageDTO = useOutputImageDTO(item);
-
- const onClick = useCallback(() => {
- onSelectItemId(item.item_id);
- }, [item.item_id, onSelectItemId]);
-
- const onDoubleClick = useCallback(() => {
- onChangeAutoSwitch(item.status === 'in_progress');
- }, [item.status, onChangeAutoSwitch]);
-
- const onLoad = useCallback(() => {
- setImageLoaded(true);
- }, []);
-
- return (
-
-
- {imageDTO && }
- {!imageLoaded && }
-
-
-
- );
-});
-MiniQueueItem.displayName = 'MiniQueueItem';
-
-const fullSizeQueueItemSx = {
- cursor: 'pointer',
- userSelect: 'none',
- pos: 'relative',
- alignItems: 'center',
- justifyContent: 'center',
- overflow: 'hidden',
- h: 'full',
- w: 'full',
-} satisfies SystemStyleObject;
-
-type FullSizeQueueItemProps = {
- item: S['SessionQueueItem'];
- number: number;
-};
-
-const FullSizeQueueItem = memo(({ item, number }: FullSizeQueueItemProps) => {
- const imageDTO = useOutputImageDTO(item);
- const [imageLoaded, setImageLoaded] = useState(false);
-
- const onLoad = useCallback(() => {
- setImageLoaded(true);
- }, []);
-
- return (
-
-
- {imageDTO && }
- {!imageLoaded && }
-
-
-
-
- );
-});
-FullSizeQueueItem.displayName = 'FullSizeQueueItem';
-
-const ProgressImage = memo(({ session_id, ...rest }: { session_id: string } & ImageProps) => {
- const { $progressData } = useStagingContext();
- const { progressImage } = useProgressData($progressData, session_id);
-
- if (!progressImage) {
- return null;
- }
-
- return (
-
- );
-});
-ProgressImage.displayName = 'ProgressImage';
-
-const getMessage = (data: S['InvocationProgressEvent']) => {
- let message = data.message;
- if (data.percentage) {
- message += ` (${round(data.percentage * 100)}%)`;
- }
- return message;
-};
-
-const ItemNumber = memo(({ number, ...rest }: { number: number } & TextProps) => {
- return {`#${number}`};
-});
-ItemNumber.displayName = 'ItemNumber';
-
-const ProgressMessage = memo(
- ({ session_id, status, ...rest }: { session_id: string; status: S['SessionQueueItem']['status'] } & TextProps) => {
- const { $progressData } = useStagingContext();
- const { progressEvent } = useProgressData($progressData, session_id);
-
- if (status === 'completed' || status === 'failed' || status === 'canceled') {
- return null;
- }
-
- return (
-
- {progressEvent ? getMessage(progressEvent) : 'Waiting to start...'}
-
- );
- }
-);
-ProgressMessage.displayName = 'ProgressMessage';
-
-const ProgressLabel = memo(({ status, ...rest }: { status: S['SessionQueueItem']['status'] } & TextProps) => {
- if (status === 'pending') {
- return (
-
- Pending
-
- );
- }
- if (status === 'canceled') {
- return (
-
- Canceled
-
- );
- }
- if (status === 'failed') {
- return (
-
- Failed
-
- );
- }
-
- if (status === 'in_progress') {
- return (
-
- In Progress
-
- );
- }
-
- return null;
-});
-ProgressLabel.displayName = 'ProgressLabel';
-
-const circleStyles: SystemStyleObject = {
- circle: {
- transitionProperty: 'none',
- transitionDuration: '0s',
- },
- position: 'absolute',
- top: 2,
- right: 2,
-};
-
-const ProgressCircle = memo(
- ({
- session_id,
- status,
- ...rest
- }: { session_id: string; status: S['SessionQueueItem']['status'] } & CircularProgressProps) => {
- const { $progressData } = useStagingContext();
- const { progressEvent } = useProgressData($progressData, session_id);
-
- if (status !== 'in_progress') {
- return null;
- }
-
- return (
-
-
-
- );
- }
-);
-ProgressCircle.displayName = 'ProgressCircle';
-
-const ImageActions = memo(({ imageDTO, ...rest }: { imageDTO: ImageDTO } & ButtonGroupProps) => {
- const { getState, dispatch } = useAppStore();
-
- const vary = useCallback(() => {
- newCanvasFromImage({
- imageDTO,
- type: 'raster_layer',
- withResize: true,
- getState,
- dispatch,
- });
- }, [dispatch, getState, imageDTO]);
-
- const useAsControl = useCallback(() => {
- newCanvasFromImage({
- imageDTO,
- type: 'control_layer',
- withResize: true,
- getState,
- dispatch,
- });
- }, [dispatch, getState, imageDTO]);
-
- const edit = useCallback(() => {
- newCanvasFromImage({
- imageDTO,
- type: 'raster_layer',
- withInpaintMask: true,
- getState,
- dispatch,
- });
- }, [dispatch, getState, imageDTO]);
- return (
-
-
- Vary
-
-
- Use as Control
-
-
- Edit
-
-
- );
-});
-ImageActions.displayName = 'ImageActions';
-
-const canvasBgSx = {
- position: 'relative',
- w: 'full',
- h: 'full',
- borderRadius: 'base',
- overflow: 'hidden',
- bg: 'base.900',
- '&[data-dynamic-grid="true"]': {
- bg: 'base.850',
- },
-};
-
-const CanvasActiveSession = memo(() => {
- const dynamicGrid = useAppSelector(selectDynamicGrid);
- const showHUD = useAppSelector(selectShowHUD);
-
- const renderMenu = useCallback(() => {
- return ;
- }, []);
-
- return (
-
-
-
-
-
- renderMenu={renderMenu} withLongPress={false}>
- {(ref) => (
-
-
-
-
- {showHUD && }
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-CanvasActiveSession.displayName = 'ActiveCanvasContent';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithControlImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithControlImage.tsx
new file mode 100644
index 0000000000..14b253330c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithControlImage.tsx
@@ -0,0 +1,68 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Button, Flex, Text } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
+import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { newCanvasFromImage } from 'features/imageActions/actions';
+import { memo, useMemo } from 'react';
+import { Trans } from 'react-i18next';
+import { PiUploadBold } from 'react-icons/pi';
+import type { ImageDTO } from 'services/api/types';
+import type { Param0 } from 'tsafe';
+
+const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getData({
+ type: 'control_layer',
+ withResize: true,
+});
+
+export const GenerateWithControlImage = memo(() => {
+ const { getState, dispatch } = useAppStore();
+ const useImageUploadButtonOptions = useMemo>(
+ () => ({
+ onUpload: (imageDTO: ImageDTO) => {
+ newCanvasFromImage({ imageDTO, type: 'control_layer', withResize: true, getState, dispatch });
+ },
+ allowMultiple: false,
+ }),
+ [dispatch, getState]
+ );
+ const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
+ const components = useMemo(
+ () => ({
+ UploadButton: (
+ }
+ />
+ ),
+ }),
+ [uploadApi]
+ );
+
+ return (
+
+
+ Generate with a Control Image
+
+
+ Generate a new image using the control image to guide the structure and composition (Text to Image with
+ Control).
+
+
+
+
+
+
+
+ );
+});
+GenerateWithControlImage.displayName = 'GenerateWithControlImage';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImage.tsx
new file mode 100644
index 0000000000..308bf2a5af
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImage.tsx
@@ -0,0 +1,65 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Button, Flex, Text } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
+import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { newCanvasFromImage } from 'features/imageActions/actions';
+import { memo, useMemo } from 'react';
+import { Trans } from 'react-i18next';
+import { PiUploadBold } from 'react-icons/pi';
+import type { ImageDTO } from 'services/api/types';
+import type { Param0 } from 'tsafe';
+
+const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({
+ type: 'raster_layer',
+ withResize: true,
+});
+
+export const GenerateWithStartingImage = memo(() => {
+ const { getState, dispatch } = useAppStore();
+ const useImageUploadButtonOptions = useMemo>(
+ () => ({
+ onUpload: (imageDTO: ImageDTO) => {
+ newCanvasFromImage({ imageDTO, type: 'raster_layer', withResize: true, getState, dispatch });
+ },
+ allowMultiple: false,
+ }),
+ [dispatch, getState]
+ );
+ const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
+ const components = useMemo(
+ () => ({
+ UploadButton: (
+ }
+ />
+ ),
+ }),
+ [uploadApi]
+ );
+
+ return (
+
+
+ Generate with a Starting Image
+
+ Regenerate the starting image using the model (Image to Image).
+
+
+
+
+
+
+ );
+});
+GenerateWithStartingImage.displayName = 'GenerateWithStartingImage';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask.tsx
new file mode 100644
index 0000000000..c5220e3304
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask.tsx
@@ -0,0 +1,65 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Button, Flex, Text } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
+import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { newCanvasFromImage } from 'features/imageActions/actions';
+import { memo, useMemo } from 'react';
+import { Trans } from 'react-i18next';
+import { PiUploadBold } from 'react-icons/pi';
+import type { ImageDTO } from 'services/api/types';
+import type { Param0 } from 'tsafe';
+
+const generateWithStartingImageAndInpaintMaskDndTargetData = newCanvasFromImageDndTarget.getData({
+ type: 'raster_layer',
+ withInpaintMask: true,
+});
+
+export const GenerateWithStartingImageAndInpaintMask = memo(() => {
+ const { getState, dispatch } = useAppStore();
+ const useImageUploadButtonOptions = useMemo>(
+ () => ({
+ onUpload: (imageDTO: ImageDTO) => {
+ newCanvasFromImage({ imageDTO, type: 'raster_layer', withInpaintMask: true, getState, dispatch });
+ },
+ allowMultiple: false,
+ }),
+ [dispatch, getState]
+ );
+ const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
+ const components = useMemo(
+ () => ({
+ UploadButton: (
+ }
+ />
+ ),
+ }),
+ [uploadApi]
+ );
+
+ return (
+
+
+ Edit Image
+
+ Edit the image by regenerating parts of it (Inpaint).
+
+
+
+
+
+
+ );
+});
+GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx
new file mode 100644
index 0000000000..4586b89573
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx
@@ -0,0 +1,32 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Button, Flex, Heading, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { GenerateWithControlImage } from 'features/controlLayers/components/NoSession/GenerateWithControlImage';
+import { GenerateWithStartingImage } from 'features/controlLayers/components/NoSession/GenerateWithStartingImage';
+import { GenerateWithStartingImageAndInpaintMask } from 'features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useCallback } from 'react';
+
+export const NoSession = memo(() => {
+ const dispatch = useAppDispatch();
+ const newSesh = useCallback(() => {
+ dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ }, [dispatch]);
+
+ return (
+
+ Get Started with Invoke
+
+ Start a new Canvas Session
+
+ or
+
+
+
+
+
+
+ );
+});
+NoSession.displayName = 'NoSession';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx
new file mode 100644
index 0000000000..96d27b8d14
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx
@@ -0,0 +1,55 @@
+/* eslint-disable i18next/no-literal-string */
+import type { ButtonGroupProps } from '@invoke-ai/ui-library';
+import { Button, ButtonGroup } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
+import { newCanvasFromImage } from 'features/imageActions/actions';
+import { memo, useCallback } from 'react';
+import type { ImageDTO } from 'services/api/types';
+
+export const ImageActions = memo(({ imageDTO, ...rest }: { imageDTO: ImageDTO } & ButtonGroupProps) => {
+ const { getState, dispatch } = useAppStore();
+
+ const vary = useCallback(() => {
+ newCanvasFromImage({
+ imageDTO,
+ type: 'raster_layer',
+ withResize: true,
+ getState,
+ dispatch,
+ });
+ }, [dispatch, getState, imageDTO]);
+
+ const useAsControl = useCallback(() => {
+ newCanvasFromImage({
+ imageDTO,
+ type: 'control_layer',
+ withResize: true,
+ getState,
+ dispatch,
+ });
+ }, [dispatch, getState, imageDTO]);
+
+ const edit = useCallback(() => {
+ newCanvasFromImage({
+ imageDTO,
+ type: 'raster_layer',
+ withInpaintMask: true,
+ getState,
+ dispatch,
+ });
+ }, [dispatch, getState, imageDTO]);
+ return (
+
+
+ Vary
+
+
+ Use as Control
+
+
+ Edit
+
+
+ );
+});
+ImageActions.displayName = 'ImageActions';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx
new file mode 100644
index 0000000000..ebf32f8082
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx
@@ -0,0 +1,46 @@
+import type { CircularProgressProps, SystemStyleObject } from '@invoke-ai/ui-library';
+import { CircularProgress, Tooltip } from '@invoke-ai/ui-library';
+import { useCanvasSessionContext,useProgressData } from 'features/controlLayers/components/SimpleSession/context';
+import { getProgressMessage } from 'features/controlLayers/components/SimpleSession/shared';
+import { memo } from 'react';
+import type { S } from 'services/api/types';
+
+const circleStyles: SystemStyleObject = {
+ circle: {
+ transitionProperty: 'none',
+ transitionDuration: '0s',
+ },
+ position: 'absolute',
+ top: 2,
+ right: 2,
+};
+
+export const QueueItemCircularProgress = memo(
+ ({
+ session_id,
+ status,
+ ...rest
+ }: { session_id: string; status: S['SessionQueueItem']['status'] } & CircularProgressProps) => {
+ const { $progressData } = useCanvasSessionContext();
+ const { progressEvent } = useProgressData($progressData, session_id);
+
+ if (status !== 'in_progress') {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ }
+);
+QueueItemCircularProgress.displayName = 'QueueItemCircularProgress';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemNumber.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemNumber.tsx
new file mode 100644
index 0000000000..33686a5c83
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemNumber.tsx
@@ -0,0 +1,9 @@
+import type { TextProps } from '@invoke-ai/ui-library';
+import { Text } from '@invoke-ai/ui-library';
+import { DROP_SHADOW } from 'features/controlLayers/components/SimpleSession/shared';
+import { memo } from 'react';
+
+export const QueueItemNumber = memo(({ number, ...rest }: { number: number } & TextProps) => {
+ return {`#${number}`};
+});
+QueueItemNumber.displayName = 'QueueItemNumber';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
new file mode 100644
index 0000000000..cc5085ba2a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
@@ -0,0 +1,62 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
+import { ImageActions } from 'features/controlLayers/components/SimpleSession/ImageActions';
+import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
+import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
+import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
+import { QueueItemProgressMessage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressMessage';
+import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
+import { getQueueItemElementId, useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/shared';
+import { DndImage } from 'features/dnd/DndImage';
+import { memo, useCallback, useState } from 'react';
+import type { S } from 'services/api/types';
+
+type Props = {
+ item: S['SessionQueueItem'];
+ number: number;
+};
+
+const sx = {
+ cursor: 'pointer',
+ userSelect: 'none',
+ pos: 'relative',
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ h: 'full',
+ w: 'full',
+} satisfies SystemStyleObject;
+
+export const QueueItemPreviewFull = memo(({ item, number }: Props) => {
+ const imageDTO = useOutputImageDTO(item);
+ const [imageLoaded, setImageLoaded] = useState(false);
+
+ const onLoad = useCallback(() => {
+ setImageLoaded(true);
+ }, []);
+
+ return (
+
+
+ {imageDTO && }
+ {!imageLoaded && }
+ {imageDTO && imageLoaded && }
+
+
+
+
+ );
+});
+QueueItemPreviewFull.displayName = 'QueueItemPreviewFull';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
new file mode 100644
index 0000000000..ffecb9c9b8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -0,0 +1,79 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
+import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
+import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
+import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
+import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
+import { getQueueItemElementId, useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/shared';
+import { DndImage } from 'features/dnd/DndImage';
+import { memo, useCallback, useState } from 'react';
+import type { S } from 'services/api/types';
+
+const sx = {
+ cursor: 'pointer',
+ userSelect: 'none',
+ pos: 'relative',
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ h: 'full',
+ maxH: 'full',
+ maxW: 'full',
+ minW: 0,
+ minH: 0,
+ borderWidth: 1,
+ borderRadius: 'base',
+ '&[data-selected="true"]': {
+ borderColor: 'invokeBlue.300',
+ },
+ aspectRatio: '1/1',
+ flexShrink: 0,
+} satisfies SystemStyleObject;
+
+type Props = {
+ item: S['SessionQueueItem'];
+ number: number;
+ isSelected: boolean;
+ onSelectItemId: (item_id: number) => void;
+ onChangeAutoSwitch: (autoSwitch: boolean) => void;
+};
+
+export const QueueItemPreviewMini = memo(({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch }: Props) => {
+ const [imageLoaded, setImageLoaded] = useState(false);
+ const imageDTO = useOutputImageDTO(item);
+
+ const onClick = useCallback(() => {
+ onSelectItemId(item.item_id);
+ }, [item.item_id, onSelectItemId]);
+
+ const onDoubleClick = useCallback(() => {
+ onChangeAutoSwitch(item.status === 'in_progress');
+ }, [item.status, onChangeAutoSwitch]);
+
+ const onLoad = useCallback(() => {
+ setImageLoaded(true);
+ }, []);
+
+ return (
+
+
+ {imageDTO && }
+ {!imageLoaded && }
+
+
+
+ );
+});
+QueueItemPreviewMini.displayName = 'QueueItemPreviewMini';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
new file mode 100644
index 0000000000..55e8d836b7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
@@ -0,0 +1,28 @@
+import type { ImageProps } from '@invoke-ai/ui-library';
+import { Image } from '@invoke-ai/ui-library';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { memo } from 'react';
+import { useProgressData } from 'services/events/stores';
+
+export const QueueItemProgressImage = memo(({ session_id, ...rest }: { session_id: string } & ImageProps) => {
+ const { $progressData } = useCanvasSessionContext();
+ const { progressImage } = useProgressData($progressData, session_id);
+
+ if (!progressImage) {
+ return null;
+ }
+
+ return (
+
+ );
+});
+QueueItemProgressImage.displayName = 'QueueItemProgressImage';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx
new file mode 100644
index 0000000000..d287811fb3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx
@@ -0,0 +1,34 @@
+/* eslint-disable i18next/no-literal-string */
+import type { TextProps } from '@invoke-ai/ui-library';
+import { Text } from '@invoke-ai/ui-library';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { DROP_SHADOW, getProgressMessage } from 'features/controlLayers/components/SimpleSession/shared';
+import { memo } from 'react';
+import type { S } from 'services/api/types';
+import { useProgressData } from 'services/events/stores';
+
+export const QueueItemProgressMessage = memo(
+ ({ session_id, status, ...rest }: { session_id: string; status: S['SessionQueueItem']['status'] } & TextProps) => {
+ const { $progressData } = useCanvasSessionContext();
+ const { progressEvent } = useProgressData($progressData, session_id);
+
+ if (status === 'completed' || status === 'failed' || status === 'canceled') {
+ return null;
+ }
+
+ if (status === 'pending') {
+ return (
+
+ Waiting to start...
+
+ );
+ }
+
+ return (
+
+ {getProgressMessage(progressEvent)}
+
+ );
+ }
+);
+QueueItemProgressMessage.displayName = 'QueueItemProgressMessage';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
new file mode 100644
index 0000000000..5924b63981
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
@@ -0,0 +1,42 @@
+/* eslint-disable i18next/no-literal-string */
+import type { TextProps } from '@invoke-ai/ui-library';
+import { Text } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import type { S } from 'services/api/types';
+
+export const QueueItemStatusLabel = memo(
+ ({ status, ...rest }: { status: S['SessionQueueItem']['status'] } & TextProps) => {
+ if (status === 'pending') {
+ return (
+
+ Pending
+
+ );
+ }
+ if (status === 'canceled') {
+ return (
+
+ Canceled
+
+ );
+ }
+ if (status === 'failed') {
+ return (
+
+ Failed
+
+ );
+ }
+
+ if (status === 'in_progress') {
+ return (
+
+ In Progress
+
+ );
+ }
+
+ return null;
+ }
+);
+QueueItemStatusLabel.displayName = 'QueueItemStatusLabel';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
new file mode 100644
index 0000000000..86d14450d2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
@@ -0,0 +1,22 @@
+import type { CanvasSessionContextValue } from 'features/controlLayers/components/SimpleSession/context';
+import {
+ buildProgressDataAtom,
+ CanvasSessionContextProvider,
+} from 'features/controlLayers/components/SimpleSession/context';
+import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea';
+import type { SimpleSessionIdentifier } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useMemo } from 'react';
+
+export const SimpleSession = memo(({ session }: { session: SimpleSessionIdentifier }) => {
+ const ctx = useMemo(
+ () => ({ session, $progressData: buildProgressDataAtom() }) satisfies CanvasSessionContextValue,
+ [session]
+ );
+
+ return (
+
+
+
+ );
+});
+SimpleSession.displayName = 'SimpleSession';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingArea.tsx
new file mode 100644
index 0000000000..8eef21c9fd
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingArea.tsx
@@ -0,0 +1,130 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Divider, Flex, Text } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { EMPTY_ARRAY } from 'app/store/constants';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
+import { StagingAreaContent } from 'features/controlLayers/components/SimpleSession/StagingAreaContent';
+import { StagingAreaHeader } from 'features/controlLayers/components/SimpleSession/StagingAreaHeader';
+import { useStagingAreaKeyboardNav } from 'features/controlLayers/components/SimpleSession/use-staging-keyboard-nav';
+import { memo, useCallback, useEffect, useMemo, useState } from 'react';
+import { useListAllQueueItemsQuery } from 'services/api/endpoints/queue';
+import type { S } from 'services/api/types';
+import { $socket, setProgress } from 'services/events/stores';
+
+const LIST_ALL_OPTIONS = {
+ selectFromResult: ({ data }) => {
+ if (!data) {
+ return { items: EMPTY_ARRAY };
+ }
+ return { items: data.filter(({ status }) => status !== 'canceled') };
+ },
+} satisfies Parameters[1];
+
+export const StagingArea = memo(() => {
+ const ctx = useCanvasSessionContext();
+ const [selectedItemId, setSelectedItemId] = useState(null);
+ const [autoSwitch, setAutoSwitch] = useState(true);
+ const { items } = useListAllQueueItemsQuery({ destination: ctx.session.id }, LIST_ALL_OPTIONS);
+ const selectedItem = useMemo(() => {
+ if (items.length === 0) {
+ return null;
+ }
+ if (selectedItemId === null) {
+ return null;
+ }
+ return items.find(({ item_id }) => item_id === selectedItemId) ?? null;
+ }, [items, selectedItemId]);
+ const selectedItemIndex = useMemo(() => {
+ if (items.length === 0) {
+ return null;
+ }
+ if (selectedItemId === null) {
+ return null;
+ }
+ return items.findIndex(({ item_id }) => item_id === selectedItemId) ?? null;
+ }, [items, selectedItemId]);
+
+ const onSelectItemId = useCallback((item_id: number | null) => {
+ setSelectedItemId(item_id);
+ if (item_id !== null) {
+ document.getElementById(getQueueItemElementId(item_id))?.scrollIntoView();
+ }
+ }, []);
+
+ useStagingAreaKeyboardNav(items, selectedItemId, onSelectItemId);
+
+ useEffect(() => {
+ if (items.length === 0) {
+ onSelectItemId(null);
+ return;
+ }
+ if (selectedItemId === null && items.length > 0) {
+ onSelectItemId(items[0]?.item_id ?? null);
+ return;
+ }
+ }, [items, onSelectItemId, selectedItem, selectedItemId]);
+
+ const socket = useStore($socket);
+ useEffect(() => {
+ if (!socket) {
+ return;
+ }
+
+ const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
+ if (data.destination !== ctx.session.id) {
+ return;
+ }
+ if (data.status === 'in_progress' && autoSwitch) {
+ onSelectItemId(data.item_id);
+ }
+ };
+
+ socket.on('queue_item_status_changed', onQueueItemStatusChanged);
+
+ return () => {
+ socket.off('queue_item_status_changed', onQueueItemStatusChanged);
+ };
+ }, [autoSwitch, ctx.$progressData, ctx.session.id, onSelectItemId, socket]);
+
+ useEffect(() => {
+ if (!socket) {
+ return;
+ }
+ const onProgress = (data: S['InvocationProgressEvent']) => {
+ if (data.destination !== ctx.session.id) {
+ return;
+ }
+ setProgress(ctx.$progressData, data);
+ };
+ socket.on('invocation_progress', onProgress);
+
+ return () => {
+ socket.off('invocation_progress', onProgress);
+ };
+ }, [ctx.$progressData, ctx.session.id, socket]);
+
+ return (
+
+
+
+ {items.length > 0 && (
+
+ )}
+ {items.length === 0 && (
+
+ No generations
+
+ )}
+
+ );
+});
+StagingArea.displayName = 'StagingArea';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
new file mode 100644
index 0000000000..ce1e29b3b2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
@@ -0,0 +1,58 @@
+/* eslint-disable i18next/no-literal-string */
+import { Divider, Flex, Text } from '@invoke-ai/ui-library';
+import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
+import { QueueItemPreviewFull } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewFull';
+import { QueueItemPreviewMini } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewMini';
+import { memo } from 'react';
+import type { S } from 'services/api/types';
+
+export const StagingAreaContent = memo(
+ ({
+ items,
+ selectedItem,
+ selectedItemId,
+ selectedItemIndex,
+ onChangeAutoSwitch,
+ onSelectItemId,
+ }: {
+ items: S['SessionQueueItem'][];
+ selectedItem: S['SessionQueueItem'] | null;
+ selectedItemId: number | null;
+ selectedItemIndex: number | null;
+ onChangeAutoSwitch: (autoSwitch: boolean) => void;
+ onSelectItemId: (itemId: number) => void;
+ }) => {
+ return (
+ <>
+
+ {selectedItem && selectedItemIndex !== null && (
+
+ )}
+ {!selectedItem && No generation selected}
+
+
+
+
+
+ {items.map((item, i) => (
+
+ ))}
+
+
+
+ >
+ );
+ }
+);
+StagingAreaContent.displayName = 'StagingAreaContent';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
new file mode 100644
index 0000000000..f9e04c1ef4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
@@ -0,0 +1,40 @@
+/* eslint-disable i18next/no-literal-string */
+import { Button, Flex, FormControl, FormLabel, Spacer, Switch, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import type { ChangeEvent } from 'react';
+import { memo, useCallback } from 'react';
+
+export const StagingAreaHeader = memo(
+ ({ autoSwitch, setAutoSwitch }: { autoSwitch: boolean; setAutoSwitch: (autoSwitch: boolean) => void }) => {
+ const dispatch = useAppDispatch();
+
+ const startOver = useCallback(() => {
+ dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ }, [dispatch]);
+
+ const onChangeAutoSwitch = useCallback(
+ (e: ChangeEvent) => {
+ setAutoSwitch(e.target.checked);
+ },
+ [setAutoSwitch]
+ );
+
+ return (
+
+
+ Generations
+
+
+
+ Auto-switch
+
+
+
+ Start Over
+
+
+ );
+ }
+);
+StagingAreaHeader.displayName = 'StagingAreaHeader';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
new file mode 100644
index 0000000000..6cbc10d334
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -0,0 +1,135 @@
+import type {
+ AdvancedSessionIdentifier,
+ SimpleSessionIdentifier,
+} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import type { ProgressImage } from 'features/nodes/types/common';
+import { atom, type WritableAtom } from 'nanostores';
+import type { PropsWithChildren } from 'react';
+import { createContext, memo, useContext, useEffect, useState } from 'react';
+import type { S } from 'services/api/types';
+import { assert } from 'tsafe';
+
+export type ProgressData = {
+ sessionId: string;
+ progressEvent: S['InvocationProgressEvent'] | null;
+ progressImage: ProgressImage | null;
+};
+
+export const buildProgressDataAtom = () => atom>({});
+
+export const useProgressData = (
+ $progressData: WritableAtom>,
+ sessionId: string
+): ProgressData => {
+ const [value, setValue] = useState(() => {
+ return $progressData.get()[sessionId] ?? { sessionId, progressEvent: null, progressImage: null };
+ });
+ useEffect(() => {
+ const unsub = $progressData.subscribe((data) => {
+ const progressData = data[sessionId];
+ if (!progressData) {
+ return;
+ }
+ setValue(progressData);
+ });
+ return () => {
+ unsub();
+ };
+ }, [$progressData, sessionId]);
+
+ return value;
+};
+
+export const useHasProgressImage = (
+ $progressData: WritableAtom>,
+ sessionId: string
+): boolean => {
+ const [value, setValue] = useState(false);
+ useEffect(() => {
+ const unsub = $progressData.subscribe((data) => {
+ const progressData = data[sessionId];
+ setValue(Boolean(progressData?.progressImage));
+ });
+ return () => {
+ unsub();
+ };
+ }, [$progressData, sessionId]);
+
+ return value;
+};
+
+export const setProgress = (
+ $progressData: WritableAtom>,
+ data: S['InvocationProgressEvent']
+) => {
+ const progressData = $progressData.get();
+ const current = progressData[data.session_id];
+ if (current) {
+ const next = { ...current };
+ next.progressEvent = data;
+ if (data.image) {
+ next.progressImage = data.image;
+ }
+ $progressData.set({
+ ...progressData,
+ [data.session_id]: next,
+ });
+ } else {
+ $progressData.set({
+ ...progressData,
+ [data.session_id]: {
+ sessionId: data.session_id,
+ progressEvent: data,
+ progressImage: data.image ?? null,
+ },
+ });
+ }
+};
+
+export const clearProgressEvent = ($progressData: WritableAtom>, sessionId: string) => {
+ const progressData = $progressData.get();
+ const current = progressData[sessionId];
+ if (!current) {
+ return;
+ }
+ const next = { ...current };
+ next.progressEvent = null;
+ $progressData.set({
+ ...progressData,
+ [sessionId]: next,
+ });
+};
+
+export const clearProgressImage = ($progressData: WritableAtom>, sessionId: string) => {
+ const progressData = $progressData.get();
+ const current = progressData[sessionId];
+ if (!current) {
+ return;
+ }
+ const next = { ...current };
+ next.progressImage = null;
+ $progressData.set({
+ ...progressData,
+ [sessionId]: next,
+ });
+};
+
+export type CanvasSessionContextValue = {
+ session: SimpleSessionIdentifier | AdvancedSessionIdentifier;
+ $progressData: WritableAtom>;
+};
+
+const CanvasSessionContext = createContext(null);
+
+export const CanvasSessionContextProvider = memo(
+ ({ value, children }: PropsWithChildren<{ value: CanvasSessionContextValue }>) => (
+ {children}
+ )
+);
+CanvasSessionContextProvider.displayName = 'CanvasSessionContextProvider';
+
+export const useCanvasSessionContext = () => {
+ const ctx = useContext(CanvasSessionContext);
+ assert(ctx !== null, "'useCanvasSessionContext' must be used within a CanvasSessionContextProvider");
+ return ctx;
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
new file mode 100644
index 0000000000..6736e2c306
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
@@ -0,0 +1,51 @@
+import { skipToken } from '@reduxjs/toolkit/query';
+import { isImageField } from 'features/nodes/types/common';
+import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
+import { round } from 'lodash-es';
+import { useMemo } from 'react';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import type { S } from 'services/api/types';
+import { objectEntries } from 'tsafe';
+
+export const getProgressMessage = (data?: S['InvocationProgressEvent'] | null) => {
+ if (!data) {
+ return 'Generating';
+ }
+
+ let message = data.message;
+ if (data.percentage) {
+ message += ` (${round(data.percentage * 100)}%)`;
+ }
+ return message;
+};
+
+export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))';
+
+export const getQueueItemElementId = (item_id: number) => `queue-item-status-card-${item_id}`;
+
+const getOutputImageName = (item: S['SessionQueueItem']) => {
+ const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
+ isCanvasOutputNodeId(nodeId)
+ )?.[1][0];
+ const output = nodeId ? item.session.results[nodeId] : undefined;
+
+ if (!output) {
+ return null;
+ }
+
+ for (const [_name, value] of objectEntries(output)) {
+ if (isImageField(value)) {
+ return value.image_name;
+ }
+ }
+
+ return null;
+};
+
+export const useOutputImageDTO = (item: S['SessionQueueItem']) => {
+ const outputImageName = useMemo(() => getOutputImageName(item), [item]);
+
+ const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
+
+ return imageDTO;
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts
new file mode 100644
index 0000000000..9e2ac2f24c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts
@@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
+import type { S } from 'services/api/types';
+
+export const useStagingAreaKeyboardNav = (
+ items: S['SessionQueueItem'][],
+ selectedItemId: number | null,
+ onSelectItemId: (item_id: number) => void
+) => {
+ const onNext = useCallback(() => {
+ if (selectedItemId === null) {
+ return;
+ }
+ const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
+ const nextIndex = (currentIndex + 1) % items.length;
+ const nextItem = items[nextIndex];
+ if (!nextItem) {
+ return;
+ }
+ onSelectItemId(nextItem.item_id);
+ }, [items, onSelectItemId, selectedItemId]);
+ const onPrev = useCallback(() => {
+ if (selectedItemId === null) {
+ return;
+ }
+ const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
+ const prevItem = items[prevIndex];
+ if (!prevItem) {
+ return;
+ }
+ onSelectItemId(prevItem.item_id);
+ }, [items, onSelectItemId, selectedItemId]);
+
+ const onFirst = useCallback(() => {
+ const first = items.at(0);
+ if (!first) {
+ return;
+ }
+ onSelectItemId(first.item_id);
+ }, [items, onSelectItemId]);
+ const onLast = useCallback(() => {
+ const last = items.at(-1);
+ if (!last) {
+ return;
+ }
+ onSelectItemId(last.item_id);
+ }, [items, onSelectItemId]);
+
+ useHotkeys('left', onPrev, { preventDefault: true });
+ useHotkeys('right', onNext, { preventDefault: true });
+ useHotkeys('meta+left', onFirst, { preventDefault: true });
+ useHotkeys('meta+right', onLast, { preventDefault: true });
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index bf035be31c..4a403b6baf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -6,8 +6,18 @@ import { canvasReset } from 'features/controlLayers/store/actions';
import type { StagingAreaImage, StagingAreaProgressImage } from 'features/controlLayers/store/types';
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
+export type SimpleSessionIdentifier = {
+ type: 'simple';
+ id: string;
+};
+
+export type AdvancedSessionIdentifier = {
+ type: 'advanced';
+ id: string;
+};
+
type CanvasStagingAreaState = {
- session: { type: 'simple'; id: string } | { type: 'advanced'; id: string } | null;
+ session: SimpleSessionIdentifier | AdvancedSessionIdentifier | null;
sessionType: 'simple' | 'advanced' | null;
images: (StagingAreaImage | StagingAreaProgressImage)[];
selectedImageIndex: number;
From 7ec511da014d4a6badb818ff7c2feef6b2f1e93a Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 16:31:07 +1000
Subject: [PATCH 041/210] feat(app): do not prune queue on startup
With the new canvas design, this will result in loss of staging area images.
---
invokeai/app/services/session_queue/session_queue_sqlite.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py
index 4ba16fb85a..0a0ef72456 100644
--- a/invokeai/app/services/session_queue/session_queue_sqlite.py
+++ b/invokeai/app/services/session_queue/session_queue_sqlite.py
@@ -45,10 +45,6 @@ class SqliteSessionQueue(SessionQueueBase):
clear_result = self.clear(DEFAULT_QUEUE_ID)
if clear_result.deleted > 0:
self.__invoker.services.logger.info(f"Cleared all {clear_result.deleted} queue items")
- else:
- prune_result = self.prune(DEFAULT_QUEUE_ID)
- if prune_result.deleted > 0:
- self.__invoker.services.logger.info(f"Pruned {prune_result.deleted} finished queue items")
def __init__(self, db: SqliteDatabase) -> None:
super().__init__()
From d2155e98efcbec441f7389efc2665a33b694d72d Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 16:36:27 +1000
Subject: [PATCH 042/210] feat(ui): remove clear queue ui components
---
.../queue/components/ClearQueueIconButton.tsx | 33 +------------------
.../components/QueueActionsMenuButton.tsx | 24 +-------------
.../components/QueueTabQueueControls.tsx | 13 +++-----
.../components/FloatingLeftPanelButtons.tsx | 23 -------------
4 files changed, 6 insertions(+), 87 deletions(-)
diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx
index 348dd38ada..9f7a4290fa 100644
--- a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx
@@ -1,28 +1,17 @@
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
-import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiTrashSimpleBold, PiXBold, PiXCircle } from 'react-icons/pi';
-
-import { useClearQueueDialog } from './ClearQueueConfirmationAlertDialog';
+import { PiXBold, PiXCircle } from 'react-icons/pi';
export const ClearQueueIconButton = memo(() => {
- const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
const shift = useShiftModifier();
if (!shift) {
- // Shift is not pressed - show cancel current
return ;
}
- if (isCancelAndClearAllEnabled) {
- // Shift is pressed and cancel and clear all is enabled - show cancel and clear all
- return ;
- }
-
- // Shift is pressed and cancel and clear all is disabled - show cancel all except current
return ;
});
@@ -48,26 +37,6 @@ const CancelCurrentIconButton = memo(() => {
CancelCurrentIconButton.displayName = 'CancelCurrentIconButton';
-const CancelAndClearAllIconButton = memo(() => {
- const { t } = useTranslation();
- const clearQueue = useClearQueueDialog();
-
- return (
- }
- colorScheme="error"
- onClick={clearQueue.openDialog}
- />
- );
-});
-
-CancelAndClearAllIconButton.displayName = 'CancelAndClearAllIconButton';
-
const CancelAllExceptCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
index 42b8d704bd..be1ebeedc7 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
@@ -2,7 +2,6 @@ import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@in
import { useAppDispatch } from 'app/store/storeHooks';
import { SessionMenuItems } from 'common/components/SessionMenuItems';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
-import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
@@ -11,15 +10,7 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-import {
- PiListBold,
- PiPauseFill,
- PiPlayFill,
- PiQueueBold,
- PiTrashSimpleBold,
- PiXBold,
- PiXCircle,
-} from 'react-icons/pi';
+import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiXBold, PiXCircle } from 'react-icons/pi';
export const QueueActionsMenuButton = memo(() => {
const ref = useRef(null);
@@ -27,10 +18,8 @@ export const QueueActionsMenuButton = memo(() => {
const { t } = useTranslation();
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
- const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
const cancelCurrent = useCancelCurrentQueueItem();
- const clearQueue = useClearQueueDialog();
const {
resumeProcessor,
isLoading: isLoadingResumeProcessor,
@@ -72,17 +61,6 @@ export const QueueActionsMenuButton = memo(() => {
>
{t('queue.cancelAllExceptCurrentTooltip')}
- {isCancelAndClearAllEnabled && (
- }
- onClick={clearQueue.openDialog}
- isLoading={clearQueue.isLoading}
- isDisabled={clearQueue.isDisabled}
- >
- {t('queue.clearTooltip')}
-
- )}
{isResumeEnabled && (
}
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
index 7e679caf74..17c3ed104f 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
@@ -5,7 +5,6 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import ClearModelCacheButton from './ClearModelCacheButton';
-import ClearQueueButton from './ClearQueueButton';
import PauseProcessorButton from './PauseProcessorButton';
import PruneQueueButton from './PruneQueueButton';
import ResumeProcessorButton from './ResumeProcessorButton';
@@ -13,23 +12,19 @@ import ResumeProcessorButton from './ResumeProcessorButton';
const QueueTabQueueControls = () => {
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
- const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
return (
- {isPauseEnabled || isResumeEnabled ? (
+ {(isPauseEnabled || isResumeEnabled) && (
- {isResumeEnabled ? : <>>}
- {isPauseEnabled ? : <>>}
+ {isResumeEnabled && }
+ {isPauseEnabled && }
- ) : (
- <>>
)}
- {isCancelAndClearAllEnabled && }
- {!isCancelAndClearAllEnabled && }
+
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
index bbd10d868c..f80a956319 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
@@ -4,7 +4,6 @@ import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
-import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke';
@@ -16,7 +15,6 @@ import {
PiLightningFill,
PiSlidersHorizontalBold,
PiSparkleFill,
- PiTrashSimpleBold,
PiXBold,
PiXCircle,
} from 'react-icons/pi';
@@ -126,27 +124,6 @@ const CancelCurrentIconButton = memo(() => {
CancelCurrentIconButton.displayName = 'CancelCurrentIconButton';
-const CancelAndClearAllIconButton = memo(() => {
- const { t } = useTranslation();
- const clearQueue = useClearQueueDialog();
-
- return (
-
- }
- colorScheme="error"
- onClick={clearQueue.openDialog}
- flexGrow={1}
- />
-
- );
-});
-
-CancelAndClearAllIconButton.displayName = 'CancelAndClearAllIconButton';
-
const CancelAllExceptCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
From 5088e700ad17f2dba9da9a259c1a5af4c14843fa Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 16:51:08 +1000
Subject: [PATCH 043/210] fix(ui): cursor on staging area preview image
---
.../components/SimpleSession/QueueItemPreviewFull.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
index cc5085ba2a..de0504909b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
@@ -17,7 +17,6 @@ type Props = {
};
const sx = {
- cursor: 'pointer',
userSelect: 'none',
pos: 'relative',
alignItems: 'center',
From b05de8634dc2f6dc15749b10c5fd731cb3602e11 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 4 Jun 2025 23:16:39 +1000
Subject: [PATCH 044/210] perf(ui): queue actions menu is lazy
---
.../src/features/queue/components/QueueActionsMenuButton.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
index be1ebeedc7..e9a81c4c19 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
@@ -36,7 +36,7 @@ export const QueueActionsMenuButton = memo(() => {
return (
<>
-
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 64eae0b8f4..c278e38c84 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -17,23 +17,21 @@ import { $socket } from 'services/events/stores';
import { assert } from 'tsafe';
export type ProgressData = {
- sessionId: string;
+ itemId: number;
progressEvent: S['InvocationProgressEvent'] | null;
progressImage: ProgressImage | null;
};
-export const buildProgressDataAtom = () => atom>({});
-
export const useProgressData = (
- $progressData: WritableAtom>,
- sessionId: string
+ $progressData: WritableAtom>,
+ itemId: number
): ProgressData => {
const [value, setValue] = useState(() => {
- return $progressData.get()[sessionId] ?? { sessionId, progressEvent: null, progressImage: null };
+ return $progressData.get()[itemId] ?? { itemId, progressEvent: null, progressImage: null };
});
useEffect(() => {
const unsub = $progressData.subscribe((data) => {
- const progressData = data[sessionId];
+ const progressData = data[itemId];
if (!progressData) {
return;
}
@@ -42,35 +40,35 @@ export const useProgressData = (
return () => {
unsub();
};
- }, [$progressData, sessionId]);
+ }, [$progressData, itemId]);
return value;
};
export const useHasProgressImage = (
- $progressData: WritableAtom>,
- sessionId: string
+ $progressData: WritableAtom>,
+ itemId: number
): boolean => {
const [value, setValue] = useState(false);
useEffect(() => {
const unsub = $progressData.subscribe((data) => {
- const progressData = data[sessionId];
+ const progressData = data[itemId];
setValue(Boolean(progressData?.progressImage));
});
return () => {
unsub();
};
- }, [$progressData, sessionId]);
+ }, [$progressData, itemId]);
return value;
};
export const setProgress = (
- $progressData: WritableAtom>,
+ $progressData: WritableAtom>,
data: S['InvocationProgressEvent']
) => {
const progressData = $progressData.get();
- const current = progressData[data.session_id];
+ const current = progressData[data.item_id];
if (current) {
const next = { ...current };
next.progressEvent = data;
@@ -79,13 +77,13 @@ export const setProgress = (
}
$progressData.set({
...progressData,
- [data.session_id]: next,
+ [data.item_id]: next,
});
} else {
$progressData.set({
...progressData,
- [data.session_id]: {
- sessionId: data.session_id,
+ [data.item_id]: {
+ itemId: data.item_id,
progressEvent: data,
progressImage: data.image ?? null,
},
@@ -93,9 +91,9 @@ export const setProgress = (
}
};
-export const clearProgressEvent = ($progressData: WritableAtom>, sessionId: string) => {
+export const clearProgressEvent = ($progressData: WritableAtom>, itemId: number) => {
const progressData = $progressData.get();
- const current = progressData[sessionId];
+ const current = progressData[itemId];
if (!current) {
return;
}
@@ -103,13 +101,13 @@ export const clearProgressEvent = ($progressData: WritableAtom>, sessionId: string) => {
+export const clearProgressImage = ($progressData: WritableAtom>, itemId: number) => {
const progressData = $progressData.get();
- const current = progressData[sessionId];
+ const current = progressData[itemId];
if (!current) {
return;
}
@@ -117,7 +115,7 @@ export const clearProgressImage = ($progressData: WritableAtom;
$selectedItemIndex: Atom;
$autoSwitch: WritableAtom;
+ $lastLoadedItemId: WritableAtom;
};
const CanvasSessionContext = createContext(null);
@@ -159,10 +158,16 @@ export const CanvasSessionContextProvider = memo(
*/
const $autoSwitch = useState(() => atom(true))[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(() => atom>({}))[0];
+ const $progressData = useState(() => atom>({}))[0];
/**
* The currently selected queue item's ID, or null if one is not selected.
@@ -231,21 +236,10 @@ export const CanvasSessionContextProvider = memo(
setProgress($progressData, data);
};
- const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
- if (data.destination !== session.id) {
- return;
- }
- if (data.status === 'completed' && $autoSwitch.get()) {
- $selectedItemId.set(data.item_id);
- }
- };
-
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);
};
}, [$autoSwitch, $progressData, $selectedItemId, session.id, socket]);
@@ -285,23 +279,37 @@ export const CanvasSessionContextProvider = memo(
// Clean up the progress data when a queue item is discarded.
const unsubCleanUpProgressData = effect([$items, $progressData], (items, progressData) => {
- const toDelete: string[] = [];
+ const toDelete: number[] = [];
for (const datum of Object.values(progressData)) {
- if (items.findIndex(({ session_id }) => session_id === datum.sessionId) === -1) {
- toDelete.push(datum.sessionId);
+ if (items.findIndex(({ item_id }) => item_id === datum.itemId) === -1) {
+ toDelete.push(datum.itemId);
}
}
if (toDelete.length === 0) {
return;
}
const newProgressData = { ...progressData };
- for (const sessionId of toDelete) {
- delete newProgressData[sessionId];
+ for (const itemId of toDelete) {
+ delete newProgressData[itemId];
}
// This will re-trigger the effect - maybe this could just be a listener on $items? Brain hurt
$progressData.set(newProgressData);
});
+ // 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.
+ const unsubHandleAutoSwitch = $lastLoadedItemId.listen((lastLoadedItemId) => {
+ if (lastLoadedItemId === null) {
+ return;
+ }
+ if ($autoSwitch.get()) {
+ $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(
@@ -310,6 +318,7 @@ export const CanvasSessionContextProvider = memo(
// Clean up all subscriptions and top-level (i.e. non-computed/derived state)
return () => {
+ unsubHandleAutoSwitch();
unsubQueueItemsQuery();
unsubReduxSyncToItemsAtom();
unsubEnsureSelectedItemIdExists();
@@ -318,7 +327,7 @@ export const CanvasSessionContextProvider = memo(
$progressData.set({});
$selectedItemId.set(null);
};
- }, [$items, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
+ }, [$autoSwitch, $items, $lastLoadedItemId, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
const value = useMemo(
() => ({
@@ -330,8 +339,19 @@ export const CanvasSessionContextProvider = memo(
$autoSwitch,
$selectedItem,
$selectedItemIndex,
+ $lastLoadedItemId,
}),
- [$autoSwitch, $hasItems, $items, $progressData, $selectedItem, $selectedItemId, $selectedItemIndex, session]
+ [
+ $autoSwitch,
+ $hasItems,
+ $items,
+ $lastLoadedItemId,
+ $progressData,
+ $selectedItem,
+ $selectedItemId,
+ $selectedItemIndex,
+ session,
+ ]
);
return {children};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
index 6736e2c306..1723b4bebf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
@@ -21,7 +21,7 @@ export const getProgressMessage = (data?: S['InvocationProgressEvent'] | null) =
export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))';
-export const getQueueItemElementId = (item_id: number) => `queue-item-status-card-${item_id}`;
+export const getQueueItemElementId = (itemId: number) => `queue-item-status-card-${itemId}`;
const getOutputImageName = (item: S['SessionQueueItem']) => {
const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index b6c8305bfe..0d62110efd 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -1,9 +1,7 @@
import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
import type { ProgressImage } from 'features/nodes/types/common';
import { round } from 'lodash-es';
-import type { WritableAtom } from 'nanostores';
import { atom, computed, map } from 'nanostores';
-import { useEffect, useState } from 'react';
import type { ImageDTO, S } from 'services/api/types';
import type { AppSocket } from 'services/events/types';
import type { ManagerOptions, SocketOptions } from 'socket.io-client';
@@ -27,103 +25,6 @@ export type ProgressData = {
progressImage: ProgressImage | null;
};
-export const useProgressData = (
- $progressData: WritableAtom>,
- sessionId: string
-): ProgressData => {
- const [value, setValue] = useState(() => {
- return $progressData.get()[sessionId] ?? { sessionId, progressEvent: null, progressImage: null };
- });
- useEffect(() => {
- const unsub = $progressData.subscribe((data) => {
- const progressData = data[sessionId];
- if (!progressData) {
- return;
- }
- setValue(progressData);
- });
- return () => {
- unsub();
- };
- }, [$progressData, sessionId]);
-
- return value;
-};
-
-export const useHasProgressImage = (
- $progressData: WritableAtom>,
- sessionId: string
-): boolean => {
- const [value, setValue] = useState(false);
- useEffect(() => {
- const unsub = $progressData.subscribe((data) => {
- const progressData = data[sessionId];
- setValue(Boolean(progressData?.progressImage));
- });
- return () => {
- unsub();
- };
- }, [$progressData, sessionId]);
-
- return value;
-};
-
-export const setProgress = (
- $progressData: WritableAtom>,
- data: S['InvocationProgressEvent']
-) => {
- const progressData = $progressData.get();
- const current = progressData[data.session_id];
- if (current) {
- const next = { ...current };
- next.progressEvent = data;
- if (data.image) {
- next.progressImage = data.image;
- }
- $progressData.set({
- ...progressData,
- [data.session_id]: next,
- });
- } else {
- $progressData.set({
- ...progressData,
- [data.session_id]: {
- sessionId: data.session_id,
- progressEvent: data,
- progressImage: data.image ?? null,
- },
- });
- }
-};
-
-export const clearProgressEvent = ($progressData: WritableAtom>, sessionId: string) => {
- const progressData = $progressData.get();
- const current = progressData[sessionId];
- if (!current) {
- return;
- }
- const next = { ...current };
- next.progressEvent = null;
- $progressData.set({
- ...progressData,
- [sessionId]: next,
- });
-};
-
-export const clearProgressImage = ($progressData: WritableAtom>, sessionId: string) => {
- const progressData = $progressData.get();
- const current = progressData[sessionId];
- if (!current) {
- return;
- }
- const next = { ...current };
- next.progressImage = null;
- $progressData.set({
- ...progressData,
- [sessionId]: next,
- });
-};
-
export const $lastCanvasProgressEvent = atom(null);
export const $lastCanvasProgressImage = atom(null);
export const $lastWorkflowsProgressEvent = atom(null);
From 86e1a37a0018b6b062a9f7792828c71955fd413a Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 12:01:16 +1000
Subject: [PATCH 049/210] docs(ui): add comment about auto-switch not being
quite right yet
---
.../controlLayers/components/SimpleSession/context.tsx | 4 ++++
1 file changed, 4 insertions(+)
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 c278e38c84..eea98d42e8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -300,6 +300,10 @@ export const CanvasSessionContextProvider = memo(
// 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;
From da4b084a8beaf0ee7c064a24f919d7c10dc1e6dc Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 17:44:02 +1000
Subject: [PATCH 050/210] feat(ui): tweak canvas scroll to zoom feel
---
.../controlLayers/konva/CanvasStageModule.ts | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts
index 1e7c9aebf0..9015017c22 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts
@@ -57,6 +57,7 @@ export class CanvasStageModule extends CanvasModuleBase {
private _intendedScale: number = 1;
private _activeSnapPoint: number | null = null;
private _snapTimeout: number | null = null;
+ private _lastScrollEventTimestamp: number | null = null;
container: HTMLDivElement;
konva: { stage: Konva.Stage };
@@ -336,10 +337,22 @@ export class CanvasStageModule extends CanvasModuleBase {
}
// When wheeling on trackpad, e.evt.ctrlKey is true - in that case, let's reverse the direction
- const delta = e.evt.ctrlKey ? -e.evt.deltaY : e.evt.deltaY;
+ const scrollAmount = e.evt.ctrlKey ? -e.evt.deltaY : e.evt.deltaY;
+
+ const now = window.performance.now();
+ const deltaT = this._lastScrollEventTimestamp === null ? Infinity : now - this._lastScrollEventTimestamp;
+ this._lastScrollEventTimestamp = now;
+
+ let dynamicScaleFactor = this.config.SCALE_FACTOR;
+
+ if (deltaT > 300) {
+ dynamicScaleFactor = this.config.SCALE_FACTOR + (1 - this.config.SCALE_FACTOR) / 2;
+ } else if (deltaT < 300) {
+ dynamicScaleFactor = this.config.SCALE_FACTOR + (1 - this.config.SCALE_FACTOR) * (deltaT / 200);
+ }
// Update the intended scale based on the last intended scale, creating a continuous zoom feel
- const newIntendedScale = this._intendedScale * this.config.SCALE_FACTOR ** delta;
+ const newIntendedScale = this._intendedScale * dynamicScaleFactor ** scrollAmount;
this._intendedScale = this.constrainScale(newIntendedScale);
// Pass control to the snapping logic
@@ -349,7 +362,7 @@ export class CanvasStageModule extends CanvasModuleBase {
// After a short delay, we can reset the intended scale to the current scale
// This allows for continuous zooming without snapping back to the last snapped scale
this._intendedScale = this.getScale();
- }, 100);
+ }, 300);
};
/**
From 5d80642ea4cf6e07056acaedc3e8f13dcee179d8 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 17:45:29 +1000
Subject: [PATCH 051/210] feat(app): support deleting queue items by id or
destination
---
invokeai/app/api/routers/session_queue.py | 28 ++++++++
.../session_queue/session_queue_base.py | 11 +++
.../session_queue/session_queue_common.py | 6 ++
.../session_queue/session_queue_sqlite.py | 69 +++++++++++++++++++
4 files changed, 114 insertions(+)
diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py
index 660d09728c..f19f4bc47c 100644
--- a/invokeai/app/api/routers/session_queue.py
+++ b/invokeai/app/api/routers/session_queue.py
@@ -14,6 +14,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByBatchIDsResult,
CancelByDestinationResult,
ClearResult,
+ DeleteByDestinationResult,
EnqueueBatchResult,
FieldIdentifier,
PruneResult,
@@ -293,6 +294,18 @@ async def get_queue_item(
return ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
+@session_queue_router.delete(
+ "/{queue_id}/i/{item_id}",
+ operation_id="delete_queue_item",
+)
+async def delete_queue_item(
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ item_id: int = Path(description="The queue item to delete"),
+) -> None:
+ """Deletes a queue item"""
+ ApiDependencies.invoker.services.session_queue.delete_queue_item(item_id)
+
+
@session_queue_router.put(
"/{queue_id}/i/{item_id}/cancel",
operation_id="cancel_queue_item",
@@ -322,3 +335,18 @@ async def counts_by_destination(
return ApiDependencies.invoker.services.session_queue.get_counts_by_destination(
queue_id=queue_id, destination=destination
)
+
+
+@session_queue_router.delete(
+ "/{queue_id}/d/{destination}",
+ operation_id="delete_by_destination",
+ responses={200: {"model": DeleteByDestinationResult}},
+)
+async def delete_by_destination(
+ queue_id: str = Path(description="The queue id to query"),
+ destination: str = Path(description="The destination to query"),
+) -> DeleteByDestinationResult:
+ """Deletes all items with the given destination"""
+ return ApiDependencies.invoker.services.session_queue.delete_by_destination(
+ queue_id=queue_id, destination=destination
+ )
diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py
index a2aa844742..17c31e77a7 100644
--- a/invokeai/app/services/session_queue/session_queue_base.py
+++ b/invokeai/app/services/session_queue/session_queue_base.py
@@ -10,6 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
+ DeleteByDestinationResult,
EnqueueBatchResult,
IsEmptyResult,
IsFullResult,
@@ -91,6 +92,11 @@ class SessionQueueBase(ABC):
"""Cancels a session queue item"""
pass
+ @abstractmethod
+ def delete_queue_item(self, item_id: int) -> None:
+ """Deletes a session queue item"""
+ pass
+
@abstractmethod
def fail_queue_item(
self, item_id: int, error_type: str, error_message: str, error_traceback: str
@@ -108,6 +114,11 @@ class SessionQueueBase(ABC):
"""Cancels all queue items with the given batch destination"""
pass
+ @abstractmethod
+ def delete_by_destination(self, queue_id: str, destination: str) -> DeleteByDestinationResult:
+ """Deletes all queue items with the given batch destination"""
+ pass
+
@abstractmethod
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
"""Cancels all queue items with matching queue ID"""
diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py
index 1861110c97..d41fb44533 100644
--- a/invokeai/app/services/session_queue/session_queue_common.py
+++ b/invokeai/app/services/session_queue/session_queue_common.py
@@ -363,6 +363,12 @@ class CancelByDestinationResult(CancelByBatchIDsResult):
pass
+class DeleteByDestinationResult(BaseModel):
+ """Result of deleting by a destination"""
+
+ deleted: int = Field(..., description="Number of queue items deleted")
+
+
class CancelByQueueIDResult(CancelByBatchIDsResult):
"""Result of canceling by queue id"""
diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py
index 0a0ef72456..c31226581a 100644
--- a/invokeai/app/services/session_queue/session_queue_sqlite.py
+++ b/invokeai/app/services/session_queue/session_queue_sqlite.py
@@ -17,6 +17,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
+ DeleteByDestinationResult,
EnqueueBatchResult,
IsEmptyResult,
IsFullResult,
@@ -212,6 +213,19 @@ class SqliteSessionQueue(SessionQueueBase):
) -> SessionQueueItem:
try:
cursor = self._conn.cursor()
+ cursor.execute(
+ """--sql
+ SELECT status FROM session_queue WHERE item_id = ?
+ """,
+ (item_id,),
+ )
+ row = cursor.fetchone()
+ if row is None:
+ raise SessionQueueItemNotFoundError(f"No queue item with id {item_id}")
+ current_status = row[0]
+ # Only update if not already finished (completed, failed or canceled)
+ if current_status in ("completed", "failed", "canceled"):
+ return self.get_queue_item(item_id)
cursor.execute(
"""--sql
UPDATE session_queue
@@ -323,6 +337,27 @@ class SqliteSessionQueue(SessionQueueBase):
queue_item = self._set_queue_item_status(item_id=item_id, status="canceled")
return queue_item
+ def delete_queue_item(self, item_id: int) -> None:
+ """Deletes a session queue item"""
+ try:
+ self.cancel_queue_item(item_id)
+ except SessionQueueItemNotFoundError:
+ pass
+ try:
+ cursor = self._conn.cursor()
+ cursor.execute(
+ """--sql
+ DELETE
+ FROM session_queue
+ WHERE item_id = ?
+ """,
+ (item_id,),
+ )
+ self._conn.commit()
+ except Exception:
+ self._conn.rollback()
+ raise
+
def complete_queue_item(self, item_id: int) -> SessionQueueItem:
queue_item = self._set_queue_item_status(item_id=item_id, status="completed")
return queue_item
@@ -420,6 +455,40 @@ class SqliteSessionQueue(SessionQueueBase):
raise
return CancelByDestinationResult(canceled=count)
+ def delete_by_destination(self, queue_id: str, destination: str) -> DeleteByDestinationResult:
+ try:
+ cursor = self._conn.cursor()
+ current_queue_item = self.get_current(queue_id)
+ if current_queue_item is not None and current_queue_item.destination == destination:
+ self.cancel_queue_item(current_queue_item.item_id)
+ params = (queue_id, destination)
+ cursor.execute(
+ """--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ WHERE
+ queue_id = ?
+ AND destination = ?;
+ """,
+ params,
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ """--sql
+ DELETE
+ FROM session_queue
+ WHERE
+ queue_id = ?
+ AND destination = ?;
+ """,
+ params,
+ )
+ self._conn.commit()
+ except Exception:
+ self._conn.rollback()
+ raise
+ return DeleteByDestinationResult(deleted=count)
+
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
try:
cursor = self._conn.cursor()
From 56938ca0a1e611ec01f0a8cf8c62aaba75e107d9 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 17:45:56 +1000
Subject: [PATCH 052/210] feat(ui): rough out canvas staging area
---
.../addCommitStagingAreaImageListener.ts | 2 +-
.../listeners/imageDeletionListeners.ts | 2 +-
.../AdvancedSession/AdvancedSession.tsx | 33 +++--
.../SimpleSession/QueueItemPreviewMini.tsx | 14 +-
.../SimpleSession/QueueItemProgressImage.tsx | 9 +-
.../SimpleSession/StagingAreaItemsList.tsx | 37 ++++-
.../components/SimpleSession/context.tsx | 132 ++++++++++++++++-
.../components/SimpleSession/shared.ts | 2 +-
.../SimpleSession/use-staging-keyboard-nav.ts | 54 +------
.../StagingArea/StagingAreaToolbar.tsx | 17 ++-
.../StagingAreaToolbarAcceptButton.tsx | 20 +--
.../StagingAreaToolbarDiscardAllButton.tsx | 11 +-
...tagingAreaToolbarDiscardSelectedButton.tsx | 25 ++--
.../StagingAreaToolbarImageCountButton.tsx | 11 +-
.../StagingAreaToolbarNextButton.tsx | 20 +--
.../StagingAreaToolbarPrevButton.tsx | 20 +--
.../StagingAreaToolbarSaveAsMenu.tsx | 86 +++++------
...AreaToolbarSaveSelectedToGalleryButton.tsx | 15 +-
.../controlLayers/hooks/useInvokeCanvas.ts | 5 +
.../controlLayers/konva/CanvasManager.ts | 6 -
.../konva/CanvasObject/CanvasObjectImage.ts | 110 +++++++++-----
.../konva/CanvasStagingAreaModule.ts | 138 +++++++-----------
.../src/features/controlLayers/store/types.ts | 9 +-
.../src/features/controlLayers/store/util.ts | 17 +++
.../deleteImageModal/store/selectors.ts | 6 +-
.../queue/hooks/useCancelCurrentQueueItem.ts | 4 +-
.../queue/hooks/useCancelQueueItem.ts | 2 +-
.../web/src/services/api/endpoints/images.ts | 7 +
.../web/src/services/api/endpoints/queue.ts | 34 ++++-
.../frontend/web/src/services/api/schema.ts | 105 ++++++++++++-
30 files changed, 622 insertions(+), 331 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
index a83da52d21..6ef2af6c2f 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
@@ -17,7 +17,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
effect: async (_, { dispatch }) => {
try {
const req = dispatch(
- queueApi.endpoints.cancelByBatchDestination.initiate(
+ queueApi.endpoints.cancelByDestination.initiate(
{ destination: 'canvas' },
{ fixedCacheKey: 'cancelByBatchOrigin' }
)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
index f0fd91e69b..150cb73e83 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
@@ -58,7 +58,7 @@ const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image
selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => {
let shouldDelete = false;
for (const obj of objects) {
- if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {
+ if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
shouldDelete = true;
break;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
index 73f50ca36f..47fd674016 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
@@ -13,7 +13,8 @@ import { Filter } from 'features/controlLayers/components/Filters/Filter';
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
-import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
+import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
+import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
@@ -52,7 +53,7 @@ const canvasBgSx = {
},
};
-export const AdvancedSession = memo((_props: { session: AdvancedSessionIdentifier }) => {
+export const AdvancedSession = memo(({ session }: { session: AdvancedSessionIdentifier }) => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
@@ -106,13 +107,27 @@ export const AdvancedSession = memo((_props: { session: AdvancedSessionIdentifie
)}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
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 2578731bfa..acb907325b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -11,24 +11,18 @@ import { memo, useCallback, useState } from 'react';
import type { S } from 'services/api/types';
const sx = {
- cursor: 'pointer',
userSelect: 'none',
pos: 'relative',
alignItems: 'center',
justifyContent: 'center',
- overflow: 'hidden',
- h: 'full',
- maxH: 'full',
- maxW: 'full',
- minW: 0,
- minH: 0,
+ h: 108,
+ w: 108,
+ flexShrink: 0,
borderWidth: 1,
borderRadius: 'base',
'&[data-selected="true"]': {
borderColor: 'invokeBlue.300',
},
- aspectRatio: '1/1',
- flexShrink: 0,
} satisfies SystemStyleObject;
type Props = {
@@ -64,7 +58,7 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
onDoubleClick={onDoubleClick}
>
- {imageDTO && }
+ {imageDTO && }
{!imageLoaded && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
index 2ea3dd827e..c21e41e12a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
@@ -1,7 +1,8 @@
import type { ImageProps } from '@invoke-ai/ui-library';
-import { Image } from '@invoke-ai/ui-library';
+import { Flex, Icon, Image } from '@invoke-ai/ui-library';
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { memo } from 'react';
+import { PiImageBold } from 'react-icons/pi';
type Props = { itemId: number } & ImageProps;
@@ -10,7 +11,11 @@ export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
const { progressImage } = useProgressData(ctx.$progressData, itemId);
if (!progressImage) {
- return null;
+ return (
+
+
+
+ );
}
return (
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 d679835707..3e9b9cee37 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
@@ -4,16 +4,49 @@ import { useStore } from '@nanostores/react';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { QueueItemPreviewMini } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewMini';
-import { memo } from 'react';
+import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
+import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { effect } from 'nanostores';
+import { memo, useEffect } from 'react';
export const StagingAreaItemsList = memo(() => {
+ const canvasManager = useCanvasManagerSafe();
const ctx = useCanvasSessionContext();
const items = useStore(ctx.$items);
const selectedItemId = useStore(ctx.$selectedItemId);
+ useEffect(() => {
+ if (!canvasManager) {
+ return;
+ }
+
+ return effect([ctx.$selectedItem, ctx.$progressData], (selectedItem, progressData) => {
+ if (!selectedItem) {
+ canvasManager.stagingArea.render();
+ return;
+ }
+
+ const outputImageName = getOutputImageName(selectedItem);
+
+ if (outputImageName) {
+ canvasManager.stagingArea.render({ type: 'imageName', data: outputImageName });
+ return;
+ }
+
+ const data = progressData[selectedItem.item_id];
+
+ if (data?.progressImage) {
+ canvasManager.stagingArea.render({ type: 'dataURL', data: data.progressImage.dataURL });
+ return;
+ }
+
+ canvasManager.stagingArea.render();
+ });
+ }, [canvasManager, ctx.$progressData, ctx.$selectedItem]);
+
return (
-
+
{items.map((item, i) => (
;
+ $itemCount: Atom;
$hasItems: Atom;
$progressData: WritableAtom>;
$selectedItemId: WritableAtom;
$selectedItem: Atom;
$selectedItemIndex: Atom;
+ $selectedItemOutputImageName: Atom;
$autoSwitch: WritableAtom;
$lastLoadedItemId: WritableAtom;
+ selectNext: () => void;
+ selectPrev: () => void;
+ selectFirst: () => void;
+ selectLast: () => void;
};
const CanvasSessionContext = createContext(null);
@@ -153,6 +160,11 @@ export const CanvasSessionContextProvider = memo(
*/
const $items = useState(() => atom([]))[0];
+ /**
+ * Manually-synced atom containing the queue items for the current session.
+ */
+ const $prevItems = useState(() => atom([]))[0];
+
/**
* Whether auto-switch is enabled.
*/
@@ -174,6 +186,11 @@ export const CanvasSessionContextProvider = memo(
*/
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.
*/
@@ -209,6 +226,23 @@ export const CanvasSessionContextProvider = memo(
})
)[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 $selectedItemOutputImageName = useState(() =>
+ computed([$selectedItem], (selectedItem) => {
+ if (selectedItem === null) {
+ return null;
+ }
+ const outputImageName = getOutputImageName(selectedItem);
+ if (outputImageName === null) {
+ return null;
+ }
+ return outputImageName;
+ })
+ )[0];
+
/**
* A redux selector to select all queue items from the RTK Query cache. It's important that this returns stable
* references if possible to reduce re-renders. All derivations of the queue items (e.g. filtering out canceled
@@ -223,6 +257,54 @@ export const CanvasSessionContextProvider = memo(
[session.id]
);
+ 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) {
@@ -236,10 +318,23 @@ export const CanvasSessionContextProvider = memo(
setProgress($progressData, data);
};
+ const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
+ if (data.destination !== session.id) {
+ return;
+ }
+
+ if (data.status === 'canceled' || data.status === 'failed') {
+ clearProgressEvent($progressData, data.item_id);
+ clearProgressImage($progressData, data.item_id);
+ }
+ };
+
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);
};
}, [$autoSwitch, $progressData, $selectedItemId, session.id, socket]);
@@ -253,6 +348,7 @@ export const CanvasSessionContextProvider = memo(
const prevItems = $items.get();
const items = selectQueueItems(store.getState());
if (items !== prevItems) {
+ $prevItems.set(prevItems);
$items.set(items);
}
});
@@ -272,13 +368,16 @@ export const CanvasSessionContextProvider = memo(
// 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.
if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
- $selectedItemId.set(null);
+ const prevIndex = $prevItems.get().findIndex(({ item_id }) => item_id === selectedItemId);
+ const nextItem = items[prevIndex];
+ $selectedItemId.set(nextItem?.item_id ?? null);
return;
}
});
// Clean up the progress data when a queue item is discarded.
- const unsubCleanUpProgressData = effect([$items, $progressData], (items, progressData) => {
+ const unsubCleanUpProgressData = $items.listen((items) => {
+ const progressData = $progressData.get();
const toDelete: number[] = [];
for (const datum of Object.values(progressData)) {
if (items.findIndex(({ item_id }) => item_id === datum.itemId) === -1) {
@@ -292,7 +391,6 @@ export const CanvasSessionContextProvider = memo(
for (const itemId of toDelete) {
delete newProgressData[itemId];
}
- // This will re-trigger the effect - maybe this could just be a listener on $items? Brain hurt
$progressData.set(newProgressData);
});
@@ -331,7 +429,17 @@ export const CanvasSessionContextProvider = memo(
$progressData.set({});
$selectedItemId.set(null);
};
- }, [$autoSwitch, $items, $lastLoadedItemId, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
+ }, [
+ $autoSwitch,
+ $items,
+ $lastLoadedItemId,
+ $prevItems,
+ $progressData,
+ $selectedItemId,
+ selectQueueItems,
+ session.id,
+ store,
+ ]);
const value = useMemo(
() => ({
@@ -344,17 +452,29 @@ export const CanvasSessionContextProvider = memo(
$selectedItem,
$selectedItemIndex,
$lastLoadedItemId,
+ $selectedItemOutputImageName,
+ $itemCount,
+ selectNext,
+ selectPrev,
+ selectFirst,
+ selectLast,
}),
[
$autoSwitch,
- $hasItems,
$items,
+ $hasItems,
$lastLoadedItemId,
$progressData,
$selectedItem,
$selectedItemId,
$selectedItemIndex,
session,
+ $selectedItemOutputImageName,
+ $itemCount,
+ selectNext,
+ selectPrev,
+ selectFirst,
+ selectLast,
]
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
index 1723b4bebf..d8b7ebc7b1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
@@ -23,7 +23,7 @@ export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0p
export const getQueueItemElementId = (itemId: number) => `queue-item-status-card-${itemId}`;
-const getOutputImageName = (item: S['SessionQueueItem']) => {
+export const getOutputImageName = (item: S['SessionQueueItem']) => {
const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
isCanvasOutputNodeId(nodeId)
)?.[1][0];
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts
index 6a11b5fa99..bb3989b565 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts
@@ -1,57 +1,11 @@
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
export const useStagingAreaKeyboardNav = () => {
const ctx = useCanvasSessionContext();
- const onNext = useCallback(() => {
- const selectedItemId = ctx.$selectedItemId.get();
- if (selectedItemId === null) {
- return;
- }
- const items = ctx.$items.get();
- const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
- const nextIndex = (currentIndex + 1) % items.length;
- const nextItem = items[nextIndex];
- if (!nextItem) {
- return;
- }
- ctx.$selectedItemId.set(nextItem.item_id);
- }, [ctx.$items, ctx.$selectedItemId]);
- const onPrev = useCallback(() => {
- const selectedItemId = ctx.$selectedItemId.get();
- if (selectedItemId === null) {
- return;
- }
- const items = ctx.$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;
- }
- ctx.$selectedItemId.set(prevItem.item_id);
- }, [ctx.$items, ctx.$selectedItemId]);
- const onFirst = useCallback(() => {
- const items = ctx.$items.get();
- const first = items.at(0);
- if (!first) {
- return;
- }
- ctx.$selectedItemId.set(first.item_id);
- }, [ctx.$items, ctx.$selectedItemId]);
- const onLast = useCallback(() => {
- const items = ctx.$items.get();
- const last = items.at(-1);
- if (!last) {
- return;
- }
- ctx.$selectedItemId.set(last.item_id);
- }, [ctx.$items, ctx.$selectedItemId]);
-
- useHotkeys('left', onPrev, { preventDefault: true });
- useHotkeys('right', onNext, { preventDefault: true });
- useHotkeys('meta+left', onFirst, { preventDefault: true });
- useHotkeys('meta+right', onLast, { preventDefault: true });
+ useHotkeys('left', ctx.selectPrev, { preventDefault: true });
+ useHotkeys('right', ctx.selectNext, { preventDefault: true });
+ useHotkeys('meta+left', ctx.selectFirst, { preventDefault: true });
+ useHotkeys('meta+right', ctx.selectLast, { preventDefault: true });
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
index 1404616380..4ac13e74a3 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
@@ -1,4 +1,6 @@
import { ButtonGroup } from '@invoke-ai/ui-library';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton';
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
@@ -8,9 +10,22 @@ import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/
import { StagingAreaToolbarSaveAsMenu } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu';
import { StagingAreaToolbarSaveSelectedToGalleryButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton';
import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton';
-import { memo } from 'react';
+import { memo, useEffect } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
export const StagingAreaToolbar = memo(() => {
+ const ctx = useCanvasSessionContext();
+
+ useEffect(() => {
+ return ctx.$selectedItemId.listen((id) => {
+ if (id !== null) {
+ document.getElementById(getQueueItemElementId(id))?.scrollIntoView();
+ }
+ });
+ }, [ctx.$selectedItemId]);
+
+ useHotkeys('meta+left', ctx.selectFirst, { preventDefault: true });
+ useHotkeys('meta+right', ctx.selectLast, { preventDefault: true });
return (
<>
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 7d01422854..3a1feddebc 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
@@ -2,48 +2,48 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
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 {
selectImageCount,
- selectSelectedImage,
stagingAreaReset,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
-import { imageDTOToImageObject } from 'features/controlLayers/store/util';
+import { imageNameToImageObject } from 'features/controlLayers/store/util';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi';
export const StagingAreaToolbarAcceptButton = memo(() => {
+ const ctx = useCanvasSessionContext();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const bboxRect = useAppSelector(selectBboxRect);
- const selectedImage = useAppSelector(selectSelectedImage);
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const imageCount = useAppSelector(selectImageCount);
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
+ const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName);
const { t } = useTranslation();
const acceptSelected = useCallback(() => {
- if (!selectedImage) {
+ if (!selectedItemImageName) {
return;
}
- const { x, y } = bboxRect;
- const { imageDTO, offsetX, offsetY } = selectedImage;
- const imageObject = imageDTOToImageObject(imageDTO);
+ const { x, y, width, height } = bboxRect;
+ const imageObject = imageNameToImageObject(selectedItemImageName, { width, height });
const overrides: Partial = {
- position: { x: x + offsetX, y: y + offsetY },
+ position: { x, y },
objects: [imageObject],
};
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
dispatch(stagingAreaReset());
- }, [bboxRect, dispatch, selectedEntityIdentifier?.type, selectedImage]);
+ }, [bboxRect, selectedItemImageName, dispatch, selectedEntityIdentifier?.type]);
useHotkeys(
['enter'],
@@ -62,7 +62,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
icon={}
onClick={acceptSelected}
colorScheme="invokeBlue"
- isDisabled={!selectedImage}
+ isDisabled={!selectedItemImageName}
/>
);
});
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 ee038c075e..dbc978a42a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
@@ -1,17 +1,18 @@
import { IconButton } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
-import { stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
+import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarDiscardAllButton = memo(() => {
- const dispatch = useAppDispatch();
+ const ctx = useCanvasSessionContext();
const { t } = useTranslation();
+ const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
const discardAll = useCallback(() => {
- dispatch(stagingAreaReset());
- }, [dispatch]);
+ deleteByDestination({ destination: ctx.session.id });
+ }, [deleteByDestination, ctx.session.id]);
return (
{
+ const ctx = useCanvasSessionContext();
const dispatch = useAppDispatch();
+ const [deleteQueueItem] = useDeleteQueueItemMutation();
+ const selectedItemId = useStore(ctx.$selectedItemId);
const index = useAppSelector(selectStagedImageIndex);
const selectedImage = useAppSelector(selectSelectedImage);
const imageCount = useAppSelector(selectImageCount);
@@ -20,15 +24,16 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
const { t } = useTranslation();
const discardSelected = useCallback(() => {
- if (!selectedImage) {
+ if (selectedItemId === null) {
return;
}
- if (imageCount === 1) {
- dispatch(stagingAreaReset());
- } else {
- dispatch(stagingAreaStagedImageDiscarded({ index }));
- }
- }, [selectedImage, imageCount, dispatch, index]);
+ deleteQueueItem({ item_id: selectedItemId });
+ // if (imageCount === 1) {
+ // dispatch(stagingAreaReset());
+ // } else {
+ // dispatch(stagingAreaStagedImageDiscarded({ index }));
+ // }
+ }, [selectedItemId, deleteQueueItem]);
return (
{
onClick={discardSelected}
colorScheme="invokeBlue"
fontSize={16}
- isDisabled={!selectedImage}
+ isDisabled={selectedItemId === null}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
index d408bf1c90..7e14ff580a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
@@ -1,19 +1,24 @@
import { Button } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { selectImageCount, selectStagedImageIndex } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useMemo } from 'react';
export const StagingAreaToolbarImageCountButton = memo(() => {
+ const ctx = useCanvasSessionContext();
+ const selectItemIndex = useStore(ctx.$selectedItemIndex);
+ const itemCount = useStore(ctx.$itemCount);
const index = useAppSelector(selectStagedImageIndex);
const imageCount = useAppSelector(selectImageCount);
const counterText = useMemo(() => {
- if (imageCount > 0) {
- return `${(index ?? 0) + 1} of ${imageCount}`;
+ if (itemCount > 0 && selectItemIndex !== null) {
+ return `${selectItemIndex + 1} of ${itemCount}`;
} else {
return `0 of 0`;
}
- }, [imageCount, index]);
+ }, [itemCount, selectItemIndex]);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
index 3809601a62..8d60420de2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
@@ -2,17 +2,17 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import {
- selectImageCount,
- stagingAreaNextStagedImageSelected,
-} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectImageCount } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiArrowRightBold } from 'react-icons/pi';
export const StagingAreaToolbarNextButton = memo(() => {
+ const ctx = useCanvasSessionContext();
+ const itemCount = useStore(ctx.$itemCount);
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const imageCount = useAppSelector(selectImageCount);
@@ -22,17 +22,17 @@ export const StagingAreaToolbarNextButton = memo(() => {
const { t } = useTranslation();
const selectNext = useCallback(() => {
- dispatch(stagingAreaNextStagedImageSelected());
- }, [dispatch]);
+ ctx.selectNext();
+ }, [ctx]);
useHotkeys(
['right'],
- selectNext,
+ ctx.selectNext,
{
preventDefault: true,
- enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
+ enabled: isCanvasFocused && shouldShowStagedImage && itemCount > 1,
},
- [isCanvasFocused, shouldShowStagedImage, imageCount]
+ [isCanvasFocused, shouldShowStagedImage, itemCount, ctx.selectNext]
);
return (
@@ -42,7 +42,7 @@ export const StagingAreaToolbarNextButton = memo(() => {
icon={}
onClick={selectNext}
colorScheme="invokeBlue"
- isDisabled={imageCount <= 1 || !shouldShowStagedImage}
+ isDisabled={itemCount <= 1 || !shouldShowStagedImage}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
index 3d064abc2f..ae1539a20a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
@@ -2,17 +2,17 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import {
- selectImageCount,
- stagingAreaPrevStagedImageSelected,
-} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectImageCount } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiArrowLeftBold } from 'react-icons/pi';
export const StagingAreaToolbarPrevButton = memo(() => {
+ const ctx = useCanvasSessionContext();
+ const itemCount = useStore(ctx.$itemCount);
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const imageCount = useAppSelector(selectImageCount);
@@ -22,17 +22,17 @@ export const StagingAreaToolbarPrevButton = memo(() => {
const { t } = useTranslation();
const selectPrev = useCallback(() => {
- dispatch(stagingAreaPrevStagedImageSelected());
- }, [dispatch]);
+ ctx.selectPrev();
+ }, [ctx]);
useHotkeys(
['left'],
- selectPrev,
+ ctx.selectPrev,
{
preventDefault: true,
- enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
+ enabled: isCanvasFocused && shouldShowStagedImage && itemCount > 1,
},
- [isCanvasFocused, shouldShowStagedImage, imageCount]
+ [isCanvasFocused, shouldShowStagedImage, itemCount, ctx.selectPrev]
);
return (
@@ -42,7 +42,7 @@ export const StagingAreaToolbarPrevButton = memo(() => {
icon={}
onClick={selectPrev}
colorScheme="invokeBlue"
- isDisabled={imageCount <= 1 || !shouldShowStagedImage}
+ isDisabled={itemCount <= 1 || !shouldShowStagedImage}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx
index 2555000c9c..fe37c536e6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx
@@ -1,29 +1,37 @@
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/nanostores/store';
-import { useAppSelector } from 'app/store/storeHooks';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
-import { selectSelectedImage } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDotsThreeBold } from 'react-icons/pi';
-import { imageDTOToFile, uploadImage } from 'services/api/endpoints/images';
+import { copyImage } from 'services/api/endpoints/images';
const uploadImageArg = { image_category: 'general', is_intermediate: true, silent: true } as const;
export const StagingAreaToolbarSaveAsMenu = memo(() => {
const { t } = useTranslation();
- const selectedImage = useAppSelector(selectSelectedImage);
+ const ctx = useCanvasSessionContext();
+ const imageName = useStore(ctx.$selectedItemOutputImageName);
const store = useAppStore();
+ const toastSentToCanvas = useCallback(() => {
+ toast({
+ id: 'SENT_TO_CANVAS',
+ title: t('toast.sentToCanvas'),
+ status: 'success',
+ });
+ }, [t]);
+
const onClickNewRasterLayerFromImage = useCallback(async () => {
- if (!selectedImage) {
+ if (!imageName) {
return;
}
const { dispatch, getState } = store;
- const file = await imageDTOToFile(selectedImage.imageDTO);
- const imageDTO = await uploadImage({ file, ...uploadImageArg });
+ const imageDTO = await copyImage(imageName, uploadImageArg);
createNewCanvasEntityFromImage({
imageDTO,
type: 'raster_layer',
@@ -31,20 +39,16 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
getState,
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
});
- toast({
- id: 'SENT_TO_CANVAS',
- title: t('toast.sentToCanvas'),
- status: 'success',
- });
- }, [selectedImage, store, t]);
+ toastSentToCanvas();
+ }, [imageName, store, toastSentToCanvas]);
const onClickNewControlLayerFromImage = useCallback(async () => {
- if (!selectedImage) {
+ if (!imageName) {
return;
}
const { dispatch, getState } = store;
- const file = await imageDTOToFile(selectedImage.imageDTO);
- const imageDTO = await uploadImage({ file, ...uploadImageArg });
+ const imageDTO = await copyImage(imageName, uploadImageArg);
+
createNewCanvasEntityFromImage({
imageDTO,
type: 'control_layer',
@@ -52,20 +56,16 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
getState,
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
});
- toast({
- id: 'SENT_TO_CANVAS',
- title: t('toast.sentToCanvas'),
- status: 'success',
- });
- }, [selectedImage, store, t]);
+ toastSentToCanvas();
+ }, [imageName, store, toastSentToCanvas]);
const onClickNewInpaintMaskFromImage = useCallback(async () => {
- if (!selectedImage) {
+ if (!imageName) {
return;
}
const { dispatch, getState } = store;
- const file = await imageDTOToFile(selectedImage.imageDTO);
- const imageDTO = await uploadImage({ file, ...uploadImageArg });
+ const imageDTO = await copyImage(imageName, uploadImageArg);
+
createNewCanvasEntityFromImage({
imageDTO,
type: 'inpaint_mask',
@@ -73,20 +73,16 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
getState,
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
});
- toast({
- id: 'SENT_TO_CANVAS',
- title: t('toast.sentToCanvas'),
- status: 'success',
- });
- }, [selectedImage, store, t]);
+ toastSentToCanvas();
+ }, [imageName, store, toastSentToCanvas]);
const onClickNewRegionalGuidanceFromImage = useCallback(async () => {
- if (!selectedImage) {
+ if (!imageName) {
return;
}
const { dispatch, getState } = store;
- const file = await imageDTOToFile(selectedImage.imageDTO);
- const imageDTO = await uploadImage({ file, ...uploadImageArg });
+ const imageDTO = await copyImage(imageName, uploadImageArg);
+
createNewCanvasEntityFromImage({
imageDTO,
type: 'regional_guidance',
@@ -94,12 +90,8 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
getState,
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
});
- toast({
- id: 'SENT_TO_CANVAS',
- title: t('toast.sentToCanvas'),
- status: 'success',
- });
- }, [selectedImage, store, t]);
+ toastSentToCanvas();
+ }, [imageName, store, toastSentToCanvas]);
return (
@@ -109,23 +101,19 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
tooltip={t('controlLayers.newLayerFromImage')}
icon={}
colorScheme="invokeBlue"
- isDisabled={!selectedImage}
+ isDisabled={!imageName}
/>
- } onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={!selectedImage}>
+ } onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={!imageName}>
{t('controlLayers.inpaintMask')}
- }
- onClickCapture={onClickNewRegionalGuidanceFromImage}
- isDisabled={!selectedImage}
- >
+ } onClickCapture={onClickNewRegionalGuidanceFromImage} isDisabled={!imageName}>
{t('controlLayers.regionalGuidance')}
- } onClickCapture={onClickNewControlLayerFromImage} isDisabled={!selectedImage}>
+ } onClickCapture={onClickNewControlLayerFromImage} isDisabled={!imageName}>
{t('controlLayers.controlLayer')}
- } onClickCapture={onClickNewRasterLayerFromImage} isDisabled={!selectedImage}>
+ } onClickCapture={onClickNewRasterLayerFromImage} isDisabled={!imageName}>
{t('controlLayers.rasterLayer')}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
index fe2afc8bd1..ce5f5707b1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
@@ -1,24 +1,28 @@
import { IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { withResultAsync } from 'common/util/result';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { selectSelectedImage } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFloppyDiskBold } from 'react-icons/pi';
-import { imageDTOToFile, uploadImage } from 'services/api/endpoints/images';
+import { copyImage } from 'services/api/endpoints/images';
const TOAST_ID = 'SAVE_STAGING_AREA_IMAGE_TO_GALLERY';
export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const selectedImage = useAppSelector(selectSelectedImage);
+ const ctx = useCanvasSessionContext();
+ const imageName = useStore(ctx.$selectedItemOutputImageName);
const { t } = useTranslation();
const saveSelectedImageToGallery = useCallback(async () => {
- if (!selectedImage) {
+ if (!imageName) {
return;
}
@@ -26,10 +30,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
// the gallery without borking the canvas, which may need this image to exist.
const result = await withResultAsync(async () => {
// Create a new file with the same name, which we will upload
- const file = await imageDTOToFile(selectedImage.imageDTO);
-
- await uploadImage({
- file,
+ await copyImage(imageName, {
// Image should show up in the Images tab
image_category: 'general',
is_intermediate: false,
@@ -53,7 +54,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
status: 'error',
});
}
- }, [autoAddBoardId, selectedImage, t]);
+ }, [autoAddBoardId, imageName, t]);
return (
void) => {
const manager = new CanvasManager(container, store, socket);
manager.initialize();
+
+ return () => {
+ manager.destroy();
+ $canvasManager.set(null);
+ };
}, [container, socket, store]);
return containerRef;
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
index 4145197edf..52592c8019 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
@@ -10,7 +10,6 @@ import { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konv
import type { CanvasEntityAdapter, CanvasEntityAdapterFromType } from 'features/controlLayers/konva/CanvasEntity/types';
import { CanvasEntityRendererModule } from 'features/controlLayers/konva/CanvasEntityRendererModule';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
-import { CanvasProgressImageModule } from 'features/controlLayers/konva/CanvasProgressImageModule';
import { CanvasStageModule } from 'features/controlLayers/konva/CanvasStageModule';
import { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStagingAreaModule';
import { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule';
@@ -66,7 +65,6 @@ export class CanvasManager extends CanvasModuleBase {
compositor: CanvasCompositorModule;
tool: CanvasToolModule;
stagingArea: CanvasStagingAreaModule;
- progressImage: CanvasProgressImageModule;
konva: {
previewLayer: Konva.Layer;
@@ -131,11 +129,9 @@ export class CanvasManager extends CanvasModuleBase {
this.stage.addLayer(this.konva.previewLayer);
this.tool = new CanvasToolModule(this);
- this.progressImage = new CanvasProgressImageModule(this);
// Must add in this order for correct z-index
this.konva.previewLayer.add(this.stagingArea.konva.group);
- this.konva.previewLayer.add(this.progressImage.konva.group);
this.konva.previewLayer.add(this.tool.konva.group);
}
@@ -238,7 +234,6 @@ export class CanvasManager extends CanvasModuleBase {
return [
this.stagingArea,
this.tool,
- this.progressImage,
this.stateApi,
this.background,
this.worker,
@@ -285,7 +280,6 @@ export class CanvasManager extends CanvasModuleBase {
stateApi: this.stateApi.repr(),
stagingArea: this.stagingArea.repr(),
tool: this.tool.repr(),
- progressImage: this.progressImage.repr(),
background: this.background.repr(),
worker: this.worker.repr(),
entityRenderer: this.entityRenderer.repr(),
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject/CanvasObjectImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject/CanvasObjectImage.ts
index dba4fe4dfb..0b00dacb7b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject/CanvasObjectImage.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject/CanvasObjectImage.ts
@@ -12,6 +12,7 @@ import { getKonvaNodeDebugAttrs, loadImage } from 'features/controlLayers/konva/
import type { CanvasImageState } from 'features/controlLayers/store/types';
import { t } from 'i18next';
import Konva from 'konva';
+import { isEqual } from 'lodash-es';
import type { Logger } from 'roarr';
import { getImageDTOSafe } from 'services/api/endpoints/images';
@@ -94,7 +95,7 @@ export class CanvasObjectImage extends CanvasModuleBase {
this.state = state;
}
- updateImageSource = async (imageName: string) => {
+ updateImageSourceByImageName = async (imageName: string) => {
this.log.trace({ imageName }, 'Updating image source');
this.isLoading = true;
@@ -121,7 +122,30 @@ export class CanvasObjectImage extends CanvasModuleBase {
this.imageElement = imageElementResult.value;
- await this.updateImageElement();
+ this.updateImageElement();
+ };
+
+ updateImageSourceByDataURL = async (dataURL: string) => {
+ this.log.trace({ dataURL: `${dataURL.substring(0, 16)}...` }, 'Updating image source');
+
+ this.isLoading = true;
+ this.konva.group.visible(true);
+
+ if (!this.konva.image) {
+ this.konva.placeholder.group.visible(false);
+ this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image'));
+ }
+
+ const imageElementResult = await withResultAsync(() => loadImage(dataURL, false));
+ if (imageElementResult.isErr()) {
+ // Image loading failed (e.g. the URL to the "physical" image is invalid)
+ this.onFailedToLoadImage(t('controlLayers.unableToLoadImage', 'Unable to load image'));
+ return;
+ }
+
+ this.imageElement = imageElementResult.value;
+
+ this.updateImageElement();
};
onFailedToLoadImage = (message: string) => {
@@ -133,43 +157,37 @@ export class CanvasObjectImage extends CanvasModuleBase {
this.konva.placeholder.group.visible(true);
};
- updateImageElement = async () => {
- const release = await this.mutex.acquire();
+ updateImageElement = () => {
+ if (this.imageElement) {
+ const { width, height } = this.state.image;
- try {
- if (this.imageElement) {
- const { width, height } = this.state.image;
-
- if (this.konva.image) {
- this.log.trace('Updating Konva image attrs');
- this.konva.image.setAttrs({
- image: this.imageElement,
- width,
- height,
- visible: true,
- });
- } else {
- this.log.trace('Creating new Konva image');
- this.konva.image = new Konva.Image({
- name: `${this.type}:image`,
- listening: false,
- image: this.imageElement,
- width,
- height,
- perfectDrawEnabled: false,
- });
- this.konva.group.add(this.konva.image);
- }
-
- this.konva.placeholder.rect.setAttrs({ width, height });
- this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 });
-
- this.isLoading = false;
- this.isError = false;
- this.konva.placeholder.group.visible(false);
+ if (this.konva.image) {
+ this.log.trace('Updating Konva image attrs');
+ this.konva.image.setAttrs({
+ image: this.imageElement,
+ width,
+ height,
+ visible: true,
+ });
+ } else {
+ this.log.trace('Creating new Konva image');
+ this.konva.image = new Konva.Image({
+ name: `${this.type}:image`,
+ listening: false,
+ image: this.imageElement,
+ width,
+ height,
+ perfectDrawEnabled: false,
+ });
+ this.konva.group.add(this.konva.image);
}
- } finally {
- release();
+
+ this.konva.placeholder.rect.setAttrs({ width, height });
+ this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 });
+
+ this.isLoading = false;
+ this.isError = false;
+ this.konva.placeholder.group.visible(false);
}
};
@@ -178,10 +196,22 @@ export class CanvasObjectImage extends CanvasModuleBase {
this.log.trace({ state }, 'Updating image');
const { image } = state;
- const { width, height, image_name } = image;
- if (force || (this.state.image.image_name !== image_name && !this.isLoading)) {
- await this.updateImageSource(image_name);
+ const { width, height } = image;
+
+ if (force || (!isEqual(this.state, state) && !this.isLoading)) {
+ const release = await this.mutex.acquire();
+
+ try {
+ if ('image_name' in image) {
+ await this.updateImageSourceByImageName(image.image_name);
+ } else {
+ await this.updateImageSourceByDataURL(image.dataURL);
+ }
+ } finally {
+ release();
+ }
}
+
this.konva.image?.setAttrs({ width, height });
this.state = state;
return true;
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
index a993ed7ef6..a0e54b8106 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
@@ -1,10 +1,9 @@
+import { Mutex } from 'async-mutex';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasStagingAreaSlice, selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import type { StagingAreaImage } from 'features/controlLayers/store/types';
-import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
+import type { CanvasImageState } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { atom } from 'nanostores';
import type { Logger } from 'roarr';
@@ -20,10 +19,10 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
subscriptions: Set<() => void> = new Set();
konva: { group: Konva.Group };
image: CanvasObjectImage | null;
- selectedImage: StagingAreaImage | null;
+ mutex = new Mutex();
$shouldShowStagedImage = atom(true);
- $isStaging = atom(false);
+ $isStaging = atom(true); //TODO: wire up to queue?
constructor(manager: CanvasManager) {
super();
@@ -37,30 +36,13 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }) };
this.image = null;
- this.selectedImage = null;
/**
* When we change this flag, we need to re-render the staging area, which hides or shows the staged image.
*/
- this.subscriptions.add(this.$shouldShowStagedImage.listen(this.render));
- /**
- * When the staging redux state changes (i.e. when the selected staged image is changed, or we add/discard a staged
- * image), we need to re-render the staging area.
- */
- this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasStagingAreaSlice, this.render));
- /**
- * Sync the $isStaging flag with the redux state. $isStaging is used by the manager to determine the global busy
- * state of the canvas.
- *
- * We also set the $shouldShowStagedImage flag when we enter staging mode, so that the staged images are shown,
- * even if the user disabled this in the last staging session.
- */
this.subscriptions.add(
- this.manager.stateApi.createStoreSubscription(selectIsStaging, (isStaging, oldIsStaging) => {
- this.$isStaging.set(isStaging);
- if (isStaging && !oldIsStaging) {
- this.$shouldShowStagedImage.set(true);
- }
+ this.$shouldShowStagedImage.listen(() => {
+ this.render();
})
);
}
@@ -68,71 +50,56 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
initialize = () => {
this.log.debug('Initializing module');
this.render();
- this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
};
- render = async () => {
- this.log.trace('Rendering staging area');
- const stagingArea = this.manager.stateApi.runSelector(selectCanvasStagingAreaSlice);
-
- const { x, y } = this.manager.stateApi.getBbox().rect;
- const shouldShowStagedImage = this.$shouldShowStagedImage.get();
-
- this.selectedImage = stagingArea.images[stagingArea.selectedImageIndex] ?? null;
- this.konva.group.position({ x, y });
-
- if (this.selectedImage) {
- const { imageDTO } = this.selectedImage;
- const image = imageDTOToImageWithDims(imageDTO);
-
- /**
- * When the final output image of a generation is received, we should clear that generation's last progress image.
- *
- * It's possible that we have already rendered the progress image from the next generation before the output image
- * from the previous is fully loaded/rendered. This race condition results in a flicker:
- * - LAST GENERATION: Render the final progress image
- * - LAST GENERATION: Start loading the final output image...
- * - NEXT GENERATION: Render the first progress image
- * - LAST GENERATION: ...Finish loading the final output image & render it, clearing the progress image <-- Flicker!
- * - NEXT GENERATION: Render the next progress image
- *
- * We can detect the race condition by stashing the session ID of the last progress image when we begin loading
- * that session's output image. After we render it, if the progress image's session ID is the same as the one we
- * stashed, we know that we have not yet gotten that next generation's first progress image. We can clear the
- * progress image without causing a flicker.
- */
- const lastProgressEventSessionId = this.manager.progressImage.$lastProgressEvent.get()?.session_id;
- const hideProgressIfSameSession = () => {
- const currentProgressEventSessionId = this.manager.progressImage.$lastProgressEvent.get()?.session_id;
- if (lastProgressEventSessionId === currentProgressEventSessionId) {
- this.manager.progressImage.$lastProgressEvent.set(null);
- }
+ getImageFromSrc = (
+ { type, data }: { type: 'imageName'; data: string } | { type: 'dataURL'; data: string },
+ width: number,
+ height: number
+ ): CanvasImageState['image'] => {
+ if (type === 'imageName') {
+ return {
+ image_name: data,
+ width,
+ height,
};
-
- if (!this.image) {
- this.image = new CanvasObjectImage(
- {
- id: 'staging-area-image',
- type: 'image',
- image,
- },
- this
- );
- await this.image.update(this.image.state, true);
- this.konva.group.add(this.image.konva.group);
- hideProgressIfSameSession();
- } else if (this.image.isLoading) {
- // noop - just wait for the image to load
- } else if (this.image.state.image.image_name !== image.image_name) {
- await this.image.update({ ...this.image.state, image }, true);
- hideProgressIfSameSession();
- } else if (this.image.isError) {
- hideProgressIfSameSession();
- }
- this.image.konva.group.visible(shouldShowStagedImage);
} else {
- this.image?.destroy();
- this.image = null;
+ return {
+ dataURL: data,
+ width,
+ height,
+ };
+ }
+ };
+
+ render = async (imageSrc?: { type: 'imageName'; data: string } | { type: 'dataURL'; data: string }) => {
+ const release = await this.mutex.acquire();
+ try {
+ this.log.trace('Rendering staging area');
+
+ const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
+ const shouldShowStagedImage = this.$shouldShowStagedImage.get();
+
+ this.konva.group.position({ x, y });
+
+ if (imageSrc) {
+ const image = this.getImageFromSrc(imageSrc, width, height);
+ if (!this.image) {
+ this.image = new CanvasObjectImage({ id: 'staging-area-image', type: 'image', image }, this);
+ await this.image.update(this.image.state, true);
+ this.konva.group.add(this.image.konva.group);
+ } else if (this.image.isLoading || this.image.isError) {
+ // noop
+ } else {
+ await this.image.update({ ...this.image.state, image }, true);
+ }
+ this.image.konva.group.visible(shouldShowStagedImage);
+ } else {
+ this.image?.destroy();
+ this.image = null;
+ }
+ } finally {
+ release();
}
};
@@ -157,7 +124,6 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
id: this.id,
type: this.type,
path: this.path,
- selectedImage: this.selectedImage,
$shouldShowStagedImage: this.$shouldShowStagedImage.get(),
$isStaging: this.$isStaging.get(),
image: this.image?.repr() ?? null,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 070547f246..8768187044 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -59,6 +59,13 @@ const zImageWithDims = z
});
export type ImageWithDims = z.infer;
+const zImageWithDimsDataURL = z.object({
+ dataURL: z.string(),
+ width: z.number().int().positive(),
+ height: z.number().int().positive(),
+});
+export type ImageWithDimsDataURL = z.infer;
+
const zBeginEndStepPct = z
.tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
.refine(([begin, end]) => begin < end, {
@@ -231,7 +238,7 @@ export type CanvasRectState = z.infer;
const zCanvasImageState = z.object({
id: zId,
type: z.literal('image'),
- image: zImageWithDims,
+ image: z.union([zImageWithDims, zImageWithDimsDataURL]),
});
export type CanvasImageState = z.infer;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/util.ts b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
index 5fb3cc27dc..a4fbfffd02 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
@@ -10,6 +10,7 @@ import type {
ChatGPT4oReferenceImageConfig,
ControlLoRAConfig,
ControlNetConfig,
+ Dimensions,
FluxKontextReferenceImageConfig,
FLUXReduxConfig,
ImageWithDims,
@@ -35,6 +36,22 @@ export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial
+): CanvasImageState => {
+ return {
+ id: getPrefixedId('image'),
+ type: 'image',
+ image: {
+ image_name: imageName,
+ ...dimensions,
+ },
+ ...overrides,
+ };
+};
+
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
image_name,
width,
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
index ccaeb9abbe..9782ea5047 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
@@ -38,15 +38,15 @@ export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: U
);
const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
- objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name)
+ objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
);
const isControlLayerImage = canvas.controlLayers.entities.some(({ objects }) =>
- objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name)
+ objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
);
const isInpaintMaskImage = canvas.inpaintMasks.entities.some(({ objects }) =>
- objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name)
+ objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
);
const isRegionalGuidanceImage = canvas.regionalGuidance.entities.some(({ referenceImages }) =>
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts
index 12173aa7aa..0f9a5c48fe 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts
@@ -13,11 +13,11 @@ export const useCancelCurrentQueueItem = () => {
const { t } = useTranslation();
const currentQueueItemId = useMemo(() => queueStatus?.queue.item_id, [queueStatus?.queue.item_id]);
const cancelQueueItem = useCallback(async () => {
- if (!currentQueueItemId) {
+ if (currentQueueItemId !== null || currentQueueItemId !== undefined) {
return;
}
try {
- await trigger(currentQueueItemId).unwrap();
+ await trigger({ item_id: currentQueueItemId }).unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts
index 4ac87a1aad..1e59f8ec46 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts
@@ -11,7 +11,7 @@ export const useCancelQueueItem = (item_id: number) => {
const { t } = useTranslation();
const cancelQueueItem = useCallback(async () => {
try {
- await trigger(item_id).unwrap();
+ await trigger({ item_id }).unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts
index 13116a88de..590b83fe57 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/images.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts
@@ -674,6 +674,13 @@ export const uploadImage = (arg: UploadImageArg): Promise => {
return req.unwrap();
};
+export const copyImage = async (imageName: string, uploadImageArg: Omit): Promise => {
+ const originalImageDTO = await getImageDTO(imageName);
+ const file = await imageDTOToFile(originalImageDTO);
+ const imageDTO = await uploadImage({ file, ...uploadImageArg });
+ return imageDTO;
+};
+
export const uploadImages = async (args: UploadImageArg[]): Promise => {
const { dispatch } = getStore();
const results = await Promise.allSettled(
diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
index 38f11b9928..6a13f7e2e4 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
@@ -79,7 +79,12 @@ export const queueApi = api.injectEndpoints({
url: buildQueueUrl('prune'),
method: 'PUT',
}),
- invalidatesTags: ['SessionQueueStatus', 'BatchStatus', { type: 'SessionQueueItem', id: LIST_TAG }],
+ invalidatesTags: [
+ 'SessionQueueStatus',
+ 'BatchStatus',
+ { type: 'SessionQueueItem', id: LIST_TAG },
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
+ ],
}),
clearQueue: build.mutation<
paths['/api/v1/queue/{queue_id}/clear']['put']['responses']['200']['content']['application/json'],
@@ -176,9 +181,9 @@ export const queueApi = api.injectEndpoints({
}),
cancelQueueItem: build.mutation<
paths['/api/v1/queue/{queue_id}/i/{item_id}/cancel']['put']['responses']['200']['content']['application/json'],
- number
+ { item_id: number }
>({
- query: (item_id) => ({
+ query: ({ item_id }) => ({
url: buildQueueUrl(`i/${item_id}/cancel`),
method: 'PUT',
}),
@@ -219,7 +224,7 @@ export const queueApi = api.injectEndpoints({
];
},
}),
- cancelByBatchDestination: build.mutation<
+ cancelByDestination: build.mutation<
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['responses']['200']['content']['application/json'],
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['parameters']['query']
>({
@@ -319,6 +324,24 @@ export const queueApi = api.injectEndpoints({
return tags;
},
}),
+ deleteQueueItem: build.mutation({
+ query: ({ item_id }) => ({
+ url: buildQueueUrl(`i/${item_id}`),
+ method: 'DELETE',
+ }),
+ invalidatesTags: (result, error, { item_id }) => [{ type: 'SessionQueueItem', id: item_id }],
+ }),
+ deleteQueueItemsByDestination: build.mutation({
+ query: ({ destination }) => ({
+ url: buildQueueUrl(`d/${destination}`),
+ method: 'DELETE',
+ }),
+ invalidatesTags: (result, error, { destination }) => [
+ { type: 'QueueCountsByDestination', id: destination },
+ { type: 'SessionQueueItem', id: LIST_TAG },
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
+ ],
+ }),
getQueueCountsByDestination: build.query<
paths['/api/v1/queue/{queue_id}/counts_by_destination']['get']['responses']['200']['content']['application/json'],
paths['/api/v1/queue/{queue_id}/counts_by_destination']['get']['parameters']['query']
@@ -345,6 +368,9 @@ export const {
useListQueueItemsQuery,
useListAllQueueItemsQuery,
useCancelQueueItemMutation,
+ useCancelByDestinationMutation,
+ useDeleteQueueItemMutation,
+ useDeleteQueueItemsByDestinationMutation,
useGetBatchStatusQuery,
useGetCurrentQueueItemQuery,
useGetQueueCountsByDestinationQuery,
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index d44246571e..e167ba36fd 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -1438,7 +1438,11 @@ export type paths = {
get: operations["get_queue_item"];
put?: never;
post?: never;
- delete?: never;
+ /**
+ * Delete Queue Item
+ * @description Deletes a queue item
+ */
+ delete: operations["delete_queue_item"];
options?: never;
head?: never;
patch?: never;
@@ -1484,6 +1488,26 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/queue/{queue_id}/d/{destination}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /**
+ * Delete By Destination
+ * @description Deletes all items with the given destination
+ */
+ delete: operations["delete_by_destination"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/workflows/i/{workflow_id}": {
parameters: {
query?: never;
@@ -5879,6 +5903,17 @@ export type components = {
*/
deleted_images: string[];
};
+ /**
+ * DeleteByDestinationResult
+ * @description Result of deleting by a destination
+ */
+ DeleteByDestinationResult: {
+ /**
+ * Deleted
+ * @description Number of queue items deleted
+ */
+ deleted: number;
+ };
/** DeleteImagesFromListResult */
DeleteImagesFromListResult: {
/** Deleted Images */
@@ -24870,6 +24905,40 @@ export interface operations {
};
};
};
+ delete_queue_item: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The queue id to perform this operation on */
+ queue_id: string;
+ /** @description The queue item to delete */
+ item_id: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": unknown;
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
cancel_queue_item: {
parameters: {
query?: never;
@@ -24939,6 +25008,40 @@ export interface operations {
};
};
};
+ delete_by_destination: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The queue id to query */
+ queue_id: string;
+ /** @description The destination to query */
+ destination: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["DeleteByDestinationResult"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
get_workflow: {
parameters: {
query?: never;
From e3fc244126814c7d918a4e4a4395be47bb1474ca Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 17:58:34 +1000
Subject: [PATCH 053/210] chore(ui): lint (partial cleanup)
---
.../listeners/imageDeletionListeners.ts | 2 +-
.../src/common/hooks/useImageUploadButton.tsx | 2 +-
.../src/common/hooks/useRelatedModelKeys.ts | 22 --
.../src/common/hooks/useSelectedModelKeys.ts | 34 ---
.../CanvasAlerts/CanvasAlertsSendingTo.tsx | 146 ----------
.../components/CanvasRightPanel.tsx | 261 ------------------
.../components/CanvasRightPanelStacked.tsx | 254 -----------------
.../components/SimpleSession/context.tsx | 27 +-
.../StagingArea/StagingAreaIsStagingGate.tsx | 24 --
.../StagingAreaToolbarAcceptButton.tsx | 5 +-
...tagingAreaToolbarDiscardSelectedButton.tsx | 15 -
.../StagingAreaToolbarImageCountButton.tsx | 4 -
.../StagingAreaToolbarNextButton.tsx | 4 -
.../StagingAreaToolbarPrevButton.tsx | 4 -
.../common/CanvasEntityHeaderWarnings.tsx | 2 +-
.../controlLayers/hooks/addLayerHooks.ts | 8 +-
.../controlLayers/hooks/saveCanvasHooks.ts | 8 +-
.../konva/CanvasProgressImageModule.ts | 190 -------------
.../src/features/controlLayers/store/types.ts | 3 +-
.../ImageGrid/SizedSkeletonLoader.tsx | 13 -
.../ImageViewer/CurrentImagePreview.tsx | 2 -
.../components/ImageViewer/ImageViewer.tsx | 35 ++-
.../web/src/features/nodes/types/common.ts | 2 +-
.../nodes/util/graph/graphBuilderUtils.ts | 2 +-
.../queue/components/ClearQueueButton.tsx | 33 ---
.../ClearQueueConfirmationAlertDialog.tsx | 2 +-
.../src/features/ui/components/AppContent.tsx | 55 +---
.../ui/components/LeftPanelContent.tsx | 2 +-
.../ui/components/MainPanelContent.tsx | 2 +-
.../web/src/services/api/endpoints/models.ts | 1 -
.../web/src/services/api/endpoints/queue.ts | 3 -
.../services/events/onInvocationComplete.tsx | 4 +-
.../src/services/events/setEventListeners.tsx | 19 --
.../web/src/services/events/stores.ts | 19 +-
34 files changed, 43 insertions(+), 1166 deletions(-)
delete mode 100644 invokeai/frontend/web/src/common/hooks/useRelatedModelKeys.ts
delete mode 100644 invokeai/frontend/web/src/common/hooks/useSelectedModelKeys.ts
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts
delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageGrid/SizedSkeletonLoader.tsx
delete mode 100644 invokeai/frontend/web/src/features/queue/components/ClearQueueButton.tsx
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
index 150cb73e83..0f286d6d1d 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
@@ -81,7 +81,7 @@ const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageD
selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => {
let shouldDelete = false;
for (const obj of objects) {
- if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {
+ if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
shouldDelete = true;
break;
}
diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
index 93b3c50ce0..9c19a31e5f 100644
--- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
+++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
@@ -198,7 +198,7 @@ type UploadImageButtonProps = {
isError?: boolean;
} & ButtonProps;
-export const UploadImageButton = memo((props: UploadImageButtonProps) => {
+const UploadImageButton = memo((props: UploadImageButtonProps) => {
const { children, isDisabled = false, onUpload, isError = false, ...rest } = props;
const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload });
return (
diff --git a/invokeai/frontend/web/src/common/hooks/useRelatedModelKeys.ts b/invokeai/frontend/web/src/common/hooks/useRelatedModelKeys.ts
deleted file mode 100644
index 9349c9b63d..0000000000
--- a/invokeai/frontend/web/src/common/hooks/useRelatedModelKeys.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { EMPTY_ARRAY } from 'app/store/constants';
-import { useMemo } from 'react';
-import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships';
-
-const options: Parameters[1] = {
- selectFromResult: ({ data }) => {
- if (!data) {
- return { related: EMPTY_ARRAY };
- }
- return data;
- },
-};
-
-/**
- * Fetches related model keys for a given set of selected model keys.
- * Returns a Set for fast lookup.
- */
-export const useRelatedModelKeys = (selectedKeys: string[]) => {
- const { related } = useGetRelatedModelIdsBatchQuery(selectedKeys, options);
-
- return useMemo(() => new Set(related), [related]);
-};
diff --git a/invokeai/frontend/web/src/common/hooks/useSelectedModelKeys.ts b/invokeai/frontend/web/src/common/hooks/useSelectedModelKeys.ts
deleted file mode 100644
index 83e1d3ac59..0000000000
--- a/invokeai/frontend/web/src/common/hooks/useSelectedModelKeys.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useAppSelector } from 'app/store/storeHooks';
-
-/**
- * Gathers all currently selected model keys from parameters and loras.
- * This includes the main model, VAE, refiner model, controlnet, and loras.
- */
-export const useSelectedModelKeys = () => {
- return useAppSelector((state) => {
- const keys = new Set();
- const main = state.params.model;
- const vae = state.params.vae;
- const refiner = state.params.refinerModel;
- const controlnet = state.params.controlLora;
- const loras = state.loras.loras.map((l) => l.model);
-
- if (main) {
- keys.add(main.key);
- }
- if (vae) {
- keys.add(vae.key);
- }
- if (refiner) {
- keys.add(refiner.key);
- }
- if (controlnet) {
- keys.add(controlnet.key);
- }
- for (const lora of loras) {
- keys.add(lora.key);
- }
-
- return keys;
- });
-};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo.tsx
deleted file mode 100644
index 96d388c70a..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import { Alert, AlertDescription, AlertIcon, AlertTitle, Button, Flex } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useBoolean } from 'common/hooks/useBoolean';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
-import { useCurrentDestination } from 'features/queue/hooks/useCurrentDestination';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
-import { activeTabCanvasRightPanelChanged, setActiveTab } from 'features/ui/store/uiSlice';
-import { AnimatePresence, motion } from 'framer-motion';
-import type { PropsWithChildren, ReactNode } from 'react';
-import { useCallback, useMemo } from 'react';
-import { Trans, useTranslation } from 'react-i18next';
-
-const ActivateImageViewerButton = (props: PropsWithChildren) => {
- const imageViewer = useImageViewer();
- const dispatch = useAppDispatch();
- const onClick = useCallback(() => {
- imageViewer.open();
- dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }, [imageViewer, dispatch]);
- return (
-
- {props.children}
-
- );
-};
-
-export const CanvasAlertsSendingToGallery = () => {
- const { t } = useTranslation();
- const destination = useCurrentDestination();
- const tab = useAppSelector(selectActiveTab);
- const isVisible = useMemo(() => {
- // This alert should only be visible when the destination is gallery and the tab is canvas
- if (tab !== 'canvas') {
- return false;
- }
- if (!destination) {
- return false;
- }
-
- return destination === 'gallery';
- }, [destination, tab]);
-
- return (
- }} />
- }
- isVisible={isVisible}
- />
- );
-};
-
-const ActivateCanvasButton = (props: PropsWithChildren) => {
- const dispatch = useAppDispatch();
- const imageViewer = useImageViewer();
- const onClick = useCallback(() => {
- dispatch(setActiveTab('canvas'));
- dispatch(activeTabCanvasRightPanelChanged('layers'));
- imageViewer.close();
- }, [dispatch, imageViewer]);
- return (
-
- {props.children}
-
- );
-};
-
-export const CanvasAlertsSendingToCanvas = () => {
- const { t } = useTranslation();
- const destination = useCurrentDestination();
- const isStaging = useAppSelector(selectIsStaging);
- const tab = useAppSelector(selectActiveTab);
- const isVisible = useMemo(() => {
- // When we are on a non-canvas tab, and the current generation's destination is not the canvas, we don't show the alert
- // For example, on the workflows tab, when the destinatin is gallery, we don't show the alert
- if (tab !== 'canvas' && destination !== 'canvas') {
- return false;
- }
- if (isStaging) {
- return true;
- }
-
- if (!destination) {
- return false;
- }
-
- return destination === 'canvas';
- }, [destination, isStaging, tab]);
-
- return (
- }} />
- }
- isVisible={isVisible}
- />
- );
-};
-
-const AlertWrapper = ({
- title,
- description,
- isVisible,
-}: {
- title: ReactNode;
- description: ReactNode;
- isVisible: boolean;
-}) => {
- const isHovered = useBoolean(false);
-
- return (
-
- {(isVisible || isHovered.isTrue) && (
-
-
-
-
- {title}
-
- {description}
-
-
- )}
-
- );
-};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
deleted file mode 100644
index 4e1b3d2c69..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
-import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
-import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
-import { Box, Button, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
-import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
-import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
-import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
-import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
-import type { DndTargetState } from 'features/dnd/types';
-import RightPanelContent from 'features/gallery/components/GalleryTopBar';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
-import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
-import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
-import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-
-export const CanvasRightPanel = memo(() => {
- const { t } = useTranslation();
- const activeTab = useAppSelector(selectActiveTabCanvasRightPanel);
- const imageViewer = useImageViewer();
- const dispatch = useAppDispatch();
-
- const tabIndex = useMemo(() => {
- if (activeTab === 'gallery') {
- return 1;
- } else {
- return 0;
- }
- }, [activeTab]);
-
- const onClickViewerToggleButton = useCallback(() => {
- imageViewer.open();
- }, [imageViewer]);
-
- const onChangeTab = useCallback(
- (index: number) => {
- if (index === 0) {
- dispatch(activeTabCanvasRightPanelChanged('layers'));
- } else {
- dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }
- },
- [dispatch]
- );
-
- return (
-
-
-
-
-
- {t('gallery.openViewer')}
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-
-CanvasRightPanel.displayName = 'CanvasRightPanel';
-
-const PanelTabs = memo(() => {
- const { t } = useTranslation();
- const store = useAppStore();
- const activeEntityCount = useAppSelector(selectEntityCountActive);
- const [layersTabDndState, setLayersTabDndState] = useState('idle');
- const [galleryTabDndState, setGalleryTabDndState] = useState('idle');
- const layersTabRef = useRef(null);
- const galleryTabRef = useRef(null);
- const timeoutRef = useRef(null);
-
- const layersTabLabel = useMemo(() => {
- if (activeEntityCount === 0) {
- return t('controlLayers.layer_other');
- }
- return `${t('controlLayers.layer_other')} (${activeEntityCount})`;
- }, [activeEntityCount, t]);
-
- useEffect(() => {
- if (!layersTabRef.current) {
- return;
- }
-
- const getIsOnLayersTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'layers';
-
- const onDragEnter = () => {
- // If we are already on the layers tab, do nothing
- if (getIsOnLayersTab()) {
- return;
- }
-
- // Else set the state to active and switch to the layers tab after a timeout
- setLayersTabDndState('over');
- timeoutRef.current = window.setTimeout(() => {
- timeoutRef.current = null;
- store.dispatch(activeTabCanvasRightPanelChanged('layers'));
- // When we switch tabs, the other tab should be pending
- setLayersTabDndState('idle');
- setGalleryTabDndState('potential');
- }, 300);
- };
- const onDragLeave = () => {
- // Set the state to idle or pending depending on the current tab
- if (getIsOnLayersTab()) {
- setLayersTabDndState('idle');
- } else {
- setLayersTabDndState('potential');
- }
- // Abort the tab switch if it hasn't happened yet
- if (timeoutRef.current !== null) {
- clearTimeout(timeoutRef.current);
- }
- };
- const onDragStart = () => {
- // Set the state to pending when a drag starts
- setLayersTabDndState('potential');
- };
- return combine(
- dropTargetForElements({
- element: layersTabRef.current,
- onDragEnter,
- onDragLeave,
- }),
- monitorForElements({
- canMonitor: ({ source }) => {
- if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
- return false;
- }
- // Only monitor if we are not already on the gallery tab
- return !getIsOnLayersTab();
- },
- onDragStart,
- }),
- dropTargetForExternal({
- element: layersTabRef.current,
- onDragEnter,
- onDragLeave,
- }),
- monitorForExternal({
- canMonitor: () => !getIsOnLayersTab(),
- onDragStart,
- })
- );
- }, [store]);
-
- useEffect(() => {
- if (!galleryTabRef.current) {
- return;
- }
-
- const getIsOnGalleryTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'gallery';
-
- const onDragEnter = () => {
- // If we are already on the gallery tab, do nothing
- if (getIsOnGalleryTab()) {
- return;
- }
-
- // Else set the state to active and switch to the gallery tab after a timeout
- setGalleryTabDndState('over');
- timeoutRef.current = window.setTimeout(() => {
- timeoutRef.current = null;
- store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
- // When we switch tabs, the other tab should be pending
- setGalleryTabDndState('idle');
- setLayersTabDndState('potential');
- }, 300);
- };
-
- const onDragLeave = () => {
- // Set the state to idle or pending depending on the current tab
- if (getIsOnGalleryTab()) {
- setGalleryTabDndState('idle');
- } else {
- setGalleryTabDndState('potential');
- }
- // Abort the tab switch if it hasn't happened yet
- if (timeoutRef.current !== null) {
- clearTimeout(timeoutRef.current);
- }
- };
-
- const onDragStart = () => {
- // Set the state to pending when a drag starts
- setGalleryTabDndState('potential');
- };
-
- return combine(
- dropTargetForElements({
- element: galleryTabRef.current,
- onDragEnter,
- onDragLeave,
- }),
- monitorForElements({
- canMonitor: ({ source }) => {
- if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
- return false;
- }
- // Only monitor if we are not already on the gallery tab
- return !getIsOnGalleryTab();
- },
- onDragStart,
- }),
- dropTargetForExternal({
- element: galleryTabRef.current,
- onDragEnter,
- onDragLeave,
- }),
- monitorForExternal({
- canMonitor: () => !getIsOnGalleryTab(),
- onDragStart,
- })
- );
- }, [store]);
-
- useEffect(() => {
- const onDrop = () => {
- // Reset the dnd state when a drop happens
- setGalleryTabDndState('idle');
- setLayersTabDndState('idle');
- };
- const cleanup = combine(monitorForElements({ onDrop }), monitorForExternal({ onDrop }));
-
- return () => {
- cleanup();
- if (timeoutRef.current !== null) {
- clearTimeout(timeoutRef.current);
- }
- };
- }, []);
-
- return (
- <>
-
-
- {layersTabLabel}
-
-
-
-
-
- {t('gallery.gallery')}
-
-
-
- >
- );
-});
-
-PanelTabs.displayName = 'PanelTabs';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx
deleted file mode 100644
index 4806728f27..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanelStacked.tsx
+++ /dev/null
@@ -1,254 +0,0 @@
-import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
-import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
-import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
-import { Box, Tab } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
-import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
-import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
-import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
-import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
-import type { DndTargetState } from 'features/dnd/types';
-import RightPanelContent from 'features/gallery/components/GalleryTopBar';
-import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
-import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
-import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
-import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
-
-export const CanvasRightPanelStacked = memo(() => {
- const { t } = useTranslation();
- const activeTab = useAppSelector(selectActiveTabCanvasRightPanel);
- const imageViewer = useImageViewer();
- const dispatch = useAppDispatch();
-
- const tabIndex = useMemo(() => {
- if (activeTab === 'gallery') {
- return 1;
- } else {
- return 0;
- }
- }, [activeTab]);
-
- const onClickViewerToggleButton = useCallback(() => {
- imageViewer.open();
- }, [imageViewer]);
-
- const onChangeTab = useCallback(
- (index: number) => {
- if (index === 0) {
- dispatch(activeTabCanvasRightPanelChanged('layers'));
- } else {
- dispatch(activeTabCanvasRightPanelChanged('gallery'));
- }
- },
- [dispatch]
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-
-CanvasRightPanelStacked.displayName = 'CanvasRightPanelStacked';
-
-const PanelTabs = memo(() => {
- const { t } = useTranslation();
- const store = useAppStore();
- const activeEntityCount = useAppSelector(selectEntityCountActive);
- const [layersTabDndState, setLayersTabDndState] = useState('idle');
- const [galleryTabDndState, setGalleryTabDndState] = useState('idle');
- const layersTabRef = useRef(null);
- const galleryTabRef = useRef(null);
- const timeoutRef = useRef(null);
-
- const layersTabLabel = useMemo(() => {
- if (activeEntityCount === 0) {
- return t('controlLayers.layer_other');
- }
- return `${t('controlLayers.layer_other')} (${activeEntityCount})`;
- }, [activeEntityCount, t]);
-
- useEffect(() => {
- if (!layersTabRef.current) {
- return;
- }
-
- const getIsOnLayersTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'layers';
-
- const onDragEnter = () => {
- // If we are already on the layers tab, do nothing
- if (getIsOnLayersTab()) {
- return;
- }
-
- // Else set the state to active and switch to the layers tab after a timeout
- setLayersTabDndState('over');
- timeoutRef.current = window.setTimeout(() => {
- timeoutRef.current = null;
- store.dispatch(activeTabCanvasRightPanelChanged('layers'));
- // When we switch tabs, the other tab should be pending
- setLayersTabDndState('idle');
- setGalleryTabDndState('potential');
- }, 300);
- };
- const onDragLeave = () => {
- // Set the state to idle or pending depending on the current tab
- if (getIsOnLayersTab()) {
- setLayersTabDndState('idle');
- } else {
- setLayersTabDndState('potential');
- }
- // Abort the tab switch if it hasn't happened yet
- if (timeoutRef.current !== null) {
- clearTimeout(timeoutRef.current);
- }
- };
- const onDragStart = () => {
- // Set the state to pending when a drag starts
- setLayersTabDndState('potential');
- };
- return combine(
- dropTargetForElements({
- element: layersTabRef.current,
- onDragEnter,
- onDragLeave,
- }),
- monitorForElements({
- canMonitor: ({ source }) => {
- if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
- return false;
- }
- // Only monitor if we are not already on the gallery tab
- return !getIsOnLayersTab();
- },
- onDragStart,
- }),
- dropTargetForExternal({
- element: layersTabRef.current,
- onDragEnter,
- onDragLeave,
- }),
- monitorForExternal({
- canMonitor: () => !getIsOnLayersTab(),
- onDragStart,
- })
- );
- }, [store]);
-
- useEffect(() => {
- if (!galleryTabRef.current) {
- return;
- }
-
- const getIsOnGalleryTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'gallery';
-
- const onDragEnter = () => {
- // If we are already on the gallery tab, do nothing
- if (getIsOnGalleryTab()) {
- return;
- }
-
- // Else set the state to active and switch to the gallery tab after a timeout
- setGalleryTabDndState('over');
- timeoutRef.current = window.setTimeout(() => {
- timeoutRef.current = null;
- store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
- // When we switch tabs, the other tab should be pending
- setGalleryTabDndState('idle');
- setLayersTabDndState('potential');
- }, 300);
- };
-
- const onDragLeave = () => {
- // Set the state to idle or pending depending on the current tab
- if (getIsOnGalleryTab()) {
- setGalleryTabDndState('idle');
- } else {
- setGalleryTabDndState('potential');
- }
- // Abort the tab switch if it hasn't happened yet
- if (timeoutRef.current !== null) {
- clearTimeout(timeoutRef.current);
- }
- };
-
- const onDragStart = () => {
- // Set the state to pending when a drag starts
- setGalleryTabDndState('potential');
- };
-
- return combine(
- dropTargetForElements({
- element: galleryTabRef.current,
- onDragEnter,
- onDragLeave,
- }),
- monitorForElements({
- canMonitor: ({ source }) => {
- if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
- return false;
- }
- // Only monitor if we are not already on the gallery tab
- return !getIsOnGalleryTab();
- },
- onDragStart,
- }),
- dropTargetForExternal({
- element: galleryTabRef.current,
- onDragEnter,
- onDragLeave,
- }),
- monitorForExternal({
- canMonitor: () => !getIsOnGalleryTab(),
- onDragStart,
- })
- );
- }, [store]);
-
- useEffect(() => {
- const onDrop = () => {
- // Reset the dnd state when a drop happens
- setGalleryTabDndState('idle');
- setLayersTabDndState('idle');
- };
- const cleanup = combine(monitorForElements({ onDrop }), monitorForExternal({ onDrop }));
-
- return () => {
- cleanup();
- if (timeoutRef.current !== null) {
- clearTimeout(timeoutRef.current);
- }
- };
- }, []);
-
- return (
- <>
-
-
- {layersTabLabel}
-
-
-
-
-
- {t('gallery.gallery')}
-
-
-
- >
- );
-});
-
-PanelTabs.displayName = 'PanelTabs';
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 5c8b667a3c..d4089d761d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -17,7 +17,7 @@ import type { S } from 'services/api/types';
import { $socket } from 'services/events/stores';
import { assert } from 'tsafe';
-export type ProgressData = {
+type ProgressData = {
itemId: number;
progressEvent: S['InvocationProgressEvent'] | null;
progressImage: ProgressImage | null;
@@ -46,28 +46,7 @@ export const useProgressData = (
return value;
};
-export const useHasProgressImage = (
- $progressData: WritableAtom>,
- itemId: number
-): boolean => {
- const [value, setValue] = useState(false);
- useEffect(() => {
- const unsub = $progressData.subscribe((data) => {
- const progressData = data[itemId];
- setValue(Boolean(progressData?.progressImage));
- });
- return () => {
- unsub();
- };
- }, [$progressData, itemId]);
-
- return value;
-};
-
-export const setProgress = (
- $progressData: WritableAtom>,
- data: S['InvocationProgressEvent']
-) => {
+const setProgress = ($progressData: WritableAtom>, data: S['InvocationProgressEvent']) => {
const progressData = $progressData.get();
const current = progressData[data.item_id];
if (current) {
@@ -120,7 +99,7 @@ export const clearProgressImage = ($progressData: WritableAtom;
$itemCount: Atom;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx
deleted file mode 100644
index d66e30d0c0..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useAppSelector } from 'app/store/storeHooks';
-import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import type { PropsWithChildren } from 'react';
-import { memo } from 'react';
-import { useGetQueueCountsByDestinationQuery } from 'services/api/endpoints/queue';
-
-// This hook just serves as a persistent subscriber for the queue count query.
-const queueCountArg = { destination: 'canvas' };
-const useCanvasQueueCountWatcher = () => {
- useGetQueueCountsByDestinationQuery(queueCountArg);
-};
-
-export const StagingAreaIsStagingGate = memo((props: PropsWithChildren) => {
- useCanvasQueueCountWatcher();
- const isStaging = useAppSelector(selectIsStaging);
-
- if (!isStaging) {
- return null;
- }
-
- return props.children;
-});
-
-StagingAreaIsStagingGate.displayName = 'StagingAreaIsStagingGate';
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 3a1feddebc..147b473af7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
@@ -5,10 +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 {
- selectImageCount,
- stagingAreaReset,
-} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectImageCount, stagingAreaReset } 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';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
index 43a548aec0..c577bc4fa6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
@@ -1,12 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import {
- selectImageCount,
- selectSelectedImage,
- selectStagedImageIndex,
-} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
@@ -14,12 +8,8 @@ import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
const ctx = useCanvasSessionContext();
- const dispatch = useAppDispatch();
const [deleteQueueItem] = useDeleteQueueItemMutation();
const selectedItemId = useStore(ctx.$selectedItemId);
- const index = useAppSelector(selectStagedImageIndex);
- const selectedImage = useAppSelector(selectSelectedImage);
- const imageCount = useAppSelector(selectImageCount);
const { t } = useTranslation();
@@ -28,11 +18,6 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
return;
}
deleteQueueItem({ item_id: selectedItemId });
- // if (imageCount === 1) {
- // dispatch(stagingAreaReset());
- // } else {
- // dispatch(stagingAreaStagedImageDiscarded({ index }));
- // }
}, [selectedItemId, deleteQueueItem]);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
index 7e14ff580a..446dea8836 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
@@ -1,16 +1,12 @@
import { Button } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { selectImageCount, selectStagedImageIndex } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useMemo } from 'react';
export const StagingAreaToolbarImageCountButton = memo(() => {
const ctx = useCanvasSessionContext();
const selectItemIndex = useStore(ctx.$selectedItemIndex);
const itemCount = useStore(ctx.$itemCount);
- const index = useAppSelector(selectStagedImageIndex);
- const imageCount = useAppSelector(selectImageCount);
const counterText = useMemo(() => {
if (itemCount > 0 && selectItemIndex !== null) {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
index 8d60420de2..0a5b5e50de 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
@@ -1,10 +1,8 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectImageCount } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@@ -13,9 +11,7 @@ import { PiArrowRightBold } from 'react-icons/pi';
export const StagingAreaToolbarNextButton = memo(() => {
const ctx = useCanvasSessionContext();
const itemCount = useStore(ctx.$itemCount);
- const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
- const imageCount = useAppSelector(selectImageCount);
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
index ae1539a20a..430fdf9629 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
@@ -1,10 +1,8 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectImageCount } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@@ -13,9 +11,7 @@ import { PiArrowLeftBold } from 'react-icons/pi';
export const StagingAreaToolbarPrevButton = memo(() => {
const ctx = useCanvasSessionContext();
const itemCount = useStore(ctx.$itemCount);
- const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
- const imageCount = useAppSelector(selectImageCount);
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx
index b3f0e21972..641b62dd98 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx
@@ -4,6 +4,7 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
+import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import {
@@ -18,7 +19,6 @@ import { upperFirst } from 'lodash-es';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiWarningBold } from 'react-icons/pi';
-import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
index dfc4780d37..a1b0ba67c5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
@@ -15,7 +15,7 @@ import {
rgNegativePromptChanged,
rgPositivePromptChanged,
} from 'features/controlLayers/store/canvasSlice';
-import { selectBase } from 'features/controlLayers/store/paramsSlice';
+import { selectBase, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
@@ -35,11 +35,7 @@ import {
} from 'features/controlLayers/store/util';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { useCallback } from 'react';
-import {
- modelConfigsAdapterSelectors,
- selectModelConfigsQuery,
-} from 'services/api/endpoints/models';
-import { selectMainModelConfig } from '../store/paramsSlice';
+import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
import type {
ControlLoRAModelConfig,
ControlNetModelConfig,
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
index 5eb153cfc7..a1b2bac014 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
@@ -14,7 +14,12 @@ import {
rgAdded,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasSlice';
-import { selectNegativePrompt, selectPositivePrompt, selectSeed } from 'features/controlLayers/store/paramsSlice';
+import {
+ selectMainModelConfig,
+ selectNegativePrompt,
+ selectPositivePrompt,
+ selectSeed,
+} from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
@@ -33,7 +38,6 @@ import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { serializeError } from 'serialize-error';
-import { selectMainModelConfig } from '../store/paramsSlice';
import type { ImageDTO } from 'services/api/types';
import type { JsonObject } from 'type-fest';
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts
deleted file mode 100644
index c36933a6cf..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import { Mutex } from 'async-mutex';
-import { parseify } from 'common/util/serialize';
-import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
-import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
-import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util';
-import { selectShowProgressOnCanvas } from 'features/controlLayers/store/canvasSettingsSlice';
-import Konva from 'konva';
-import { atom } from 'nanostores';
-import type { Logger } from 'roarr';
-import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
-import type { S } from 'services/api/types';
-import type { SetNonNullable } from 'type-fest';
-
-type ProgressEventWithImage = SetNonNullable;
-const isProgressEventWithImage = (val: S['InvocationProgressEvent']): val is ProgressEventWithImage =>
- Boolean(val.image);
-
-export class CanvasProgressImageModule extends CanvasModuleBase {
- readonly type = 'progress_image';
- readonly id: string;
- readonly path: string[];
- readonly parent: CanvasManager;
- readonly manager: CanvasManager;
- readonly log: Logger;
-
- konva: {
- group: Konva.Group;
- image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
- };
- $isLoading = atom(false);
- $isError = atom(false);
- imageElement: HTMLImageElement | null = null;
-
- subscriptions = new Set<() => void>();
- $lastProgressEvent = atom(null);
- $hasActiveGeneration = atom(false);
- mutex: Mutex = new Mutex();
-
- constructor(manager: CanvasManager) {
- super();
- this.id = getPrefixedId(this.type);
- this.parent = manager;
- this.manager = manager;
- this.path = this.manager.buildPath(this);
- this.log = this.manager.buildLogger(this);
-
- this.log.debug('Creating progress image module');
-
- this.konva = {
- group: new Konva.Group({ name: `${this.type}:group`, listening: false }),
- image: null,
- };
-
- this.subscriptions.add(this.manager.stagingArea.$shouldShowStagedImage.listen(this.render));
- this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectShowProgressOnCanvas, this.render));
- this.subscriptions.add(this.setSocketEventListeners());
- this.subscriptions.add(
- this.manager.stateApi.createStoreSubscription(selectCanvasQueueCounts, ({ data }) => {
- if (data && (data.in_progress > 0 || data.pending > 0)) {
- this.$hasActiveGeneration.set(true);
- } else {
- this.$hasActiveGeneration.set(false);
- }
- })
- );
- this.subscriptions.add(this.$lastProgressEvent.listen(this.render));
- }
-
- setSocketEventListeners = (): (() => void) => {
- const progressListener = (data: S['InvocationProgressEvent']) => {
- if (data.destination !== 'canvas') {
- return;
- }
- if (!isProgressEventWithImage(data)) {
- return;
- }
- if (!this.$hasActiveGeneration.get()) {
- return;
- }
- this.$lastProgressEvent.set(data);
- };
-
- // Handle a canceled or failed canvas generation. We should clear the progress image in this case.
- const queueItemStatusChangedListener = (data: S['QueueItemStatusChangedEvent']) => {
- if (data.destination !== 'canvas') {
- return;
- }
-
- // The staging area module handles _completed_ events. Only care about failed or canceled here.
- if (data.status === 'failed' || data.status === 'canceled') {
- this.$lastProgressEvent.set(null);
- this.$hasActiveGeneration.set(false);
- }
- };
-
- const clearProgress = () => {
- this.$lastProgressEvent.set(null);
- };
-
- this.manager.socket.on('invocation_progress', progressListener);
- this.manager.socket.on('queue_item_status_changed', queueItemStatusChangedListener);
- this.manager.socket.on('connect', clearProgress);
- this.manager.socket.on('connect_error', clearProgress);
- this.manager.socket.on('disconnect', clearProgress);
-
- return () => {
- this.manager.socket.off('invocation_progress', progressListener);
- this.manager.socket.off('queue_item_status_changed', queueItemStatusChangedListener);
- this.manager.socket.off('connect', clearProgress);
- this.manager.socket.off('connect_error', clearProgress);
- this.manager.socket.off('disconnect', clearProgress);
- };
- };
-
- getNodes = () => {
- return [this.konva.group];
- };
-
- render = async () => {
- const release = await this.mutex.acquire();
-
- const event = this.$lastProgressEvent.get();
- const showProgressOnCanvas = this.manager.stateApi.runSelector(selectShowProgressOnCanvas);
-
- if (!event || !showProgressOnCanvas) {
- this.konva.group.visible(false);
- this.konva.image?.destroy();
- this.konva.image = null;
- this.imageElement = null;
- this.$isLoading.set(false);
- this.$isError.set(false);
- release();
- return;
- }
-
- this.$isLoading.set(true);
-
- const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
- try {
- this.imageElement = await loadImage(event.image.dataURL);
- if (this.konva.image) {
- this.konva.image.setAttrs({
- image: this.imageElement,
- x,
- y,
- width,
- height,
- });
- } else {
- this.konva.image = new Konva.Image({
- name: `${this.type}:image`,
- listening: false,
- image: this.imageElement,
- x,
- y,
- width,
- height,
- perfectDrawEnabled: false,
- });
- this.konva.group.add(this.konva.image);
- }
- // Should not be visible if the user has disabled showing staging images
- this.konva.group.visible(this.manager.stagingArea.$shouldShowStagedImage.get());
- } catch {
- this.$isError.set(true);
- } finally {
- this.$isLoading.set(false);
- release();
- }
- };
-
- destroy = () => {
- this.log.debug('Destroying module');
- this.subscriptions.forEach((unsubscribe) => unsubscribe());
- this.subscriptions.clear();
- this.konva.group.destroy();
- };
-
- repr = () => {
- return {
- id: this.id,
- type: this.type,
- path: this.path,
- $lastProgressEvent: parseify(this.$lastProgressEvent.get()),
- $hasActiveGeneration: this.$hasActiveGeneration.get(),
- $isError: this.$isError.get(),
- $isLoading: this.$isLoading.get(),
- };
- };
-}
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 8768187044..83912c5b4d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -64,7 +64,6 @@ const zImageWithDimsDataURL = z.object({
width: z.number().int().positive(),
height: z.number().int().positive(),
});
-export type ImageWithDimsDataURL = z.infer;
const zBeginEndStepPct = z
.tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
@@ -100,7 +99,7 @@ const zRgbColor = z.object({
b: z.number().int().min(0).max(255),
});
export type RgbColor = z.infer;
-export const zRgbaColor = zRgbColor.extend({
+const zRgbaColor = zRgbColor.extend({
a: z.number().min(0).max(1),
});
export type RgbaColor = z.infer;
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/SizedSkeletonLoader.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/SizedSkeletonLoader.tsx
deleted file mode 100644
index 82c4a52d14..0000000000
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/SizedSkeletonLoader.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Skeleton } from '@invoke-ai/ui-library';
-import { memo } from 'react';
-
-type Props = {
- width: number;
- height: number;
-};
-
-export const SizedSkeletonLoader = memo(({ width, height }: Props) => {
- return ;
-});
-
-SizedSkeletonLoader.displayName = 'SizedSkeletonLoader';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
index 29b6a46374..b2f5158b7a 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx
@@ -2,7 +2,6 @@ import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
-import { CanvasAlertsSendingToCanvas } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
import { DndImage } from 'features/dnd/DndImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
@@ -52,7 +51,6 @@ const CurrentImagePreview = ({ imageDTO }: { imageDTO?: ImageDTO }) => {
pointerEvents="none"
alignItems="flex-start"
>
-
{shouldShowImageDetails && imageDTO && (
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
index 34d9329532..521c0a2486 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
@@ -7,7 +7,6 @@ import CurrentImagePreview from 'features/gallery/components/ImageViewer/Current
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
-import type { ReactNode } from 'react';
import { memo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@@ -16,25 +15,25 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useImageViewer } from './useImageViewer';
-type Props = {
- closeButton?: ReactNode;
-};
+// type Props = {
+// closeButton?: ReactNode;
+// };
-const useFocusRegionOptions = {
- focusOnMount: true,
-};
+// const useFocusRegionOptions = {
+// focusOnMount: true,
+// };
-const FOCUS_REGION_STYLES: SystemStyleObject = {
- display: 'flex',
- width: 'full',
- height: 'full',
- position: 'absolute',
- flexDirection: 'column',
- inset: 0,
- alignItems: 'center',
- justifyContent: 'center',
- overflow: 'hidden',
-};
+// const FOCUS_REGION_STYLES: SystemStyleObject = {
+// display: 'flex',
+// width: 'full',
+// height: 'full',
+// position: 'absolute',
+// flexDirection: 'column',
+// inset: 0,
+// alignItems: 'center',
+// justifyContent: 'center',
+// overflow: 'hidden',
+// };
export const ImageViewer = memo(() => {
const lastSelectedImageName = useAppSelector(selectLastSelectedImageName);
diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts
index c57954b2ec..7b1386b5ba 100644
--- a/invokeai/frontend/web/src/features/nodes/types/common.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/common.ts
@@ -93,7 +93,7 @@ export const zMainModelBase = z.enum([
'chatgpt-4o',
'flux-kontext',
]);
-export type MainModelBase = z.infer;
+type MainModelBase = z.infer;
export const isMainModelBase = (base: unknown): base is MainModelBase => zMainModelBase.safeParse(base).success;
const zModelType = z.enum([
'main',
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 ce87203eeb..2d0eb03d44 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -149,7 +149,7 @@ export const getInfill = (
assert(false, 'Unknown infill method');
};
-export const CANVAS_OUTPUT_PREFIX = 'canvas_output';
+const CANVAS_OUTPUT_PREFIX = 'canvas_output';
export const isMainModelWithoutUnet = (modelLoader: Invocation) => {
return (
diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueButton.tsx
deleted file mode 100644
index 72fc05723e..0000000000
--- a/invokeai/frontend/web/src/features/queue/components/ClearQueueButton.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { ButtonProps } from '@invoke-ai/ui-library';
-import { Button } from '@invoke-ai/ui-library';
-import { memo } from 'react';
-import { useTranslation } from 'react-i18next';
-import { PiTrashSimpleFill } from 'react-icons/pi';
-
-import { useClearQueueDialog } from './ClearQueueConfirmationAlertDialog';
-
-type Props = ButtonProps;
-
-const ClearQueueButton = (props: Props) => {
- const { t } = useTranslation();
- const clearQueue = useClearQueueDialog();
-
- return (
- <>
- }
- colorScheme="error"
- onClick={clearQueue.openDialog}
- data-testid={t('queue.clear')}
- {...props}
- >
- {t('queue.clear')}
-
- >
- );
-};
-
-export default memo(ClearQueueButton);
diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx
index d4306e9473..9de86f9eb7 100644
--- a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false);
-export const useClearQueueDialog = () => {
+const useClearQueueDialog = () => {
const dialog = useClearQueueConfirmationAlertDialog();
const { clearQueue, isLoading, isDisabled, queueStatus } = useClearQueue();
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 83f220e4a6..199e5d8c43 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -1,22 +1,15 @@
-import { Box, Flex } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
-import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
-import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
-import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
-import QueueControls from 'features/queue/components/QueueControls';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
-import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
+import { LeftPanelContent } from 'features/ui/components/LeftPanelContent';
+import { MainPanelContent } from 'features/ui/components/MainPanelContent';
import { RightPanelContent } from 'features/ui/components/RightPanelContent';
-import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
-import QueueTab from 'features/ui/components/tabs/QueueTab';
-import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
import {
$isLeftPanelOpen,
$isRightPanelOpen,
@@ -29,10 +22,7 @@ import type { CSSProperties } from 'react';
import { memo, useMemo, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
-import type { Equals } from 'tsafe';
-import { assert } from 'tsafe';
-import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
import { VerticalResizeHandle } from './tabs/ResizeHandle';
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
@@ -153,42 +143,3 @@ export const AppContent = memo(() => {
);
});
AppContent.displayName = 'AppContent';
-
-const LeftPanelContent = memo(() => {
- const tab = useAppSelector(selectActiveTab);
-
- return (
-
-
-
- {tab === 'canvas' && }
- {tab === 'upscaling' && }
- {tab === 'workflows' && }
-
-
- );
-});
-LeftPanelContent.displayName = 'LeftPanelContent';
-
-const MainPanelContent = memo(() => {
- const tab = useAppSelector(selectActiveTab);
-
- if (tab === 'canvas') {
- return ;
- }
- if (tab === 'upscaling') {
- return ;
- }
- if (tab === 'workflows') {
- return ;
- }
- if (tab === 'models') {
- return ;
- }
- if (tab === 'queue') {
- return ;
- }
-
- assert>(false);
-});
-MainPanelContent.displayName = 'MainPanelContent';
diff --git a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
index 873e803186..f6190ed58c 100644
--- a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
@@ -8,7 +8,7 @@ import { memo } from 'react';
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
-const LeftPanelContent = memo(() => {
+export const LeftPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
return (
diff --git a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
index 58ac5a4558..48c812e6a1 100644
--- a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
@@ -9,7 +9,7 @@ import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
-const MainPanelContent = memo(() => {
+export const MainPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
if (tab === 'canvas') {
diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts
index 405c794582..bb526fab36 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/models.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts
@@ -324,4 +324,3 @@ export const {
} = modelsApi;
export const selectModelConfigsQuery = modelsApi.endpoints.getModelConfigs.select();
-
diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
index 6a13f7e2e4..2239a5df3a 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
@@ -364,11 +364,8 @@ export const {
useClearQueueMutation,
usePruneQueueMutation,
useGetQueueStatusQuery,
- useGetQueueItemQuery,
useListQueueItemsQuery,
- useListAllQueueItemsQuery,
useCancelQueueItemMutation,
- useCancelByDestinationMutation,
useDeleteQueueItemMutation,
useDeleteQueueItemsByDestinationMutation,
useGetBatchStatusQuery,
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index e26a2d05d7..8e299202e9 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -11,9 +11,7 @@ import { boardsApi } from 'services/api/endpoints/boards';
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO, S } from 'services/api/types';
import { getCategories, getListImagesUrl } from 'services/api/util';
-import {
- $lastProgressEvent,
-} from 'services/events/stores';
+import { $lastProgressEvent } from 'services/events/stores';
import type { Param0 } from 'tsafe';
import { objectEntries } from 'tsafe';
import type { JsonObject } from 'type-fest';
diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
index 72ec0feb95..a5b4c069c5 100644
--- a/invokeai/frontend/web/src/services/events/setEventListeners.tsx
+++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
@@ -31,14 +31,11 @@ import type { Socket } from 'socket.io-client';
import type { JsonObject } from 'type-fest';
import {
- $lastCanvasProgressEvent,
- $lastCanvasProgressImage,
$lastProgressEvent,
$lastUpscalingProgressEvent,
$lastUpscalingProgressImage,
$lastWorkflowsProgressEvent,
$lastWorkflowsProgressImage,
- $progressImages,
} from './stores';
const log = logger('events');
@@ -116,22 +113,6 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
$lastProgressEvent.set(data);
- if (data.image) {
- const progressData = $progressImages.get()[session_id];
- if (progressData) {
- $progressImages.setKey(session_id, { ...progressData, progressImage: data.image });
- } else {
- $progressImages.setKey(session_id, { sessionId: session_id, isFinished: false, progressImage: data.image });
- }
- }
-
- if (origin === 'canvas') {
- $lastCanvasProgressEvent.set(data);
- if (image) {
- $lastCanvasProgressImage.set({ sessionId: session_id, image });
- }
- }
-
if (origin === 'upscaling') {
$lastUpscalingProgressEvent.set(data);
if (image) {
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index 0d62110efd..c86e147ea4 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -1,8 +1,7 @@
import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
-import type { ProgressImage } from 'features/nodes/types/common';
import { round } from 'lodash-es';
import { atom, computed, map } from 'nanostores';
-import type { ImageDTO, S } from 'services/api/types';
+import type { S } from 'services/api/types';
import type { AppSocket } from 'services/events/types';
import type { ManagerOptions, SocketOptions } from 'socket.io-client';
@@ -11,22 +10,6 @@ export const $socketOptions = map>({});
export const $isConnected = atom(false);
export const $lastProgressEvent = atom(null);
-export type ProgressAndResult = {
- sessionId: string;
- isFinished: boolean;
- progressImage?: ProgressImage;
- resultImage?: ImageDTO;
-};
-export const $progressImages = map({} as Record);
-
-export type ProgressData = {
- sessionId: string;
- progressEvent: S['InvocationProgressEvent'] | null;
- progressImage: ProgressImage | null;
-};
-
-export const $lastCanvasProgressEvent = atom(null);
-export const $lastCanvasProgressImage = atom(null);
export const $lastWorkflowsProgressEvent = atom(null);
export const $lastWorkflowsProgressImage = atom(null);
export const $lastUpscalingProgressEvent = atom(null);
From 3a08ea799a19a3ad2ab065009106e07cb2d7e2a6 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 18:46:45 +1000
Subject: [PATCH 054/210] feat(ui): update canvas session state handling for
new staging strat
---
.../web/src/app/hooks/useStudioInitAction.ts | 8 +-
.../middleware/listenerMiddleware/index.ts | 4 -
.../addCommitStagingAreaImageListener.ts | 46 ------
.../listeners/enqueueRequestedLinear.ts | 14 +-
.../AdvancedSession/AdvancedSession.tsx | 45 ++---
.../components/CanvasMainPanelContent.tsx | 23 +--
.../NewSessionConfirmationAlertDialog.tsx | 6 +-
.../components/NoSession/NoSession.tsx | 4 +-
.../SimpleSession/SimpleSession.tsx | 5 +-
.../SimpleSession/StagingAreaHeader.tsx | 4 +-
.../components/SimpleSession/context.tsx | 9 +-
.../StagingAreaToolbarAcceptButton.tsx | 7 +-
...AreaToolbarSaveSelectedToGalleryButton.tsx | 4 +-
.../konva/CanvasStateApiModule.ts | 4 +-
.../controlLayers/store/canvasSlice.ts | 4 +-
.../store/canvasStagingAreaSlice.ts | 154 +++---------------
.../controlLayers/store/lorasSlice.ts | 4 +-
.../web/src/features/hrf/store/hrfSlice.ts | 4 +-
.../web/src/features/imageActions/actions.ts | 14 +-
.../nodes/util/graph/graphBuilderUtils.ts | 6 +-
.../stylePresets/store/stylePresetSlice.ts | 4 +-
.../components/FloatingLeftPanelButtons.tsx | 6 +-
.../ui/components/RightPanelContent.tsx | 6 +-
.../web/src/features/ui/store/uiSlice.ts | 4 +-
24 files changed, 117 insertions(+), 272 deletions(-)
delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
diff --git a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
index 34d2f8ea88..29db2ff0b3 100644
--- a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
+++ b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
@@ -3,7 +3,7 @@ import { useAppStore } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { withResultAsync } from 'common/util/result';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
@@ -90,7 +90,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
const overrides: Partial = {
objects: [imageObject],
};
- store.dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ store.dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
store.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
store.dispatch(setActiveTab('canvas'));
store.dispatch(sentImageToCanvas());
@@ -162,14 +162,14 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
switch (destination) {
case 'generation':
// Go to the canvas tab, open the image viewer, and enable send-to-gallery mode
- store.dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ store.dispatch(canvasSessionTypeChanged({ type: 'simple' }));
store.dispatch(setActiveTab('canvas'));
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
$imageViewer.set(true);
break;
case 'canvas':
// Go to the canvas tab, close the image viewer, and disable send-to-gallery mode
- store.dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ store.dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
store.dispatch(setActiveTab('canvas'));
$imageViewer.set(false);
break;
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
index 2d8942f9e5..5db4acb243 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -1,7 +1,6 @@
import type { TypedStartListening } from '@reduxjs/toolkit';
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
-import { addStagingListeners } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
@@ -65,9 +64,6 @@ addEnqueueRequestedUpscale(startAppListening);
addAnyEnqueuedListener(startAppListening);
addBatchEnqueuedListener(startAppListening);
-// Canvas actions
-addStagingListeners(startAppListening);
-
// Socket.IO
addSocketConnectedEventListener(startAppListening);
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
deleted file mode 100644
index 6ef2af6c2f..0000000000
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { isAnyOf } from '@reduxjs/toolkit';
-import { logger } from 'app/logging/logger';
-import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
-import { canvasReset } from 'features/controlLayers/store/actions';
-import { stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { toast } from 'features/toast/toast';
-import { t } from 'i18next';
-import { queueApi } from 'services/api/endpoints/queue';
-
-const log = logger('canvas');
-
-const matchCanvasOrStagingAreaReset = isAnyOf(stagingAreaReset, canvasReset);
-
-export const addStagingListeners = (startAppListening: AppStartListening) => {
- startAppListening({
- matcher: matchCanvasOrStagingAreaReset,
- effect: async (_, { dispatch }) => {
- try {
- const req = dispatch(
- queueApi.endpoints.cancelByDestination.initiate(
- { destination: 'canvas' },
- { fixedCacheKey: 'cancelByBatchOrigin' }
- )
- );
- const { canceled } = await req.unwrap();
- req.reset();
-
- if (canceled > 0) {
- log.debug(`Canceled ${canceled} canvas batches`);
- toast({
- id: 'CANCEL_BATCH_SUCCEEDED',
- title: t('queue.cancelBatchSucceeded'),
- status: 'success',
- });
- }
- } catch {
- log.error('Failed to cancel canvas batches');
- toast({
- id: 'CANCEL_BATCH_FAILED',
- title: t('queue.cancelBatchFailed'),
- status: 'error',
- });
- }
- },
- });
-};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
index 79413b94a0..8deabbe967 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
@@ -5,7 +5,10 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
import { withResult, withResultAsync } from 'common/util/result';
import { parseify } from 'common/util/serialize';
-import { canvasSessionStarted, selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import {
+ canvasSessionGenerationStarted,
+ selectCanvasSessionId,
+} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
@@ -33,11 +36,14 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
effect: async (action, { getState, dispatch }) => {
log.debug('Enqueue requested');
- if (!selectCanvasSession(getState())) {
- dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ if (!selectCanvasSessionId(getState())) {
+ dispatch(canvasSessionGenerationStarted());
}
const state = getState();
+ const destination = state.canvasSession.id;
+ assert(destination !== null);
+
const { prepend } = action.payload;
const manager = $canvasManager.get();
@@ -96,8 +102,6 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
const { g, seedFieldIdentifier, positivePromptFieldIdentifier } = buildGraphResult.value;
- const destination = state.canvasSession.session?.id ?? 'canvas';
-
const prepareBatchResult = withResult(() =>
prepareLinearUIBatch({
state,
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
index 47fd674016..0155f52c06 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
@@ -20,7 +20,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 type { AdvancedSessionIdentifier } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
@@ -53,7 +52,7 @@ const canvasBgSx = {
},
};
-export const AdvancedSession = memo(({ session }: { session: AdvancedSessionIdentifier }) => {
+export const AdvancedSession = memo(({ id }: { id: string | null }) => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
@@ -107,27 +106,29 @@ export const AdvancedSession = memo(({ session }: { session: AdvancedSessionIden
)}
-
-
-
-
-
+ {id !== null && (
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+ )}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 98b7bf3a5b..242e3e363c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -2,26 +2,27 @@ import { useAppSelector } from 'app/store/storeHooks';
import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
import { NoSession } from 'features/controlLayers/components/NoSession/NoSession';
import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
-import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectCanvasSessionId, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
export const CanvasMainPanelContent = memo(() => {
- const session = useAppSelector(selectCanvasSession);
+ const type = useAppSelector(selectCanvasSessionType);
+ const id = useAppSelector(selectCanvasSessionId);
- if (session === null) {
- return ;
+ if (type === 'simple') {
+ if (id === null) {
+ return ;
+ } else {
+ return ;
+ }
}
- if (session.type === 'simple') {
- return ;
+ if (type === 'advanced') {
+ return ;
}
- if (session.type === 'advanced') {
- return ;
- }
-
- assert>(false, 'Unexpected session');
+ assert>(false, 'Unexpected session type');
});
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
index fd65ae8095..84466bf0cf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
@@ -2,7 +2,7 @@ import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text }
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
selectSystemShouldConfirmOnNewSession,
shouldConfirmOnNewSessionToggled,
@@ -20,7 +20,7 @@ export const useNewGallerySession = () => {
const newSessionDialog = useNewGallerySessionDialog();
const newGallerySessionImmediate = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ dispatch(canvasSessionTypeChanged({ type: 'simple' }));
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
@@ -41,7 +41,7 @@ export const useNewCanvasSession = () => {
const newSessionDialog = useNewCanvasSessionDialog();
const newCanvasSessionImmediate = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(activeTabCanvasRightPanelChanged('layers'));
}, [dispatch]);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx
index 4586b89573..607fcc5df8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx
@@ -5,13 +5,13 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { GenerateWithControlImage } from 'features/controlLayers/components/NoSession/GenerateWithControlImage';
import { GenerateWithStartingImage } from 'features/controlLayers/components/NoSession/GenerateWithStartingImage';
import { GenerateWithStartingImageAndInpaintMask } from 'features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
export const NoSession = memo(() => {
const dispatch = useAppDispatch();
const newSesh = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
}, [dispatch]);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
index 2b543c6361..621bf95141 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
@@ -1,11 +1,10 @@
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea';
-import type { SimpleSessionIdentifier } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo } from 'react';
-export const SimpleSession = memo(({ session }: { session: SimpleSessionIdentifier }) => {
+export const SimpleSession = memo(({ id }: { id: string }) => {
return (
-
+
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
index 7e8963c298..4e265a5e18 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
@@ -3,7 +3,7 @@ import { Button, Flex, FormControl, FormLabel, Spacer, Switch, Text } from '@inv
import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
@@ -13,7 +13,7 @@ export const StagingAreaHeader = memo(() => {
const dispatch = useAppDispatch();
const startOver = useCallback(() => {
- dispatch(canvasSessionStarted({ sessionType: 'simple' }));
+ dispatch(canvasSessionTypeChanged({ type: 'simple' }));
}, [dispatch]);
const onChangeAutoSwitch = useCallback(
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 d4089d761d..cd4758c56b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -3,10 +3,6 @@ import { createSelector } from '@reduxjs/toolkit';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppStore } from 'app/store/nanostores/store';
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
-import type {
- AdvancedSessionIdentifier,
- SimpleSessionIdentifier,
-} from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { ProgressImage } from 'features/nodes/types/common';
import type { Atom, WritableAtom } from 'nanostores';
import { atom, computed, effect } from 'nanostores';
@@ -100,7 +96,7 @@ export const clearProgressImage = ($progressData: WritableAtom;
$itemCount: Atom;
$hasItems: Atom;
@@ -120,12 +116,13 @@ type CanvasSessionContextValue = {
const CanvasSessionContext = createContext(null);
export const CanvasSessionContextProvider = memo(
- ({ session, children }: PropsWithChildren<{ session: SimpleSessionIdentifier | AdvancedSessionIdentifier }>) => {
+ ({ 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
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 147b473af7..692aa774e2 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,6 @@ 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 { selectImageCount, stagingAreaReset } 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';
@@ -20,7 +19,6 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
const canvasManager = useCanvasManager();
const bboxRect = useAppSelector(selectBboxRect);
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
- const imageCount = useAppSelector(selectImageCount);
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName);
@@ -39,7 +37,6 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
};
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
- dispatch(stagingAreaReset());
}, [bboxRect, selectedItemImageName, dispatch, selectedEntityIdentifier?.type]);
useHotkeys(
@@ -47,9 +44,9 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
acceptSelected,
{
preventDefault: true,
- enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
+ enabled: isCanvasFocused && shouldShowStagedImage && selectedItemImageName !== null,
},
- [isCanvasFocused, shouldShowStagedImage, imageCount]
+ [isCanvasFocused, shouldShowStagedImage, selectedItemImageName]
);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
index ce5f5707b1..4301183fe8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
@@ -3,7 +3,6 @@ import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { withResultAsync } from 'common/util/result';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { selectSelectedImage } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
@@ -15,7 +14,6 @@ const TOAST_ID = 'SAVE_STAGING_AREA_IMAGE_TO_GALLERY';
export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
- const selectedImage = useAppSelector(selectSelectedImage);
const ctx = useCanvasSessionContext();
const imageName = useStore(ctx.$selectedItemOutputImageName);
@@ -63,7 +61,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
icon={}
onClick={saveSelectedImageToGallery}
colorScheme="invokeBlue"
- isDisabled={!selectedImage}
+ isDisabled={!imageName}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts
index f517d96b02..60bfd2f309 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts
@@ -29,7 +29,7 @@ import {
rasterLayerAdded,
rgAdded,
} from 'features/controlLayers/store/canvasSlice';
-import { selectCanvasStagingAreaSlice } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
selectAllRenderableEntities,
selectBbox,
@@ -537,7 +537,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
* Gets the canvas staging area state from redux.
*/
getStagingArea = () => {
- return this.runSelector(selectCanvasStagingAreaSlice);
+ return this.runSelector(selectCanvasSessionSlice);
};
/**
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index d3181c7e9c..cda6f3742b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -6,7 +6,7 @@ import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
import {
selectAllEntities,
@@ -1846,7 +1846,7 @@ export const canvasSlice = createSlice({
syncScaledSize(state);
}
});
- builder.addCase(canvasSessionStarted, (state) => resetState(state));
+ builder.addCase(canvasSessionReset, (state) => resetState(state));
},
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index 4a403b6baf..bbaabff71a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -3,31 +3,15 @@ import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
-import type { StagingAreaImage, StagingAreaProgressImage } from 'features/controlLayers/store/types';
-import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
-
-export type SimpleSessionIdentifier = {
- type: 'simple';
- id: string;
-};
-
-export type AdvancedSessionIdentifier = {
- type: 'advanced';
- id: string;
-};
type CanvasStagingAreaState = {
- session: SimpleSessionIdentifier | AdvancedSessionIdentifier | null;
- sessionType: 'simple' | 'advanced' | null;
- images: (StagingAreaImage | StagingAreaProgressImage)[];
- selectedImageIndex: number;
+ type: 'simple' | 'advanced';
+ id: string | null;
};
const INITIAL_STATE: CanvasStagingAreaState = {
- session: null,
- sessionType: null,
- images: [],
- selectedImageIndex: 0,
+ type: 'simple',
+ id: null,
};
const getInitialState = (): CanvasStagingAreaState => deepClone(INITIAL_STATE);
@@ -36,68 +20,24 @@ export const canvasSessionSlice = createSlice({
name: 'canvasSession',
initialState: getInitialState(),
reducers: {
- sessionChanged: (state, action: PayloadAction<{ session: CanvasStagingAreaState['session'] }>) => {
- const { session } = action.payload;
- state.session = session;
+ canvasSessionTypeChanged: (state, action: PayloadAction<{ type: CanvasStagingAreaState['type'] }>) => {
+ const { type } = action.payload;
+ state.type = type;
+ state.id = null;
},
- stagingAreaImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
- const { stagingAreaImage } = action.payload;
- let didReplace = false;
- const newImages = [];
- for (const i of state.images) {
- if (i.sessionId === stagingAreaImage.sessionId) {
- newImages.push(stagingAreaImage);
- didReplace = true;
- } else {
- newImages.push(i);
- }
- }
- if (!didReplace) {
- newImages.push(stagingAreaImage);
- }
- state.images = newImages;
- },
- stagingAreaGenerationStarted: (state, action: PayloadAction<{ sessionId: string }>) => {
- const { sessionId } = action.payload;
- state.images.push({ type: 'progress', sessionId });
- },
- stagingAreaGenerationFinished: (state, action: PayloadAction<{ sessionId: string }>) => {
- const { sessionId } = action.payload;
- state.images = state.images.filter((data) => data.sessionId !== sessionId);
- },
- stagingAreaImageSelected: (state, action: PayloadAction<{ index: number }>) => {
- const { index } = action.payload;
- state.selectedImageIndex = index;
- },
- stagingAreaNextStagedImageSelected: (state) => {
- state.selectedImageIndex = (state.selectedImageIndex + 1) % state.images.length;
- },
- stagingAreaPrevStagedImageSelected: (state) => {
- state.selectedImageIndex = (state.selectedImageIndex - 1 + state.images.length) % state.images.length;
- },
- stagingAreaStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => {
- const { index } = action.payload;
- state.images.splice(index, 1);
- state.selectedImageIndex = Math.min(state.selectedImageIndex, state.images.length - 1);
- },
- stagingAreaReset: (state) => {
- state.images = [];
- state.selectedImageIndex = 0;
- },
- canvasSessionStarted: {
- reducer: (state, action: PayloadAction<{ session: CanvasStagingAreaState['session'] }>) => {
- const { session } = action.payload;
- state.session = session;
+ canvasSessionGenerationStarted: {
+ reducer: (state, action: PayloadAction<{ id: string }>) => {
+ const { id } = action.payload;
+ state.id = id;
},
- prepare: (payload: { sessionType: 'simple' | 'advanced' }) => ({
- payload: {
- session: {
- type: payload.sessionType,
- id: getPrefixedId(`canvas:${payload.sessionType}`),
- },
- },
+ prepare: () => ({
+ payload: { id: getPrefixedId('canvas') },
}),
},
+ canvasSessionGenerationFinished: (state) => {
+ state.id = null;
+ },
+ canvasSessionReset: () => getInitialState(),
},
extraReducers(builder) {
builder.addCase(canvasReset, () => getInitialState());
@@ -105,16 +45,10 @@ export const canvasSessionSlice = createSlice({
});
export const {
- sessionChanged,
- stagingAreaImageStaged,
- stagingAreaGenerationStarted,
- stagingAreaGenerationFinished,
- stagingAreaStagedImageDiscarded,
- stagingAreaReset,
- stagingAreaImageSelected,
- stagingAreaNextStagedImageSelected,
- stagingAreaPrevStagedImageSelected,
- canvasSessionStarted,
+ canvasSessionTypeChanged,
+ canvasSessionGenerationStarted,
+ canvasSessionReset,
+ canvasSessionGenerationFinished,
} = canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -129,44 +63,8 @@ export const canvasStagingAreaPersistConfig: PersistConfig s[canvasSessionSlice.name];
+export const selectCanvasSessionSlice = (s: RootState) => s[canvasSessionSlice.name];
-/**
- * Selects if we should be staging images. This is true if:
- * - There are staged images.
- * - There are any in-progress or pending canvas queue items.
- */
-export const selectIsStaging = createSelector(
- selectCanvasQueueCounts,
- selectCanvasStagingAreaSlice,
- ({ data }, staging) => {
- if (staging.images.length > 0) {
- return true;
- }
- if (!data) {
- return false;
- }
- return data.in_progress > 0 || data.pending > 0;
- }
-);
-export const selectStagedImageIndex = createSelector(
- selectCanvasStagingAreaSlice,
- (stagingArea) => stagingArea.selectedImageIndex
-);
-export const selectSelectedImage = createSelector(
- [selectCanvasStagingAreaSlice, selectStagedImageIndex],
- (stagingArea, index) => stagingArea.images[index] ?? null
-);
-export const selectStagedImages = createSelector(selectCanvasStagingAreaSlice, (stagingArea) => stagingArea.images);
-export const selectImageCount = createSelector(
- selectCanvasStagingAreaSlice,
- (stagingArea) => stagingArea.images.length
-);
-export const selectCanvasSessionType = createSelector(
- selectCanvasStagingAreaSlice,
- (canvasSession) => canvasSession.sessionType
-);
-export const selectCanvasSession = createSelector(
- selectCanvasStagingAreaSlice,
- (canvasSession) => canvasSession.session
-);
+export const selectIsStaging = createSelector(selectCanvasSessionSlice, ({ id }) => id !== null);
+export const selectCanvasSessionType = createSelector(selectCanvasSessionSlice, ({ type }) => type);
+export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ id }) => id);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts
index e4d91cc2f2..ea34668a84 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts
@@ -1,7 +1,7 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { LoRA } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { LoRAModelConfig } from 'services/api/types';
@@ -64,7 +64,7 @@ export const lorasSlice = createSlice({
},
},
extraReducers(builder) {
- builder.addCase(canvasSessionStarted, () => {
+ builder.addCase(canvasSessionTypeChanged, () => {
// When a new session is requested, clear all LoRAs
return deepClone(initialState);
});
diff --git a/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts b/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts
index c9499ad613..0bfe797ad1 100644
--- a/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts
+++ b/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts
@@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { ParameterHRFMethod, ParameterStrength } from 'features/parameters/types/parameterSchemas';
interface HRFState {
@@ -34,7 +34,7 @@ export const hrfSlice = createSlice({
},
},
extraReducers(builder) {
- builder.addCase(canvasSessionStarted, () => {
+ builder.addCase(canvasSessionTypeChanged, () => {
return deepClone(initialHRFState);
});
},
diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts
index d432433903..b73577bbdf 100644
--- a/invokeai/frontend/web/src/features/imageActions/actions.ts
+++ b/invokeai/frontend/web/src/features/imageActions/actions.ts
@@ -15,7 +15,7 @@ import {
rgAdded,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasSlice';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
@@ -194,7 +194,7 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
@@ -211,7 +211,7 @@ export const newCanvasFromImage = async (arg: {
controlAdapter: deepClone(initialControlNet),
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(controlLayerAdded({ overrides, isSelected: true }));
@@ -227,7 +227,7 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
@@ -243,7 +243,7 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rgAdded({ overrides, isSelected: true }));
@@ -256,7 +256,7 @@ export const newCanvasFromImage = async (arg: {
case 'reference_image': {
const ipAdapter = deepClone(selectDefaultRefImageConfig(getState()));
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true }));
if (withInpaintMask) {
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
@@ -268,7 +268,7 @@ export const newCanvasFromImage = async (arg: {
const ipAdapter = deepClone(selectDefaultIPAdapter(getState()));
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }];
- dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
+ dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
if (withInpaintMask) {
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
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 2d0eb03d44..a34b7b95b2 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -36,9 +36,9 @@ export const getBoardField = (state: RootState): BoardField | undefined => {
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 sessionType = selectCanvasSessionType(state);
- const is_intermediate = sessionType === 'advanced';
- const board = sessionType === 'advanced' ? undefined : getBoardField(state);
+ const type = selectCanvasSessionType(state);
+ const is_intermediate = type === 'advanced';
+ const board = type === 'advanced' ? undefined : getBoardField(state);
return {
is_intermediate,
diff --git a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts
index efee56c9fe..11a775c26d 100644
--- a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts
+++ b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts
@@ -2,7 +2,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionGenerationStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { atom } from 'nanostores';
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
@@ -29,7 +29,7 @@ export const stylePresetSlice = createSlice({
},
},
extraReducers(builder) {
- builder.addCase(canvasSessionStarted, () => {
+ builder.addCase(canvasSessionGenerationStarted, () => {
return deepClone(initialState);
});
builder.addMatcher(stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled, (state, action) => {
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
index f80a956319..973fb1de87 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
@@ -2,7 +2,7 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
@@ -22,11 +22,11 @@ import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) => {
const tab = useAppSelector(selectActiveTab);
- const session = useAppSelector(selectCanvasSession);
+ const type = useAppSelector(selectCanvasSessionType);
return (
- {tab === 'canvas' && session?.type === 'advanced' && (
+ {tab === 'canvas' && type === 'advanced' && (
diff --git a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
index 049e52f901..80d3ed935f 100644
--- a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
@@ -4,7 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
import { Gallery } from 'features/gallery/components/Gallery';
import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
@@ -28,7 +28,7 @@ export const RightPanelContent = memo(() => {
const boardSearchText = useAppSelector(selectBoardSearchText);
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const imperativePanelGroupRef = useRef(null);
- const session = useAppSelector(selectCanvasSession);
+ const type = useAppSelector(selectCanvasSessionType);
const boardsListPanelOptions = useMemo(
() => ({
@@ -77,7 +77,7 @@ export const RightPanelContent = memo(() => {
- {session?.type === 'advanced' && (
+ {type === 'advanced' && (
<>
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
index 3c4bf462bc..aa4b3ec0e0 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
@@ -1,7 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
-import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { Dimensions } from 'features/controlLayers/store/types';
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
import { atom } from 'nanostores';
@@ -56,7 +56,7 @@ export const uiSlice = createSlice({
builder.addCase(workflowLoaded, (state) => {
state.activeTab = 'workflows';
});
- builder.addCase(canvasSessionStarted, (state) => {
+ builder.addCase(canvasSessionTypeChanged, (state) => {
state.activeTab = 'canvas';
});
},
From 6570c0c3b90cafabc9a12da8564419e51f097abd Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 19:15:04 +1000
Subject: [PATCH 055/210] feat(ui): more staging fixes
---
.../SimpleSession/QueueItemPreviewMini.tsx | 8 +++--
.../SimpleSession/StagingAreaItemsList.tsx | 8 ++---
.../components/SimpleSession/context.tsx | 4 +--
.../StagingAreaToolbarAcceptButton.tsx | 9 +++--
.../StagingAreaToolbarDiscardAllButton.tsx | 11 +++++-
...tagingAreaToolbarDiscardSelectedButton.tsx | 5 ++-
.../StagingAreaToolbarSaveAsMenu.tsx | 29 ++++++++++++---
...AreaToolbarSaveSelectedToGalleryButton.tsx | 5 ++-
...gingAreaToolbarToggleShowResultsButton.tsx | 4 +--
.../konva/CanvasStagingAreaModule.ts | 35 +++++++++++++++----
10 files changed, 91 insertions(+), 27 deletions(-)
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 acb907325b..3427f0cb9b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -18,7 +18,7 @@ const sx = {
h: 108,
w: 108,
flexShrink: 0,
- borderWidth: 1,
+ borderWidth: 2,
borderRadius: 'base',
'&[data-selected="true"]': {
borderColor: 'invokeBlue.300',
@@ -46,8 +46,10 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
const onLoad = useCallback(() => {
setImageLoaded(true);
- ctx.$lastLoadedItemId.set(item.item_id);
- }, [ctx.$lastLoadedItemId, item.item_id]);
+ if (ctx.$progressData.get()[item.item_id]) {
+ ctx.$lastLoadedItemId.set(item.item_id);
+ }
+ }, [ctx.$lastLoadedItemId, ctx.$progressData, item.item_id]);
return (
{
return effect([ctx.$selectedItem, ctx.$progressData], (selectedItem, progressData) => {
if (!selectedItem) {
- canvasManager.stagingArea.render();
+ canvasManager.stagingArea.$imageSrc.set(null);
return;
}
const outputImageName = getOutputImageName(selectedItem);
if (outputImageName) {
- canvasManager.stagingArea.render({ type: 'imageName', data: outputImageName });
+ canvasManager.stagingArea.$imageSrc.set({ type: 'imageName', data: outputImageName });
return;
}
const data = progressData[selectedItem.item_id];
if (data?.progressImage) {
- canvasManager.stagingArea.render({ type: 'dataURL', data: data.progressImage.dataURL });
+ canvasManager.stagingArea.$imageSrc.set({ type: 'dataURL', data: data.progressImage.dataURL });
return;
}
- canvasManager.stagingArea.render();
+ canvasManager.stagingArea.$imageSrc.set(null);
});
}, [canvasManager, ctx.$progressData, ctx.$selectedItem]);
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 cd4758c56b..5f1c73fdb0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -67,7 +67,7 @@ const setProgress = ($progressData: WritableAtom>,
}
};
-export const clearProgressEvent = ($progressData: WritableAtom>, itemId: number) => {
+const clearProgressEvent = ($progressData: WritableAtom>, itemId: number) => {
const progressData = $progressData.get();
const current = progressData[itemId];
if (!current) {
@@ -81,7 +81,7 @@ export const clearProgressEvent = ($progressData: WritableAtom>, itemId: number) => {
+const clearProgressImage = ($progressData: WritableAtom>, itemId: number) => {
const progressData = $progressData.get();
const current = progressData[itemId];
if (!current) {
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 692aa774e2..b7280327eb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
@@ -5,6 +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 { canvasSessionGenerationFinished } 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';
@@ -12,6 +13,7 @@ import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi';
+import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarAcceptButton = memo(() => {
const ctx = useCanvasSessionContext();
@@ -22,6 +24,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName);
+ const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
const { t } = useTranslation();
@@ -37,7 +40,9 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
};
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
- }, [bboxRect, selectedItemImageName, dispatch, selectedEntityIdentifier?.type]);
+ dispatch(canvasSessionGenerationFinished());
+ deleteByDestination({ destination: ctx.session.id });
+ }, [selectedItemImageName, bboxRect, dispatch, selectedEntityIdentifier?.type, deleteByDestination, ctx.session.id]);
useHotkeys(
['enter'],
@@ -56,7 +61,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
icon={}
onClick={acceptSelected}
colorScheme="invokeBlue"
- isDisabled={!selectedItemImageName}
+ isDisabled={!selectedItemImageName || !shouldShowStagedImage}
/>
);
});
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 dbc978a42a..99514b2b96 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
@@ -1,18 +1,26 @@
import { IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarDiscardAllButton = memo(() => {
+ const canvasManager = useCanvasManager();
const ctx = useCanvasSessionContext();
+ const dispatch = useAppDispatch();
const { t } = useTranslation();
const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const discardAll = useCallback(() => {
deleteByDestination({ destination: ctx.session.id });
- }, [deleteByDestination, ctx.session.id]);
+ dispatch(canvasSessionGenerationFinished());
+ }, [deleteByDestination, ctx.session.id, dispatch]);
return (
{
onClick={discardAll}
colorScheme="error"
fontSize={16}
+ isDisabled={!shouldShowStagedImage}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
index c577bc4fa6..f6608672b5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
@@ -1,15 +1,18 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
+ const canvasManager = useCanvasManager();
const ctx = useCanvasSessionContext();
const [deleteQueueItem] = useDeleteQueueItemMutation();
const selectedItemId = useStore(ctx.$selectedItemId);
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const { t } = useTranslation();
@@ -28,7 +31,7 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
onClick={discardSelected}
colorScheme="invokeBlue"
fontSize={16}
- isDisabled={selectedItemId === null}
+ isDisabled={selectedItemId === null || !shouldShowStagedImage}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx
index fe37c536e6..455eb97319 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx
@@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/nanostores/store';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
@@ -13,10 +14,12 @@ import { copyImage } from 'services/api/endpoints/images';
const uploadImageArg = { image_category: 'general', is_intermediate: true, silent: true } as const;
export const StagingAreaToolbarSaveAsMenu = memo(() => {
+ const canvasManager = useCanvasManager();
const { t } = useTranslation();
const ctx = useCanvasSessionContext();
const imageName = useStore(ctx.$selectedItemOutputImageName);
const store = useAppStore();
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const toastSentToCanvas = useCallback(() => {
toast({
@@ -101,19 +104,35 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
tooltip={t('controlLayers.newLayerFromImage')}
icon={}
colorScheme="invokeBlue"
- isDisabled={!imageName}
+ isDisabled={!imageName || !shouldShowStagedImage}
/>
- } onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={!imageName}>
+ }
+ onClickCapture={onClickNewInpaintMaskFromImage}
+ isDisabled={!imageName || !shouldShowStagedImage}
+ >
{t('controlLayers.inpaintMask')}
- } onClickCapture={onClickNewRegionalGuidanceFromImage} isDisabled={!imageName}>
+ }
+ onClickCapture={onClickNewRegionalGuidanceFromImage}
+ isDisabled={!imageName || !shouldShowStagedImage}
+ >
{t('controlLayers.regionalGuidance')}
- } onClickCapture={onClickNewControlLayerFromImage} isDisabled={!imageName}>
+ }
+ onClickCapture={onClickNewControlLayerFromImage}
+ isDisabled={!imageName || !shouldShowStagedImage}
+ >
{t('controlLayers.controlLayer')}
- } onClickCapture={onClickNewRasterLayerFromImage} isDisabled={!imageName}>
+ }
+ onClickCapture={onClickNewRasterLayerFromImage}
+ isDisabled={!imageName || !shouldShowStagedImage}
+ >
{t('controlLayers.rasterLayer')}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
index 4301183fe8..7f7f6bea37 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
@@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { withResultAsync } from 'common/util/result';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
@@ -13,9 +14,11 @@ import { copyImage } from 'services/api/endpoints/images';
const TOAST_ID = 'SAVE_STAGING_AREA_IMAGE_TO_GALLERY';
export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
+ const canvasManager = useCanvasManager();
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const ctx = useCanvasSessionContext();
const imageName = useStore(ctx.$selectedItemOutputImageName);
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const { t } = useTranslation();
@@ -61,7 +64,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
icon={}
onClick={saveSelectedImageToGallery}
colorScheme="invokeBlue"
- isDisabled={!imageName}
+ isDisabled={!imageName || !shouldShowStagedImage}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton.tsx
index ce1b3227fd..1bc40f3fe4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton.tsx
@@ -12,8 +12,8 @@ export const StagingAreaToolbarToggleShowResultsButton = memo(() => {
const { t } = useTranslation();
const toggleShowResults = useCallback(() => {
- canvasManager.stagingArea.$shouldShowStagedImage.set(!shouldShowStagedImage);
- }, [canvasManager.stagingArea.$shouldShowStagedImage, shouldShowStagedImage]);
+ canvasManager.stagingArea.$shouldShowStagedImage.set(!canvasManager.stagingArea.$shouldShowStagedImage.get());
+ }, [canvasManager.stagingArea.$shouldShowStagedImage]);
return (
(null);
+
$shouldShowStagedImage = atom(true);
- $isStaging = atom(true); //TODO: wire up to queue?
+ $isStaging = atom(false);
constructor(manager: CanvasManager) {
super();
@@ -40,9 +43,26 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
/**
* When we change this flag, we need to re-render the staging area, which hides or shows the staged image.
*/
+ this.subscriptions.add(this.$shouldShowStagedImage.listen(this.render));
+
+ /**
+ * Rerender when the image source changes.
+ */
+ this.subscriptions.add(this.$imageSrc.listen(this.render));
+
+ /**
+ * Sync the $isStaging flag with the redux state. $isStaging is used by the manager to determine the global busy
+ * state of the canvas.
+ *
+ * We also set the $shouldShowStagedImage flag when we enter staging mode, so that the staged images are shown,
+ * even if the user disabled this in the last staging session.
+ */
this.subscriptions.add(
- this.$shouldShowStagedImage.listen(() => {
- this.render();
+ this.manager.stateApi.createStoreSubscription(selectIsStaging, (isStaging, oldIsStaging) => {
+ this.$isStaging.set(isStaging);
+ if (isStaging && !oldIsStaging) {
+ this.$shouldShowStagedImage.set(true);
+ }
})
);
}
@@ -50,6 +70,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
initialize = () => {
this.log.debug('Initializing module');
this.render();
+ this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
};
getImageFromSrc = (
@@ -72,7 +93,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
}
};
- render = async (imageSrc?: { type: 'imageName'; data: string } | { type: 'dataURL'; data: string }) => {
+ render = async () => {
const release = await this.mutex.acquire();
try {
this.log.trace('Rendering staging area');
@@ -82,6 +103,8 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
this.konva.group.position({ x, y });
+ const imageSrc = this.$imageSrc.get();
+
if (imageSrc) {
const image = this.getImageFromSrc(imageSrc, width, height);
if (!this.image) {
@@ -91,13 +114,13 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
} else if (this.image.isLoading || this.image.isError) {
// noop
} else {
- await this.image.update({ ...this.image.state, image }, true);
+ await this.image.update({ ...this.image.state, image });
}
- this.image.konva.group.visible(shouldShowStagedImage);
} else {
this.image?.destroy();
this.image = null;
}
+ this.konva.group.visible(shouldShowStagedImage);
} finally {
release();
}
From 1412c079ad1caad4bb9752449c1c5369702d5b73 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 19:29:23 +1000
Subject: [PATCH 056/210] feat(ui): improved staging placeholders
---
.../SimpleSession/QueueItemPreviewMini.tsx | 1 +
.../SimpleSession/QueueItemProgressImage.tsx | 9 +--
.../konva/CanvasStagingAreaModule.ts | 78 +++++++++++++++++--
3 files changed, 74 insertions(+), 14 deletions(-)
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 3427f0cb9b..9a902b0ab7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -11,6 +11,7 @@ import { memo, useCallback, useState } from 'react';
import type { S } from 'services/api/types';
const sx = {
+ cursor: 'pointer',
userSelect: 'none',
pos: 'relative',
alignItems: 'center',
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
index c21e41e12a..64c9924c27 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
@@ -1,8 +1,7 @@
import type { ImageProps } from '@invoke-ai/ui-library';
-import { Flex, Icon, Image } from '@invoke-ai/ui-library';
+import { Flex, Image } from '@invoke-ai/ui-library';
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { memo } from 'react';
-import { PiImageBold } from 'react-icons/pi';
type Props = { itemId: number } & ImageProps;
@@ -11,11 +10,7 @@ export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
const { progressImage } = useProgressData(ctx.$progressData, itemId);
if (!progressImage) {
- return (
-
-
-
- );
+ return ;
}
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
index 0d31f282cf..52cbc54611 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
@@ -9,6 +9,9 @@ import Konva from 'konva';
import { atom } from 'nanostores';
import type { Logger } from 'roarr';
+type ImageNameSrc = { type: 'imageName'; data: string };
+type DataURLSrc = { type: 'dataURL'; data: string };
+
export class CanvasStagingAreaModule extends CanvasModuleBase {
readonly type = 'staging_area';
readonly id: string;
@@ -18,11 +21,18 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
readonly log: Logger;
subscriptions: Set<() => void> = new Set();
- konva: { group: Konva.Group };
+ konva: {
+ group: Konva.Group;
+ placeholder: {
+ group: Konva.Group;
+ rect: Konva.Rect;
+ text: Konva.Text;
+ };
+ };
image: CanvasObjectImage | null;
mutex = new Mutex();
- $imageSrc = atom<{ type: 'imageName'; data: string } | { type: 'dataURL'; data: string } | null>(null);
+ $imageSrc = atom(null);
$shouldShowStagedImage = atom(true);
$isStaging = atom(false);
@@ -37,7 +47,48 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
this.log.debug('Creating module');
- this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }) };
+ const { width, height } = this.manager.stateApi.getBbox().rect;
+
+ this.konva = {
+ group: new Konva.Group({
+ name: `${this.type}:group`,
+ listening: false,
+ }),
+ placeholder: {
+ group: new Konva.Group({
+ name: `${this.type}:placeholder_group`,
+ listening: false,
+ visible: false,
+ }),
+ rect: new Konva.Rect({
+ name: `${this.type}:placeholder_rect`,
+ fill: 'hsl(220 12% 10% / 1)', // 'base.900'
+ width,
+ height,
+ listening: false,
+ perfectDrawEnabled: false,
+ }),
+ text: new Konva.Text({
+ name: `${this.type}:placeholder_text`,
+ fill: 'hsl(220 12% 80% / 1)', // 'base.900'
+ width,
+ height,
+ align: 'center',
+ verticalAlign: 'middle',
+ fontFamily: '"Inter Variable", sans-serif',
+ fontSize: width / 24,
+ fontStyle: '600',
+ text: 'Waiting for Image',
+ listening: false,
+ perfectDrawEnabled: false,
+ }),
+ },
+ };
+
+ this.konva.placeholder.group.add(this.konva.placeholder.rect);
+ this.konva.placeholder.group.add(this.konva.placeholder.text);
+ this.konva.group.add(this.konva.placeholder.group);
+
this.image = null;
/**
@@ -67,14 +118,23 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
);
}
+ syncPlaceholderSize = () => {
+ const { width, height } = this.manager.stateApi.getBbox().rect;
+ this.konva.placeholder.rect.width(width);
+ this.konva.placeholder.rect.height(height);
+ this.konva.placeholder.text.width(width);
+ this.konva.placeholder.text.height(height);
+ this.konva.placeholder.text.fontSize(width / 24);
+ };
+
initialize = () => {
this.log.debug('Initializing module');
this.render();
this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
};
- getImageFromSrc = (
- { type, data }: { type: 'imageName'; data: string } | { type: 'dataURL'; data: string },
+ private _getImageFromSrc = (
+ { type, data }: ImageNameSrc | DataURLSrc,
width: number,
height: number
): CanvasImageState['image'] => {
@@ -106,7 +166,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
const imageSrc = this.$imageSrc.get();
if (imageSrc) {
- const image = this.getImageFromSrc(imageSrc, width, height);
+ const image = this._getImageFromSrc(imageSrc, width, height);
if (!this.image) {
this.image = new CanvasObjectImage({ id: 'staging-area-image', type: 'image', image }, this);
await this.image.update(this.image.state, true);
@@ -116,11 +176,15 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
} else {
await this.image.update({ ...this.image.state, image });
}
+ this.konva.placeholder.group.visible(false);
} else {
this.image?.destroy();
this.image = null;
+ this.syncPlaceholderSize();
+ this.konva.placeholder.group.visible(true);
}
- this.konva.group.visible(shouldShowStagedImage);
+
+ this.konva.group.visible(shouldShowStagedImage && this.$isStaging.get());
} finally {
release();
}
From 526e6335a133ec73b204c278e312b37a1a09501b Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 19:34:36 +1000
Subject: [PATCH 057/210] feat(ui): improved staging placeholders
---
.../components/SimpleSession/QueueItemProgressImage.tsx | 2 +-
.../components/SimpleSession/StagingAreaContent.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
index 64c9924c27..0cf59fd290 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
@@ -10,7 +10,7 @@ export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
const { progressImage } = useProgressData(ctx.$progressData, itemId);
if (!progressImage) {
- return ;
+ return ;
}
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
index 76cafe25ea..0f60379962 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
@@ -11,7 +11,7 @@ export const StagingAreaContent = memo(() => {
-
+
>
From 9bbc31b2d97ae47b0e184116fe2a411a32c83a7e Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 19:38:08 +1000
Subject: [PATCH 058/210] fix(ui): reset layers when changing session type
---
.../web/src/features/controlLayers/store/canvasSlice.ts | 4 ++--
.../controlLayers/store/canvasStagingAreaSlice.ts | 9 ++-------
2 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index cda6f3742b..22eee9b0c2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -6,7 +6,7 @@ import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
-import { canvasSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
import {
selectAllEntities,
@@ -1846,7 +1846,7 @@ export const canvasSlice = createSlice({
syncScaledSize(state);
}
});
- builder.addCase(canvasSessionReset, (state) => resetState(state));
+ builder.addCase(canvasSessionTypeChanged, (state) => resetState(state));
},
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index bbaabff71a..3bb050a4bb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -37,19 +37,14 @@ export const canvasSessionSlice = createSlice({
canvasSessionGenerationFinished: (state) => {
state.id = null;
},
- canvasSessionReset: () => getInitialState(),
},
extraReducers(builder) {
builder.addCase(canvasReset, () => getInitialState());
},
});
-export const {
- canvasSessionTypeChanged,
- canvasSessionGenerationStarted,
- canvasSessionReset,
- canvasSessionGenerationFinished,
-} = canvasSessionSlice.actions;
+export const { canvasSessionTypeChanged, canvasSessionGenerationStarted, canvasSessionGenerationFinished } =
+ canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
From 3038a797a6ae3726deb5ec684d6af2ad77b1b561 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 19:42:07 +1000
Subject: [PATCH 059/210] fix(ui): ensure canvas tool modules are destroyed
---
.../controlLayers/konva/CanvasTool/CanvasToolModule.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
index 762ae693d3..b110f97d0d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
@@ -641,6 +641,9 @@ export class CanvasToolModule extends CanvasModuleBase {
this.log.debug('Destroying module');
this.subscriptions.forEach((unsubscribe) => unsubscribe());
this.subscriptions.clear();
+ for (const tool of Object.values(this.tools)) {
+ tool.destroy();
+ }
this.konva.group.destroy();
};
}
From bf5ed61b84f4cfd8b898474be9d3a74e62f6389e Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 19:58:50 +1000
Subject: [PATCH 060/210] feat(ui): add staging area toolbar to simple session
---
.../SimpleSession/StagingAreaContent.tsx | 4 +++
.../StagingArea/SimpleStagingAreaToolbar.tsx | 25 +++++++++++++++++++
.../StagingArea/StagingAreaToolbar.tsx | 13 +++++++---
.../StagingAreaToolbarDiscardAllButton.tsx | 8 ++----
...tagingAreaToolbarDiscardSelectedButton.tsx | 7 ++----
.../StagingAreaToolbarNextButton.tsx | 11 +++-----
.../StagingAreaToolbarPrevButton.tsx | 11 +++-----
7 files changed, 50 insertions(+), 29 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
index 0f60379962..0e5c230204 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
@@ -2,6 +2,7 @@
import { Divider, Flex } from '@invoke-ai/ui-library';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaSelectedItem } from 'features/controlLayers/components/SimpleSession/StagingAreaSelectedItem';
+import { SimpleStagingAreaToolbar } from 'features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar';
import { memo } from 'react';
export const StagingAreaContent = memo(() => {
@@ -14,6 +15,9 @@ export const StagingAreaContent = memo(() => {
+
+
+
>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx
new file mode 100644
index 0000000000..26511981f3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx
@@ -0,0 +1,25 @@
+import { ButtonGroup } from '@invoke-ai/ui-library';
+import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
+import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
+import { StagingAreaToolbarImageCountButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton';
+import { StagingAreaToolbarNextButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton';
+import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton';
+import { memo } from 'react';
+
+export const SimpleStagingAreaToolbar = memo(() => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+});
+
+SimpleStagingAreaToolbar.displayName = 'SimpleStagingAreaToolbar';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
index 4ac13e74a3..63926e766d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
@@ -1,4 +1,5 @@
import { ButtonGroup } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton';
@@ -10,10 +11,14 @@ import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/
import { StagingAreaToolbarSaveAsMenu } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu';
import { StagingAreaToolbarSaveSelectedToGalleryButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton';
import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useEffect } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
export const StagingAreaToolbar = memo(() => {
+ const canvasManager = useCanvasManager();
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+
const ctx = useCanvasSessionContext();
useEffect(() => {
@@ -29,17 +34,17 @@ export const StagingAreaToolbar = memo(() => {
return (
<>
-
+
-
+
-
-
+
+
>
);
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 99514b2b96..d3cc4b20e4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
@@ -1,21 +1,17 @@
import { IconButton } from '@invoke-ai/ui-library';
-import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
-export const StagingAreaToolbarDiscardAllButton = memo(() => {
- const canvasManager = useCanvasManager();
+export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
const ctx = useCanvasSessionContext();
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
- const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const discardAll = useCallback(() => {
deleteByDestination({ destination: ctx.session.id });
@@ -30,7 +26,7 @@ export const StagingAreaToolbarDiscardAllButton = memo(() => {
onClick={discardAll}
colorScheme="error"
fontSize={16}
- isDisabled={!shouldShowStagedImage}
+ isDisabled={isDisabled}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
index f6608672b5..7006a30aa3 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
@@ -1,18 +1,15 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
-export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
- const canvasManager = useCanvasManager();
+export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
const ctx = useCanvasSessionContext();
const [deleteQueueItem] = useDeleteQueueItemMutation();
const selectedItemId = useStore(ctx.$selectedItemId);
- const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const { t } = useTranslation();
@@ -31,7 +28,7 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
onClick={discardSelected}
colorScheme="invokeBlue"
fontSize={16}
- isDisabled={selectedItemId === null || !shouldShowStagedImage}
+ isDisabled={selectedItemId === null || isDisabled}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
index 0a5b5e50de..91eec2f6de 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
@@ -2,17 +2,14 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiArrowRightBold } from 'react-icons/pi';
-export const StagingAreaToolbarNextButton = memo(() => {
+export const StagingAreaToolbarNextButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
const ctx = useCanvasSessionContext();
const itemCount = useStore(ctx.$itemCount);
- const canvasManager = useCanvasManager();
- const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
const { t } = useTranslation();
@@ -26,9 +23,9 @@ export const StagingAreaToolbarNextButton = memo(() => {
ctx.selectNext,
{
preventDefault: true,
- enabled: isCanvasFocused && shouldShowStagedImage && itemCount > 1,
+ enabled: isCanvasFocused && !isDisabled && itemCount > 1,
},
- [isCanvasFocused, shouldShowStagedImage, itemCount, ctx.selectNext]
+ [isCanvasFocused, isDisabled, itemCount, ctx.selectNext]
);
return (
@@ -38,7 +35,7 @@ export const StagingAreaToolbarNextButton = memo(() => {
icon={}
onClick={selectNext}
colorScheme="invokeBlue"
- isDisabled={itemCount <= 1 || !shouldShowStagedImage}
+ isDisabled={itemCount <= 1 || isDisabled}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
index 430fdf9629..cbed5ab675 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
@@ -2,17 +2,14 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiArrowLeftBold } from 'react-icons/pi';
-export const StagingAreaToolbarPrevButton = memo(() => {
+export const StagingAreaToolbarPrevButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
const ctx = useCanvasSessionContext();
const itemCount = useStore(ctx.$itemCount);
- const canvasManager = useCanvasManager();
- const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
const { t } = useTranslation();
@@ -26,9 +23,9 @@ export const StagingAreaToolbarPrevButton = memo(() => {
ctx.selectPrev,
{
preventDefault: true,
- enabled: isCanvasFocused && shouldShowStagedImage && itemCount > 1,
+ enabled: isCanvasFocused && !isDisabled && itemCount > 1,
},
- [isCanvasFocused, shouldShowStagedImage, itemCount, ctx.selectPrev]
+ [isCanvasFocused, isDisabled, itemCount, ctx.selectPrev]
);
return (
@@ -38,7 +35,7 @@ export const StagingAreaToolbarPrevButton = memo(() => {
icon={}
onClick={selectPrev}
colorScheme="invokeBlue"
- isDisabled={itemCount <= 1 || !shouldShowStagedImage}
+ isDisabled={itemCount <= 1 || isDisabled}
/>
);
});
From cd0668dd0bbcd8cb7c0f41cd65c30fc66353fca5 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 19:58:59 +1000
Subject: [PATCH 061/210] feat(ui): tweak staging image display
---
.../components/SimpleSession/QueueItemPreviewMini.tsx | 2 +-
.../SimpleSession/QueueItemProgressImage.tsx | 10 +++++++---
invokeai/frontend/web/src/features/dnd/DndImage.tsx | 3 ++-
3 files changed, 10 insertions(+), 5 deletions(-)
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 9a902b0ab7..ee27ae5f35 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -62,7 +62,7 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
>
{imageDTO && }
- {!imageLoaded && }
+ {!imageLoaded && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
index 0cf59fd290..e9f934f92b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
@@ -3,14 +3,18 @@ import { Flex, Image } from '@invoke-ai/ui-library';
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { memo } from 'react';
-type Props = { itemId: number } & ImageProps;
+type Props = { itemId: number; withBg?: boolean } & ImageProps;
-export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
+export const QueueItemProgressImage = memo(({ itemId, withBg, ...rest }: Props) => {
const ctx = useCanvasSessionContext();
const { progressImage } = useProgressData(ctx.$progressData, itemId);
if (!progressImage) {
- return ;
+ if (withBg) {
+ return ;
+ } else {
+ return null;
+ }
}
return (
diff --git a/invokeai/frontend/web/src/features/dnd/DndImage.tsx b/invokeai/frontend/web/src/features/dnd/DndImage.tsx
index 9009fe440f..9aed1798b4 100644
--- a/invokeai/frontend/web/src/features/dnd/DndImage.tsx
+++ b/invokeai/frontend/web/src/features/dnd/DndImage.tsx
@@ -77,7 +77,8 @@ export const DndImage = memo(
ref={ref}
src={asThumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
fallbackSrc={asThumbnail ? undefined : imageDTO.thumbnail_url}
- w={imageDTO.width}
+ width={imageDTO.width}
+ height={imageDTO.height}
sx={sx}
data-is-dragging={isDragging}
{...rest}
From 23511d68dbd8b29dba8e603a11bd7b3663550e37 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 20:03:26 +1000
Subject: [PATCH 062/210] feat(ui): when discarding last item, select new last
instead of first
---
.../controlLayers/components/SimpleSession/context.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
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 5f1c73fdb0..33b5fc9cc1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -344,7 +344,10 @@ export const CanvasSessionContextProvider = memo(
// 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.
if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
- const prevIndex = $prevItems.get().findIndex(({ item_id }) => item_id === selectedItemId);
+ let prevIndex = $prevItems.get().findIndex(({ item_id }) => item_id === selectedItemId);
+ if (prevIndex >= items.length) {
+ prevIndex = items.length - 1;
+ }
const nextItem = items[prevIndex];
$selectedItemId.set(nextItem?.item_id ?? null);
return;
From bc3550f23820540eee8c949a7fa637660d589ae0 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 20:03:47 +1000
Subject: [PATCH 063/210] feat(ui): finish generation when discarding last item
---
.../StagingAreaToolbarDiscardSelectedButton.tsx | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
index 7006a30aa3..e1c0738fe9 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
@@ -1,12 +1,15 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
+import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
+ const dispatch = useAppDispatch();
const ctx = useCanvasSessionContext();
const [deleteQueueItem] = useDeleteQueueItemMutation();
const selectedItemId = useStore(ctx.$selectedItemId);
@@ -17,8 +20,12 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i
if (selectedItemId === null) {
return;
}
+ const itemCount = ctx.$itemCount.get();
deleteQueueItem({ item_id: selectedItemId });
- }, [selectedItemId, deleteQueueItem]);
+ if (itemCount <= 1) {
+ dispatch(canvasSessionGenerationFinished());
+ }
+ }, [selectedItemId, ctx.$itemCount, deleteQueueItem, dispatch]);
return (
Date: Thu, 5 Jun 2025 20:10:08 +1000
Subject: [PATCH 064/210] build(ui): temporarily ignore all knip issues
---
invokeai/frontend/web/knip.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/invokeai/frontend/web/knip.ts b/invokeai/frontend/web/knip.ts
index 9aeb58061c..3f741d0f76 100644
--- a/invokeai/frontend/web/knip.ts
+++ b/invokeai/frontend/web/knip.ts
@@ -3,6 +3,8 @@ import type { KnipConfig } from 'knip';
const config: KnipConfig = {
project: ['src/**/*.{ts,tsx}!'],
ignore: [
+ // TODO(psyche): temporarily ignored all files for test build purposes
+ 'src/**',
// This file is only used during debugging
'src/app/store/middleware/debugLoggerMiddleware.ts',
// Autogenerated types - shouldn't ever touch these
From 2431060a7ed9a03b039c571b480515962775a7f8 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 21:09:42 +1000
Subject: [PATCH 065/210] fix(ui): hide layers when not on canvas tab
---
.../web/src/features/ui/components/RightPanelContent.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
index 80d3ed935f..6eb9eb761f 100644
--- a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
@@ -12,6 +12,7 @@ import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
@@ -29,6 +30,7 @@ export const RightPanelContent = memo(() => {
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const imperativePanelGroupRef = useRef(null);
const type = useAppSelector(selectCanvasSessionType);
+ const tab = useAppSelector(selectActiveTab);
const boardsListPanelOptions = useMemo(
() => ({
@@ -77,7 +79,7 @@ export const RightPanelContent = memo(() => {
- {type === 'advanced' && (
+ {tab === 'canvas' && type === 'advanced' && (
<>
From cc5083599d591d740f873a6575c47961b785af38 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 21:13:26 +1000
Subject: [PATCH 066/210] fix(ui): mini preview bg color
---
.../SimpleSession/QueueItemPreviewMini.tsx | 3 ++-
.../SimpleSession/QueueItemProgressImage.tsx | 12 ++++--------
2 files changed, 6 insertions(+), 9 deletions(-)
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 ee27ae5f35..3393d7a18e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -21,6 +21,7 @@ const sx = {
flexShrink: 0,
borderWidth: 2,
borderRadius: 'base',
+ bg: 'base.900',
'&[data-selected="true"]': {
borderColor: 'invokeBlue.300',
},
@@ -62,7 +63,7 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
>
{imageDTO && }
- {!imageLoaded && }
+ {!imageLoaded && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
index e9f934f92b..2ea3dd827e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
@@ -1,20 +1,16 @@
import type { ImageProps } from '@invoke-ai/ui-library';
-import { Flex, Image } from '@invoke-ai/ui-library';
+import { Image } from '@invoke-ai/ui-library';
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { memo } from 'react';
-type Props = { itemId: number; withBg?: boolean } & ImageProps;
+type Props = { itemId: number } & ImageProps;
-export const QueueItemProgressImage = memo(({ itemId, withBg, ...rest }: Props) => {
+export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
const ctx = useCanvasSessionContext();
const { progressImage } = useProgressData(ctx.$progressData, itemId);
if (!progressImage) {
- if (withBg) {
- return ;
- } else {
- return null;
- }
+ return null;
}
return (
From 2ddcde13ffc67acde1fd86f1ab70365bd8dfc304 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 6 Jun 2025 18:29:43 +1000
Subject: [PATCH 067/210] refactor(ui): migrate from canceling queue items to
deleteing, make queue hook APIs consistent
---
invokeai/app/api/routers/session_queue.py | 13 +++
.../session_queue/session_queue_base.py | 6 ++
.../session_queue/session_queue_common.py | 6 ++
.../session_queue/session_queue_sqlite.py | 32 +++++++
.../app/components/GlobalModalIsolator.tsx | 2 +
.../web/src/common/hooks/useGlobalHotkeys.ts | 22 ++---
.../components/SimpleSession/context.tsx | 86 ++++++-------------
.../StagingAreaToolbarAcceptButton.tsx | 18 ++--
.../StagingAreaToolbarDiscardAllButton.tsx | 11 +--
...tagingAreaToolbarDiscardSelectedButton.tsx | 9 +-
.../CancelAllExceptCurrentIconButton.tsx | 25 ++++++
...urrentQueueItemConfirmationAlertDialog.tsx | 11 ++-
.../components/ClearInvocationCacheButton.tsx | 8 +-
.../ClearQueueConfirmationAlertDialog.tsx | 11 ++-
.../queue/components/ClearQueueIconButton.tsx | 58 -------------
...urrentQueueItemConfirmationAlertDialog.tsx | 46 ++++++++++
.../DeleteCurrentQueueItemIconButton.tsx | 25 ++++++
.../queue/components/PauseProcessorButton.tsx | 8 +-
.../queue/components/PruneQueueButton.tsx | 11 +--
.../components/QueueActionsMenuButton.tsx | 44 ++++------
.../queue/components/QueueControls.tsx | 19 +++-
.../QueueList/QueueItemComponent.tsx | 26 +++---
.../components/QueueList/QueueItemDetail.tsx | 42 ++++++---
.../components/QueueTabQueueControls.tsx | 4 +-
.../components/ResumeProcessorButton.tsx | 8 +-
.../ToggleInvocationCacheButton.tsx | 24 +++---
.../queue/hooks/useBatchIsCanceled.ts | 20 +++++
.../useCancelAllExceptCurrentQueueItem.ts | 22 ++---
.../features/queue/hooks/useCancelBatch.ts | 60 +++++--------
.../queue/hooks/useCancelCurrentQueueItem.ts | 47 +++-------
.../queue/hooks/useCancelQueueItem.ts | 41 +++++----
.../hooks/useCancelQueueItemsByDestination.ts | 33 +++++++
.../queue/hooks/useClearInvocationCache.ts | 18 ++--
.../src/features/queue/hooks/useClearQueue.ts | 19 ++--
.../queue/hooks/useCurrentDestination.ts | 11 ---
.../queue/hooks/useCurrentQueueItemId.ts | 11 +++
.../useDeleteAllExceptCurrentQueueItem.ts | 38 ++++++++
.../queue/hooks/useDeleteCurrentQueueItem.ts | 20 +++++
.../queue/hooks/useDeleteQueueItem.ts | 33 +++++++
.../hooks/useDeleteQueueItemsByDestination.ts | 33 +++++++
.../queue/hooks/useDisableInvocationCache.ts | 26 +++---
.../queue/hooks/useEnableInvocationCache.ts | 25 +++---
.../features/queue/hooks/usePauseProcessor.ts | 19 ++--
.../src/features/queue/hooks/usePruneQueue.ts | 45 +++++-----
.../queue/hooks/useResumeProcessor.ts | 19 ++--
.../features/queue/hooks/useRetryQueueItem.ts | 45 +++++-----
.../system/components/ProgressBar.tsx | 14 +--
.../components/FloatingLeftPanelButtons.tsx | 32 +++----
.../web/src/services/api/endpoints/queue.ts | 20 ++++-
.../frontend/web/src/services/api/schema.ts | 63 ++++++++++++++
50 files changed, 773 insertions(+), 516 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentIconButton.tsx
delete mode 100644 invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx
create mode 100644 invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.tsx
create mode 100644 invokeai/frontend/web/src/features/queue/components/DeleteCurrentQueueItemIconButton.tsx
create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useBatchIsCanceled.ts
create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts
delete mode 100644 invokeai/frontend/web/src/features/queue/hooks/useCurrentDestination.ts
create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts
create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts
create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useDeleteCurrentQueueItem.ts
create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts
create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItemsByDestination.ts
diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py
index f19f4bc47c..5902a492d4 100644
--- a/invokeai/app/api/routers/session_queue.py
+++ b/invokeai/app/api/routers/session_queue.py
@@ -14,6 +14,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByBatchIDsResult,
CancelByDestinationResult,
ClearResult,
+ DeleteAllExceptCurrentResult,
DeleteByDestinationResult,
EnqueueBatchResult,
FieldIdentifier,
@@ -146,6 +147,18 @@ async def cancel_all_except_current(
return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(queue_id=queue_id)
+@session_queue_router.put(
+ "/{queue_id}/delete_all_except_current",
+ operation_id="delete_all_except_current",
+ responses={200: {"model": DeleteAllExceptCurrentResult}},
+)
+async def delete_all_except_current(
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> DeleteAllExceptCurrentResult:
+ """Immediately deletes all queue items except in-processing items"""
+ return ApiDependencies.invoker.services.session_queue.delete_all_except_current(queue_id=queue_id)
+
+
@session_queue_router.put(
"/{queue_id}/cancel_by_batch_ids",
operation_id="cancel_by_batch_ids",
diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py
index 17c31e77a7..aa1126576d 100644
--- a/invokeai/app/services/session_queue/session_queue_base.py
+++ b/invokeai/app/services/session_queue/session_queue_base.py
@@ -10,6 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
+ DeleteAllExceptCurrentResult,
DeleteByDestinationResult,
EnqueueBatchResult,
IsEmptyResult,
@@ -129,6 +130,11 @@ class SessionQueueBase(ABC):
"""Cancels all queue items except in-progress items"""
pass
+ @abstractmethod
+ def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult:
+ """Deletes all queue items except in-progress items"""
+ pass
+
@abstractmethod
def list_queue_items(
self,
diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py
index d41fb44533..9075732901 100644
--- a/invokeai/app/services/session_queue/session_queue_common.py
+++ b/invokeai/app/services/session_queue/session_queue_common.py
@@ -369,6 +369,12 @@ class DeleteByDestinationResult(BaseModel):
deleted: int = Field(..., description="Number of queue items deleted")
+class DeleteAllExceptCurrentResult(DeleteByDestinationResult):
+ """Result of deleting all except current"""
+
+ pass
+
+
class CancelByQueueIDResult(CancelByBatchIDsResult):
"""Result of canceling by queue id"""
diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py
index c31226581a..36b4f05cf3 100644
--- a/invokeai/app/services/session_queue/session_queue_sqlite.py
+++ b/invokeai/app/services/session_queue/session_queue_sqlite.py
@@ -17,6 +17,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
+ DeleteAllExceptCurrentResult,
DeleteByDestinationResult,
EnqueueBatchResult,
IsEmptyResult,
@@ -489,6 +490,37 @@ class SqliteSessionQueue(SessionQueueBase):
raise
return DeleteByDestinationResult(deleted=count)
+ def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult:
+ try:
+ cursor = self._conn.cursor()
+ where = """--sql
+ WHERE
+ queue_id == ?
+ AND status == 'pending'
+ """
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where};
+ """,
+ (queue_id,),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ DELETE
+ FROM session_queue
+ {where};
+ """,
+ (queue_id,),
+ )
+ self._conn.commit()
+ except Exception:
+ self._conn.rollback()
+ raise
+ return DeleteAllExceptCurrentResult(deleted=count)
+
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
try:
cursor = self._conn.cursor()
diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
index ae83f0c5c2..782b338fe4 100644
--- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
@@ -16,6 +16,7 @@ import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
+import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
@@ -40,6 +41,7 @@ export const GlobalModalIsolator = memo(() => {
+
diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
index 76c2db9a6d..88fc2bfdfb 100644
--- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
+++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
@@ -1,6 +1,6 @@
import { useAppDispatch } from 'app/store/storeHooks';
-import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
+import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@@ -35,34 +35,30 @@ export const useGlobalHotkeys = () => {
dependencies: [queue],
});
- const {
- cancelQueueItem,
- isDisabled: isDisabledCancelQueueItem,
- isLoading: isLoadingCancelQueueItem,
- } = useCancelCurrentQueueItem();
+ const deleteCurrentQueueItem = useDeleteCurrentQueueItem();
useRegisteredHotkeys({
id: 'cancelQueueItem',
category: 'app',
- callback: cancelQueueItem,
+ callback: deleteCurrentQueueItem.trigger,
options: {
- enabled: !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
+ enabled: !deleteCurrentQueueItem.isDisabled && !deleteCurrentQueueItem.isLoading,
preventDefault: true,
},
- dependencies: [cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem],
+ dependencies: [deleteCurrentQueueItem],
});
- const { clearQueue, isDisabled: isDisabledClearQueue, isLoading: isLoadingClearQueue } = useClearQueue();
+ const clearQueue = useClearQueue();
useRegisteredHotkeys({
id: 'clearQueue',
category: 'app',
- callback: clearQueue,
+ callback: clearQueue.trigger,
options: {
- enabled: !isDisabledClearQueue && !isLoadingClearQueue,
+ enabled: !clearQueue.isDisabled && !clearQueue.isLoading,
preventDefault: true,
},
- dependencies: [clearQueue, isDisabledClearQueue, isLoadingClearQueue],
+ dependencies: [clearQueue],
});
useRegisteredHotkeys({
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 33b5fc9cc1..daf843f947 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -67,34 +67,6 @@ const setProgress = ($progressData: WritableAtom>,
}
};
-const clearProgressEvent = ($progressData: WritableAtom>, itemId: number) => {
- const progressData = $progressData.get();
- const current = progressData[itemId];
- if (!current) {
- return;
- }
- const next = { ...current };
- next.progressEvent = null;
- $progressData.set({
- ...progressData,
- [itemId]: next,
- });
-};
-
-const clearProgressImage = ($progressData: WritableAtom>, itemId: number) => {
- const progressData = $progressData.get();
- const current = progressData[itemId];
- if (!current) {
- return;
- }
- const next = { ...current };
- next.progressImage = null;
- $progressData.set({
- ...progressData,
- [itemId]: next,
- });
-};
-
type CanvasSessionContextValue = {
session: { id: string; type: 'simple' | 'advanced' };
$items: Atom;
@@ -132,15 +104,11 @@ export const CanvasSessionContextProvider = memo(
const socket = useStore($socket);
/**
- * Manually-synced atom containing the queue items for the current session.
+ * 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];
- /**
- * Manually-synced atom containing the queue items for the current session.
- */
- const $prevItems = useState(() => atom([]))[0];
-
/**
* Whether auto-switch is enabled.
*/
@@ -294,28 +262,16 @@ export const CanvasSessionContextProvider = memo(
setProgress($progressData, data);
};
- const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
- if (data.destination !== session.id) {
- return;
- }
-
- if (data.status === 'canceled' || data.status === 'failed') {
- clearProgressEvent($progressData, data.item_id);
- clearProgressImage($progressData, data.item_id);
- }
- };
-
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);
};
}, [$autoSwitch, $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()));
@@ -324,7 +280,7 @@ export const CanvasSessionContextProvider = memo(
const prevItems = $items.get();
const items = selectQueueItems(store.getState());
if (items !== prevItems) {
- $prevItems.set(prevItems);
+ _prevItems = prevItems;
$items.set(items);
}
});
@@ -344,7 +300,7 @@ export const CanvasSessionContextProvider = memo(
// 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.
if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
- let prevIndex = $prevItems.get().findIndex(({ item_id }) => item_id === selectedItemId);
+ let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
if (prevIndex >= items.length) {
prevIndex = items.length - 1;
}
@@ -357,19 +313,37 @@ export const CanvasSessionContextProvider = memo(
// Clean up the progress data when a queue item is discarded.
const unsubCleanUpProgressData = $items.listen((items) => {
const progressData = $progressData.get();
+
const toDelete: number[] = [];
+ const toClear: number[] = [];
+
for (const datum of Object.values(progressData)) {
- if (items.findIndex(({ item_id }) => item_id === datum.itemId) === -1) {
+ const item = items.find(({ item_id }) => item_id === datum.itemId);
+ if (!item) {
toDelete.push(datum.itemId);
+ } else if (item.status === 'canceled' || item.status === 'failed') {
+ toClear.push(datum.itemId);
}
}
+
if (toDelete.length === 0) {
return;
}
+
const newProgressData = { ...progressData };
+
for (const itemId of toDelete) {
delete newProgressData[itemId];
}
+
+ for (const itemId of toClear) {
+ const current = newProgressData[itemId];
+ if (current) {
+ current.progressEvent = null;
+ current.progressImage = null;
+ }
+ }
+
$progressData.set(newProgressData);
});
@@ -408,17 +382,7 @@ export const CanvasSessionContextProvider = memo(
$progressData.set({});
$selectedItemId.set(null);
};
- }, [
- $autoSwitch,
- $items,
- $lastLoadedItemId,
- $prevItems,
- $progressData,
- $selectedItemId,
- selectQueueItems,
- session.id,
- store,
- ]);
+ }, [$autoSwitch, $items, $lastLoadedItemId, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
const value = useMemo(
() => ({
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 b7280327eb..1272887abc 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
@@ -9,11 +9,11 @@ import { canvasSessionGenerationFinished } from 'features/controlLayers/store/ca
import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageNameToImageObject } from 'features/controlLayers/store/util';
+import { useDeleteQueueItemsByDestination } from 'features/queue/hooks/useDeleteQueueItemsByDestination';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi';
-import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarAcceptButton = memo(() => {
const ctx = useCanvasSessionContext();
@@ -24,7 +24,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName);
- const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
+ const deleteQueueItemsByDestination = useDeleteQueueItemsByDestination();
const { t } = useTranslation();
@@ -41,8 +41,15 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
dispatch(canvasSessionGenerationFinished());
- deleteByDestination({ destination: ctx.session.id });
- }, [selectedItemImageName, bboxRect, dispatch, selectedEntityIdentifier?.type, deleteByDestination, ctx.session.id]);
+ deleteQueueItemsByDestination.trigger(ctx.session.id);
+ }, [
+ selectedItemImageName,
+ bboxRect,
+ dispatch,
+ selectedEntityIdentifier?.type,
+ deleteQueueItemsByDestination,
+ ctx.session.id,
+ ]);
useHotkeys(
['enter'],
@@ -61,7 +68,8 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
icon={}
onClick={acceptSelected}
colorScheme="invokeBlue"
- isDisabled={!selectedItemImageName || !shouldShowStagedImage}
+ isDisabled={!selectedItemImageName || !shouldShowStagedImage || deleteQueueItemsByDestination.isDisabled}
+ isLoading={deleteQueueItemsByDestination.isLoading}
/>
);
});
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 d3cc4b20e4..9c7f653898 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
@@ -2,21 +2,21 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useDeleteQueueItemsByDestination } from 'features/queue/hooks/useDeleteQueueItemsByDestination';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
-import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
const ctx = useCanvasSessionContext();
const dispatch = useAppDispatch();
const { t } = useTranslation();
- const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
+ const deleteQueueItemsByDestination = useDeleteQueueItemsByDestination();
const discardAll = useCallback(() => {
- deleteByDestination({ destination: ctx.session.id });
+ deleteQueueItemsByDestination.trigger(ctx.session.id);
dispatch(canvasSessionGenerationFinished());
- }, [deleteByDestination, ctx.session.id, dispatch]);
+ }, [deleteQueueItemsByDestination, ctx.session.id, dispatch]);
return (
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
index e1c0738fe9..b6fc9d934a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
@@ -3,15 +3,15 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
-import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
const dispatch = useAppDispatch();
const ctx = useCanvasSessionContext();
- const [deleteQueueItem] = useDeleteQueueItemMutation();
+ const deleteQueueItem = useDeleteQueueItem();
const selectedItemId = useStore(ctx.$selectedItemId);
const { t } = useTranslation();
@@ -21,10 +21,10 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i
return;
}
const itemCount = ctx.$itemCount.get();
- deleteQueueItem({ item_id: selectedItemId });
if (itemCount <= 1) {
dispatch(canvasSessionGenerationFinished());
}
+ deleteQueueItem.trigger(selectedItemId);
}, [selectedItemId, ctx.$itemCount, deleteQueueItem, dispatch]);
return (
@@ -35,7 +35,8 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i
onClick={discardSelected}
colorScheme="invokeBlue"
fontSize={16}
- isDisabled={selectedItemId === null || isDisabled}
+ isDisabled={selectedItemId === null || deleteQueueItem.isDisabled || isDisabled}
+ isLoading={deleteQueueItem.isLoading}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentIconButton.tsx
new file mode 100644
index 0000000000..1d83b409b6
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentIconButton.tsx
@@ -0,0 +1,25 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiXCircle } from 'react-icons/pi';
+
+export const CancelAllExceptCurrentIconButton = memo(() => {
+ const { t } = useTranslation();
+ const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
+
+ return (
+ }
+ colorScheme="error"
+ onClick={cancelAllExceptCurrent.openDialog}
+ />
+ );
+});
+
+CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton';
diff --git a/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx
index cdaaebcdf0..a27914a4c1 100644
--- a/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx
@@ -9,16 +9,15 @@ const [useCancelAllExceptCurrentQueueItemConfirmationAlertDialog] = buildUseBool
export const useCancelAllExceptCurrentQueueItemDialog = () => {
const dialog = useCancelAllExceptCurrentQueueItemConfirmationAlertDialog();
- const { cancelAllExceptCurrentQueueItem, isLoading, isDisabled, queueStatus } = useCancelAllExceptCurrentQueueItem();
+ const cancelAllExceptCurrentQueueItem = useCancelAllExceptCurrentQueueItem();
return {
- cancelAllExceptCurrentQueueItem,
+ trigger: cancelAllExceptCurrentQueueItem.trigger,
isOpen: dialog.isTrue,
openDialog: dialog.setTrue,
closeDialog: dialog.setFalse,
- isLoading,
- queueStatus,
- isDisabled,
+ isLoading: cancelAllExceptCurrentQueueItem.isLoading,
+ isDisabled: cancelAllExceptCurrentQueueItem.isDisabled,
};
};
@@ -32,7 +31,7 @@ export const CancelAllExceptCurrentQueueItemConfirmationAlertDialog = memo(() =>
isOpen={cancelAllExceptCurrentQueueItem.isOpen}
onClose={cancelAllExceptCurrentQueueItem.closeDialog}
title={t('queue.cancelAllExceptCurrentTooltip')}
- acceptCallback={cancelAllExceptCurrentQueueItem.cancelAllExceptCurrentQueueItem}
+ acceptCallback={cancelAllExceptCurrentQueueItem.trigger}
acceptButtonText={t('queue.confirm')}
useInert={false}
>
diff --git a/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx
index 52b1b237e7..6a9f157361 100644
--- a/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx
@@ -5,10 +5,14 @@ import { useTranslation } from 'react-i18next';
const ClearInvocationCacheButton = () => {
const { t } = useTranslation();
- const { clearInvocationCache, isDisabled, isLoading } = useClearInvocationCache();
+ const clearInvocationCache = useClearInvocationCache();
return (
-
+
{t('invocationCache.clear')}
);
diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx
index 9de86f9eb7..be1bf7ccb5 100644
--- a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx
@@ -9,16 +9,15 @@ const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false);
const useClearQueueDialog = () => {
const dialog = useClearQueueConfirmationAlertDialog();
- const { clearQueue, isLoading, isDisabled, queueStatus } = useClearQueue();
+ const clearQueue = useClearQueue();
return {
- clearQueue,
isOpen: dialog.isTrue,
openDialog: dialog.setTrue,
closeDialog: dialog.setFalse,
- isLoading,
- queueStatus,
- isDisabled,
+ trigger: clearQueue.trigger,
+ isLoading: clearQueue.isLoading,
+ isDisabled: clearQueue.isDisabled,
};
};
@@ -32,7 +31,7 @@ export const ClearQueueConfirmationsAlertDialog = memo(() => {
isOpen={clearQueue.isOpen}
onClose={clearQueue.closeDialog}
title={t('queue.clearTooltip')}
- acceptCallback={clearQueue.clearQueue}
+ acceptCallback={clearQueue.trigger}
acceptButtonText={t('queue.clear')}
useInert={false}
>
diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx
deleted file mode 100644
index 9f7a4290fa..0000000000
--- a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
-import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
-import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
-import { memo } from 'react';
-import { useTranslation } from 'react-i18next';
-import { PiXBold, PiXCircle } from 'react-icons/pi';
-
-export const ClearQueueIconButton = memo(() => {
- const shift = useShiftModifier();
-
- if (!shift) {
- return ;
- }
-
- return ;
-});
-
-ClearQueueIconButton.displayName = 'ClearQueueIconButton';
-
-const CancelCurrentIconButton = memo(() => {
- const { t } = useTranslation();
- const cancelCurrentQueueItem = useCancelCurrentQueueItem();
-
- return (
- }
- colorScheme="error"
- onClick={cancelCurrentQueueItem.cancelQueueItem}
- />
- );
-});
-
-CancelCurrentIconButton.displayName = 'CancelCurrentIconButton';
-
-const CancelAllExceptCurrentIconButton = memo(() => {
- const { t } = useTranslation();
- const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
-
- return (
- }
- colorScheme="error"
- onClick={cancelAllExceptCurrent.openDialog}
- />
- );
-});
-
-CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton';
diff --git a/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.tsx
new file mode 100644
index 0000000000..ac5dc4ef93
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.tsx
@@ -0,0 +1,46 @@
+import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library';
+import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
+import { buildUseBoolean } from 'common/hooks/useBoolean';
+import { useDeleteAllExceptCurrentQueueItem } from 'features/queue/hooks/useDeleteAllExceptCurrentQueueItem';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+const [useDeleteAllExceptCurrentQueueItemConfirmationAlertDialog] = buildUseBoolean(false);
+
+export const useDeleteAllExceptCurrentQueueItemDialog = () => {
+ const dialog = useDeleteAllExceptCurrentQueueItemConfirmationAlertDialog();
+ const deleteAllExceptCurrentQueueItem = useDeleteAllExceptCurrentQueueItem();
+
+ return {
+ trigger: deleteAllExceptCurrentQueueItem.trigger,
+ isOpen: dialog.isTrue,
+ openDialog: dialog.setTrue,
+ closeDialog: dialog.setFalse,
+ isLoading: deleteAllExceptCurrentQueueItem.isLoading,
+ isDisabled: deleteAllExceptCurrentQueueItem.isDisabled,
+ };
+};
+
+export const DeleteAllExceptCurrentQueueItemConfirmationAlertDialog = memo(() => {
+ useAssertSingleton('DeleteAllExceptCurrentQueueItemConfirmationAlertDialog');
+ const { t } = useTranslation();
+ const deleteAllExceptCurrentQueueItem = useDeleteAllExceptCurrentQueueItemDialog();
+
+ return (
+
+ {t('queue.cancelAllExceptCurrentQueueItemAlertDialog')}
+
+ {t('queue.cancelAllExceptCurrentQueueItemAlertDialog2')}
+
+ );
+});
+
+DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.displayName =
+ 'DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
diff --git a/invokeai/frontend/web/src/features/queue/components/DeleteCurrentQueueItemIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/DeleteCurrentQueueItemIconButton.tsx
new file mode 100644
index 0000000000..22009632ac
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/components/DeleteCurrentQueueItemIconButton.tsx
@@ -0,0 +1,25 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiXBold } from 'react-icons/pi';
+
+export const DeleteCurrentQueueItemIconButton = memo(() => {
+ const { t } = useTranslation();
+ const deleteCurrentQueueItem = useDeleteCurrentQueueItem();
+
+ return (
+ }
+ colorScheme="error"
+ />
+ );
+});
+
+DeleteCurrentQueueItemIconButton.displayName = 'DeleteCurrentQueueItemIconButton';
diff --git a/invokeai/frontend/web/src/features/queue/components/PauseProcessorButton.tsx b/invokeai/frontend/web/src/features/queue/components/PauseProcessorButton.tsx
index 45a95951f9..36d07a1c59 100644
--- a/invokeai/frontend/web/src/features/queue/components/PauseProcessorButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/PauseProcessorButton.tsx
@@ -11,17 +11,17 @@ type Props = {
const PauseProcessorButton = ({ asIconButton }: Props) => {
const { t } = useTranslation();
- const { pauseProcessor, isLoading, isDisabled } = usePauseProcessor();
+ const pauseProcessor = usePauseProcessor();
return (
}
- onClick={pauseProcessor}
+ onClick={pauseProcessor.trigger}
colorScheme="gold"
/>
);
diff --git a/invokeai/frontend/web/src/features/queue/components/PruneQueueButton.tsx b/invokeai/frontend/web/src/features/queue/components/PruneQueueButton.tsx
index 8ecf7a60c9..21f818b9ba 100644
--- a/invokeai/frontend/web/src/features/queue/components/PruneQueueButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/PruneQueueButton.tsx
@@ -1,4 +1,4 @@
-import { usePruneQueue } from 'features/queue/hooks/usePruneQueue';
+import { useFinishedCount, usePruneQueue } from 'features/queue/hooks/usePruneQueue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBroomBold } from 'react-icons/pi';
@@ -11,17 +11,18 @@ type Props = {
const PruneQueueButton = ({ asIconButton }: Props) => {
const { t } = useTranslation();
- const { pruneQueue, isLoading, finishedCount, isDisabled } = usePruneQueue();
+ const pruneQueue = usePruneQueue();
+ const finishedCount = useFinishedCount();
return (
}
- onClick={pruneQueue}
colorScheme="invokeBlue"
/>
);
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
index e9a81c4c19..fa7546a608 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
@@ -1,9 +1,9 @@
import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SessionMenuItems } from 'common/components/SessionMenuItems';
-import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
+import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
-import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
+import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@@ -18,18 +18,10 @@ export const QueueActionsMenuButton = memo(() => {
const { t } = useTranslation();
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
- const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
- const cancelCurrent = useCancelCurrentQueueItem();
- const {
- resumeProcessor,
- isLoading: isLoadingResumeProcessor,
- isDisabled: isDisabledResumeProcessor,
- } = useResumeProcessor();
- const {
- pauseProcessor,
- isLoading: isLoadingPauseProcessor,
- isDisabled: isDisabledPauseProcessor,
- } = usePauseProcessor();
+ const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog();
+ const deleteCurrentQueueItem = useDeleteCurrentQueueItem();
+ const resumeProcessor = useResumeProcessor();
+ const pauseProcessor = usePauseProcessor();
const openQueue = useCallback(() => {
dispatch(setActiveTab('queue'));
}, [dispatch]);
@@ -46,27 +38,27 @@ export const QueueActionsMenuButton = memo(() => {
}
- onClick={cancelCurrent.cancelQueueItem}
- isLoading={cancelCurrent.isLoading}
- isDisabled={cancelCurrent.isDisabled}
+ onClick={deleteCurrentQueueItem.trigger}
+ isLoading={deleteCurrentQueueItem.isLoading}
+ isDisabled={deleteCurrentQueueItem.isDisabled}
>
{t('queue.cancelTooltip')}
}
- onClick={cancelAllExceptCurrent.openDialog}
- isLoading={cancelAllExceptCurrent.isLoading}
- isDisabled={cancelAllExceptCurrent.isDisabled}
+ onClick={deleteAllExceptCurrent.openDialog}
+ isLoading={deleteAllExceptCurrent.isLoading}
+ isDisabled={deleteAllExceptCurrent.isDisabled}
>
{t('queue.cancelAllExceptCurrentTooltip')}
{isResumeEnabled && (
}
- onClick={resumeProcessor}
- isLoading={isLoadingResumeProcessor}
- isDisabled={isDisabledResumeProcessor}
+ onClick={resumeProcessor.trigger}
+ isLoading={resumeProcessor.isLoading}
+ isDisabled={resumeProcessor.isDisabled}
>
{t('queue.resumeTooltip')}
@@ -74,9 +66,9 @@ export const QueueActionsMenuButton = memo(() => {
{isPauseEnabled && (
}
- onClick={pauseProcessor}
- isLoading={isLoadingPauseProcessor}
- isDisabled={isDisabledPauseProcessor}
+ onClick={pauseProcessor.trigger}
+ isLoading={pauseProcessor.isLoading}
+ isDisabled={pauseProcessor.isDisabled}
>
{t('queue.pauseTooltip')}
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
index d3f05cb30b..040ecc6de9 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
@@ -1,5 +1,6 @@
-import { Flex, Spacer } from '@invoke-ai/ui-library';
-import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
+import { Flex, Spacer, useShiftModifier } from '@invoke-ai/ui-library';
+import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
+import { DeleteCurrentQueueItemIconButton } from 'features/queue/components/DeleteCurrentQueueItemIconButton';
import { QueueActionsMenuButton } from 'features/queue/components/QueueActionsMenuButton';
import ProgressBar from 'features/system/components/ProgressBar';
import { memo } from 'react';
@@ -13,7 +14,7 @@ const QueueControls = () => {
-
+
@@ -21,3 +22,15 @@ const QueueControls = () => {
};
export default memo(QueueControls);
+
+export const DeleteIconButton = memo(() => {
+ const shift = useShiftModifier();
+
+ if (!shift) {
+ return ;
+ }
+
+ return ;
+});
+
+DeleteIconButton.displayName = 'DeleteIconButton';
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx
index d645dcbfe5..1f6cea985a 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx
@@ -3,7 +3,7 @@ import { Badge, ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai
import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge';
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
-import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
+import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem';
import { useRetryQueueItem } from 'features/queue/hooks/useRetryQueueItem';
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@@ -38,21 +38,21 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
const handleToggle = useCallback(() => {
context.toggleQueueItem(item.item_id);
}, [context, item.item_id]);
- const { cancelQueueItem, isLoading: isLoadingCancelQueueItem } = useCancelQueueItem(item.item_id);
- const handleCancelQueueItem = useCallback(
+ const deleteQueueItem = useDeleteQueueItem();
+ const onClickDeleteQueueItem = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
- cancelQueueItem();
+ deleteQueueItem.trigger(item.item_id);
},
- [cancelQueueItem]
+ [deleteQueueItem, item.item_id]
);
- const { retryQueueItem, isLoading: isLoadingRetryQueueItem } = useRetryQueueItem(item.item_id);
- const handleRetryQueueItem = useCallback(
+ const retryQueueItem = useRetryQueueItem();
+ const onClickRetryQueueItem = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
- retryQueueItem();
+ retryQueueItem.trigger(item.item_id);
},
- [retryQueueItem]
+ [item.item_id, retryQueueItem]
);
const isOpen = useMemo(() => context.openQueueItems.includes(item.item_id), [context.openQueueItems, item.item_id]);
@@ -135,17 +135,17 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
{(!isFailed || !isRetryEnabled || isValidationRun) && (
}
/>
)}
{isFailed && isRetryEnabled && !isValidationRun && (
}
/>
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx
index a06e82ac5b..f4fc5f7c53 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx
@@ -2,14 +2,15 @@ import { Button, ButtonGroup, Flex, Heading, Spinner, Text } from '@invoke-ai/ui
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
+import { useBatchIsCanceled } from 'features/queue/hooks/useBatchIsCanceled';
import { useCancelBatch } from 'features/queue/hooks/useCancelBatch';
-import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
+import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem';
import { useRetryQueueItem } from 'features/queue/hooks/useRetryQueueItem';
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { get } from 'lodash-es';
import type { ReactNode } from 'react';
-import { memo, useMemo } from 'react';
+import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiXBold } from 'react-icons/pi';
import type { S } from 'services/api/types';
@@ -22,9 +23,10 @@ const QueueItemComponent = ({ queueItem }: Props) => {
const { session_id, batch_id, item_id, origin, destination } = queueItem;
const { t } = useTranslation();
const isRetryEnabled = useFeatureStatus('retryQueueItem');
- const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled: isBatchCanceled } = useCancelBatch(batch_id);
- const { cancelQueueItem, isLoading: isLoadingCancelQueueItem } = useCancelQueueItem(item_id);
- const { retryQueueItem, isLoading: isLoadingRetryQueueItem } = useRetryQueueItem(item_id);
+ const isBatchCanceled = useBatchIsCanceled(batch_id);
+ const cancelBatch = useCancelBatch();
+ const deleteQueueItem = useDeleteQueueItem();
+ const retryQueueItem = useRetryQueueItem();
const originText = useOriginText(origin);
const destinationText = useDestinationText(destination);
@@ -50,6 +52,18 @@ const QueueItemComponent = ({ queueItem }: Props) => {
const isFailed = useMemo(() => !!queueItem && ['canceled', 'failed'].includes(queueItem.status), [queueItem]);
+ const onCancelBatch = useCallback(() => {
+ cancelBatch.trigger(batch_id);
+ }, [cancelBatch, batch_id]);
+
+ const onCancelQueueItem = useCallback(() => {
+ deleteQueueItem.trigger(item_id);
+ }, [deleteQueueItem, item_id]);
+
+ const onRetryQueueItem = useCallback(() => {
+ retryQueueItem.trigger(item_id);
+ }, [retryQueueItem, item_id]);
+
return (
{
{(!isFailed || !isRetryEnabled) && (
}
colorScheme="error"
@@ -82,9 +96,9 @@ const QueueItemComponent = ({ queueItem }: Props) => {
)}
{isFailed && isRetryEnabled && (
}
colorScheme="invokeBlue"
@@ -93,9 +107,9 @@ const QueueItemComponent = ({ queueItem }: Props) => {
)}
}
colorScheme="error"
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
index 17c3ed104f..6432b4e913 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
@@ -1,6 +1,6 @@
/* eslint-disable i18next/no-literal-string */
import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
-import { CancelAllExceptCurrentButton } from 'features/queue/components/CancelAllExceptCurrentButton';
+import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
@@ -24,7 +24,7 @@ const QueueTabQueueControls = () => {
)}
-
+
diff --git a/invokeai/frontend/web/src/features/queue/components/ResumeProcessorButton.tsx b/invokeai/frontend/web/src/features/queue/components/ResumeProcessorButton.tsx
index cf62f8cb2a..31f073558a 100644
--- a/invokeai/frontend/web/src/features/queue/components/ResumeProcessorButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/ResumeProcessorButton.tsx
@@ -11,17 +11,17 @@ type Props = {
const ResumeProcessorButton = ({ asIconButton }: Props) => {
const { t } = useTranslation();
- const { resumeProcessor, isLoading, isDisabled } = useResumeProcessor();
+ const resumeProcessor = useResumeProcessor();
return (
}
- onClick={resumeProcessor}
+ onClick={resumeProcessor.trigger}
colorScheme="green"
/>
);
diff --git a/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx b/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx
index 1720e6a69d..310ee46ca5 100644
--- a/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx
@@ -9,28 +9,28 @@ const ToggleInvocationCacheButton = () => {
const { t } = useTranslation();
const { data: cacheStatus } = useGetInvocationCacheStatusQuery();
- const {
- enableInvocationCache,
- isDisabled: isEnableDisabled,
- isLoading: isEnableLoading,
- } = useEnableInvocationCache();
+ const enableInvocationCache = useEnableInvocationCache();
- const {
- disableInvocationCache,
- isDisabled: isDisableDisabled,
- isLoading: isDisableLoading,
- } = useDisableInvocationCache();
+ const disableInvocationCache = useDisableInvocationCache();
if (cacheStatus?.enabled) {
return (
-
+
{t('invocationCache.disable')}
);
}
return (
-
+
{t('invocationCache.enable')}
);
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useBatchIsCanceled.ts b/invokeai/frontend/web/src/features/queue/hooks/useBatchIsCanceled.ts
new file mode 100644
index 0000000000..2fd82c5b3b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/hooks/useBatchIsCanceled.ts
@@ -0,0 +1,20 @@
+import { useGetBatchStatusQuery } from 'services/api/endpoints/queue';
+
+export const useBatchIsCanceled = (batch_id: string) => {
+ const { isCanceled } = useGetBatchStatusQuery(
+ { batch_id },
+ {
+ selectFromResult: ({ data }) => {
+ if (!data) {
+ return { isCanceled: true };
+ }
+
+ return {
+ isCanceled: data?.in_progress === 0 && data?.pending === 0,
+ };
+ },
+ }
+ );
+
+ return isCanceled;
+};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts
index 3d38891545..8e6c79b96a 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts
@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCancelAllExceptCurrentMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -9,17 +9,17 @@ export const useCancelAllExceptCurrentQueueItem = () => {
const { t } = useTranslation();
const { data: queueStatus } = useGetQueueStatusQuery();
const isConnected = useStore($isConnected);
- const [trigger, { isLoading }] = useCancelAllExceptCurrentMutation({
+ const [_trigger, { isLoading }] = useCancelAllExceptCurrentMutation({
fixedCacheKey: 'cancelAllExceptCurrent',
});
- const cancelAllExceptCurrentQueueItem = useCallback(async () => {
+ const trigger = useCallback(async () => {
if (!queueStatus?.queue.pending) {
return;
}
try {
- await trigger().unwrap();
+ await _trigger().unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
@@ -32,17 +32,7 @@ export const useCancelAllExceptCurrentQueueItem = () => {
status: 'error',
});
}
- }, [queueStatus?.queue.pending, trigger, t]);
+ }, [queueStatus?.queue.pending, _trigger, t]);
- const isDisabled = useMemo(
- () => !isConnected || !queueStatus?.queue.pending,
- [isConnected, queueStatus?.queue.pending]
- );
-
- return {
- cancelAllExceptCurrentQueueItem,
- isLoading,
- queueStatus,
- isDisabled,
- };
+ return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.queue.pending } as const;
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts
index 92d0cbb5a6..ce9202f3cf 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts
@@ -2,48 +2,34 @@ import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { useCancelByBatchIdsMutation, useGetBatchStatusQuery } from 'services/api/endpoints/queue';
+import { useCancelByBatchIdsMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
-export const useCancelBatch = (batch_id: string) => {
+export const useCancelBatch = () => {
const isConnected = useStore($isConnected);
- const { isCanceled } = useGetBatchStatusQuery(
- { batch_id },
- {
- selectFromResult: ({ data }) => {
- if (!data) {
- return { isCanceled: true };
- }
-
- return {
- isCanceled: data?.in_progress === 0 && data?.pending === 0,
- };
- },
- }
- );
- const [trigger, { isLoading }] = useCancelByBatchIdsMutation({
+ const [_trigger, { isLoading }] = useCancelByBatchIdsMutation({
fixedCacheKey: 'cancelByBatchIds',
});
const { t } = useTranslation();
- const cancelBatch = useCallback(async () => {
- if (isCanceled) {
- return;
- }
- try {
- await trigger({ batch_ids: [batch_id] }).unwrap();
- toast({
- id: 'CANCEL_BATCH_SUCCEEDED',
- title: t('queue.cancelBatchSucceeded'),
- status: 'success',
- });
- } catch {
- toast({
- id: 'CANCEL_BATCH_FAILED',
- title: t('queue.cancelBatchFailed'),
- status: 'error',
- });
- }
- }, [batch_id, isCanceled, t, trigger]);
+ const trigger = useCallback(
+ async (batch_id: string) => {
+ try {
+ await _trigger({ batch_ids: [batch_id] }).unwrap();
+ toast({
+ id: 'CANCEL_BATCH_SUCCEEDED',
+ title: t('queue.cancelBatchSucceeded'),
+ status: 'success',
+ });
+ } catch {
+ toast({
+ id: 'CANCEL_BATCH_FAILED',
+ title: t('queue.cancelBatchFailed'),
+ status: 'error',
+ });
+ }
+ },
+ [t, _trigger]
+ );
- return { cancelBatch, isLoading, isCanceled, isDisabled: !isConnected };
+ return { trigger, isLoading, isDisabled: !isConnected };
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts
index 0f9a5c48fe..509efd8a75 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts
@@ -1,43 +1,20 @@
-import { useStore } from '@nanostores/react';
-import { toast } from 'features/toast/toast';
-import { isNil } from 'lodash-es';
-import { useCallback, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useCancelQueueItemMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
-import { $isConnected } from 'services/events/stores';
+import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
+import { useCurrentQueueItemId } from 'features/queue/hooks/useCurrentQueueItemId';
+import { useCallback } from 'react';
export const useCancelCurrentQueueItem = () => {
- const isConnected = useStore($isConnected);
- const { data: queueStatus } = useGetQueueStatusQuery();
- const [trigger, { isLoading }] = useCancelQueueItemMutation();
- const { t } = useTranslation();
- const currentQueueItemId = useMemo(() => queueStatus?.queue.item_id, [queueStatus?.queue.item_id]);
- const cancelQueueItem = useCallback(async () => {
- if (currentQueueItemId !== null || currentQueueItemId !== undefined) {
+ const currentQueueItemId = useCurrentQueueItemId();
+ const cancelQueueItem = useCancelQueueItem();
+ const trigger = useCallback(() => {
+ if (currentQueueItemId === null) {
return;
}
- try {
- await trigger({ item_id: currentQueueItemId }).unwrap();
- toast({
- id: 'QUEUE_CANCEL_SUCCEEDED',
- title: t('queue.cancelSucceeded'),
- status: 'success',
- });
- } catch {
- toast({
- id: 'QUEUE_CANCEL_FAILED',
- title: t('queue.cancelFailed'),
- status: 'error',
- });
- }
- }, [currentQueueItemId, t, trigger]);
-
- const isDisabled = useMemo(() => !isConnected || isNil(currentQueueItemId), [isConnected, currentQueueItemId]);
+ cancelQueueItem.trigger(currentQueueItemId);
+ }, [currentQueueItemId, cancelQueueItem]);
return {
- cancelQueueItem,
- isLoading,
- currentQueueItemId,
- isDisabled,
+ trigger,
+ isLoading: cancelQueueItem.isLoading,
+ isDisabled: cancelQueueItem.isDisabled || currentQueueItemId === null,
};
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts
index 1e59f8ec46..15de37e030 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts
@@ -5,26 +5,29 @@ import { useTranslation } from 'react-i18next';
import { useCancelQueueItemMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
-export const useCancelQueueItem = (item_id: number) => {
+export const useCancelQueueItem = () => {
const isConnected = useStore($isConnected);
- const [trigger, { isLoading }] = useCancelQueueItemMutation();
+ const [_trigger, { isLoading }] = useCancelQueueItemMutation();
const { t } = useTranslation();
- const cancelQueueItem = useCallback(async () => {
- try {
- await trigger({ item_id }).unwrap();
- toast({
- id: 'QUEUE_CANCEL_SUCCEEDED',
- title: t('queue.cancelSucceeded'),
- status: 'success',
- });
- } catch {
- toast({
- id: 'QUEUE_CANCEL_FAILED',
- title: t('queue.cancelFailed'),
- status: 'error',
- });
- }
- }, [item_id, t, trigger]);
+ const trigger = useCallback(
+ async (item_id: number) => {
+ try {
+ await _trigger({ item_id }).unwrap();
+ toast({
+ id: 'QUEUE_CANCEL_SUCCEEDED',
+ title: t('queue.cancelSucceeded'),
+ status: 'success',
+ });
+ } catch {
+ toast({
+ id: 'QUEUE_CANCEL_FAILED',
+ title: t('queue.cancelFailed'),
+ status: 'error',
+ });
+ }
+ },
+ [t, _trigger]
+ );
- return { cancelQueueItem, isLoading, isDisabled: !isConnected };
+ return { trigger, isLoading, isDisabled: !isConnected };
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts
new file mode 100644
index 0000000000..0d85a933c7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts
@@ -0,0 +1,33 @@
+import { useStore } from '@nanostores/react';
+import { toast } from 'features/toast/toast';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useCancelQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
+import { $isConnected } from 'services/events/stores';
+
+export const useCancelQueueItemsByDestination = () => {
+ const isConnected = useStore($isConnected);
+ const [_trigger, { isLoading }] = useCancelQueueItemsByDestinationMutation();
+ const { t } = useTranslation();
+ const trigger = useCallback(
+ async (destination: string) => {
+ try {
+ await _trigger({ destination }).unwrap();
+ toast({
+ id: 'QUEUE_CANCEL_SUCCEEDED',
+ title: t('queue.cancelSucceeded'),
+ status: 'success',
+ });
+ } catch {
+ toast({
+ id: 'QUEUE_CANCEL_FAILED',
+ title: t('queue.cancelFailed'),
+ status: 'error',
+ });
+ }
+ },
+ [t, _trigger]
+ );
+
+ return { trigger, isLoading, isDisabled: !isConnected };
+};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts
index 19ab3cf45f..90c5a0dd3a 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts
@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useClearInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo';
import { $isConnected } from 'services/events/stores';
@@ -9,19 +9,13 @@ export const useClearInvocationCache = () => {
const { t } = useTranslation();
const { data: cacheStatus } = useGetInvocationCacheStatusQuery();
const isConnected = useStore($isConnected);
- const [trigger, { isLoading }] = useClearInvocationCacheMutation({
+ const [_trigger, { isLoading }] = useClearInvocationCacheMutation({
fixedCacheKey: 'clearInvocationCache',
});
- const isDisabled = useMemo(() => !cacheStatus?.size || !isConnected, [cacheStatus?.size, isConnected]);
-
- const clearInvocationCache = useCallback(async () => {
- if (isDisabled) {
- return;
- }
-
+ const trigger = useCallback(async () => {
try {
- await trigger().unwrap();
+ await _trigger().unwrap();
toast({
id: 'INVOCATION_CACHE_CLEAR_SUCCEEDED',
title: t('invocationCache.clearSucceeded'),
@@ -34,7 +28,7 @@ export const useClearInvocationCache = () => {
status: 'error',
});
}
- }, [isDisabled, trigger, t]);
+ }, [_trigger, t]);
- return { clearInvocationCache, isLoading, cacheStatus, isDisabled };
+ return { trigger, isLoading, isDisabled: !isConnected || !cacheStatus?.size };
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts b/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts
index 5d5f7713f2..9c24448ed4 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
import { toast } from 'features/toast/toast';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -12,17 +12,17 @@ export const useClearQueue = () => {
const dispatch = useAppDispatch();
const { data: queueStatus } = useGetQueueStatusQuery();
const isConnected = useStore($isConnected);
- const [trigger, { isLoading }] = useClearQueueMutation({
+ const [_trigger, { isLoading }] = useClearQueueMutation({
fixedCacheKey: 'clearQueue',
});
- const clearQueue = useCallback(async () => {
+ const trigger = useCallback(async () => {
if (!queueStatus?.queue.total) {
return;
}
try {
- await trigger().unwrap();
+ await _trigger().unwrap();
toast({
id: 'QUEUE_CLEAR_SUCCEEDED',
title: t('queue.clearSucceeded'),
@@ -37,14 +37,7 @@ export const useClearQueue = () => {
status: 'error',
});
}
- }, [queueStatus?.queue.total, trigger, dispatch, t]);
+ }, [queueStatus?.queue.total, _trigger, dispatch, t]);
- const isDisabled = useMemo(() => !isConnected || !queueStatus?.queue.total, [isConnected, queueStatus?.queue.total]);
-
- return {
- clearQueue,
- isLoading,
- queueStatus,
- isDisabled,
- };
+ return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.queue.total };
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCurrentDestination.ts b/invokeai/frontend/web/src/features/queue/hooks/useCurrentDestination.ts
deleted file mode 100644
index 773d966634..0000000000
--- a/invokeai/frontend/web/src/features/queue/hooks/useCurrentDestination.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { useGetCurrentQueueItemQuery } from 'services/api/endpoints/queue';
-
-export const useCurrentDestination = () => {
- const { destination } = useGetCurrentQueueItemQuery(undefined, {
- selectFromResult: ({ data }) => ({
- destination: data ? data.destination : null,
- }),
- });
-
- return destination;
-};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts b/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts
new file mode 100644
index 0000000000..daa3a82704
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts
@@ -0,0 +1,11 @@
+import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
+
+export const useCurrentQueueItemId = () => {
+ const { currentQueueItemId } = useGetQueueStatusQuery(undefined, {
+ selectFromResult: ({ data }) => ({
+ currentQueueItemId: data?.queue.item_id ?? null,
+ }),
+ });
+
+ return currentQueueItemId;
+};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts
new file mode 100644
index 0000000000..1f34a76d24
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts
@@ -0,0 +1,38 @@
+import { useStore } from '@nanostores/react';
+import { toast } from 'features/toast/toast';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useDeleteAllExceptCurrentMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
+import { $isConnected } from 'services/events/stores';
+
+export const useDeleteAllExceptCurrentQueueItem = () => {
+ const { t } = useTranslation();
+ const { data: queueStatus } = useGetQueueStatusQuery();
+ const isConnected = useStore($isConnected);
+ const [_trigger, { isLoading }] = useDeleteAllExceptCurrentMutation({
+ fixedCacheKey: 'deleteAllExceptCurrent',
+ });
+
+ const trigger = useCallback(async () => {
+ if (!queueStatus?.queue.pending) {
+ return;
+ }
+
+ try {
+ await _trigger().unwrap();
+ toast({
+ id: 'QUEUE_CANCEL_SUCCEEDED',
+ title: t('queue.cancelSucceeded'),
+ status: 'success',
+ });
+ } catch {
+ toast({
+ id: 'QUEUE_CANCEL_FAILED',
+ title: t('queue.cancelFailed'),
+ status: 'error',
+ });
+ }
+ }, [queueStatus?.queue.pending, _trigger, t]);
+
+ return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.queue.pending } as const;
+};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteCurrentQueueItem.ts
new file mode 100644
index 0000000000..d119311235
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteCurrentQueueItem.ts
@@ -0,0 +1,20 @@
+import { useCurrentQueueItemId } from 'features/queue/hooks/useCurrentQueueItemId';
+import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem';
+import { useCallback } from 'react';
+
+export const useDeleteCurrentQueueItem = () => {
+ const currentQueueItemId = useCurrentQueueItemId();
+ const deleteQueueItem = useDeleteQueueItem();
+ const trigger = useCallback(() => {
+ if (currentQueueItemId === null) {
+ return;
+ }
+ deleteQueueItem.trigger(currentQueueItemId);
+ }, [currentQueueItemId, deleteQueueItem]);
+
+ return {
+ trigger,
+ isLoading: deleteQueueItem.isLoading,
+ isDisabled: deleteQueueItem.isDisabled || currentQueueItemId === null,
+ };
+};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts
new file mode 100644
index 0000000000..8e37d376b4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts
@@ -0,0 +1,33 @@
+import { useStore } from '@nanostores/react';
+import { toast } from 'features/toast/toast';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
+import { $isConnected } from 'services/events/stores';
+
+export const useDeleteQueueItem = () => {
+ const isConnected = useStore($isConnected);
+ const [_trigger, { isLoading }] = useDeleteQueueItemMutation();
+ const { t } = useTranslation();
+ const trigger = useCallback(
+ async (item_id: number) => {
+ try {
+ await _trigger({ item_id }).unwrap();
+ toast({
+ id: 'QUEUE_CANCEL_SUCCEEDED',
+ title: t('queue.cancelSucceeded'),
+ status: 'success',
+ });
+ } catch {
+ toast({
+ id: 'QUEUE_CANCEL_FAILED',
+ title: t('queue.cancelFailed'),
+ status: 'error',
+ });
+ }
+ },
+ [t, _trigger]
+ );
+
+ return { trigger, isLoading, isDisabled: !isConnected };
+};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItemsByDestination.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItemsByDestination.ts
new file mode 100644
index 0000000000..376063cb44
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItemsByDestination.ts
@@ -0,0 +1,33 @@
+import { useStore } from '@nanostores/react';
+import { toast } from 'features/toast/toast';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
+import { $isConnected } from 'services/events/stores';
+
+export const useDeleteQueueItemsByDestination = () => {
+ const isConnected = useStore($isConnected);
+ const [_trigger, { isLoading }] = useDeleteQueueItemsByDestinationMutation();
+ const { t } = useTranslation();
+ const trigger = useCallback(
+ async (destination: string) => {
+ try {
+ await _trigger({ destination }).unwrap();
+ toast({
+ id: 'QUEUE_CANCEL_SUCCEEDED',
+ title: t('queue.cancelSucceeded'),
+ status: 'success',
+ });
+ } catch {
+ toast({
+ id: 'QUEUE_CANCEL_FAILED',
+ title: t('queue.cancelFailed'),
+ status: 'error',
+ });
+ }
+ },
+ [t, _trigger]
+ );
+
+ return { trigger, isLoading, isDisabled: !isConnected };
+};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts
index 703cf8d4cb..17ac6bec44 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts
@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDisableInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo';
import { $isConnected } from 'services/events/stores';
@@ -9,22 +9,13 @@ export const useDisableInvocationCache = () => {
const { t } = useTranslation();
const { data: cacheStatus } = useGetInvocationCacheStatusQuery();
const isConnected = useStore($isConnected);
- const [trigger, { isLoading }] = useDisableInvocationCacheMutation({
+ const [_trigger, { isLoading }] = useDisableInvocationCacheMutation({
fixedCacheKey: 'disableInvocationCache',
});
- const isDisabled = useMemo(
- () => !cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0,
- [cacheStatus?.enabled, cacheStatus?.max_size, isConnected]
- );
-
- const disableInvocationCache = useCallback(async () => {
- if (isDisabled) {
- return;
- }
-
+ const trigger = useCallback(async () => {
try {
- await trigger().unwrap();
+ await _trigger().unwrap();
toast({
id: 'INVOCATION_CACHE_DISABLE_SUCCEEDED',
title: t('invocationCache.disableSucceeded'),
@@ -37,7 +28,12 @@ export const useDisableInvocationCache = () => {
status: 'error',
});
}
- }, [isDisabled, trigger, t]);
+ }, [_trigger, t]);
- return { disableInvocationCache, isLoading, cacheStatus, isDisabled };
+ return {
+ trigger,
+ isLoading,
+ cacheStatus,
+ isDisabled: !cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0,
+ };
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts
index 2589d50717..02fcf444fa 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts
@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useEnableInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo';
import { $isConnected } from 'services/events/stores';
@@ -9,22 +9,13 @@ export const useEnableInvocationCache = () => {
const { t } = useTranslation();
const { data: cacheStatus } = useGetInvocationCacheStatusQuery();
const isConnected = useStore($isConnected);
- const [trigger, { isLoading }] = useEnableInvocationCacheMutation({
+ const [_trigger, { isLoading }] = useEnableInvocationCacheMutation({
fixedCacheKey: 'enableInvocationCache',
});
- const isDisabled = useMemo(
- () => cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0,
- [cacheStatus?.enabled, cacheStatus?.max_size, isConnected]
- );
-
- const enableInvocationCache = useCallback(async () => {
- if (isDisabled) {
- return;
- }
-
+ const trigger = useCallback(async () => {
try {
- await trigger().unwrap();
+ await _trigger().unwrap();
toast({
id: 'INVOCATION_CACHE_ENABLE_SUCCEEDED',
title: t('invocationCache.enableSucceeded'),
@@ -37,7 +28,11 @@ export const useEnableInvocationCache = () => {
status: 'error',
});
}
- }, [isDisabled, trigger, t]);
+ }, [_trigger, t]);
- return { enableInvocationCache, isLoading, cacheStatus, isDisabled };
+ return {
+ trigger,
+ isLoading,
+ isDisabled: cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0,
+ };
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts
index d4712ad2b8..9e82576a4f 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts
@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery, usePauseProcessorMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -9,18 +9,13 @@ export const usePauseProcessor = () => {
const { t } = useTranslation();
const isConnected = useStore($isConnected);
const { data: queueStatus } = useGetQueueStatusQuery();
- const [trigger, { isLoading }] = usePauseProcessorMutation({
+ const [_trigger, { isLoading }] = usePauseProcessorMutation({
fixedCacheKey: 'pauseProcessor',
});
- const isStarted = useMemo(() => Boolean(queueStatus?.processor.is_started), [queueStatus?.processor.is_started]);
-
- const pauseProcessor = useCallback(async () => {
- if (!isStarted) {
- return;
- }
+ const trigger = useCallback(async () => {
try {
- await trigger().unwrap();
+ await _trigger().unwrap();
toast({
id: 'PAUSE_SUCCEEDED',
title: t('queue.pauseSucceeded'),
@@ -33,9 +28,7 @@ export const usePauseProcessor = () => {
status: 'error',
});
}
- }, [isStarted, trigger, t]);
+ }, [_trigger, t]);
- const isDisabled = useMemo(() => !isConnected || !isStarted, [isConnected, isStarted]);
-
- return { pauseProcessor, isLoading, isStarted, isDisabled };
+ return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.processor.is_started };
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts b/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts
index 09e77e23d6..c186db96df 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
import { toast } from 'features/toast/toast';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery, usePruneQueueMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -11,27 +11,14 @@ export const usePruneQueue = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const isConnected = useStore($isConnected);
- const [trigger, { isLoading }] = usePruneQueueMutation({
+ const finishedCount = useFinishedCount();
+ const [_trigger, { isLoading }] = usePruneQueueMutation({
fixedCacheKey: 'pruneQueue',
});
- const { finishedCount } = useGetQueueStatusQuery(undefined, {
- selectFromResult: ({ data }) => {
- if (!data) {
- return { finishedCount: 0 };
- }
- return {
- finishedCount: data.queue.completed + data.queue.canceled + data.queue.failed,
- };
- },
- });
-
- const pruneQueue = useCallback(async () => {
- if (!finishedCount) {
- return;
- }
+ const trigger = useCallback(async () => {
try {
- const data = await trigger().unwrap();
+ const data = await _trigger().unwrap();
toast({
id: 'PRUNE_SUCCEEDED',
title: t('queue.pruneSucceeded', { item_count: data.deleted }),
@@ -46,9 +33,23 @@ export const usePruneQueue = () => {
status: 'error',
});
}
- }, [finishedCount, trigger, dispatch, t]);
+ }, [_trigger, dispatch, t]);
- const isDisabled = useMemo(() => !isConnected || !finishedCount, [finishedCount, isConnected]);
-
- return { pruneQueue, isLoading, finishedCount, isDisabled };
+ return { trigger, isLoading, isDisabled: !isConnected || !finishedCount };
+};
+
+export const useFinishedCount = () => {
+ const { finishedCount } = useGetQueueStatusQuery(undefined, {
+ selectFromResult: ({ data }) => {
+ if (!data) {
+ return { finishedCount: 0 };
+ }
+
+ return {
+ finishedCount: data.queue.completed + data.queue.canceled + data.queue.failed,
+ };
+ },
+ });
+
+ return finishedCount;
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts
index 058a3b2b3e..baa02ece03 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts
@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery, useResumeProcessorMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -9,18 +9,13 @@ export const useResumeProcessor = () => {
const isConnected = useStore($isConnected);
const { data: queueStatus } = useGetQueueStatusQuery();
const { t } = useTranslation();
- const [trigger, { isLoading }] = useResumeProcessorMutation({
+ const [_trigger, { isLoading }] = useResumeProcessorMutation({
fixedCacheKey: 'resumeProcessor',
});
- const isStarted = useMemo(() => Boolean(queueStatus?.processor.is_started), [queueStatus?.processor.is_started]);
-
- const resumeProcessor = useCallback(async () => {
- if (isStarted) {
- return;
- }
+ const trigger = useCallback(async () => {
try {
- await trigger().unwrap();
+ await _trigger().unwrap();
toast({
id: 'PROCESSOR_RESUMED',
title: t('queue.resumeSucceeded'),
@@ -33,9 +28,7 @@ export const useResumeProcessor = () => {
status: 'error',
});
}
- }, [isStarted, trigger, t]);
+ }, [_trigger, t]);
- const isDisabled = useMemo(() => !isConnected || isStarted, [isConnected, isStarted]);
-
- return { resumeProcessor, isLoading, isStarted, isDisabled };
+ return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.processor.is_started };
};
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useRetryQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useRetryQueueItem.ts
index cf3442d448..580f88f9ad 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useRetryQueueItem.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useRetryQueueItem.ts
@@ -5,29 +5,32 @@ import { useTranslation } from 'react-i18next';
import { useRetryItemsByIdMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
-export const useRetryQueueItem = (item_id: number) => {
+export const useRetryQueueItem = () => {
const isConnected = useStore($isConnected);
- const [trigger, { isLoading }] = useRetryItemsByIdMutation();
+ const [_trigger, { isLoading }] = useRetryItemsByIdMutation();
const { t } = useTranslation();
- const retryQueueItem = useCallback(async () => {
- try {
- const result = await trigger([item_id]).unwrap();
- if (!result.retried_item_ids.includes(item_id)) {
- throw new Error('Failed to retry item');
+ const trigger = useCallback(
+ async (item_id: number) => {
+ try {
+ const result = await _trigger([item_id]).unwrap();
+ if (!result.retried_item_ids.includes(item_id)) {
+ throw new Error('Failed to retry item');
+ }
+ toast({
+ id: 'QUEUE_RETRY_SUCCEEDED',
+ title: t('queue.retrySucceeded'),
+ status: 'success',
+ });
+ } catch {
+ toast({
+ id: 'QUEUE_RETRY_FAILED',
+ title: t('queue.retryFailed'),
+ status: 'error',
+ });
}
- toast({
- id: 'QUEUE_RETRY_SUCCEEDED',
- title: t('queue.retrySucceeded'),
- status: 'success',
- });
- } catch {
- toast({
- id: 'QUEUE_RETRY_FAILED',
- title: t('queue.retryFailed'),
- status: 'error',
- });
- }
- }, [item_id, t, trigger]);
+ },
+ [t, _trigger]
+ );
- return { retryQueueItem, isLoading, isDisabled: !isConnected };
+ return { trigger, isLoading, isDisabled: !isConnected };
};
diff --git a/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx b/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx
index 5b688097f3..218ca382b8 100644
--- a/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx
+++ b/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx
@@ -1,6 +1,5 @@
import { Progress } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { useCurrentDestination } from 'features/queue/hooks/useCurrentDestination';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
@@ -8,7 +7,6 @@ import { $isConnected, $lastProgressEvent } from 'services/events/stores';
const ProgressBar = () => {
const { t } = useTranslation();
- const destination = useCurrentDestination();
const { data: queueStatus } = useGetQueueStatusQuery();
const isConnected = useStore($isConnected);
const lastProgressEvent = useStore($lastProgressEvent);
@@ -39,16 +37,6 @@ const ProgressBar = () => {
return false;
}, [isConnected, lastProgressEvent, queueStatus?.queue.in_progress]);
- const colorScheme = useMemo(() => {
- if (destination === 'canvas') {
- return 'invokeGreen';
- } else if (destination === 'gallery') {
- return 'invokeBlue';
- } else {
- return 'base';
- }
- }, [destination]);
-
return (
);
@@ -103,18 +103,18 @@ const InvokeIconButtonIcon = memo(() => {
});
InvokeIconButtonIcon.displayName = 'InvokeIconButtonIcon';
-const CancelCurrentIconButton = memo(() => {
+const DeleteCurrentIconButton = memo(() => {
const { t } = useTranslation();
- const cancelCurrentQueueItem = useCancelCurrentQueueItem();
+ const deleteCurrentQueueItem = useDeleteCurrentQueueItem();
return (
}
- onClick={cancelCurrentQueueItem.cancelQueueItem}
colorScheme="error"
flexGrow={1}
/>
@@ -122,25 +122,25 @@ const CancelCurrentIconButton = memo(() => {
);
});
-CancelCurrentIconButton.displayName = 'CancelCurrentIconButton';
+DeleteCurrentIconButton.displayName = 'DeleteCurrentIconButton';
-const CancelAllExceptCurrentIconButton = memo(() => {
+const DeleteAllExceptCurrentIconButton = memo(() => {
const { t } = useTranslation();
- const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
+ const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog();
return (
}
colorScheme="error"
- onClick={cancelAllExceptCurrent.openDialog}
+ onClick={deleteAllExceptCurrent.openDialog}
flexGrow={1}
/>
);
});
-CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton';
+DeleteAllExceptCurrentIconButton.displayName = 'DeleteAllExceptCurrentIconButton';
diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
index 2239a5df3a..75ff92e58d 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
@@ -224,7 +224,7 @@ export const queueApi = api.injectEndpoints({
];
},
}),
- cancelByDestination: build.mutation<
+ cancelQueueItemsByDestination: build.mutation<
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['responses']['200']['content']['application/json'],
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['parameters']['query']
>({
@@ -256,6 +256,16 @@ export const queueApi = api.injectEndpoints({
}),
invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination', 'SessionQueueItem'],
}),
+ deleteAllExceptCurrent: build.mutation<
+ paths['/api/v1/queue/{queue_id}/delete_all_except_current']['put']['responses']['200']['content']['application/json'],
+ void
+ >({
+ query: () => ({
+ url: buildQueueUrl('delete_all_except_current'),
+ method: 'PUT',
+ }),
+ invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination', 'SessionQueueItem'],
+ }),
retryItemsById: build.mutation<
paths['/api/v1/queue/{queue_id}/retry_items_by_id']['put']['responses']['200']['content']['application/json'],
paths['/api/v1/queue/{queue_id}/retry_items_by_id']['put']['requestBody']['content']['application/json']
@@ -329,7 +339,11 @@ export const queueApi = api.injectEndpoints({
url: buildQueueUrl(`i/${item_id}`),
method: 'DELETE',
}),
- invalidatesTags: (result, error, { item_id }) => [{ type: 'SessionQueueItem', id: item_id }],
+ invalidatesTags: (result, error, { item_id }) => [
+ { type: 'SessionQueueItem', id: item_id },
+ { type: 'SessionQueueItem', id: LIST_TAG },
+ { type: 'SessionQueueItem', id: LIST_ALL_TAG },
+ ],
}),
deleteQueueItemsByDestination: build.mutation({
query: ({ destination }) => ({
@@ -366,8 +380,10 @@ export const {
useGetQueueStatusQuery,
useListQueueItemsQuery,
useCancelQueueItemMutation,
+ useCancelQueueItemsByDestinationMutation,
useDeleteQueueItemMutation,
useDeleteQueueItemsByDestinationMutation,
+ useDeleteAllExceptCurrentMutation,
useGetBatchStatusQuery,
useGetCurrentQueueItemQuery,
useGetQueueCountsByDestinationQuery,
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index e167ba36fd..20a5903edb 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -1244,6 +1244,26 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/queue/{queue_id}/delete_all_except_current": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /**
+ * Delete All Except Current
+ * @description Immediately deletes all queue items except in-processing items
+ */
+ put: operations["delete_all_except_current"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/queue/{queue_id}/cancel_by_batch_ids": {
parameters: {
query?: never;
@@ -5885,6 +5905,17 @@ export type components = {
*/
type: "dw_openpose_detection";
};
+ /**
+ * DeleteAllExceptCurrentResult
+ * @description Result of deleting all except current
+ */
+ DeleteAllExceptCurrentResult: {
+ /**
+ * Deleted
+ * @description Number of queue items deleted
+ */
+ deleted: number;
+ };
/** DeleteBoardResult */
DeleteBoardResult: {
/**
@@ -24570,6 +24601,38 @@ export interface operations {
};
};
};
+ delete_all_except_current: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The queue id to perform this operation on */
+ queue_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["DeleteAllExceptCurrentResult"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
cancel_by_batch_ids: {
parameters: {
query?: never;
From 9df69496e43a63b76975c1c68aae3775ac9b482d Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 6 Jun 2025 18:30:54 +1000
Subject: [PATCH 068/210] feat(ui): remove vary and edit as control buttons
---
.../components/SimpleSession/ImageActions.tsx | 26 -------------------
1 file changed, 26 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx
index 96d27b8d14..7e877afce4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx
@@ -9,26 +9,6 @@ import type { ImageDTO } from 'services/api/types';
export const ImageActions = memo(({ imageDTO, ...rest }: { imageDTO: ImageDTO } & ButtonGroupProps) => {
const { getState, dispatch } = useAppStore();
- const vary = useCallback(() => {
- newCanvasFromImage({
- imageDTO,
- type: 'raster_layer',
- withResize: true,
- getState,
- dispatch,
- });
- }, [dispatch, getState, imageDTO]);
-
- const useAsControl = useCallback(() => {
- newCanvasFromImage({
- imageDTO,
- type: 'control_layer',
- withResize: true,
- getState,
- dispatch,
- });
- }, [dispatch, getState, imageDTO]);
-
const edit = useCallback(() => {
newCanvasFromImage({
imageDTO,
@@ -40,12 +20,6 @@ export const ImageActions = memo(({ imageDTO, ...rest }: { imageDTO: ImageDTO }
}, [dispatch, getState, imageDTO]);
return (
-
- Vary
-
-
- Use as Control
-
Edit
From 25313663869134cc5c05f524eec7fc90dbf8a434 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 6 Jun 2025 20:05:02 +1000
Subject: [PATCH 069/210] feat(ui): simple session initial state
---
.../components/CanvasMainPanelContent.tsx | 7 +-
.../NoSession/GenerateWithControlImage.tsx | 68 -------------------
.../NoSession/GenerateWithStartingImage.tsx | 65 ------------------
...enerateWithStartingImageAndInpaintMask.tsx | 65 ------------------
.../components/NoSession/NoSession.tsx | 32 ---------
.../components/SimpleSession/InitialState.tsx | 50 ++++++++++++++
.../InitialStateAddAStyleReference.tsx | 39 +++++++++++
.../InitialStateCardGridItem.tsx | 24 +++++++
.../InitialStateEditImageCard.tsx | 39 +++++++++++
.../InitialStateGenerateFromText.tsx | 33 +++++++++
.../InitialStateUseALayoutImageCard.tsx | 39 +++++++++++
.../SimpleSession/SimpleSession.tsx | 6 +-
.../DeleteAllExceptCurrentButton.tsx | 32 +++++++++
.../DeleteAllExceptCurrentIconButton.tsx | 25 +++++++
.../queue/components/QueueControls.tsx | 4 +-
.../components/QueueTabQueueControls.tsx | 5 +-
16 files changed, 291 insertions(+), 242 deletions(-)
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithControlImage.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImage.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateCardGridItem.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
create mode 100644 invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentButton.tsx
create mode 100644 invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentIconButton.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
index 242e3e363c..b42a2f716a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
@@ -1,6 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks';
import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
-import { NoSession } from 'features/controlLayers/components/NoSession/NoSession';
import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
import { selectCanvasSessionId, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo } from 'react';
@@ -12,11 +11,7 @@ export const CanvasMainPanelContent = memo(() => {
const id = useAppSelector(selectCanvasSessionId);
if (type === 'simple') {
- if (id === null) {
- return ;
- } else {
- return ;
- }
+ return ;
}
if (type === 'advanced') {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithControlImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithControlImage.tsx
deleted file mode 100644
index 14b253330c..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithControlImage.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-/* eslint-disable i18next/no-literal-string */
-
-import { Button, Flex, Text } from '@invoke-ai/ui-library';
-import { useAppStore } from 'app/store/nanostores/store';
-import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
-import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
-import { DndDropTarget } from 'features/dnd/DndDropTarget';
-import { newCanvasFromImage } from 'features/imageActions/actions';
-import { memo, useMemo } from 'react';
-import { Trans } from 'react-i18next';
-import { PiUploadBold } from 'react-icons/pi';
-import type { ImageDTO } from 'services/api/types';
-import type { Param0 } from 'tsafe';
-
-const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getData({
- type: 'control_layer',
- withResize: true,
-});
-
-export const GenerateWithControlImage = memo(() => {
- const { getState, dispatch } = useAppStore();
- const useImageUploadButtonOptions = useMemo>(
- () => ({
- onUpload: (imageDTO: ImageDTO) => {
- newCanvasFromImage({ imageDTO, type: 'control_layer', withResize: true, getState, dispatch });
- },
- allowMultiple: false,
- }),
- [dispatch, getState]
- );
- const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
- const components = useMemo(
- () => ({
- UploadButton: (
- }
- />
- ),
- }),
- [uploadApi]
- );
-
- return (
-
-
- Generate with a Control Image
-
-
- Generate a new image using the control image to guide the structure and composition (Text to Image with
- Control).
-
-
-
-
-
-
-
- );
-});
-GenerateWithControlImage.displayName = 'GenerateWithControlImage';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImage.tsx
deleted file mode 100644
index 308bf2a5af..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImage.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/* eslint-disable i18next/no-literal-string */
-
-import { Button, Flex, Text } from '@invoke-ai/ui-library';
-import { useAppStore } from 'app/store/nanostores/store';
-import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
-import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
-import { DndDropTarget } from 'features/dnd/DndDropTarget';
-import { newCanvasFromImage } from 'features/imageActions/actions';
-import { memo, useMemo } from 'react';
-import { Trans } from 'react-i18next';
-import { PiUploadBold } from 'react-icons/pi';
-import type { ImageDTO } from 'services/api/types';
-import type { Param0 } from 'tsafe';
-
-const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({
- type: 'raster_layer',
- withResize: true,
-});
-
-export const GenerateWithStartingImage = memo(() => {
- const { getState, dispatch } = useAppStore();
- const useImageUploadButtonOptions = useMemo>(
- () => ({
- onUpload: (imageDTO: ImageDTO) => {
- newCanvasFromImage({ imageDTO, type: 'raster_layer', withResize: true, getState, dispatch });
- },
- allowMultiple: false,
- }),
- [dispatch, getState]
- );
- const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
- const components = useMemo(
- () => ({
- UploadButton: (
- }
- />
- ),
- }),
- [uploadApi]
- );
-
- return (
-
-
- Generate with a Starting Image
-
- Regenerate the starting image using the model (Image to Image).
-
-
-
-
-
-
- );
-});
-GenerateWithStartingImage.displayName = 'GenerateWithStartingImage';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask.tsx
deleted file mode 100644
index c5220e3304..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/* eslint-disable i18next/no-literal-string */
-
-import { Button, Flex, Text } from '@invoke-ai/ui-library';
-import { useAppStore } from 'app/store/nanostores/store';
-import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
-import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
-import { DndDropTarget } from 'features/dnd/DndDropTarget';
-import { newCanvasFromImage } from 'features/imageActions/actions';
-import { memo, useMemo } from 'react';
-import { Trans } from 'react-i18next';
-import { PiUploadBold } from 'react-icons/pi';
-import type { ImageDTO } from 'services/api/types';
-import type { Param0 } from 'tsafe';
-
-const generateWithStartingImageAndInpaintMaskDndTargetData = newCanvasFromImageDndTarget.getData({
- type: 'raster_layer',
- withInpaintMask: true,
-});
-
-export const GenerateWithStartingImageAndInpaintMask = memo(() => {
- const { getState, dispatch } = useAppStore();
- const useImageUploadButtonOptions = useMemo>(
- () => ({
- onUpload: (imageDTO: ImageDTO) => {
- newCanvasFromImage({ imageDTO, type: 'raster_layer', withInpaintMask: true, getState, dispatch });
- },
- allowMultiple: false,
- }),
- [dispatch, getState]
- );
- const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
- const components = useMemo(
- () => ({
- UploadButton: (
- }
- />
- ),
- }),
- [uploadApi]
- );
-
- return (
-
-
- Edit Image
-
- Edit the image by regenerating parts of it (Inpaint).
-
-
-
-
-
-
- );
-});
-GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx
deleted file mode 100644
index 607fcc5df8..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/* eslint-disable i18next/no-literal-string */
-
-import { Button, Flex, Heading, Text } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
-import { GenerateWithControlImage } from 'features/controlLayers/components/NoSession/GenerateWithControlImage';
-import { GenerateWithStartingImage } from 'features/controlLayers/components/NoSession/GenerateWithStartingImage';
-import { GenerateWithStartingImageAndInpaintMask } from 'features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { memo, useCallback } from 'react';
-
-export const NoSession = memo(() => {
- const dispatch = useAppDispatch();
- const newSesh = useCallback(() => {
- dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
- }, [dispatch]);
-
- return (
-
- Get Started with Invoke
-
- Start a new Canvas Session
-
- or
-
-
-
-
-
-
- );
-});
-NoSession.displayName = 'NoSession';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
new file mode 100644
index 0000000000..31dc291cda
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -0,0 +1,50 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
+import { InitialStateCardGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateCardGridItem';
+import { InitialStateEditImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateEditImageCard';
+import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
+import { InitialStateUseALayoutImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useCallback } from 'react';
+
+export const InitialState = memo(() => {
+ const dispatch = useAppDispatch();
+ const newCanvasSession = useCallback(() => {
+ dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ }, [dispatch]);
+
+ return (
+
+ Choose a starting method.
+
+ Drag an image onto a card or click the upload icon.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ or{' '}
+
+ start from a blank canvas.
+
+
+
+ );
+});
+InitialState.displayName = 'InitialState';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
new file mode 100644
index 0000000000..f41ad61ffa
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
@@ -0,0 +1,39 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
+import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
+import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { newCanvasFromImage } from 'features/imageActions/actions';
+import { memo, useCallback } from 'react';
+import { PiUserCircleGearBold } from 'react-icons/pi';
+import type { ImageDTO } from 'services/api/types';
+
+const NEW_CANVAS_OPTIONS = { type: 'reference_image' } as const;
+
+const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
+
+export const InitialStateAddAStyleReference = memo(() => {
+ const { getState, dispatch } = useAppStore();
+
+ const onUpload = useCallback(
+ (imageDTO: ImageDTO) => {
+ newCanvasFromImage({ imageDTO, getState, dispatch, ...NEW_CANVAS_OPTIONS });
+ },
+ [dispatch, getState]
+ );
+
+ return (
+ <>
+
+ Add a Style Reference
+ Add an image to transfer its look.
+
+
+
+
+ >
+ );
+});
+InitialStateAddAStyleReference.displayName = 'InitialStateAddAStyleReference';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateCardGridItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateCardGridItem.tsx
new file mode 100644
index 0000000000..708b005db1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateCardGridItem.tsx
@@ -0,0 +1,24 @@
+import { GridItem } from '@invoke-ai/ui-library';
+import { memo, type PropsWithChildren } from 'react';
+
+export const InitialStateCardGridItem = memo((props: PropsWithChildren) => {
+ return (
+
+ {props.children}
+
+ );
+});
+
+InitialStateCardGridItem.displayName = 'InitialStateCardGridItem';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx
new file mode 100644
index 0000000000..d0c7d0032c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx
@@ -0,0 +1,39 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
+import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
+import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { newCanvasFromImage } from 'features/imageActions/actions';
+import { memo, useCallback } from 'react';
+import { PiPencilBold } from 'react-icons/pi';
+import type { ImageDTO } from 'services/api/types';
+
+const NEW_CANVAS_OPTIONS = { type: 'raster_layer', withInpaintMask: true } as const;
+
+const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
+
+export const InitialStateEditImageCard = memo(() => {
+ const { getState, dispatch } = useAppStore();
+
+ const onUpload = useCallback(
+ (imageDTO: ImageDTO) => {
+ newCanvasFromImage({ imageDTO, getState, dispatch, ...NEW_CANVAS_OPTIONS });
+ },
+ [dispatch, getState]
+ );
+
+ return (
+ <>
+
+ Edit Image
+ Add an image to refine.
+
+
+
+
+ >
+ );
+});
+InitialStateEditImageCard.displayName = 'InitialStateEditImageCard';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
new file mode 100644
index 0000000000..6d4fa5a2cf
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
@@ -0,0 +1,33 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Flex, Heading, Icon, IconButton, Text } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { PiCursorTextBold, PiTextAaBold } from 'react-icons/pi';
+
+const focusOnPrompt = () => {
+ const promptElement = document.getElementById('prompt');
+ if (promptElement instanceof HTMLTextAreaElement) {
+ promptElement.focus();
+ promptElement.select();
+ }
+};
+
+export const InitialStateGenerateFromText = memo(() => {
+ return (
+ <>
+
+ Generate from Text
+ Enter a prompt and Invoke.
+
+ }
+ variant="link"
+ h={8}
+ />
+
+ >
+ );
+});
+InitialStateGenerateFromText.displayName = 'InitialStateGenerateFromText';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
new file mode 100644
index 0000000000..54d2ef6f75
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
@@ -0,0 +1,39 @@
+/* eslint-disable i18next/no-literal-string */
+
+import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
+import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
+import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { newCanvasFromImage } from 'features/imageActions/actions';
+import { memo, useCallback } from 'react';
+import { PiRectangleDashedBold } from 'react-icons/pi';
+import type { ImageDTO } from 'services/api/types';
+
+const NEW_CANVAS_OPTIONS = { type: 'control_layer', withResize: true } as const;
+
+const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
+
+export const InitialStateUseALayoutImageCard = memo(() => {
+ const { getState, dispatch } = useAppStore();
+
+ const onUpload = useCallback(
+ (imageDTO: ImageDTO) => {
+ newCanvasFromImage({ imageDTO, getState, dispatch, ...NEW_CANVAS_OPTIONS });
+ },
+ [dispatch, getState]
+ );
+
+ return (
+ <>
+
+ Use a Layout Image
+ Add an image to control composition.
+
+
+
+
+ >
+ );
+});
+InitialStateUseALayoutImageCard.displayName = 'InitialStateUseALayoutImageCard';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
index 621bf95141..750066526e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
@@ -1,8 +1,12 @@
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
+import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea';
import { memo } from 'react';
-export const SimpleSession = memo(({ id }: { id: string }) => {
+export const SimpleSession = memo(({ id }: { id: string | null }) => {
+ if (id === null) {
+ return ;
+ }
return (
diff --git a/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentButton.tsx b/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentButton.tsx
new file mode 100644
index 0000000000..a3ffd7d519
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentButton.tsx
@@ -0,0 +1,32 @@
+import type { ButtonProps } from '@invoke-ai/ui-library';
+import { Button } from '@invoke-ai/ui-library';
+import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiXCircle } from 'react-icons/pi';
+
+type Props = ButtonProps;
+
+export const DeleteAllExceptCurrentButton = memo((props: Props) => {
+ const { t } = useTranslation();
+ const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog();
+
+ return (
+ <>
+ }
+ colorScheme="error"
+ data-testid={t('queue.clear')}
+ {...props}
+ >
+ {t('queue.clear')}
+
+ >
+ );
+});
+
+DeleteAllExceptCurrentButton.displayName = 'DeleteAllExceptCurrentButton';
diff --git a/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentIconButton.tsx
new file mode 100644
index 0000000000..6cf1b8f435
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentIconButton.tsx
@@ -0,0 +1,25 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiXCircle } from 'react-icons/pi';
+
+export const DeleteAllExceptCurrentIconButton = memo(() => {
+ const { t } = useTranslation();
+ const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog();
+
+ return (
+ }
+ colorScheme="error"
+ onClick={deleteAllExceptCurrent.openDialog}
+ />
+ );
+});
+
+DeleteAllExceptCurrentIconButton.displayName = 'DeleteAllExceptCurrentIconButton';
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
index 040ecc6de9..52fb354d87 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
@@ -1,5 +1,5 @@
import { Flex, Spacer, useShiftModifier } from '@invoke-ai/ui-library';
-import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
+import { DeleteAllExceptCurrentIconButton } from 'features/queue/components/DeleteAllExceptCurrentIconButton';
import { DeleteCurrentQueueItemIconButton } from 'features/queue/components/DeleteCurrentQueueItemIconButton';
import { QueueActionsMenuButton } from 'features/queue/components/QueueActionsMenuButton';
import ProgressBar from 'features/system/components/ProgressBar';
@@ -30,7 +30,7 @@ export const DeleteIconButton = memo(() => {
return ;
}
- return ;
+ return ;
});
DeleteIconButton.displayName = 'DeleteIconButton';
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
index 6432b4e913..303687007e 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
@@ -1,6 +1,5 @@
-/* eslint-disable i18next/no-literal-string */
import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
-import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
+import { DeleteAllExceptCurrentButton } from 'features/queue/components/DeleteAllExceptCurrentButton';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
@@ -24,7 +23,7 @@ const QueueTabQueueControls = () => {
)}
-
+
From 9f392c8c3ce1e5b68d6e63436a46025e8d2ed78a Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 6 Jun 2025 20:06:48 +1000
Subject: [PATCH 070/210] feat(ui): remove technical progress message from full
preview
---
.../components/SimpleSession/QueueItemPreviewFull.tsx | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
index 11a3904bd9..029ad29454 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
@@ -4,7 +4,6 @@ import { ImageActions } from 'features/controlLayers/components/SimpleSession/Im
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
-import { QueueItemProgressMessage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressMessage';
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
import { getQueueItemElementId, useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/shared';
import { DndImage } from 'features/dnd/DndImage';
@@ -41,8 +40,14 @@ export const QueueItemPreviewFull = memo(({ item, number }: Props) => {
{!imageLoaded && }
{imageDTO && }
-
-
+
);
});
From e81dde0933f9280cf0bc82259e3c0e1041eb7401 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 6 Jun 2025 20:09:23 +1000
Subject: [PATCH 071/210] feat(ui): fiddle w/ staging area header
---
.../components/SimpleSession/StagingAreaHeader.tsx | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
index 4e265a5e18..82bc9c9009 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
@@ -1,5 +1,5 @@
/* eslint-disable i18next/no-literal-string */
-import { Button, Flex, FormControl, FormLabel, Spacer, Switch, Text } from '@invoke-ai/ui-library';
+import { Button, Divider, Flex, FormControl, FormLabel, Spacer, Switch, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
@@ -24,16 +24,17 @@ export const StagingAreaHeader = memo(() => {
);
return (
-
+
- Generations
+ Staging Area
Auto-switch
-
+
+
Start Over
From c316f07fb215394060dfe8c89d63d1b291ef824b Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 6 Jun 2025 20:16:56 +1000
Subject: [PATCH 072/210] feat(ui): add startover button to canvas toolbar
---
.../SimpleSession/StagingAreaHeader.tsx | 18 +++++------------
.../components/StartOverButton.tsx | 20 +++++++++++++++++++
.../components/Toolbar/CanvasToolbar.tsx | 5 ++++-
3 files changed, 29 insertions(+), 14 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StartOverButton.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
index 82bc9c9009..bb50398eb9 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
@@ -1,20 +1,14 @@
/* eslint-disable i18next/no-literal-string */
-import { Button, Divider, Flex, FormControl, FormLabel, Spacer, Switch, Text } from '@invoke-ai/ui-library';
+import { Divider, Flex, FormControl, FormLabel, Spacer, Switch, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { StartOverButton } from 'features/controlLayers/components/StartOverButton';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
export const StagingAreaHeader = memo(() => {
const ctx = useCanvasSessionContext();
const autoSwitch = useStore(ctx.$autoSwitch);
- const dispatch = useAppDispatch();
-
- const startOver = useCallback(() => {
- dispatch(canvasSessionTypeChanged({ type: 'simple' }));
- }, [dispatch]);
const onChangeAutoSwitch = useCallback(
(e: ChangeEvent) => {
@@ -24,19 +18,17 @@ export const StagingAreaHeader = memo(() => {
);
return (
-
+
Staging Area
-
+
Auto-switch
-
- Start Over
-
+
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StartOverButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StartOverButton.tsx
new file mode 100644
index 0000000000..4205f772a3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StartOverButton.tsx
@@ -0,0 +1,20 @@
+/* eslint-disable i18next/no-literal-string */
+import { Button } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useCallback } from 'react';
+
+export const StartOverButton = memo(() => {
+ const dispatch = useAppDispatch();
+
+ const startOver = useCallback(() => {
+ dispatch(canvasSessionTypeChanged({ type: 'simple' }));
+ }, [dispatch]);
+
+ return (
+
+ Start Over
+
+ );
+});
+StartOverButton.displayName = 'StartOverButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
index c2c4b5451f..3b7b231875 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
@@ -1,6 +1,7 @@
/* eslint-disable i18next/no-literal-string */
import { Divider, Flex, Spacer } from '@invoke-ai/ui-library';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
+import { StartOverButton } from 'features/controlLayers/components/StartOverButton';
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
import { CanvasToolbarFitBboxToLayersButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton';
@@ -29,7 +30,7 @@ export const CanvasToolbar = memo(() => {
useCanvasFilterHotkey();
return (
-
+
@@ -46,6 +47,8 @@ export const CanvasToolbar = memo(() => {
+
+
);
});
From ac206f476778a19898bc2a48fc4137cfd8cdd70b Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 6 Jun 2025 20:35:14 +1000
Subject: [PATCH 073/210] feat(ui): make main panel styling and title
consistent
---
.../AdvancedSession/AdvancedSession.tsx | 3 +-
.../components/SimpleSession/InitialState.tsx | 58 ++++++++++---------
.../SimpleSession/StagingAreaHeader.tsx | 6 +-
.../components/Tool/ToolBrushWidth.tsx | 4 --
.../components/Tool/ToolEraserWidth.tsx | 4 --
.../components/Toolbar/CanvasToolbar.tsx | 9 +--
6 files changed, 41 insertions(+), 43 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
index 0155f52c06..88fc045919 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
@@ -1,6 +1,6 @@
/* eslint-disable i18next/no-literal-string */
import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { ContextMenu, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
+import { ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
@@ -77,6 +77,7 @@ export const AdvancedSession = memo(({ id }: { id: string | null }) => {
+
renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
index 31dc291cda..25bace52b6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -1,6 +1,6 @@
/* eslint-disable i18next/no-literal-string */
-import { Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
+import { Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
import { InitialStateCardGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateCardGridItem';
@@ -17,33 +17,39 @@ export const InitialState = memo(() => {
}, [dispatch]);
return (
-
- Choose a starting method.
-
- Drag an image onto a card or click the upload icon.
-
+
+
+ Get Started
+
+
+
+ Choose a starting method.
+
+ Drag an image onto a card or click the upload icon.
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- or{' '}
-
- start from a blank canvas.
-
-
+
+ or{' '}
+
+ start from a blank canvas.
+
+
+
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
index bb50398eb9..b048d7dd59 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
@@ -1,5 +1,5 @@
/* eslint-disable i18next/no-literal-string */
-import { Divider, Flex, FormControl, FormLabel, Spacer, Switch, Text } from '@invoke-ai/ui-library';
+import { Divider, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { StartOverButton } from 'features/controlLayers/components/StartOverButton';
@@ -19,9 +19,7 @@ export const StagingAreaHeader = memo(() => {
return (
-
- Staging Area
-
+ Review Session
Auto-switch
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx
index 6d14493d50..a1d827ca36 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx
@@ -1,7 +1,6 @@
import {
CompositeSlider,
FormControl,
- FormLabel,
IconButton,
NumberInput,
NumberInputField,
@@ -20,7 +19,6 @@ import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/us
import { clamp } from 'lodash-es';
import type { KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
import { PiCaretDownBold } from 'react-icons/pi';
const selectBrushWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.brushWidth);
@@ -67,7 +65,6 @@ const sliderDefaultValue = mapRawValueToSliderValue(50);
export const ToolBrushWidth = memo(() => {
const dispatch = useAppDispatch();
- const { t } = useTranslation();
const isSelected = useToolIsSelected('brush');
const width = useAppSelector(selectBrushWidth);
const [localValue, setLocalValue] = useState(width);
@@ -145,7 +142,6 @@ export const ToolBrushWidth = memo(() => {
return (
- {t('controlLayers.width')}
settings.eraserWidth);
@@ -70,7 +68,6 @@ const sliderDefaultValue = mapRawValueToSliderValue(50);
export const ToolEraserWidth = memo(() => {
const dispatch = useAppDispatch();
- const { t } = useTranslation();
const isSelected = useToolIsSelected('eraser');
const width = useAppSelector(selectEraserWidth);
const [localValue, setLocalValue] = useState(width);
@@ -148,7 +145,6 @@ export const ToolEraserWidth = memo(() => {
return (
- {t('controlLayers.width')}
{
useCanvasFilterHotkey();
return (
-
+
+ Canvas
+
-
-
+
From 6754fde935f93269de5d1f43802b7431f284c32d Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 6 Jun 2025 20:39:40 +1000
Subject: [PATCH 074/210] chore(ui): lint
---
.../controlLayers/components/Toolbar/CanvasToolbar.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
index c329eabe24..8939df125b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
@@ -31,7 +31,9 @@ export const CanvasToolbar = memo(() => {
return (
- Canvas
+
+ Canvas
+
From 2f26657c17dfc4241e4244a2d00dc4f86bdca1b2 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 10 Jun 2025 12:16:33 +1000
Subject: [PATCH 075/210] feat(ui): move canvas-specific staging subscriptions
to CanvasStagingAreaModule
---
.../SimpleSession/StagingAreaItemsList.tsx | 27 +---------
.../components/SimpleSession/context.tsx | 54 +++++++++++++++----
.../konva/CanvasStagingAreaModule.ts | 27 +++++++++-
3 files changed, 71 insertions(+), 37 deletions(-)
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 c195fff0b1..36c6d21ec8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
@@ -4,9 +4,7 @@ import { useStore } from '@nanostores/react';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { QueueItemPreviewMini } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewMini';
-import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { effect } from 'nanostores';
import { memo, useEffect } from 'react';
export const StagingAreaItemsList = memo(() => {
@@ -20,29 +18,8 @@ export const StagingAreaItemsList = memo(() => {
return;
}
- return effect([ctx.$selectedItem, ctx.$progressData], (selectedItem, progressData) => {
- if (!selectedItem) {
- canvasManager.stagingArea.$imageSrc.set(null);
- return;
- }
-
- const outputImageName = getOutputImageName(selectedItem);
-
- if (outputImageName) {
- canvasManager.stagingArea.$imageSrc.set({ type: 'imageName', data: outputImageName });
- return;
- }
-
- const data = progressData[selectedItem.item_id];
-
- if (data?.progressImage) {
- canvasManager.stagingArea.$imageSrc.set({ type: 'dataURL', data: data.progressImage.dataURL });
- return;
- }
-
- canvasManager.stagingArea.$imageSrc.set(null);
- });
- }, [canvasManager, ctx.$progressData, ctx.$selectedItem]);
+ return canvasManager.stagingArea.connectToSession(ctx.$selectedItemId, ctx.$progressData);
+ }, [canvasManager, ctx.$progressData, ctx.$selectedItemId]);
return (
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 daf843f947..172a8188ce 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -13,18 +13,26 @@ import type { S } from 'services/api/types';
import { $socket } from 'services/events/stores';
import { assert } from 'tsafe';
-type ProgressData = {
+export type ProgressData = {
itemId: number;
progressEvent: S['InvocationProgressEvent'] | null;
progressImage: ProgressImage | null;
+ outputImageName: string | null;
};
+const getInitialProgressData = (itemId: number): ProgressData => ({
+ itemId,
+ progressEvent: null,
+ progressImage: null,
+ outputImageName: null,
+});
+
export const useProgressData = (
$progressData: WritableAtom>,
itemId: number
): ProgressData => {
const [value, setValue] = useState(() => {
- return $progressData.get()[itemId] ?? { itemId, progressEvent: null, progressImage: null };
+ return $progressData.get()[itemId] ?? getInitialProgressData(itemId);
});
useEffect(() => {
const unsub = $progressData.subscribe((data) => {
@@ -62,6 +70,7 @@ const setProgress = ($progressData: WritableAtom>,
itemId: data.item_id,
progressEvent: data,
progressImage: data.image ?? null,
+ outputImageName: null,
},
});
}
@@ -315,18 +324,45 @@ export const CanvasSessionContextProvider = memo(
const progressData = $progressData.get();
const toDelete: number[] = [];
- const toClear: number[] = [];
+ const toUpdate: ProgressData[] = [];
for (const datum of Object.values(progressData)) {
const item = items.find(({ item_id }) => item_id === datum.itemId);
if (!item) {
toDelete.push(datum.itemId);
} else if (item.status === 'canceled' || item.status === 'failed') {
- toClear.push(datum.itemId);
+ toUpdate[datum.itemId] = {
+ ...datum,
+ progressEvent: null,
+ progressImage: null,
+ outputImageName: null,
+ };
}
}
- if (toDelete.length === 0) {
+ for (const item of items) {
+ const datum = progressData[item.item_id];
+
+ if (datum) {
+ if (datum.outputImageName) {
+ continue;
+ }
+ const outputImageName = getOutputImageName(item);
+ if (!outputImageName) {
+ continue;
+ }
+ toUpdate.push({
+ ...datum,
+ outputImageName,
+ });
+ } else {
+ const _datum = getInitialProgressData(item.item_id);
+ _datum.outputImageName = getOutputImageName(item);
+ toUpdate.push(_datum);
+ }
+ }
+
+ if (toDelete.length === 0 && toUpdate.length === 0) {
return;
}
@@ -336,12 +372,8 @@ export const CanvasSessionContextProvider = memo(
delete newProgressData[itemId];
}
- for (const itemId of toClear) {
- const current = newProgressData[itemId];
- if (current) {
- current.progressEvent = null;
- current.progressImage = null;
- }
+ for (const datum of toUpdate) {
+ newProgressData[datum.itemId] = datum;
}
$progressData.set(newProgressData);
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
index 52cbc54611..fa3ca84ec6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
@@ -1,4 +1,5 @@
import { Mutex } from 'async-mutex';
+import type { ProgressData } from 'features/controlLayers/components/SimpleSession/context';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
@@ -6,7 +7,8 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { CanvasImageState } from 'features/controlLayers/store/types';
import Konva from 'konva';
-import { atom } from 'nanostores';
+import type { Atom, WritableAtom } from 'nanostores';
+import { atom, effect } from 'nanostores';
import type { Logger } from 'roarr';
type ImageNameSrc = { type: 'imageName'; data: string };
@@ -133,6 +135,29 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
};
+ connectToSession = (
+ $selectedItemId: Atom,
+ $progressData: WritableAtom>
+ ) =>
+ effect([$selectedItemId, $progressData], (selectedItemId, progressData) => {
+ if (!selectedItemId) {
+ this.$imageSrc.set(null);
+ return;
+ }
+
+ const datum = progressData[selectedItemId];
+
+ if (datum?.outputImageName) {
+ this.$imageSrc.set({ type: 'imageName', data: datum.outputImageName });
+ return;
+ } else if (datum?.progressImage) {
+ this.$imageSrc.set({ type: 'dataURL', data: datum.progressImage.dataURL });
+ return;
+ } else {
+ this.$imageSrc.set(null);
+ }
+ });
+
private _getImageFromSrc = (
{ type, data }: ImageNameSrc | DataURLSrc,
width: number,
From 711fe91b24d89a7609f2359f4e112f306416e4e3 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 10 Jun 2025 12:42:07 +1000
Subject: [PATCH 076/210] feat(ui): add on first progress autoswitch mode
---
.../SimpleSession/QueueItemPreviewMini.tsx | 12 +--
.../SimpleSession/StagingAreaHeader.tsx | 22 +-----
.../components/SimpleSession/context.tsx | 23 ++++--
.../StagingArea/SimpleStagingAreaToolbar.tsx | 2 +
.../SimpleStagingAreaToolbarMenu.tsx | 17 +++++
.../StagingArea/StagingAreaToolbar.tsx | 4 +-
.../StagingArea/StagingAreaToolbarMenu.tsx | 20 +++++
.../StagingAreaToolbarMenuAutoSwitch.tsx | 34 +++++++++
...agingAreaToolbarMenuNewLayerFromImage.tsx} | 75 ++++++++-----------
9 files changed, 125 insertions(+), 84 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenu.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx
rename invokeai/frontend/web/src/features/controlLayers/components/StagingArea/{StagingAreaToolbarSaveAsMenu.tsx => StagingAreaToolbarMenuNewLayerFromImage.tsx} (68%)
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 3393d7a18e..5851178b0f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -42,10 +42,6 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
ctx.$selectedItemId.set(item.item_id);
}, [ctx.$selectedItemId, item.item_id]);
- const onDoubleClick = useCallback(() => {
- ctx.$autoSwitch.set(item.status === 'in_progress');
- }, [ctx.$autoSwitch, item.status]);
-
const onLoad = useCallback(() => {
setImageLoaded(true);
if (ctx.$progressData.get()[item.item_id]) {
@@ -54,13 +50,7 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
}, [ctx.$lastLoadedItemId, ctx.$progressData, item.item_id]);
return (
-
+
{imageDTO && }
{!imageLoaded && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
index b048d7dd59..0f4828c93f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
@@ -1,31 +1,13 @@
/* eslint-disable i18next/no-literal-string */
-import { Divider, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library';
-import { useStore } from '@nanostores/react';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { Flex, Heading, Spacer } from '@invoke-ai/ui-library';
import { StartOverButton } from 'features/controlLayers/components/StartOverButton';
-import type { ChangeEvent } from 'react';
-import { memo, useCallback } from 'react';
+import { memo } from 'react';
export const StagingAreaHeader = memo(() => {
- const ctx = useCanvasSessionContext();
- const autoSwitch = useStore(ctx.$autoSwitch);
-
- const onChangeAutoSwitch = useCallback(
- (e: ChangeEvent) => {
- ctx.$autoSwitch.set(e.target.checked);
- },
- [ctx.$autoSwitch]
- );
-
return (
Review Session
-
- Auto-switch
-
-
-
);
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 172a8188ce..3f7a608b7b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -12,6 +12,10 @@ import { queueApi } from 'services/api/endpoints/queue';
import type { S } from 'services/api/types';
import { $socket } from 'services/events/stores';
import { assert } from 'tsafe';
+import { z } from 'zod';
+
+export const zAutoSwitchMode = z.enum(['off', 'first_progress', 'completed']);
+export type AutoSwitchMode = z.infer;
export type ProgressData = {
itemId: number;
@@ -86,7 +90,7 @@ type CanvasSessionContextValue = {
$selectedItem: Atom;
$selectedItemIndex: Atom;
$selectedItemOutputImageName: Atom;
- $autoSwitch: WritableAtom;
+ $autoSwitch: WritableAtom;
$lastLoadedItemId: WritableAtom;
selectNext: () => void;
selectPrev: () => void;
@@ -121,7 +125,7 @@ export const CanvasSessionContextProvider = memo(
/**
* Whether auto-switch is enabled.
*/
- const $autoSwitch = useState(() => atom(true))[0];
+ const $autoSwitch = useState(() => atom('first_progress'))[0];
/**
* An internal flag used to work around race conditions with auto-switch switching to queue items before their
@@ -184,15 +188,15 @@ export const CanvasSessionContextProvider = memo(
* image recorded.
*/
const $selectedItemOutputImageName = useState(() =>
- computed([$selectedItem], (selectedItem) => {
- if (selectedItem === null) {
+ computed([$selectedItemId, $progressData], (selectedItemId, progressData) => {
+ if (selectedItemId === null) {
return null;
}
- const outputImageName = getOutputImageName(selectedItem);
- if (outputImageName === null) {
+ const datum = progressData[selectedItemId];
+ if (!datum) {
return null;
}
- return outputImageName;
+ return datum.outputImageName;
})
)[0];
@@ -269,6 +273,9 @@ export const CanvasSessionContextProvider = memo(
return;
}
setProgress($progressData, data);
+ if ($autoSwitch.get() === 'first_progress') {
+ $selectedItemId.set(data.item_id);
+ }
};
socket.on('invocation_progress', onProgress);
@@ -391,7 +398,7 @@ export const CanvasSessionContextProvider = memo(
if (lastLoadedItemId === null) {
return;
}
- if ($autoSwitch.get()) {
+ if ($autoSwitch.get() === 'completed') {
$selectedItemId.set(lastLoadedItemId);
}
$lastLoadedItemId.set(null);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx
index 26511981f3..3fa5e3e0ca 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx
@@ -1,4 +1,5 @@
import { ButtonGroup } from '@invoke-ai/ui-library';
+import { SimpleStagingAreaToolbarMenu } from 'features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu';
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
import { StagingAreaToolbarImageCountButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton';
@@ -16,6 +17,7 @@ export const SimpleStagingAreaToolbar = memo(() => {
+
>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu.tsx
new file mode 100644
index 0000000000..60b3a39203
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu.tsx
@@ -0,0 +1,17 @@
+import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
+import { StagingAreaToolbarMenuAutoSwitch } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch';
+import { memo } from 'react';
+import { PiDotsThreeBold } from 'react-icons/pi';
+
+export const SimpleStagingAreaToolbarMenu = memo(() => {
+ return (
+
+ } colorScheme="invokeBlue" />
+
+
+
+
+ );
+});
+
+SimpleStagingAreaToolbarMenu.displayName = 'SimpleStagingAreaToolbarMenu';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
index 63926e766d..6b57e5cf93 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
@@ -6,9 +6,9 @@ import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/component
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
import { StagingAreaToolbarImageCountButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton';
+import { StagingAreaToolbarMenu } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenu';
import { StagingAreaToolbarNextButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton';
import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton';
-import { StagingAreaToolbarSaveAsMenu } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu';
import { StagingAreaToolbarSaveSelectedToGalleryButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton';
import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
@@ -42,7 +42,7 @@ export const StagingAreaToolbar = memo(() => {
-
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenu.tsx
new file mode 100644
index 0000000000..73710521da
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenu.tsx
@@ -0,0 +1,20 @@
+import { IconButton, Menu, MenuButton, MenuDivider, MenuList } from '@invoke-ai/ui-library';
+import { StagingAreaToolbarMenuAutoSwitch } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch';
+import { StagingAreaToolbarNewLayerFromImageMenuItems } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage';
+import { memo } from 'react';
+import { PiDotsThreeBold } from 'react-icons/pi';
+
+export const StagingAreaToolbarMenu = memo(() => {
+ return (
+
+ } colorScheme="invokeBlue" />
+
+
+
+
+
+
+ );
+});
+
+StagingAreaToolbarMenu.displayName = 'StagingAreaToolbarMenu';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx
new file mode 100644
index 0000000000..567e6beb5e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx
@@ -0,0 +1,34 @@
+/* eslint-disable i18next/no-literal-string */
+import { MenuItemOption, MenuOptionGroup } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useCanvasSessionContext, zAutoSwitchMode } from 'features/controlLayers/components/SimpleSession/context';
+import { memo, useCallback } from 'react';
+
+export const StagingAreaToolbarMenuAutoSwitch = memo(() => {
+ const ctx = useCanvasSessionContext();
+ const autoSwitch = useStore(ctx.$autoSwitch);
+
+ const onChange = useCallback(
+ (val: string | string[]) => {
+ const newAutoSwitch = zAutoSwitchMode.parse(val);
+ ctx.$autoSwitch.set(newAutoSwitch);
+ },
+ [ctx.$autoSwitch]
+ );
+
+ return (
+
+
+ Off
+
+
+ First Progress
+
+
+ Completed
+
+
+ );
+});
+
+StagingAreaToolbarMenuAutoSwitch.displayName = 'StagingAreaToolbarMenuAutoSwitch';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx
similarity index 68%
rename from invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx
index 455eb97319..54ab1578db 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx
@@ -1,4 +1,4 @@
-import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
+import { MenuGroup, MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/nanostores/store';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
@@ -8,12 +8,11 @@ import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiDotsThreeBold } from 'react-icons/pi';
import { copyImage } from 'services/api/endpoints/images';
const uploadImageArg = { image_category: 'general', is_intermediate: true, silent: true } as const;
-export const StagingAreaToolbarSaveAsMenu = memo(() => {
+export const StagingAreaToolbarNewLayerFromImageMenuItems = memo(() => {
const canvasManager = useCanvasManager();
const { t } = useTranslation();
const ctx = useCanvasSessionContext();
@@ -97,47 +96,37 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
}, [imageName, store, toastSentToCanvas]);
return (
-
- }
- colorScheme="invokeBlue"
+
+ }
+ onClickCapture={onClickNewInpaintMaskFromImage}
isDisabled={!imageName || !shouldShowStagedImage}
- />
-
- }
- onClickCapture={onClickNewInpaintMaskFromImage}
- isDisabled={!imageName || !shouldShowStagedImage}
- >
- {t('controlLayers.inpaintMask')}
-
- }
- onClickCapture={onClickNewRegionalGuidanceFromImage}
- isDisabled={!imageName || !shouldShowStagedImage}
- >
- {t('controlLayers.regionalGuidance')}
-
- }
- onClickCapture={onClickNewControlLayerFromImage}
- isDisabled={!imageName || !shouldShowStagedImage}
- >
- {t('controlLayers.controlLayer')}
-
- }
- onClickCapture={onClickNewRasterLayerFromImage}
- isDisabled={!imageName || !shouldShowStagedImage}
- >
- {t('controlLayers.rasterLayer')}
-
-
-
+ >
+ {t('controlLayers.inpaintMask')}
+
+ }
+ onClickCapture={onClickNewRegionalGuidanceFromImage}
+ isDisabled={!imageName || !shouldShowStagedImage}
+ >
+ {t('controlLayers.regionalGuidance')}
+
+ }
+ onClickCapture={onClickNewControlLayerFromImage}
+ isDisabled={!imageName || !shouldShowStagedImage}
+ >
+ {t('controlLayers.controlLayer')}
+
+ }
+ onClickCapture={onClickNewRasterLayerFromImage}
+ isDisabled={!imageName || !shouldShowStagedImage}
+ >
+ {t('controlLayers.rasterLayer')}
+
+
);
});
-StagingAreaToolbarSaveAsMenu.displayName = 'StagingAreaToolbarSaveAsMenu';
+StagingAreaToolbarNewLayerFromImageMenuItems.displayName = 'StagingAreaToolbarNewLayerFromImageMenuItems';
From d640a9001b42bd1c65ddf93ac36683054c0e2a59 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 10 Jun 2025 17:59:08 +1000
Subject: [PATCH 077/210] fix(ui): switch only on first progress image
---
.../controlLayers/components/SimpleSession/context.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
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 3f7a608b7b..f89d76fe8d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -272,8 +272,9 @@ export const CanvasSessionContextProvider = memo(
if (data.destination !== session.id) {
return;
}
+ const isFirstProgressImage = !$progressData.get()[data.item_id]?.progressImage && !!data.image;
setProgress($progressData, data);
- if ($autoSwitch.get() === 'first_progress') {
+ if ($autoSwitch.get() === 'first_progress' && isFirstProgressImage) {
$selectedItemId.set(data.item_id);
}
};
From 94afc13813fe86680b8aff24d0fe310e0834ecd3 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 10 Jun 2025 18:15:26 +1000
Subject: [PATCH 078/210] feat(ui): close viewer on escape
---
.gitignore | 1 +
.../gallery/components/ImageViewer/ImageViewer.tsx | 10 ++++++++++
invokeai/version/invokeai_version.py | 2 +-
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 8c1ccbddde..fba699d5e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -180,6 +180,7 @@ cython_debug/
# Scratch folder
.scratch/
.vscode/
+.zed/
# source installer files
installer/*zip
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
index 521c0a2486..decb9a2c78 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
@@ -73,6 +73,16 @@ export const ImageViewerModal = memo(() => {
handler: imageViewer.close,
});
+ useHotkeys(
+ 'esc',
+ imageViewer.close,
+ {
+ preventDefault: true,
+ enabled: imageViewer.isOpen,
+ },
+ [imageViewer.isOpen]
+ );
+
return (
Date: Wed, 11 Jun 2025 11:39:44 +1000
Subject: [PATCH 079/210] feat(ui): add AppGetState type
---
invokeai/frontend/web/src/app/store/store.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index a65416924d..32f386fc44 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -210,3 +210,4 @@ export type RootState = ReturnType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AppThunkDispatch = ThunkDispatch;
export type AppDispatch = ReturnType['dispatch'];
+export type AppGetState = ReturnType['getState'];
From 01784fb3bf2da1cb9ff5082b382ef0cc1581ffb3 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 11 Jun 2025 11:44:31 +1000
Subject: [PATCH 080/210] feat(ui): store output image DTO in session context
instead of just the name
---
.../components/SimpleSession/context.tsx | 46 ++++++++++++-------
.../components/SimpleSession/shared.ts | 15 +++---
2 files changed, 39 insertions(+), 22 deletions(-)
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 f89d76fe8d..0f79457f0d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -8,8 +8,9 @@ import type { Atom, WritableAtom } from 'nanostores';
import { atom, computed, effect } from 'nanostores';
import type { PropsWithChildren } from 'react';
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { getImageDTOSafe } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
-import type { S } from 'services/api/types';
+import type { ImageDTO, S } from 'services/api/types';
import { $socket } from 'services/events/stores';
import { assert } from 'tsafe';
import { z } from 'zod';
@@ -21,14 +22,14 @@ export type ProgressData = {
itemId: number;
progressEvent: S['InvocationProgressEvent'] | null;
progressImage: ProgressImage | null;
- outputImageName: string | null;
+ imageDTO: ImageDTO | null;
};
const getInitialProgressData = (itemId: number): ProgressData => ({
itemId,
progressEvent: null,
progressImage: null,
- outputImageName: null,
+ imageDTO: null,
});
export const useProgressData = (
@@ -74,7 +75,7 @@ const setProgress = ($progressData: WritableAtom>,
itemId: data.item_id,
progressEvent: data,
progressImage: data.image ?? null,
- outputImageName: null,
+ imageDTO: null,
},
});
}
@@ -89,7 +90,7 @@ type CanvasSessionContextValue = {
$selectedItemId: WritableAtom;
$selectedItem: Atom;
$selectedItemIndex: Atom;
- $selectedItemOutputImageName: Atom;
+ $selectedItemOutputImageDTO: Atom;
$autoSwitch: WritableAtom;
$lastLoadedItemId: WritableAtom;
selectNext: () => void;
@@ -187,7 +188,7 @@ export const CanvasSessionContextProvider = memo(
* The currently selected queue item's output image name, or null if one is not selected or there is no output
* image recorded.
*/
- const $selectedItemOutputImageName = useState(() =>
+ const $selectedItemOutputImageDTO = useState(() =>
computed([$selectedItemId, $progressData], (selectedItemId, progressData) => {
if (selectedItemId === null) {
return null;
@@ -196,7 +197,7 @@ export const CanvasSessionContextProvider = memo(
if (!datum) {
return null;
}
- return datum.outputImageName;
+ return datum.imageDTO;
})
)[0];
@@ -328,7 +329,7 @@ export const CanvasSessionContextProvider = memo(
});
// Clean up the progress data when a queue item is discarded.
- const unsubCleanUpProgressData = $items.listen((items) => {
+ const unsubCleanUpProgressData = $items.listen(async (items) => {
const progressData = $progressData.get();
const toDelete: number[] = [];
@@ -343,7 +344,7 @@ export const CanvasSessionContextProvider = memo(
...datum,
progressEvent: null,
progressImage: null,
- outputImageName: null,
+ imageDTO: null,
};
}
}
@@ -352,21 +353,34 @@ export const CanvasSessionContextProvider = memo(
const datum = progressData[item.item_id];
if (datum) {
- if (datum.outputImageName) {
+ if (datum.imageDTO) {
continue;
}
const outputImageName = getOutputImageName(item);
if (!outputImageName) {
continue;
}
+ const imageDTO = await getImageDTOSafe(outputImageName);
+ if (!imageDTO) {
+ continue;
+ }
toUpdate.push({
...datum,
- outputImageName,
+ imageDTO,
});
} else {
- const _datum = getInitialProgressData(item.item_id);
- _datum.outputImageName = getOutputImageName(item);
- toUpdate.push(_datum);
+ const outputImageName = getOutputImageName(item);
+ if (!outputImageName) {
+ continue;
+ }
+ const imageDTO = await getImageDTOSafe(outputImageName);
+ if (!imageDTO) {
+ continue;
+ }
+ toUpdate.push({
+ ...getInitialProgressData(item.item_id),
+ imageDTO,
+ });
}
}
@@ -435,7 +449,7 @@ export const CanvasSessionContextProvider = memo(
$selectedItem,
$selectedItemIndex,
$lastLoadedItemId,
- $selectedItemOutputImageName,
+ $selectedItemOutputImageDTO,
$itemCount,
selectNext,
selectPrev,
@@ -452,7 +466,7 @@ export const CanvasSessionContextProvider = memo(
$selectedItemId,
$selectedItemIndex,
session,
- $selectedItemOutputImageName,
+ $selectedItemOutputImageDTO,
$itemCount,
selectNext,
selectPrev,
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
index d8b7ebc7b1..c0eee11713 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
@@ -1,9 +1,10 @@
-import { skipToken } from '@reduxjs/toolkit/query';
+import { useStore } from '@nanostores/react';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { isImageField } from 'features/nodes/types/common';
import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
import { round } from 'lodash-es';
-import { useMemo } from 'react';
-import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import { computed } from 'nanostores';
+import { useState } from 'react';
import type { S } from 'services/api/types';
import { objectEntries } from 'tsafe';
@@ -43,9 +44,11 @@ export const getOutputImageName = (item: S['SessionQueueItem']) => {
};
export const useOutputImageDTO = (item: S['SessionQueueItem']) => {
- const outputImageName = useMemo(() => getOutputImageName(item), [item]);
-
- const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
+ const ctx = useCanvasSessionContext();
+ const $imageDTO = useState(() =>
+ computed([ctx.$progressData], (progressData) => progressData[item.item_id]?.imageDTO ?? null)
+ )[0];
+ const imageDTO = useStore($imageDTO);
return imageDTO;
};
From ba082ccc2f9bc334e5fced359209f56aa8fd78f1 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 11 Jun 2025 11:45:19 +1000
Subject: [PATCH 081/210] fix(ui): wait until last queue item deleted before
flagging canvas session finished
---
.../StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
index b6fc9d934a..5554ce9744 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
@@ -16,15 +16,15 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i
const { t } = useTranslation();
- const discardSelected = useCallback(() => {
+ const discardSelected = useCallback(async () => {
if (selectedItemId === null) {
return;
}
+ await deleteQueueItem.trigger(selectedItemId);
const itemCount = ctx.$itemCount.get();
if (itemCount <= 1) {
dispatch(canvasSessionGenerationFinished());
}
- deleteQueueItem.trigger(selectedItemId);
}, [selectedItemId, ctx.$itemCount, deleteQueueItem, dispatch]);
return (
From 8c17bde4eaab8da1a1f256c7bc58b7fafa460c25 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 11 Jun 2025 12:19:43 +1000
Subject: [PATCH 082/210] fix(ui): use imageDTO in staging area
---
.../StagingAreaToolbarAcceptButton.tsx | 14 ++++----
...tagingAreaToolbarMenuNewLayerFromImage.tsx | 34 +++++++++----------
...AreaToolbarSaveSelectedToGalleryButton.tsx | 10 +++---
.../konva/CanvasStagingAreaModule.ts | 4 +--
4 files changed, 31 insertions(+), 31 deletions(-)
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 1272887abc..8718385567 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
@@ -23,17 +23,17 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
- const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName);
+ const selectedItemImageDTO = useStore(ctx.$selectedItemOutputImageDTO);
const deleteQueueItemsByDestination = useDeleteQueueItemsByDestination();
const { t } = useTranslation();
const acceptSelected = useCallback(() => {
- if (!selectedItemImageName) {
+ if (!selectedItemImageDTO) {
return;
}
const { x, y, width, height } = bboxRect;
- const imageObject = imageNameToImageObject(selectedItemImageName, { width, height });
+ const imageObject = imageNameToImageObject(selectedItemImageDTO.image_name, { width, height });
const overrides: Partial = {
position: { x, y },
objects: [imageObject],
@@ -43,7 +43,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
dispatch(canvasSessionGenerationFinished());
deleteQueueItemsByDestination.trigger(ctx.session.id);
}, [
- selectedItemImageName,
+ selectedItemImageDTO,
bboxRect,
dispatch,
selectedEntityIdentifier?.type,
@@ -56,9 +56,9 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
acceptSelected,
{
preventDefault: true,
- enabled: isCanvasFocused && shouldShowStagedImage && selectedItemImageName !== null,
+ enabled: isCanvasFocused && shouldShowStagedImage && selectedItemImageDTO !== null,
},
- [isCanvasFocused, shouldShowStagedImage, selectedItemImageName]
+ [isCanvasFocused, shouldShowStagedImage, selectedItemImageDTO]
);
return (
@@ -68,7 +68,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
icon={}
onClick={acceptSelected}
colorScheme="invokeBlue"
- isDisabled={!selectedItemImageName || !shouldShowStagedImage || deleteQueueItemsByDestination.isDisabled}
+ isDisabled={!selectedItemImageDTO || !shouldShowStagedImage || deleteQueueItemsByDestination.isDisabled}
isLoading={deleteQueueItemsByDestination.isLoading}
/>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx
index 54ab1578db..1c748139c5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx
@@ -16,7 +16,7 @@ export const StagingAreaToolbarNewLayerFromImageMenuItems = memo(() => {
const canvasManager = useCanvasManager();
const { t } = useTranslation();
const ctx = useCanvasSessionContext();
- const imageName = useStore(ctx.$selectedItemOutputImageName);
+ const selectedItemOutputImageDTO = useStore(ctx.$selectedItemOutputImageDTO);
const store = useAppStore();
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
@@ -29,11 +29,11 @@ export const StagingAreaToolbarNewLayerFromImageMenuItems = memo(() => {
}, [t]);
const onClickNewRasterLayerFromImage = useCallback(async () => {
- if (!imageName) {
+ if (!selectedItemOutputImageDTO) {
return;
}
const { dispatch, getState } = store;
- const imageDTO = await copyImage(imageName, uploadImageArg);
+ const imageDTO = await copyImage(selectedItemOutputImageDTO.image_name, uploadImageArg);
createNewCanvasEntityFromImage({
imageDTO,
type: 'raster_layer',
@@ -42,14 +42,14 @@ export const StagingAreaToolbarNewLayerFromImageMenuItems = memo(() => {
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
});
toastSentToCanvas();
- }, [imageName, store, toastSentToCanvas]);
+ }, [selectedItemOutputImageDTO, store, toastSentToCanvas]);
const onClickNewControlLayerFromImage = useCallback(async () => {
- if (!imageName) {
+ if (!selectedItemOutputImageDTO) {
return;
}
const { dispatch, getState } = store;
- const imageDTO = await copyImage(imageName, uploadImageArg);
+ const imageDTO = await copyImage(selectedItemOutputImageDTO.image_name, uploadImageArg);
createNewCanvasEntityFromImage({
imageDTO,
@@ -59,14 +59,14 @@ export const StagingAreaToolbarNewLayerFromImageMenuItems = memo(() => {
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
});
toastSentToCanvas();
- }, [imageName, store, toastSentToCanvas]);
+ }, [selectedItemOutputImageDTO, store, toastSentToCanvas]);
const onClickNewInpaintMaskFromImage = useCallback(async () => {
- if (!imageName) {
+ if (!selectedItemOutputImageDTO) {
return;
}
const { dispatch, getState } = store;
- const imageDTO = await copyImage(imageName, uploadImageArg);
+ const imageDTO = await copyImage(selectedItemOutputImageDTO.image_name, uploadImageArg);
createNewCanvasEntityFromImage({
imageDTO,
@@ -76,14 +76,14 @@ export const StagingAreaToolbarNewLayerFromImageMenuItems = memo(() => {
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
});
toastSentToCanvas();
- }, [imageName, store, toastSentToCanvas]);
+ }, [selectedItemOutputImageDTO, store, toastSentToCanvas]);
const onClickNewRegionalGuidanceFromImage = useCallback(async () => {
- if (!imageName) {
+ if (!selectedItemOutputImageDTO) {
return;
}
const { dispatch, getState } = store;
- const imageDTO = await copyImage(imageName, uploadImageArg);
+ const imageDTO = await copyImage(selectedItemOutputImageDTO.image_name, uploadImageArg);
createNewCanvasEntityFromImage({
imageDTO,
@@ -93,35 +93,35 @@ export const StagingAreaToolbarNewLayerFromImageMenuItems = memo(() => {
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
});
toastSentToCanvas();
- }, [imageName, store, toastSentToCanvas]);
+ }, [selectedItemOutputImageDTO, store, toastSentToCanvas]);
return (
}
onClickCapture={onClickNewInpaintMaskFromImage}
- isDisabled={!imageName || !shouldShowStagedImage}
+ isDisabled={!selectedItemOutputImageDTO || !shouldShowStagedImage}
>
{t('controlLayers.inpaintMask')}
}
onClickCapture={onClickNewRegionalGuidanceFromImage}
- isDisabled={!imageName || !shouldShowStagedImage}
+ isDisabled={!selectedItemOutputImageDTO || !shouldShowStagedImage}
>
{t('controlLayers.regionalGuidance')}
}
onClickCapture={onClickNewControlLayerFromImage}
- isDisabled={!imageName || !shouldShowStagedImage}
+ isDisabled={!selectedItemOutputImageDTO || !shouldShowStagedImage}
>
{t('controlLayers.controlLayer')}
}
onClickCapture={onClickNewRasterLayerFromImage}
- isDisabled={!imageName || !shouldShowStagedImage}
+ isDisabled={!selectedItemOutputImageDTO || !shouldShowStagedImage}
>
{t('controlLayers.rasterLayer')}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
index 7f7f6bea37..82476c1e4d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
@@ -17,13 +17,13 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
const canvasManager = useCanvasManager();
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const ctx = useCanvasSessionContext();
- const imageName = useStore(ctx.$selectedItemOutputImageName);
+ const selectedItemOutputImageDTO = useStore(ctx.$selectedItemOutputImageDTO);
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const { t } = useTranslation();
const saveSelectedImageToGallery = useCallback(async () => {
- if (!imageName) {
+ if (!selectedItemOutputImageDTO) {
return;
}
@@ -31,7 +31,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
// the gallery without borking the canvas, which may need this image to exist.
const result = await withResultAsync(async () => {
// Create a new file with the same name, which we will upload
- await copyImage(imageName, {
+ await copyImage(selectedItemOutputImageDTO.image_name, {
// Image should show up in the Images tab
image_category: 'general',
is_intermediate: false,
@@ -55,7 +55,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
status: 'error',
});
}
- }, [autoAddBoardId, imageName, t]);
+ }, [autoAddBoardId, selectedItemOutputImageDTO, t]);
return (
{
icon={}
onClick={saveSelectedImageToGallery}
colorScheme="invokeBlue"
- isDisabled={!imageName || !shouldShowStagedImage}
+ isDisabled={!selectedItemOutputImageDTO || !shouldShowStagedImage}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
index fa3ca84ec6..8262c70886 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
@@ -147,8 +147,8 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
const datum = progressData[selectedItemId];
- if (datum?.outputImageName) {
- this.$imageSrc.set({ type: 'imageName', data: datum.outputImageName });
+ if (datum?.imageDTO) {
+ this.$imageSrc.set({ type: 'imageName', data: datum.imageDTO.image_name });
return;
} else if (datum?.progressImage) {
this.$imageSrc.set({ type: 'dataURL', data: datum.progressImage.dataURL });
From c7ed351bab1dd74b9d3a781918bf9b72adf51fc8 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 11 Jun 2025 12:29:38 +1000
Subject: [PATCH 083/210] refactor(ui): async modal pattern; use for deleting
images
This was needed for a canvas flow change which is currently paused, but the new API is much much nicer to use, so I am keeping it.
---
.../app/components/GlobalModalIsolator.tsx | 2 +-
.../middleware/listenerMiddleware/index.ts | 7 +-
.../listeners/boardAndImagesDeleted.ts | 2 +-
.../ensureImageIsSelectedListener.ts | 16 +
.../listeners/imageDeletionListeners.ts | 221 -------------
.../listeners/imageToDeleteSelected.ts | 32 --
invokeai/frontend/web/src/app/store/store.ts | 2 -
.../components/DeleteImageModal.tsx | 97 ++----
.../deleteImageModal/store/actions.ts | 9 -
.../deleteImageModal/store/initialState.ts | 6 -
.../deleteImageModal/store/selectors.ts | 85 -----
.../features/deleteImageModal/store/slice.ts | 27 --
.../features/deleteImageModal/store/state.ts | 298 ++++++++++++++++++
.../components/Boards/DeleteBoardModal.tsx | 2 +-
.../ImageContextMenu/ImageMenuItemDelete.tsx | 15 +-
.../MultipleSelectionMenuItems.tsx | 7 +-
.../GalleryImageDeleteIconButton.tsx | 10 +-
.../gallery/hooks/useGalleryHotkeys.ts | 8 +-
.../features/gallery/hooks/useImageActions.ts | 7 +-
.../web/src/services/api/endpoints/images.ts | 21 +-
20 files changed, 376 insertions(+), 498 deletions(-)
create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener.ts
delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts
delete mode 100644 invokeai/frontend/web/src/features/deleteImageModal/store/actions.ts
delete mode 100644 invokeai/frontend/web/src/features/deleteImageModal/store/initialState.ts
delete mode 100644 invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
delete mode 100644 invokeai/frontend/web/src/features/deleteImageModal/store/slice.ts
create mode 100644 invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
index 782b338fe4..b7bacf3d2c 100644
--- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
@@ -6,7 +6,7 @@ import {
NewGallerySessionDialog,
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
+import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
index 5db4acb243..aaf4c52c7a 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -9,15 +9,14 @@ import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/l
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
+import { addEnsureImageIsSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener';
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
import { addGalleryOffsetChangedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged';
import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema';
import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard';
-import { addImageDeletionListeners } from 'app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners';
import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard';
import { addImagesStarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesStarred';
import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred';
-import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected';
import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded';
import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected';
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
@@ -46,9 +45,7 @@ export const addAppListener = addListener.withTypes();
addImageUploadedFulfilledListener(startAppListening);
// Image deleted
-addImageDeletionListeners(startAppListening);
addDeleteBoardAndImagesFulfilledListener(startAppListening);
-addImageToDeleteSelectedListener(startAppListening);
// Image starred
addImagesStarredListener(startAppListening);
@@ -91,3 +88,5 @@ addAppConfigReceivedListener(startAppListening);
addAdHocPostProcessingRequestedListener(startAppListening);
addSetDefaultSettingsListener(startAppListening);
+
+addEnsureImageIsSelectedListener(startAppListening);
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
index 4a0c79c72e..3ec4d7698e 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
@@ -1,6 +1,6 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
-import { getImageUsage } from 'features/deleteImageModal/store/selectors';
+import { getImageUsage } from 'features/deleteImageModal/store/state';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener.ts
new file mode 100644
index 0000000000..f07fe68c1b
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener.ts
@@ -0,0 +1,16 @@
+import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
+import { imageSelected } from 'features/gallery/store/gallerySlice';
+import { imagesApi } from 'services/api/endpoints/images';
+
+export const addEnsureImageIsSelectedListener = (startAppListening: AppStartListening) => {
+ // When we list images, if no images is selected, select the first one.
+ startAppListening({
+ matcher: imagesApi.endpoints.listImages.matchFulfilled,
+ effect: (action, { dispatch, getState }) => {
+ const selection = getState().gallery.selection;
+ if (selection.length === 0) {
+ dispatch(imageSelected(action.payload.items[0] ?? null));
+ }
+ },
+ });
+};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
deleted file mode 100644
index 0f286d6d1d..0000000000
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-import { logger } from 'app/logging/logger';
-import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
-import type { AppDispatch, RootState } from 'app/store/store';
-import { entityDeleted, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
-import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
-import { getEntityIdentifier } from 'features/controlLayers/store/types';
-import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
-import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
-import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
-import { imageSelected } from 'features/gallery/store/gallerySlice';
-import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
-import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field';
-import { isInvocationNode } from 'features/nodes/types/invocation';
-import { forEach, intersectionBy } from 'lodash-es';
-import { imagesApi } from 'services/api/endpoints/images';
-import type { ImageDTO } from 'services/api/types';
-import type { Param0 } from 'tsafe';
-
-const log = logger('gallery');
-
-//TODO(psyche): handle image deletion (canvas staging area?)
-
-// Some utils to delete images from different parts of the app
-const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
- const actions: Param0[] = [];
- state.nodes.present.nodes.forEach((node) => {
- if (!isInvocationNode(node)) {
- return;
- }
-
- forEach(node.data.inputs, (input) => {
- if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
- actions.push(
- fieldImageValueChanged({
- nodeId: node.data.id,
- fieldName: input.name,
- value: undefined,
- })
- );
- return;
- }
- if (isImageFieldCollectionInputInstance(input)) {
- actions.push(
- fieldImageCollectionValueChanged({
- nodeId: node.data.id,
- fieldName: input.name,
- value: input.value?.filter((value) => value?.image_name !== imageDTO.image_name),
- })
- );
- }
- });
- });
-
- actions.forEach(dispatch);
-};
-
-const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
- selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => {
- let shouldDelete = false;
- for (const obj of objects) {
- if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
- shouldDelete = true;
- break;
- }
- }
- if (shouldDelete) {
- dispatch(entityDeleted({ entityIdentifier: { id, type: 'control_layer' } }));
- }
- });
-};
-
-const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
- selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
- if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
- dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
- }
- });
-};
-
-const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
- selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => {
- let shouldDelete = false;
- for (const obj of objects) {
- if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
- shouldDelete = true;
- break;
- }
- }
- if (shouldDelete) {
- dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } }));
- }
- });
-};
-
-export const addImageDeletionListeners = (startAppListening: AppStartListening) => {
- // Handle single image deletion
- startAppListening({
- actionCreator: imageDeletionConfirmed,
- effect: async (action, { dispatch, getState }) => {
- const { imageDTOs, imagesUsage } = action.payload;
-
- if (imageDTOs.length !== 1 || imagesUsage.length !== 1) {
- // handle multiples in separate listener
- return;
- }
-
- const imageDTO = imageDTOs[0];
- const imageUsage = imagesUsage[0];
-
- if (!imageDTO || !imageUsage) {
- // satisfy noUncheckedIndexedAccess
- return;
- }
-
- try {
- const state = getState();
- await dispatch(imagesApi.endpoints.deleteImage.initiate(imageDTO)).unwrap();
-
- if (state.gallery.selection.some((i) => i.image_name === imageDTO.image_name)) {
- // The deleted image was a selected image, we need to select the next image
- const newSelection = state.gallery.selection.filter((i) => i.image_name !== imageDTO.image_name);
-
- if (newSelection.length > 0) {
- return;
- }
-
- // Get the current list of images and select the same index
- const baseQueryArgs = selectListImagesQueryArgs(state);
- const data = imagesApi.endpoints.listImages.select(baseQueryArgs)(state).data;
-
- if (data) {
- const deletedImageIndex = data.items.findIndex((i) => i.image_name === imageDTO.image_name);
- const nextImage = data.items[deletedImageIndex + 1] ?? data.items[0] ?? null;
- if (nextImage?.image_name === imageDTO.image_name) {
- // If the next image is the same as the deleted one, it means it was the last image, reset selection
- dispatch(imageSelected(null));
- } else {
- dispatch(imageSelected(nextImage));
- }
- }
- }
-
- deleteNodesImages(state, dispatch, imageDTO);
- deleteReferenceImages(state, dispatch, imageDTO);
- deleteRasterLayerImages(state, dispatch, imageDTO);
- deleteControlLayerImages(state, dispatch, imageDTO);
- } catch {
- // no-op
- } finally {
- dispatch(isModalOpenChanged(false));
- }
- },
- });
-
- // Handle multiple image deletion
- startAppListening({
- actionCreator: imageDeletionConfirmed,
- effect: async (action, { dispatch, getState }) => {
- const { imageDTOs, imagesUsage } = action.payload;
-
- if (imageDTOs.length <= 1 || imagesUsage.length <= 1) {
- // handle singles in separate listener
- return;
- }
-
- try {
- const state = getState();
- await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap();
-
- if (intersectionBy(state.gallery.selection, imageDTOs, 'image_name').length > 0) {
- // Some selected images were deleted, need to select the next image
- const queryArgs = selectListImagesQueryArgs(state);
- const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
- if (data) {
- // When we delete multiple images, we clear the selection. Then, the the next time we load images, we will
- // select the first one. This is handled below in the listener for `imagesApi.endpoints.listImages.matchFulfilled`.
- dispatch(imageSelected(null));
- }
- }
-
- // We need to reset the features where the image is in use - none of these work if their image(s) don't exist
-
- imageDTOs.forEach((imageDTO) => {
- deleteNodesImages(state, dispatch, imageDTO);
- deleteControlLayerImages(state, dispatch, imageDTO);
- deleteReferenceImages(state, dispatch, imageDTO);
- deleteRasterLayerImages(state, dispatch, imageDTO);
- });
- } catch {
- // no-op
- } finally {
- dispatch(isModalOpenChanged(false));
- }
- },
- });
-
- // When we list images, if no images is selected, select the first one.
- startAppListening({
- matcher: imagesApi.endpoints.listImages.matchFulfilled,
- effect: (action, { dispatch, getState }) => {
- const selection = getState().gallery.selection;
- if (selection.length === 0) {
- dispatch(imageSelected(action.payload.items[0] ?? null));
- }
- },
- });
-
- startAppListening({
- matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
- effect: (action) => {
- log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Image deleted');
- },
- });
-
- startAppListening({
- matcher: imagesApi.endpoints.deleteImage.matchRejected,
- effect: (action) => {
- log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Unable to delete image');
- },
- });
-};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts
deleted file mode 100644
index c8f80ef451..0000000000
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
-import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
-import { selectImageUsage } from 'features/deleteImageModal/store/selectors';
-import { imagesToDeleteSelected, isModalOpenChanged } from 'features/deleteImageModal/store/slice';
-
-export const addImageToDeleteSelectedListener = (startAppListening: AppStartListening) => {
- startAppListening({
- actionCreator: imagesToDeleteSelected,
- effect: (action, { dispatch, getState }) => {
- const imageDTOs = action.payload;
- const state = getState();
- const { shouldConfirmOnDelete } = state.system;
- const imagesUsage = selectImageUsage(getState());
-
- const isImageInUse =
- imagesUsage.some((i) => i.isRasterLayerImage) ||
- imagesUsage.some((i) => i.isControlLayerImage) ||
- imagesUsage.some((i) => i.isReferenceImage) ||
- imagesUsage.some((i) => i.isInpaintMaskImage) ||
- imagesUsage.some((i) => i.isUpscaleImage) ||
- imagesUsage.some((i) => i.isNodesImage) ||
- imagesUsage.some((i) => i.isRegionalGuidanceImage);
-
- if (shouldConfirmOnDelete || isImageInUse) {
- dispatch(isModalOpenChanged(true));
- return;
- }
-
- dispatch(imageDeletionConfirmed({ imageDTOs, imagesUsage }));
- },
- });
-};
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index 32f386fc44..8853d157ea 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -13,7 +13,6 @@ import {
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
-import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
@@ -54,7 +53,6 @@ const allReducers = {
[configSlice.name]: configSlice.reducer,
[uiSlice.name]: uiSlice.reducer,
[dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer,
- [deleteImageModalSlice.name]: deleteImageModalSlice.reducer,
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
[modelManagerV2Slice.name]: modelManagerV2Slice.reducer,
[queueSlice.name]: queueSlice.reducer,
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx
index 4fc9d6fa96..1e1213b1f2 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx
+++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx
@@ -1,98 +1,38 @@
import { ConfirmationAlertDialog, Divider, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library';
-import { createSelector } from '@reduxjs/toolkit';
-import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
-import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
-import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
-import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
-import {
- imageDeletionCanceled,
- isModalOpenChanged,
- selectDeleteImageModalSlice,
-} from 'features/deleteImageModal/store/slice';
-import type { ImageUsage } from 'features/deleteImageModal/store/types';
-import { selectNodesSlice } from 'features/nodes/store/selectors';
-import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
-import { selectSystemSlice, setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
-import { some } from 'lodash-es';
+import { useAppStore } from 'app/store/nanostores/store';
+import { useAppSelector } from 'app/store/storeHooks';
+import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
+import { useDeleteImageModalApi, useDeleteImageModalState } from 'features/deleteImageModal/store/state';
+import { selectSystemShouldConfirmOnDelete, setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import ImageUsageMessage from './ImageUsageMessage';
-
-const selectImageUsages = createMemoizedSelector(
- [selectDeleteImageModalSlice, selectNodesSlice, selectCanvasSlice, selectImageUsage, selectUpscaleSlice],
- (deleteImageModal, nodes, canvas, imagesUsage, upscale) => {
- const { imagesToDelete } = deleteImageModal;
-
- const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) =>
- getImageUsage(nodes, canvas, upscale, image_name)
- );
-
- const imageUsageSummary: ImageUsage = {
- isUpscaleImage: some(allImageUsage, (i) => i.isUpscaleImage),
- isRasterLayerImage: some(allImageUsage, (i) => i.isRasterLayerImage),
- isInpaintMaskImage: some(allImageUsage, (i) => i.isInpaintMaskImage),
- isRegionalGuidanceImage: some(allImageUsage, (i) => i.isRegionalGuidanceImage),
- isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
- isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
- isReferenceImage: some(allImageUsage, (i) => i.isReferenceImage),
- };
-
- return {
- imagesToDelete,
- imagesUsage,
- imageUsageSummary,
- };
- }
-);
-
-const selectShouldConfirmOnDelete = createSelector(selectSystemSlice, (system) => system.shouldConfirmOnDelete);
-const selectIsModalOpen = createSelector(
- selectDeleteImageModalSlice,
- (deleteImageModal) => deleteImageModal.isModalOpen
-);
-
-const DeleteImageModal = () => {
- useAssertSingleton('DeleteImageModal');
- const dispatch = useAppDispatch();
+export const DeleteImageModal = memo(() => {
+ const state = useDeleteImageModalState();
+ const api = useDeleteImageModalApi();
+ const { dispatch } = useAppStore();
const { t } = useTranslation();
- const shouldConfirmOnDelete = useAppSelector(selectShouldConfirmOnDelete);
- const isModalOpen = useAppSelector(selectIsModalOpen);
- const { imagesToDelete, imagesUsage, imageUsageSummary } = useAppSelector(selectImageUsages);
+ const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete);
const handleChangeShouldConfirmOnDelete = useCallback(
(e: ChangeEvent) => dispatch(setShouldConfirmOnDelete(!e.target.checked)),
[dispatch]
);
- const handleClose = useCallback(() => {
- dispatch(imageDeletionCanceled());
- dispatch(isModalOpenChanged(false));
- }, [dispatch]);
-
- const handleDelete = useCallback(() => {
- if (!imagesToDelete.length || !imagesUsage.length) {
- return;
- }
- dispatch(imageDeletionCanceled());
- dispatch(imageDeletionConfirmed({ imageDTOs: imagesToDelete, imagesUsage }));
- }, [dispatch, imagesToDelete, imagesUsage]);
-
return (
-
+
{t('gallery.deleteImagePermanent')}
{t('common.areYouSure')}
@@ -103,6 +43,5 @@ const DeleteImageModal = () => {
);
-};
-
-export default memo(DeleteImageModal);
+});
+DeleteImageModal.displayName = 'DeleteImageModal';
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/actions.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/actions.ts
deleted file mode 100644
index 7ec2a79bd1..0000000000
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/actions.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createAction } from '@reduxjs/toolkit';
-import type { ImageDTO } from 'services/api/types';
-
-import type { ImageUsage } from './types';
-
-export const imageDeletionConfirmed = createAction<{
- imageDTOs: ImageDTO[];
- imagesUsage: ImageUsage[];
-}>('deleteImageModal/imageDeletionConfirmed');
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/initialState.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/initialState.ts
deleted file mode 100644
index 112ad6858d..0000000000
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/initialState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { DeleteImageState } from './types';
-
-export const initialDeleteImageState: DeleteImageState = {
- imagesToDelete: [],
- isModalOpen: false,
-};
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
deleted file mode 100644
index 9782ea5047..0000000000
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
-import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
-import type { CanvasState } from 'features/controlLayers/store/types';
-import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
-import { selectNodesSlice } from 'features/nodes/store/selectors';
-import type { NodesState } from 'features/nodes/store/types';
-import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field';
-import { isInvocationNode } from 'features/nodes/types/invocation';
-import type { UpscaleState } from 'features/parameters/store/upscaleSlice';
-import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
-import { some } from 'lodash-es';
-
-import type { ImageUsage } from './types';
-// TODO(psyche): handle image deletion (canvas staging area?)
-export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: UpscaleState, image_name: string) => {
- const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) =>
- some(node.data.inputs, (input) => {
- if (isImageFieldInputInstance(input)) {
- if (input.value?.image_name === image_name) {
- return true;
- }
- }
-
- if (isImageFieldCollectionInputInstance(input)) {
- if (input.value?.some((value) => value?.image_name === image_name)) {
- return true;
- }
- }
-
- return false;
- })
- );
-
- const isUpscaleImage = upscale.upscaleInitialImage?.image_name === image_name;
-
- const isReferenceImage = canvas.referenceImages.entities.some(
- ({ ipAdapter }) => ipAdapter.image?.image_name === image_name
- );
-
- const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
- objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
- );
-
- const isControlLayerImage = canvas.controlLayers.entities.some(({ objects }) =>
- objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
- );
-
- const isInpaintMaskImage = canvas.inpaintMasks.entities.some(({ objects }) =>
- objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
- );
-
- const isRegionalGuidanceImage = canvas.regionalGuidance.entities.some(({ referenceImages }) =>
- referenceImages.some(({ ipAdapter }) => ipAdapter.image?.image_name === image_name)
- );
-
- const imageUsage: ImageUsage = {
- isUpscaleImage,
- isRasterLayerImage,
- isInpaintMaskImage,
- isRegionalGuidanceImage,
- isNodesImage,
- isControlLayerImage,
- isReferenceImage,
- };
-
- return imageUsage;
-};
-
-export const selectImageUsage = createMemoizedSelector(
- selectDeleteImageModalSlice,
- selectNodesSlice,
- selectCanvasSlice,
- selectUpscaleSlice,
- (deleteImageModal, nodes, canvas, upscale) => {
- const { imagesToDelete } = deleteImageModal;
-
- if (!imagesToDelete.length) {
- return [];
- }
-
- const imagesUsage = imagesToDelete.map((i) => getImageUsage(nodes, canvas, upscale, i.image_name));
-
- return imagesUsage;
- }
-);
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/slice.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/slice.ts
deleted file mode 100644
index 9efb7c395f..0000000000
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/slice.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { PayloadAction } from '@reduxjs/toolkit';
-import { createSlice } from '@reduxjs/toolkit';
-import type { RootState } from 'app/store/store';
-import type { ImageDTO } from 'services/api/types';
-
-import { initialDeleteImageState } from './initialState';
-
-export const deleteImageModalSlice = createSlice({
- name: 'deleteImageModal',
- initialState: initialDeleteImageState,
- reducers: {
- isModalOpenChanged: (state, action: PayloadAction) => {
- state.isModalOpen = action.payload;
- },
- imagesToDeleteSelected: (state, action: PayloadAction) => {
- state.imagesToDelete = action.payload;
- },
- imageDeletionCanceled: (state) => {
- state.imagesToDelete = [];
- state.isModalOpen = false;
- },
- },
-});
-
-export const { isModalOpenChanged, imagesToDeleteSelected, imageDeletionCanceled } = deleteImageModalSlice.actions;
-
-export const selectDeleteImageModalSlice = (state: RootState) => state.deleteImageModal;
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
new file mode 100644
index 0000000000..1d88a77edc
--- /dev/null
+++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
@@ -0,0 +1,298 @@
+import { useStore } from '@nanostores/react';
+import { getStore, useAppStore } from 'app/store/nanostores/store';
+import type { AppDispatch, AppGetState, RootState } from 'app/store/store';
+import { entityDeleted, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
+import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
+import { type CanvasState, getEntityIdentifier } from 'features/controlLayers/store/types';
+import type { ImageUsage } from 'features/deleteImageModal/store/types';
+import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
+import { imageSelected } from 'features/gallery/store/gallerySlice';
+import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
+import { selectNodesSlice } from 'features/nodes/store/selectors';
+import type { NodesState } from 'features/nodes/store/types';
+import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field';
+import { isInvocationNode } from 'features/nodes/types/invocation';
+import { selectUpscaleSlice, type UpscaleState } from 'features/parameters/store/upscaleSlice';
+import { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice';
+import { forEach, intersectionBy, some } from 'lodash-es';
+import { atom } from 'nanostores';
+import { useMemo } from 'react';
+import { imagesApi } from 'services/api/endpoints/images';
+import type { ImageDTO } from 'services/api/types';
+import type { Param0 } from 'tsafe';
+
+// Implements an awaitable modal dialog for deleting images
+
+type DeleteImagesModalState = {
+ imageDTOs: ImageDTO[];
+ usagePerImage: ImageUsage[];
+ usageSummary: ImageUsage;
+ isOpen: boolean;
+ resolve?: () => void;
+ reject?: (reason?: string) => void;
+};
+
+const getInitialState = (): DeleteImagesModalState => ({
+ imageDTOs: [],
+ usagePerImage: [],
+ usageSummary: {
+ isControlLayerImage: false,
+ isInpaintMaskImage: false,
+ isNodesImage: false,
+ isRasterLayerImage: false,
+ isRegionalGuidanceImage: false,
+ isReferenceImage: false,
+ isUpscaleImage: false,
+ },
+ isOpen: false,
+});
+
+const $deleteModalState = atom(getInitialState());
+
+const deleteImagesWithDialog = async (imageDTOs: ImageDTO[]): Promise => {
+ const { getState, dispatch } = getStore();
+ const imageUsage = getImageUsageFromImageDTOs(imageDTOs, getState());
+ const shouldConfirmOnDelete = selectSystemShouldConfirmOnDelete(getState());
+
+ if (!shouldConfirmOnDelete && !isAnyImageInUse(imageUsage)) {
+ // If we don't need to confirm and the images are not in use, delete them directly
+ await handleDeletions(imageDTOs, dispatch, getState);
+ }
+
+ return new Promise((resolve, reject) => {
+ $deleteModalState.set({
+ usagePerImage: imageUsage,
+ usageSummary: getImageUsageSummary(imageUsage),
+ imageDTOs,
+ isOpen: true,
+ resolve,
+ reject,
+ });
+ });
+};
+
+const handleDeletions = async (imageDTOs: ImageDTO[], dispatch: AppDispatch, getState: AppGetState) => {
+ try {
+ const state = getState();
+ await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap();
+
+ if (intersectionBy(state.gallery.selection, imageDTOs, 'image_name').length > 0) {
+ // Some selected images were deleted, need to select the next image
+ const queryArgs = selectListImagesQueryArgs(state);
+ const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
+ if (data) {
+ // When we delete multiple images, we clear the selection. Then, the the next time we load images, we will
+ // select the first one. This is handled below in the listener for `imagesApi.endpoints.listImages.matchFulfilled`.
+ dispatch(imageSelected(null));
+ }
+ }
+
+ // We need to reset the features where the image is in use - none of these work if their image(s) don't exist
+ for (const imageDTO of imageDTOs) {
+ deleteNodesImages(state, dispatch, imageDTO);
+ deleteControlLayerImages(state, dispatch, imageDTO);
+ deleteReferenceImages(state, dispatch, imageDTO);
+ deleteRasterLayerImages(state, dispatch, imageDTO);
+ }
+ } catch {
+ // no-op
+ }
+};
+
+const confirmDeletion = async (dispatch: AppDispatch, getState: AppGetState) => {
+ const state = $deleteModalState.get();
+ await handleDeletions(state.imageDTOs, dispatch, getState);
+ state.resolve?.();
+ closeSilently();
+};
+
+const cancelDeletion = () => {
+ const state = $deleteModalState.get();
+ state.reject?.('User canceled');
+ closeSilently();
+};
+
+const closeSilently = () => {
+ $deleteModalState.set(getInitialState());
+};
+
+export const useDeleteImageModalState = () => {
+ const state = useStore($deleteModalState);
+ return state;
+};
+
+export const useDeleteImageModalApi = () => {
+ const { dispatch, getState } = useAppStore();
+ const api = useMemo(
+ () => ({
+ delete: deleteImagesWithDialog,
+ confirm: () => confirmDeletion(dispatch, getState),
+ cancel: cancelDeletion,
+ close: closeSilently,
+ getUsageSummary: getImageUsageSummary,
+ }),
+ [dispatch, getState]
+ );
+
+ return api;
+};
+
+const getImageUsageFromImageDTOs = (imageDTOs: ImageDTO[], state: RootState): ImageUsage[] => {
+ if (imageDTOs.length === 0) {
+ return [];
+ }
+
+ const nodes = selectNodesSlice(state);
+ const canvas = selectCanvasSlice(state);
+ const upscale = selectUpscaleSlice(state);
+
+ return imageDTOs.map(({ image_name }) => getImageUsage(nodes, canvas, upscale, image_name));
+};
+
+const getImageUsageSummary = (imageUsage: ImageUsage[]): ImageUsage => ({
+ isUpscaleImage: some(imageUsage, (i) => i.isUpscaleImage),
+ isRasterLayerImage: some(imageUsage, (i) => i.isRasterLayerImage),
+ isInpaintMaskImage: some(imageUsage, (i) => i.isInpaintMaskImage),
+ isRegionalGuidanceImage: some(imageUsage, (i) => i.isRegionalGuidanceImage),
+ isNodesImage: some(imageUsage, (i) => i.isNodesImage),
+ isControlLayerImage: some(imageUsage, (i) => i.isControlLayerImage),
+ isReferenceImage: some(imageUsage, (i) => i.isReferenceImage),
+});
+
+const isAnyImageInUse = (imageUsage: ImageUsage[]): boolean =>
+ imageUsage.some(
+ (i) =>
+ i.isRasterLayerImage ||
+ i.isControlLayerImage ||
+ i.isReferenceImage ||
+ i.isInpaintMaskImage ||
+ i.isUpscaleImage ||
+ i.isNodesImage ||
+ i.isRegionalGuidanceImage
+ );
+
+// Some utils to delete images from different parts of the app
+const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
+ const actions: Param0[] = [];
+ state.nodes.present.nodes.forEach((node) => {
+ if (!isInvocationNode(node)) {
+ return;
+ }
+
+ forEach(node.data.inputs, (input) => {
+ if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
+ actions.push(
+ fieldImageValueChanged({
+ nodeId: node.data.id,
+ fieldName: input.name,
+ value: undefined,
+ })
+ );
+ return;
+ }
+ if (isImageFieldCollectionInputInstance(input)) {
+ actions.push(
+ fieldImageCollectionValueChanged({
+ nodeId: node.data.id,
+ fieldName: input.name,
+ value: input.value?.filter((value) => value?.image_name !== imageDTO.image_name),
+ })
+ );
+ }
+ });
+ });
+
+ actions.forEach(dispatch);
+};
+
+const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
+ selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => {
+ let shouldDelete = false;
+ for (const obj of objects) {
+ if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
+ shouldDelete = true;
+ break;
+ }
+ }
+ if (shouldDelete) {
+ dispatch(entityDeleted({ entityIdentifier: { id, type: 'control_layer' } }));
+ }
+ });
+};
+
+const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
+ selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
+ if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
+ dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
+ }
+ });
+};
+
+const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
+ selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => {
+ let shouldDelete = false;
+ for (const obj of objects) {
+ if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
+ shouldDelete = true;
+ break;
+ }
+ }
+ if (shouldDelete) {
+ dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } }));
+ }
+ });
+};
+
+export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: UpscaleState, image_name: string) => {
+ const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) =>
+ some(node.data.inputs, (input) => {
+ if (isImageFieldInputInstance(input)) {
+ if (input.value?.image_name === image_name) {
+ return true;
+ }
+ }
+
+ if (isImageFieldCollectionInputInstance(input)) {
+ if (input.value?.some((value) => value?.image_name === image_name)) {
+ return true;
+ }
+ }
+
+ return false;
+ })
+ );
+
+ const isUpscaleImage = upscale.upscaleInitialImage?.image_name === image_name;
+
+ const isReferenceImage = canvas.referenceImages.entities.some(
+ ({ ipAdapter }) => ipAdapter.image?.image_name === image_name
+ );
+
+ const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
+ objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
+ );
+
+ const isControlLayerImage = canvas.controlLayers.entities.some(({ objects }) =>
+ objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
+ );
+
+ const isInpaintMaskImage = canvas.inpaintMasks.entities.some(({ objects }) =>
+ objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
+ );
+
+ const isRegionalGuidanceImage = canvas.regionalGuidance.entities.some(({ referenceImages }) =>
+ referenceImages.some(({ ipAdapter }) => ipAdapter.image?.image_name === image_name)
+ );
+
+ const imageUsage: ImageUsage = {
+ isUpscaleImage,
+ isRasterLayerImage,
+ isInpaintMaskImage,
+ isRegionalGuidanceImage,
+ isNodesImage,
+ isControlLayerImage,
+ isReferenceImage,
+ };
+
+ return imageUsage;
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
index 43ae24ed70..3247f7e0a0 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
@@ -17,7 +17,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
-import { getImageUsage } from 'features/deleteImageModal/store/selectors';
+import { getImageUsage } from 'features/deleteImageModal/store/state';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemDelete.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemDelete.tsx
index 4f13848124..fcddd75483 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemDelete.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemDelete.tsx
@@ -1,6 +1,5 @@
-import { useAppDispatch } from 'app/store/storeHooks';
import { IconMenuItem } from 'common/components/IconMenuItem';
-import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
+import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,12 +7,16 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
export const ImageMenuItemDelete = memo(() => {
const { t } = useTranslation();
- const dispatch = useAppDispatch();
+ const deleteImageModal = useDeleteImageModalApi();
const imageDTO = useImageDTOContext();
- const onClick = useCallback(() => {
- dispatch(imagesToDeleteSelected([imageDTO]));
- }, [dispatch, imageDTO]);
+ const onClick = useCallback(async () => {
+ try {
+ await deleteImageModal.delete([imageDTO]);
+ } catch {
+ // noop;
+ }
+ }, [deleteImageModal, imageDTO]);
return (
{
const dispatch = useAppDispatch();
const selection = useAppSelector((s) => s.gallery.selection);
const customStarUi = useStore($customStarUI);
+ const deleteImageModal = useDeleteImageModalApi();
const isBulkDownloadEnabled = useFeatureStatus('bulkDownload');
@@ -32,8 +33,8 @@ const MultipleSelectionMenuItems = () => {
}, [dispatch, selection]);
const handleDeleteSelection = useCallback(() => {
- dispatch(imagesToDeleteSelected(selection));
- }, [dispatch, selection]);
+ deleteImageModal.delete(selection);
+ }, [deleteImageModal, selection]);
const handleStarSelection = useCallback(() => {
starImages({ imageDTOs: selection });
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageDeleteIconButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageDeleteIconButton.tsx
index 93953381a9..574890f0ed 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageDeleteIconButton.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageDeleteIconButton.tsx
@@ -1,6 +1,5 @@
import { useShiftModifier } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
-import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
+import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import { DndImageIcon } from 'features/dnd/DndImageIcon';
import type { MouseEvent } from 'react';
import { memo, useCallback } from 'react';
@@ -15,16 +14,17 @@ type Props = {
export const GalleryImageDeleteIconButton = memo(({ imageDTO }: Props) => {
const shift = useShiftModifier();
const { t } = useTranslation();
- const dispatch = useAppDispatch();
+ const deleteImageModal = useDeleteImageModalApi();
+
const onClick = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
if (!imageDTO) {
return;
}
- dispatch(imagesToDeleteSelected([imageDTO]));
+ deleteImageModal.delete([imageDTO]);
},
- [dispatch, imageDTO]
+ [deleteImageModal, imageDTO]
);
if (!shift) {
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
index b69c6aa949..ab6da024fd 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
@@ -1,7 +1,7 @@
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
-import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
+import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
@@ -16,7 +16,6 @@ import { useListImagesQuery } from 'services/api/endpoints/images';
export const useGalleryHotkeys = () => {
useAssertSingleton('useGalleryHotkeys');
const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
- const dispatch = useAppDispatch();
const selection = useAppSelector((s) => s.gallery.selection);
const queryArgs = useAppSelector(selectListImagesQueryArgs);
const queryResult = useListImagesQuery(queryArgs);
@@ -25,6 +24,7 @@ export const useGalleryHotkeys = () => {
const isWorkflowsFocused = useIsRegionFocused('workflows');
const isGalleryFocused = useIsRegionFocused('gallery');
const isImageViewerFocused = useIsRegionFocused('viewer');
+ const deleteImageModal = useDeleteImageModalApi();
// When we are on the canvas tab, we need to disable the delete hotkey when the user is focused on the layers tab in
// the right hand panel, because the same hotkey is used to delete layers.
@@ -209,7 +209,7 @@ export const useGalleryHotkeys = () => {
if (!selection.length) {
return;
}
- dispatch(imagesToDeleteSelected(selection));
+ deleteImageModal.delete(selection);
},
options: {
enabled: (isGalleryFocused || isImageViewerFocused) && isDeleteEnabledByTab && !isWorkflowsFocused,
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
index 1ba22baf4d..b63da55d89 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
+import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import {
handlers,
parseAndRecallAllMetadata,
@@ -34,6 +34,7 @@ export const useImageActions = (imageDTO: ImageDTO) => {
const [hasSeed, setHasSeed] = useState(false);
const [hasPrompts, setHasPrompts] = useState(false);
const hasTemplates = useStore($hasTemplates);
+ const deleteImageModal = useDeleteImageModalApi();
useEffect(() => {
const parseMetadata = async () => {
@@ -169,8 +170,8 @@ export const useImageActions = (imageDTO: ImageDTO) => {
}, [dispatch, imageDTO]);
const _delete = useCallback(() => {
- dispatch(imagesToDeleteSelected([imageDTO]));
- }, [dispatch, imageDTO]);
+ deleteImageModal.delete([imageDTO]);
+ }, [deleteImageModal, imageDTO]);
return {
hasMetadata,
diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts
index 590b83fe57..30aad43449 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/images.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts
@@ -2,6 +2,7 @@ import { $authToken } from 'app/store/nanostores/authToken';
import { getStore } from 'app/store/nanostores/store';
import type { BoardId } from 'features/gallery/store/types';
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
+import { uniqBy } from 'lodash-es';
import type { components, paths } from 'services/api/schema';
import type {
DeleteBoardResult,
@@ -14,6 +15,7 @@ import type {
UploadImageArg,
} from 'services/api/types';
import { getCategories, getListImagesUrl } from 'services/api/util';
+import stableHash from 'stable-hash';
import type { Param0 } from 'tsafe';
import type { JsonObject } from 'type-fest';
@@ -133,11 +135,12 @@ export const imagesApi = api.injectEndpoints({
};
},
invalidatesTags: (result, error, { imageDTOs }) => {
- if (imageDTOs[0]) {
- const categories = getCategories(imageDTOs[0]);
- const boardId = imageDTOs[0].board_id ?? 'none';
+ const tags: ApiTagDescription[] = [];
+ for (const imageDTO of imageDTOs) {
+ const categories = getCategories(imageDTO);
+ const boardId = imageDTO.board_id ?? 'none';
- const tags: ApiTagDescription[] = [
+ tags.push(
{
type: 'ImageList',
id: getListImagesUrl({
@@ -152,12 +155,12 @@ export const imagesApi = api.injectEndpoints({
{
type: 'BoardImagesTotal',
id: boardId,
- },
- ];
-
- return tags;
+ }
+ );
}
- return [];
+
+ const dedupedTags = uniqBy(tags, stableHash);
+ return dedupedTags;
},
}),
deleteUncategorizedImages: build.mutation({
From baa9141be3766540954242ece778ed5221d05a92 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 11 Jun 2025 12:31:07 +1000
Subject: [PATCH 084/210] chore(ui): dpdm
---
.../SimpleSession/QueueItemPreviewFull.tsx | 3 ++-
.../SimpleSession/QueueItemPreviewMini.tsx | 4 ++--
.../components/SimpleSession/context.tsx | 10 ++++++++++
.../components/SimpleSession/shared.ts | 14 --------------
4 files changed, 14 insertions(+), 17 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
index 029ad29454..ba45a9f2ba 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
@@ -1,11 +1,12 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
+import { useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/context';
import { ImageActions } from 'features/controlLayers/components/SimpleSession/ImageActions';
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
-import { getQueueItemElementId, useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/shared';
+import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
import { DndImage } from 'features/dnd/DndImage';
import { memo, useCallback, useState } from 'react';
import type { S } from 'services/api/types';
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 5851178b0f..cd69cd1bdb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -1,11 +1,11 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useCanvasSessionContext, useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/context';
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
-import { getQueueItemElementId, useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/shared';
+import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
import { DndImage } from 'features/dnd/DndImage';
import { memo, useCallback, useState } from 'react';
import type { S } from 'services/api/types';
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 0f79457f0d..7529c6995d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -485,3 +485,13 @@ export const useCanvasSessionContext = () => {
assert(ctx !== null, "'useCanvasSessionContext' must be used within a CanvasSessionContextProvider");
return ctx;
};
+
+export const useOutputImageDTO = (item: S['SessionQueueItem']) => {
+ const ctx = useCanvasSessionContext();
+ const $imageDTO = useState(() =>
+ computed([ctx.$progressData], (progressData) => progressData[item.item_id]?.imageDTO ?? null)
+ )[0];
+ const imageDTO = useStore($imageDTO);
+
+ return imageDTO;
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
index c0eee11713..0026b2a3fa 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
@@ -1,10 +1,6 @@
-import { useStore } from '@nanostores/react';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { isImageField } from 'features/nodes/types/common';
import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
import { round } from 'lodash-es';
-import { computed } from 'nanostores';
-import { useState } from 'react';
import type { S } from 'services/api/types';
import { objectEntries } from 'tsafe';
@@ -42,13 +38,3 @@ export const getOutputImageName = (item: S['SessionQueueItem']) => {
return null;
};
-
-export const useOutputImageDTO = (item: S['SessionQueueItem']) => {
- const ctx = useCanvasSessionContext();
- const $imageDTO = useState(() =>
- computed([ctx.$progressData], (progressData) => progressData[item.item_id]?.imageDTO ?? null)
- )[0];
- const imageDTO = useStore($imageDTO);
-
- return imageDTO;
-};
From a5e5cbd7c3687b04ac2060c336b4435477ff7b89 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 11 Jun 2025 12:47:46 +1000
Subject: [PATCH 085/210] feat(ui): simple session initial state cards are
buttons
---
.../components/SimpleSession/InitialState.tsx | 17 +++--------
.../InitialStateAddAStyleReference.tsx | 15 ++++++----
.../InitialStateButtonGridItem.tsx | 28 +++++++++++++++++++
.../InitialStateCardGridItem.tsx | 24 ----------------
.../InitialStateEditImageCard.tsx | 15 ++++++----
.../InitialStateGenerateFromText.tsx | 17 ++++-------
.../InitialStateUseALayoutImageCard.tsx | 15 ++++++----
7 files changed, 65 insertions(+), 66 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateCardGridItem.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
index 25bace52b6..2e8cf7a29b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -3,7 +3,6 @@
import { Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
-import { InitialStateCardGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateCardGridItem';
import { InitialStateEditImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateEditImageCard';
import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
import { InitialStateUseALayoutImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard';
@@ -29,18 +28,10 @@ export const InitialState = memo(() => {
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
index f41ad61ffa..0b97e5637e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
@@ -2,12 +2,13 @@
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
-import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
+import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { memo, useCallback } from 'react';
-import { PiUserCircleGearBold } from 'react-icons/pi';
+import { PiUploadBold, PiUserCircleGearBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
const NEW_CANVAS_OPTIONS = { type: 'reference_image' } as const;
@@ -23,17 +24,19 @@ export const InitialStateAddAStyleReference = memo(() => {
},
[dispatch, getState]
);
+ const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
return (
- <>
+
Add a Style Reference
Add an image to transfer its look.
-
-
+
+
+
- >
+
);
});
InitialStateAddAStyleReference.displayName = 'InitialStateAddAStyleReference';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
new file mode 100644
index 0000000000..4861e4f475
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
@@ -0,0 +1,28 @@
+import type { GridItemProps } from '@invoke-ai/ui-library';
+import { Button, GridItem } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+
+export const InitialStateButtonGridItem = memo(({ children, ...rest }: GridItemProps) => {
+ return (
+
+ {children}
+
+ );
+});
+
+InitialStateButtonGridItem.displayName = 'InitialStateButtonGridItem';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateCardGridItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateCardGridItem.tsx
deleted file mode 100644
index 708b005db1..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateCardGridItem.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { GridItem } from '@invoke-ai/ui-library';
-import { memo, type PropsWithChildren } from 'react';
-
-export const InitialStateCardGridItem = memo((props: PropsWithChildren) => {
- return (
-
- {props.children}
-
- );
-});
-
-InitialStateCardGridItem.displayName = 'InitialStateCardGridItem';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx
index d0c7d0032c..b6448f41d0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx
@@ -2,12 +2,13 @@
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
-import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
+import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { memo, useCallback } from 'react';
-import { PiPencilBold } from 'react-icons/pi';
+import { PiPencilBold, PiUploadBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
const NEW_CANVAS_OPTIONS = { type: 'raster_layer', withInpaintMask: true } as const;
@@ -23,17 +24,19 @@ export const InitialStateEditImageCard = memo(() => {
},
[dispatch, getState]
);
+ const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
return (
- <>
+
Edit Image
Add an image to refine.
-
-
+
+
+
- >
+
);
});
InitialStateEditImageCard.displayName = 'InitialStateEditImageCard';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
index 6d4fa5a2cf..67048c383f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
@@ -1,6 +1,7 @@
/* eslint-disable i18next/no-literal-string */
-import { Flex, Heading, Icon, IconButton, Text } from '@invoke-ai/ui-library';
+import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
+import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
import { memo } from 'react';
import { PiCursorTextBold, PiTextAaBold } from 'react-icons/pi';
@@ -14,20 +15,14 @@ const focusOnPrompt = () => {
export const InitialStateGenerateFromText = memo(() => {
return (
- <>
+
Generate from Text
Enter a prompt and Invoke.
-
- }
- variant="link"
- h={8}
- />
+
+
- >
+
);
});
InitialStateGenerateFromText.displayName = 'InitialStateGenerateFromText';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
index 54d2ef6f75..955ace441f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
@@ -2,12 +2,13 @@
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
-import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
+import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { memo, useCallback } from 'react';
-import { PiRectangleDashedBold } from 'react-icons/pi';
+import { PiRectangleDashedBold, PiUploadBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
const NEW_CANVAS_OPTIONS = { type: 'control_layer', withResize: true } as const;
@@ -23,17 +24,19 @@ export const InitialStateUseALayoutImageCard = memo(() => {
},
[dispatch, getState]
);
+ const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
return (
- <>
+
Use a Layout Image
Add an image to control composition.
-
-
+
+
+
- >
+
);
});
InitialStateUseALayoutImageCard.displayName = 'InitialStateUseALayoutImageCard';
From aa93e95a9447dcd1c2ee506400c5ffe4f5205dbc Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 12 Jun 2025 17:18:06 +1000
Subject: [PATCH 086/210] feat(ui): split out ref images into own slice (WIP)
---
.../listeners/boardAndImagesDeleted.ts | 4 +-
.../listeners/modelsLoaded.ts | 15 +-
invokeai/frontend/web/src/app/store/store.ts | 3 +
.../common/hooks/useFilterableOutsideClick.ts | 159 +++++++++
.../components/CanvasAddEntityButtons.tsx | 18 -
.../components/CanvasDropArea.tsx | 11 -
.../CanvasEntityGroupList.tsx | 5 +-
.../CanvasEntityList/CanvasEntityList.tsx | 2 -
.../EntityListGlobalActionBarAddLayerMenu.tsx | 8 -
...tityListSelectedEntityActionBarOpacity.tsx | 14 +-
.../components/IPAdapter/IPAdapter.tsx | 30 +-
.../components/IPAdapter/IPAdapterList.tsx | 52 +--
.../IPAdapter/IPAdapterMenuItemPullBbox.tsx | 6 +-
.../components/IPAdapter/IPAdapterPreview.tsx | 75 ++++
.../IPAdapter/IPAdapterSettings.tsx | 141 ++++----
.../IPAdapter/IPAdapterSettingsEmptyState.tsx | 14 +-
.../RegionalGuidanceIPAdapterSettings.tsx | 6 +-
.../RegionalReferenceImageModel.tsx | 0
.../{IPAdapter => common}/CLIPVisionModel.tsx | 0
.../common/CanvasEntityAddOfTypeButton.tsx | 9 +-
.../CanvasEntityHeaderCommonActions.tsx | 5 +-
.../common/CanvasEntityMenuItemsArrange.tsx | 5 -
.../common/CanvasEntityMenuItemsMergeDown.tsx | 4 +-
.../common/CanvasEntityMergeVisibleButton.tsx | 4 +-
.../FLUXReduxImageInfluence.tsx | 0
.../contexts/EntityAdapterContext.tsx | 8 +-
.../contexts/RefImageIdContext.ts | 10 +
.../controlLayers/hooks/addLayerHooks.ts | 2 +-
.../controlLayers/hooks/saveCanvasHooks.ts | 9 +-
.../controlLayers/hooks/useEntityTitle.ts | 2 -
.../controlLayers/hooks/useEntityTypeCount.ts | 2 -
.../useEntityTypeInformationalPopover.ts | 3 -
.../hooks/useEntityTypeIsHidden.ts | 1 -
.../hooks/useEntityTypeString.ts | 4 -
.../controlLayers/hooks/useEntityTypeTitle.ts | 2 -
.../hooks/useIsEntityTypeEnabled.ts | 9 +-
.../useNextRenderableEntityIdentifier.ts | 4 +-
.../hooks/useVisibleEntityCountByType.ts | 3 -
.../konva/CanvasCompositorModule.ts | 18 +-
.../CanvasEntity/CanvasEntityAdapterBase.ts | 4 +-
.../CanvasEntity/CanvasEntityFilterer.ts | 4 +-
.../controlLayers/konva/CanvasEntity/types.ts | 4 +-
.../controlLayers/konva/CanvasManager.ts | 14 +-
.../konva/CanvasSegmentAnythingModule.ts | 4 +-
.../konva/CanvasStateApiModule.ts | 5 +-
.../konva/CanvasTool/CanvasToolModule.ts | 3 +-
.../controlLayers/store/canvasSlice.ts | 277 +--------------
.../controlLayers/store/refImagesSlice.ts | 323 ++++++++++++++++++
.../features/controlLayers/store/selectors.ts | 41 +--
.../src/features/controlLayers/store/types.ts | 46 +--
.../src/features/controlLayers/store/util.ts | 27 +-
.../features/deleteImageModal/store/state.ts | 28 +-
invokeai/frontend/web/src/features/dnd/dnd.ts | 16 +-
.../components/Boards/DeleteBoardModal.tsx | 34 +-
.../ImageMenuItemNewLayerFromImageSubMenu.tsx | 6 +-
.../web/src/features/imageActions/actions.ts | 28 +-
.../graph/generation/buildChatGPT4oGraph.ts | 4 +-
.../util/graph/generation/buildFLUXGraph.ts | 6 +-
.../util/graph/generation/buildSD1Graph.ts | 4 +-
.../util/graph/generation/buildSDXLGraph.ts | 4 +-
.../components/Core/ParamPositivePrompt.tsx | 2 +
.../web/src/features/queue/store/readiness.ts | 19 +-
62 files changed, 871 insertions(+), 699 deletions(-)
create mode 100644 invokeai/frontend/web/src/common/hooks/useFilterableOutsideClick.ts
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter => RegionalGuidance}/RegionalReferenceImageModel.tsx (100%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter => common}/CLIPVisionModel.tsx (100%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter => common}/FLUXReduxImageInfluence.tsx (100%)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/contexts/RefImageIdContext.ts
create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
index 3ec4d7698e..74a9145cc0 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
@@ -1,4 +1,5 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { getImageUsage } from 'features/deleteImageModal/store/state';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
@@ -20,9 +21,10 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
const nodes = selectNodesSlice(state);
const canvas = selectCanvasSlice(state);
const upscale = selectUpscaleSlice(state);
+ const refImages = selectRefImagesSlice(state);
deleted_images.forEach((image_name) => {
- const imageUsage = getImageUsage(nodes, canvas, upscale, image_name);
+ const imageUsage = getImageUsage(nodes, canvas, upscale, refImages, image_name);
if (imageUsage.isNodesImage && !wasNodeEditorReset) {
dispatch(nodeEditorReset());
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
index fa142e85bc..8590c35810 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
@@ -1,11 +1,7 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
-import {
- controlLayerModelChanged,
- referenceImageIPAdapterModelChanged,
- rgIPAdapterModelChanged,
-} from 'features/controlLayers/store/canvasSlice';
+import { controlLayerModelChanged, rgIPAdapterModelChanged } from 'features/controlLayers/store/canvasSlice';
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
import {
clipEmbedModelSelected,
@@ -15,6 +11,7 @@ import {
t5EncoderModelSelected,
vaeSelected,
} from 'features/controlLayers/store/paramsSlice';
+import { referenceImageIPAdapterModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { modelSelected } from 'features/parameters/store/actions';
@@ -210,7 +207,7 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, log)
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
const ipaModels = models.filter(isIPAdapterModelConfig);
- selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
+ selectRefImagesSlice(state).entities.forEach((entity) => {
if (entity.ipAdapter.type !== 'ip_adapter') {
return;
}
@@ -225,7 +222,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
return;
}
log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing');
- dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
+ dispatch(referenceImageIPAdapterModelChanged({ id: entity.id, modelConfig: null }));
});
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
@@ -254,7 +251,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
const fluxReduxModels = models.filter(isFluxReduxModelConfig);
- selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
+ selectRefImagesSlice(state).entities.forEach((entity) => {
if (entity.ipAdapter.type !== 'flux_redux') {
return;
}
@@ -268,7 +265,7 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
return;
}
log.debug({ selectedFLUXReduxModel }, 'Selected FLUX Redux model is not available, clearing');
- dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
+ dispatch(referenceImageIPAdapterModelChanged({ id: entity.id, modelConfig: null }));
});
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index 8853d157ea..ec757494f5 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -13,6 +13,7 @@ import {
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
+import { refImagesPersistConfig, refImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
@@ -66,6 +67,7 @@ const allReducers = {
[canvasSessionSlice.name]: canvasSessionSlice.reducer,
[lorasSlice.name]: lorasSlice.reducer,
[workflowLibrarySlice.name]: workflowLibrarySlice.reducer,
+ [refImagesSlice.name]: refImagesSlice.reducer,
};
const rootReducer = combineReducers(allReducers);
@@ -111,6 +113,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[canvasStagingAreaPersistConfig.name]: canvasStagingAreaPersistConfig,
[lorasPersistConfig.name]: lorasPersistConfig,
[workflowLibraryPersistConfig.name]: workflowLibraryPersistConfig,
+ [refImagesSlice.name]: refImagesPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {
diff --git a/invokeai/frontend/web/src/common/hooks/useFilterableOutsideClick.ts b/invokeai/frontend/web/src/common/hooks/useFilterableOutsideClick.ts
new file mode 100644
index 0000000000..6b45cb8554
--- /dev/null
+++ b/invokeai/frontend/web/src/common/hooks/useFilterableOutsideClick.ts
@@ -0,0 +1,159 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+/**
+ * Adapted from https://github.com/chakra-ui/chakra-ui/blob/v2/packages/hooks/src/use-outside-click.ts
+ *
+ * The main change here is to support filtering of outside clicks via a `filter` function.
+ *
+ * This lets us work around issues with portals and components like popovers, which typically close on an outside click.
+ *
+ * For example, consider a popover that has a custom drop-down component inside it, which uses a portal to render
+ * the drop-down options. The original outside click handler would close the popover when clicking on the drop-down options,
+ * because the click is outside the popover - but we expect the popover to stay open in this case.
+ *
+ * A filter function like this can fix that:
+ *
+ * ```ts
+ * const filter = (el: HTMLElement) => el.className.includes('chakra-portal') || el.id.includes('react-select')
+ * ```
+ *
+ * This ignores clicks on react-select-based drop-downs and Chakra UI portals and is used as the default filter.
+ */
+
+import { useCallback, useEffect, useRef } from 'react';
+
+export function useCallbackRef any>(
+ callback: T | undefined,
+ deps: React.DependencyList = []
+) {
+ const callbackRef = useRef(callback);
+
+ useEffect(() => {
+ callbackRef.current = callback;
+ });
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ return useCallback(((...args) => callbackRef.current?.(...args)) as T, deps);
+}
+
+export interface UseOutsideClickProps {
+ /**
+ * Whether the hook is enabled
+ */
+ enabled?: boolean;
+ /**
+ * The reference to a DOM element.
+ */
+ ref: React.RefObject;
+ /**
+ * Function invoked when a click is triggered outside the referenced element.
+ */
+ handler?: (e: Event) => void;
+ /**
+ * A function that filters the elements that should be considered as outside clicks.
+ *
+ * If omitted, a default filter function that ignores clicks in Chakra UI portals and react-select components is used.
+ */
+ filter?: (el: HTMLElement) => boolean;
+}
+
+const DEFAULT_FILTER = (el: HTMLElement) => el.className.includes('chakra-portal') || el.id.includes('react-select');
+
+/**
+ * Example, used in components like Dialogs and Popovers, so they can close
+ * when a user clicks outside them.
+ */
+export function useFilterableOutsideClick(props: UseOutsideClickProps) {
+ const { ref, handler, enabled = true, filter = DEFAULT_FILTER } = props;
+ const savedHandler = useCallbackRef(handler);
+
+ const stateRef = useRef({
+ isPointerDown: false,
+ ignoreEmulatedMouseEvents: false,
+ });
+
+ const state = stateRef.current;
+
+ useEffect(() => {
+ if (!enabled) {
+ return;
+ }
+ const onPointerDown: any = (e: PointerEvent) => {
+ if (isValidEvent(e, ref, filter)) {
+ state.isPointerDown = true;
+ }
+ };
+
+ const onMouseUp: any = (event: MouseEvent) => {
+ if (state.ignoreEmulatedMouseEvents) {
+ state.ignoreEmulatedMouseEvents = false;
+ return;
+ }
+
+ if (state.isPointerDown && handler && isValidEvent(event, ref)) {
+ state.isPointerDown = false;
+ savedHandler(event);
+ }
+ };
+
+ const onTouchEnd = (event: TouchEvent) => {
+ state.ignoreEmulatedMouseEvents = true;
+ if (handler && state.isPointerDown && isValidEvent(event, ref)) {
+ state.isPointerDown = false;
+ savedHandler(event);
+ }
+ };
+
+ const doc = getOwnerDocument(ref.current);
+ doc.addEventListener('mousedown', onPointerDown, true);
+ doc.addEventListener('mouseup', onMouseUp, true);
+ doc.addEventListener('touchstart', onPointerDown, true);
+ doc.addEventListener('touchend', onTouchEnd, true);
+
+ return () => {
+ doc.removeEventListener('mousedown', onPointerDown, true);
+ doc.removeEventListener('mouseup', onMouseUp, true);
+ doc.removeEventListener('touchstart', onPointerDown, true);
+ doc.removeEventListener('touchend', onTouchEnd, true);
+ };
+ }, [handler, ref, savedHandler, state, enabled, filter]);
+}
+
+function isValidEvent(
+ event: Event,
+ ref: React.RefObject,
+ filter?: (el: HTMLElement) => boolean
+): boolean {
+ const target = (event.composedPath?.()[0] ?? event.target) as HTMLElement;
+
+ if (target) {
+ const doc = getOwnerDocument(target);
+ if (!doc.contains(target)) {
+ return false;
+ }
+ }
+
+ if (ref.current?.contains(target)) {
+ return false;
+ }
+
+ if (filter) {
+ // Check if the click is inside an element matching the filter.
+ // This is used for portal-awareness or other general exclusion cases.
+ let currentElement: HTMLElement | null = target;
+ // Traverse up the DOM tree from the target element.
+ while (currentElement && currentElement !== document.body) {
+ if (filter(currentElement)) {
+ return false;
+ }
+ currentElement = currentElement.parentElement;
+ }
+ }
+
+ // If the click is not inside the ref and not inside a portal, it's a valid outside click.
+ return true;
+}
+
+function getOwnerDocument(node?: Element | null): Document {
+ return node?.ownerDocument ?? document;
+}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
index b39c590b9b..6027ed14d2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
@@ -2,7 +2,6 @@ import { Button, Flex, Heading } from '@invoke-ai/ui-library';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import {
useAddControlLayer,
- useAddGlobalReferenceImage,
useAddInpaintMask,
useAddRasterLayer,
useAddRegionalGuidance,
@@ -19,9 +18,7 @@ export const CanvasAddEntityButtons = memo(() => {
const addRegionalGuidance = useAddRegionalGuidance();
const addRasterLayer = useAddRasterLayer();
const addControlLayer = useAddControlLayer();
- const addGlobalReferenceImage = useAddGlobalReferenceImage();
const addRegionalReferenceImage = useAddRegionalReferenceImage();
- const isReferenceImageEnabled = useIsEntityTypeEnabled('reference_image');
const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance');
const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer');
const isInpaintLayerEnabled = useIsEntityTypeEnabled('inpaint_mask');
@@ -29,21 +26,6 @@ export const CanvasAddEntityButtons = memo(() => {
return (
-
- {t('controlLayers.global')}
-
- }
- onClick={addGlobalReferenceImage}
- isDisabled={!isReferenceImageEnabled}
- >
- {t('controlLayers.globalReferenceImage')}
-
-
-
{t('controlLayers.regional')}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx
index adf21bd318..4bde508d2b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx
@@ -12,9 +12,6 @@ const addControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.
const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
type: 'regional_guidance_with_reference_image',
});
-const addGlobalReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
- type: 'reference_image',
-});
export const CanvasDropArea = memo(() => {
const { t } = useTranslation();
@@ -57,14 +54,6 @@ export const CanvasDropArea = memo(() => {
isDisabled={isBusy}
/>
-
-
-
>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList.tsx
index 07ca2093a5..8f47075b9d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList.tsx
@@ -14,7 +14,6 @@ import { useEntityTypeInformationalPopover } from 'features/controlLayers/hooks/
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
import { entitiesReordered } from 'features/controlLayers/store/canvasSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
-import { isRenderableEntityType } from 'features/controlLayers/store/types';
import { singleCanvasEntityDndSource } from 'features/dnd/dnd';
import { triggerPostMoveFlash } from 'features/dnd/util';
import type { PropsWithChildren } from 'react';
@@ -165,8 +164,8 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityI
- {isRenderableEntityType(type) && }
- {isRenderableEntityType(type) && }
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx
index e63641dab3..f8fdb7c66c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx
@@ -2,7 +2,6 @@ import { Flex } from '@invoke-ai/ui-library';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList';
import { InpaintMaskList } from 'features/controlLayers/components/InpaintMask/InpaintMaskList';
-import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList';
import { RasterLayerEntityList } from 'features/controlLayers/components/RasterLayer/RasterLayerEntityList';
import { RegionalGuidanceEntityList } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList';
import { memo } from 'react';
@@ -11,7 +10,6 @@ export const CanvasEntityList = memo(() => {
return (
-
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx
index b1bea0e292..9ffdc58bf6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx
@@ -1,7 +1,6 @@
import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
import {
useAddControlLayer,
- useAddGlobalReferenceImage,
useAddInpaintMask,
useAddRasterLayer,
useAddRegionalGuidance,
@@ -16,13 +15,11 @@ import { PiPlusBold } from 'react-icons/pi';
export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
const { t } = useTranslation();
const isBusy = useCanvasIsBusy();
- const addGlobalReferenceImage = useAddGlobalReferenceImage();
const addInpaintMask = useAddInpaintMask();
const addRegionalGuidance = useAddRegionalGuidance();
const addRegionalReferenceImage = useAddRegionalReferenceImage();
const addRasterLayer = useAddRasterLayer();
const addControlLayer = useAddControlLayer();
- const isReferenceImageEnabled = useIsEntityTypeEnabled('reference_image');
const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance');
const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer');
const isInpaintLayerEnabled = useIsEntityTypeEnabled('inpaint_mask');
@@ -41,11 +38,6 @@ export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
isDisabled={isBusy}
/>
-
- } onClick={addGlobalReferenceImage} isDisabled={!isReferenceImageEnabled}>
- {t('controlLayers.globalReferenceImage')}
-
-
} onClick={addInpaintMask} isDisabled={!isInpaintLayerEnabled}>
{t('controlLayers.inpaintMask')}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx
index e59e09dfa1..0f47c5b7f6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx
@@ -22,7 +22,6 @@ import {
selectEntity,
selectSelectedEntityIdentifier,
} from 'features/controlLayers/store/selectors';
-import { isRenderableEntity } from 'features/controlLayers/store/types';
import { clamp, round } from 'lodash-es';
import type { KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
@@ -70,9 +69,6 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
if (!selectedEntity) {
return 1; // fallback to 100% opacity
}
- if (!isRenderableEntity(selectedEntity)) {
- return 1; // fallback to 100% opacity
- }
// Opacity is a float from 0-1, but we want to display it as a percentage
return selectedEntity.opacity;
});
@@ -134,11 +130,7 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
return (
-
+
{t('controlLayers.opacity')}
{
position="absolute"
insetInlineEnd={0}
h="full"
- isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'reference_image'}
+ isDisabled={selectedEntityIdentifier === null}
/>
@@ -185,7 +177,7 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
marks={marks}
formatValue={formatSliderValue}
alwaysShowMarks
- isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'reference_image'}
+ isDisabled={selectedEntityIdentifier === null}
/>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx
index 2c034b80f9..a31841a716 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx
@@ -4,31 +4,25 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings';
-import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
-import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
-import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
-import { memo, useMemo } from 'react';
+import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
+import { memo } from 'react';
type Props = {
id: string;
};
export const IPAdapter = memo(({ id }: Props) => {
- const entityIdentifier = useMemo(() => ({ id, type: 'reference_image' }), [id]);
-
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx
index 38cdbde8c7..c803bae13c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx
@@ -1,37 +1,37 @@
/* eslint-disable i18next/no-literal-string */
-import { createSelector } from '@reduxjs/toolkit';
-import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
+import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
-import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
-import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter';
-import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
-import { getEntityIdentifier } from 'features/controlLayers/store/types';
+import { RefImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterPreview';
+import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
+import { selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
import { memo } from 'react';
-const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
- return canvas.referenceImages.entities.map(getEntityIdentifier).toReversed();
-});
-const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
- return selectedEntityIdentifier?.type === 'reference_image';
-});
+const sx: SystemStyleObject = {
+ opacity: 0.3,
+ _hover: {
+ opacity: 1,
+ },
+ transitionProperty: 'opacity',
+ transitionDuration: '0.2s',
+};
-export const IPAdapterList = memo(() => {
- const isSelected = useAppSelector(selectIsSelected);
- const entityIdentifiers = useAppSelector(selectEntityIdentifiers);
+export const RefImageList = memo((props: FlexProps) => {
+ const ids = useAppSelector(selectRefImageEntityIds);
- if (entityIdentifiers.length === 0) {
+ if (ids.length === 0) {
return null;
}
- if (entityIdentifiers.length > 0) {
- return (
-
- {entityIdentifiers.map((entityIdentifiers) => (
-
- ))}
-
- );
- }
+ return (
+
+ {ids.map((id) => (
+
+
+
+ ))}
+
+ );
});
-IPAdapterList.displayName = 'IPAdapterList';
+RefImageList.displayName = 'RefImageList';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItemPullBbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItemPullBbox.tsx
index 504ac54f27..0dd01927c2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItemPullBbox.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItemPullBbox.tsx
@@ -1,5 +1,5 @@
import { MenuItem } from '@invoke-ai/ui-library';
-import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
+import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { memo } from 'react';
@@ -8,8 +8,8 @@ import { PiBoundingBoxBold } from 'react-icons/pi';
export const IPAdapterMenuItemPullBbox = memo(() => {
const { t } = useTranslation();
- const entityIdentifier = useEntityIdentifierContext('reference_image');
- const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
+ const id = useRefImageIdContext();
+ const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
const isBusy = useCanvasIsBusy();
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx
new file mode 100644
index 0000000000..58d6db2912
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx
@@ -0,0 +1,75 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import {
+ Flex,
+ Image,
+ Popover,
+ PopoverAnchor,
+ PopoverArrow,
+ PopoverBody,
+ PopoverContent,
+ Portal,
+} from '@invoke-ai/ui-library';
+import { createSelector } from '@reduxjs/toolkit';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { useAppSelector } from 'app/store/storeHooks';
+import { useDisclosure } from 'common/hooks/useBoolean';
+import { useFilterableOutsideClick } from 'common/hooks/useFilterableOutsideClick';
+import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings';
+import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
+import { selectRefImageEntityOrThrow, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
+import type { ImageWithDims } from 'features/controlLayers/store/types';
+import { memo, useMemo, useRef } from 'react';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+
+const sx: SystemStyleObject = {
+ opacity: 0.5,
+ _hover: {
+ opacity: 1,
+ },
+ "&[data-is-open='true']": {
+ opacity: 1,
+ pointerEvents: 'none',
+ },
+ transitionProperty: 'opacity',
+ transitionDuration: '0.2s',
+};
+
+export const RefImagePreview = memo(() => {
+ const id = useRefImageIdContext();
+ const ref = useRef(null);
+ const disclosure = useDisclosure(false);
+ const selectEntity = useMemo(
+ () =>
+ createSelector(selectRefImagesSlice, (refImages) =>
+ selectRefImageEntityOrThrow(refImages, id, 'RefImagePreview')
+ ),
+ [id]
+ );
+ const entity = useAppSelector(selectEntity);
+ useFilterableOutsideClick({ ref, handler: disclosure.close });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+RefImagePreview.displayName = 'RefImagePreview';
+
+const Thumbnail = memo(({ image }: { image: ImageWithDims | null }) => {
+ const { data: imageDTO } = useGetImageDTOQuery(image?.image_name ?? skipToken);
+ return ;
+});
+Thumbnail.displayName = 'Thumbnail';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
index bc568dbcc7..fe2cd460b7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
@@ -1,17 +1,15 @@
-import { Flex, IconButton } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
-import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
+import { CLIPVisionModel } from 'features/controlLayers/components/common/CLIPVisionModel';
+import { FLUXReduxImageInfluence } from 'features/controlLayers/components/common/FLUXReduxImageInfluence';
import { Weight } from 'features/controlLayers/components/common/Weight';
-import { CLIPVisionModel } from 'features/controlLayers/components/IPAdapter/CLIPVisionModel';
-import { FLUXReduxImageInfluence } from 'features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence';
import { GlobalReferenceImageModel } from 'features/controlLayers/components/IPAdapter/GlobalReferenceImageModel';
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
import { IPAdapterSettingsEmptyState } from 'features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState';
-import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
-import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
-import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
+import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
+import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import {
referenceImageIPAdapterBeginEndStepPctChanged,
referenceImageIPAdapterCLIPVisionModelChanged,
@@ -20,11 +18,11 @@ import {
referenceImageIPAdapterMethodChanged,
referenceImageIPAdapterModelChanged,
referenceImageIPAdapterWeightChanged,
-} from 'features/controlLayers/store/canvasSlice';
-import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
-import { selectCanvasSlice, selectEntity, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
+ selectRefImageEntity,
+ selectRefImageEntityOrThrow,
+ selectRefImagesSlice,
+} from 'features/controlLayers/store/refImagesSlice';
import type {
- CanvasEntityIdentifier,
CLIPVisionModelV2,
FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
IPMethodV2,
@@ -33,141 +31,138 @@ import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiBoundingBoxBold } from 'react-icons/pi';
import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { IPAdapterImagePreview } from './IPAdapterImagePreview';
-const buildSelectIPAdapter = (entityIdentifier: CanvasEntityIdentifier<'reference_image'>) =>
+const buildSelectIPAdapter = (id: string) =>
createSelector(
- selectCanvasSlice,
- (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'IPAdapterSettings').ipAdapter
+ selectRefImagesSlice,
+ (refImages) => selectRefImageEntityOrThrow(refImages, id, 'IPAdapterSettings').ipAdapter
);
const IPAdapterSettingsContent = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const entityIdentifier = useEntityIdentifierContext('reference_image');
- const selectIPAdapter = useMemo(() => buildSelectIPAdapter(entityIdentifier), [entityIdentifier]);
+ const id = useRefImageIdContext();
+ const selectIPAdapter = useMemo(() => buildSelectIPAdapter(id), [id]);
const ipAdapter = useAppSelector(selectIPAdapter);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
- dispatch(referenceImageIPAdapterBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct }));
+ dispatch(referenceImageIPAdapterBeginEndStepPctChanged({ id, beginEndStepPct }));
},
- [dispatch, entityIdentifier]
+ [dispatch, id]
);
const onChangeWeight = useCallback(
(weight: number) => {
- dispatch(referenceImageIPAdapterWeightChanged({ entityIdentifier, weight }));
+ dispatch(referenceImageIPAdapterWeightChanged({ id, weight }));
},
- [dispatch, entityIdentifier]
+ [dispatch, id]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
- dispatch(referenceImageIPAdapterMethodChanged({ entityIdentifier, method }));
+ dispatch(referenceImageIPAdapterMethodChanged({ id, method }));
},
- [dispatch, entityIdentifier]
+ [dispatch, id]
);
const onChangeFLUXReduxImageInfluence = useCallback(
(imageInfluence: FLUXReduxImageInfluenceType) => {
- dispatch(referenceImageIPAdapterFLUXReduxImageInfluenceChanged({ entityIdentifier, imageInfluence }));
+ dispatch(referenceImageIPAdapterFLUXReduxImageInfluenceChanged({ id, imageInfluence }));
},
- [dispatch, entityIdentifier]
+ [dispatch, id]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig) => {
- dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier, modelConfig }));
+ dispatch(referenceImageIPAdapterModelChanged({ id, modelConfig }));
},
- [dispatch, entityIdentifier]
+ [dispatch, id]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
- dispatch(referenceImageIPAdapterCLIPVisionModelChanged({ entityIdentifier, clipVisionModel }));
+ dispatch(referenceImageIPAdapterCLIPVisionModelChanged({ id, clipVisionModel }));
},
- [dispatch, entityIdentifier]
+ [dispatch, id]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
- dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier, imageDTO }));
+ dispatch(referenceImageIPAdapterImageChanged({ id, imageDTO }));
},
- [dispatch, entityIdentifier]
+ [dispatch, id]
);
const dndTargetData = useMemo(
- () => setGlobalReferenceImageDndTarget.getData({ entityIdentifier }, ipAdapter.image?.image_name),
- [entityIdentifier, ipAdapter.image?.image_name]
+ () => setGlobalReferenceImageDndTarget.getData({ id }, ipAdapter.image?.image_name),
+ [id, ipAdapter.image?.image_name]
);
- const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
- const isBusy = useCanvasIsBusy();
+ // const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
+ // const isBusy = useCanvasIsBusy();
const isFLUX = useAppSelector(selectIsFLUX);
return (
-
-
-
-
- {ipAdapter.type === 'ip_adapter' && (
-
- )}
-
+
+
+ {ipAdapter.type === 'ip_adapter' && (
+
+ )}
+ {/* }
- />
-
-
- {ipAdapter.type === 'ip_adapter' && (
-
- {!isFLUX && }
-
-
-
- )}
- {ipAdapter.type === 'flux_redux' && (
-
-
-
- )}
-
- */}
+
+
+ {ipAdapter.type === 'ip_adapter' && (
+
+ {!isFLUX && }
+
+
+
+ )}
+ {ipAdapter.type === 'flux_redux' && (
+
+
+ )}
+
+
-
+
);
});
IPAdapterSettingsContent.displayName = 'IPAdapterSettingsContent';
-const buildSelectIPAdapterHasImage = (entityIdentifier: CanvasEntityIdentifier<'reference_image'>) =>
- createSelector(selectCanvasSlice, (canvas) => {
- const referenceImage = selectEntity(canvas, entityIdentifier);
+const buildSelectIPAdapterHasImage = (id: string) =>
+ createSelector(selectRefImagesSlice, (refImages) => {
+ const referenceImage = selectRefImageEntity(refImages, id);
return !!referenceImage && referenceImage.ipAdapter.image !== null;
});
export const IPAdapterSettings = memo(() => {
- const entityIdentifier = useEntityIdentifierContext('reference_image');
+ const id = useRefImageIdContext();
- const selectIPAdapterHasImage = useMemo(() => buildSelectIPAdapterHasImage(entityIdentifier), [entityIdentifier]);
+ const selectIPAdapterHasImage = useMemo(() => buildSelectIPAdapterHasImage(id), [id]);
const hasImage = useAppSelector(selectIPAdapterHasImage);
if (!hasImage) {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState.tsx
index 12b059e292..3bf6744d99 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState.tsx
@@ -1,7 +1,7 @@
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
-import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
+import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
@@ -15,24 +15,24 @@ import type { ImageDTO } from 'services/api/types';
export const IPAdapterSettingsEmptyState = memo(() => {
const { t } = useTranslation();
- const entityIdentifier = useEntityIdentifierContext('reference_image');
+ const id = useRefImageIdContext();
const dispatch = useAppDispatch();
const isBusy = useCanvasIsBusy();
const onUpload = useCallback(
(imageDTO: ImageDTO) => {
- setGlobalReferenceImage({ imageDTO, entityIdentifier, dispatch });
+ setGlobalReferenceImage({ imageDTO, id, dispatch });
},
- [dispatch, entityIdentifier]
+ [dispatch, id]
);
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
const onClickGalleryButton = useCallback(() => {
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
- const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
+ const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
const dndTargetData = useMemo(
- () => setGlobalReferenceImageDndTarget.getData({ entityIdentifier }),
- [entityIdentifier]
+ () => setGlobalReferenceImageDndTarget.getData({ id }),
+ [id]
);
const components = useMemo(
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
index 17e148a231..bd5c7c544c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
@@ -2,13 +2,13 @@ import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
+import { CLIPVisionModel } from 'features/controlLayers/components/common/CLIPVisionModel';
+import { FLUXReduxImageInfluence } from 'features/controlLayers/components/common/FLUXReduxImageInfluence';
import { Weight } from 'features/controlLayers/components/common/Weight';
-import { CLIPVisionModel } from 'features/controlLayers/components/IPAdapter/CLIPVisionModel';
-import { FLUXReduxImageInfluence } from 'features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence';
import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
-import { RegionalReferenceImageModel } from 'features/controlLayers/components/IPAdapter/RegionalReferenceImageModel';
import { RegionalGuidanceIPAdapterSettingsEmptyState } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState';
+import { RegionalReferenceImageModel } from 'features/controlLayers/components/RegionalGuidance/RegionalReferenceImageModel';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/RegionalReferenceImageModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalReferenceImageModel.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/RegionalReferenceImageModel.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalReferenceImageModel.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/CLIPVisionModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CLIPVisionModel.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/CLIPVisionModel.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/common/CLIPVisionModel.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx
index 9fd9d614d1..957443c001 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx
@@ -2,7 +2,6 @@ import { IconButton } from '@invoke-ai/ui-library';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import {
useAddControlLayer,
- useAddGlobalReferenceImage,
useAddInpaintMask,
useAddRasterLayer,
useAddRegionalGuidance,
@@ -23,7 +22,6 @@ export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
const addRegionalGuidance = useAddRegionalGuidance();
const addRasterLayer = useAddRasterLayer();
const addControlLayer = useAddControlLayer();
- const addGlobalReferenceImage = useAddGlobalReferenceImage();
const onClick = useCallback(() => {
switch (type) {
@@ -39,11 +37,8 @@ export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
case 'control_layer':
addControlLayer();
break;
- case 'reference_image':
- addGlobalReferenceImage();
- break;
}
- }, [addControlLayer, addGlobalReferenceImage, addInpaintMask, addRasterLayer, addRegionalGuidance, type]);
+ }, [addControlLayer, addInpaintMask, addRasterLayer, addRegionalGuidance, type]);
const label = useMemo(() => {
switch (type) {
@@ -55,8 +50,6 @@ export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
return t('controlLayers.addRasterLayer');
case 'control_layer':
return t('controlLayers.addControlLayer');
- case 'reference_image':
- return t('controlLayers.addGlobalReferenceImage');
}
}, [type, t]);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx
index 9c22778ff6..f630373f66 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx
@@ -4,17 +4,14 @@ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/com
import { CanvasEntityHeaderWarnings } from 'features/controlLayers/components/common/CanvasEntityHeaderWarnings';
import { CanvasEntityIsBookmarkedForQuickSwitchToggle } from 'features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle';
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
-import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { memo } from 'react';
export const CanvasEntityHeaderCommonActions = memo(() => {
- const entityIdentifier = useEntityIdentifierContext();
-
return (
- {entityIdentifier.type !== 'reference_image' && }
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx
index 897b8fdde5..f9f625c1df 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx
@@ -39,11 +39,6 @@ const getIndexAndCount = (
index: canvas.inpaintMasks.entities.findIndex((entity) => entity.id === id),
count: canvas.inpaintMasks.entities.length,
};
- } else if (type === 'reference_image') {
- return {
- index: canvas.referenceImages.entities.findIndex((entity) => entity.id === id),
- count: canvas.referenceImages.entities.length,
- };
} else {
return {
index: -1,
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown.tsx
index 076d639027..2e1eef13dc 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown.tsx
@@ -3,7 +3,7 @@ import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerP
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/useNextRenderableEntityIdentifier';
-import type { CanvasRenderableEntityType } from 'features/controlLayers/store/types';
+import type { CanvasEntityType } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStackSimpleBold } from 'react-icons/pi';
@@ -12,7 +12,7 @@ export const CanvasEntityMenuItemsMergeDown = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const isBusy = useCanvasIsBusy();
- const entityIdentifier = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext();
const entityIdentifierBelowThisOne = useEntityIdentifierBelowThisOne(entityIdentifier);
const mergeDown = useCallback(() => {
if (entityIdentifierBelowThisOne === null) {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx
index da3d46c433..d5e1fdded7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx
@@ -2,13 +2,13 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useVisibleEntityCountByType } from 'features/controlLayers/hooks/useVisibleEntityCountByType';
-import type { CanvasRenderableEntityType } from 'features/controlLayers/store/types';
+import type { CanvasEntityType } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStackBold } from 'react-icons/pi';
type Props = {
- type: CanvasRenderableEntityType;
+ type: CanvasEntityType;
};
export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/FLUXReduxImageInfluence.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/common/FLUXReduxImageInfluence.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx
index 096a182a00..151dbc95a2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx
@@ -5,7 +5,7 @@ import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konv
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance';
import type { CanvasEntityAdapterFromType } from 'features/controlLayers/konva/CanvasEntity/types';
-import type { CanvasEntityIdentifier, CanvasRenderableEntityType } from 'features/controlLayers/store/types';
+import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
import type { PropsWithChildren } from 'react';
import { createContext, memo, useContext, useMemo, useSyncExternalStore } from 'react';
import { assert } from 'tsafe';
@@ -96,15 +96,15 @@ export const RegionalGuidanceAdapterGate = memo(({ children }: PropsWithChildren
return {children};
});
-export const useEntityAdapterContext = (
+export const useEntityAdapterContext = (
type?: T
-): CanvasEntityAdapterFromType => {
+): CanvasEntityAdapterFromType => {
const adapter = useContext(EntityAdapterContext);
assert(adapter, 'useEntityIdentifier must be used within a EntityIdentifierProvider');
if (type) {
assert(adapter.entityIdentifier.type === type, 'useEntityIdentifier must be used with the correct type');
}
- return adapter as CanvasEntityAdapterFromType;
+ return adapter as CanvasEntityAdapterFromType;
};
RegionalGuidanceAdapterGate.displayName = 'RegionalGuidanceAdapterGate';
diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/RefImageIdContext.ts b/invokeai/frontend/web/src/features/controlLayers/contexts/RefImageIdContext.ts
new file mode 100644
index 0000000000..cd4a51c7b2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/contexts/RefImageIdContext.ts
@@ -0,0 +1,10 @@
+import { createContext, useContext } from 'react';
+import { assert } from 'tsafe';
+
+export const RefImageIdContext = createContext(null);
+
+export const useRefImageIdContext = (): string => {
+ const id = useContext(RefImageIdContext);
+ assert(id, 'useRefImageIdContext must be used within a RefImageIdContext.Provider');
+ return id;
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
index a1b0ba67c5..5bdbfb5555 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
@@ -9,13 +9,13 @@ import {
inpaintMaskDenoiseLimitAdded,
inpaintMaskNoiseAdded,
rasterLayerAdded,
- referenceImageAdded,
rgAdded,
rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectBase, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
+import { referenceImageAdded } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
index a1b2bac014..2e6437cd0b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
@@ -9,8 +9,6 @@ import {
controlLayerAdded,
entityRasterized,
rasterLayerAdded,
- referenceImageAdded,
- referenceImageIPAdapterImageChanged,
rgAdded,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasSlice';
@@ -20,6 +18,7 @@ import {
selectPositivePrompt,
selectSeed,
} from 'features/controlLayers/store/paramsSlice';
+import { referenceImageAdded, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasMetadata } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
@@ -306,13 +305,13 @@ export const usePullBboxIntoLayer = (entityIdentifier: CanvasEntityIdentifier<'c
return func;
};
-export const usePullBboxIntoGlobalReferenceImage = (entityIdentifier: CanvasEntityIdentifier<'reference_image'>) => {
+export const usePullBboxIntoGlobalReferenceImage = (id: string) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const arg = useMemo(() => {
const onSave = (imageDTO: ImageDTO, _: Rect) => {
- dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier, imageDTO }));
+ dispatch(referenceImageIPAdapterImageChanged({ id, imageDTO }));
};
return {
@@ -322,7 +321,7 @@ export const usePullBboxIntoGlobalReferenceImage = (entityIdentifier: CanvasEnti
toastOk: t('controlLayers.pullBboxIntoReferenceImageOk'),
toastError: t('controlLayers.pullBboxIntoReferenceImageError'),
};
- }, [dispatch, entityIdentifier, t]);
+ }, [dispatch, id, t]);
const func = useSaveCanvas(arg);
return func;
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts
index f04f000c41..fde57b6a78 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts
@@ -32,8 +32,6 @@ export const useEntityTypeName = (type: CanvasEntityIdentifier['type']) => {
return t('controlLayers.controlLayer');
case 'raster_layer':
return t('controlLayers.rasterLayer');
- case 'reference_image':
- return t('controlLayers.globalReferenceImage');
case 'regional_guidance':
return t('controlLayers.regionalGuidance');
default:
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts
index 693df61b61..cf70719c91 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts
@@ -17,8 +17,6 @@ export const useEntityTypeCount = (type: CanvasEntityIdentifier['type']): number
return canvas.inpaintMasks.entities.length;
case 'regional_guidance':
return canvas.regionalGuidance.entities.length;
- case 'reference_image':
- return canvas.referenceImages.entities.length;
default:
return 0;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeInformationalPopover.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeInformationalPopover.ts
index 3ec856f03c..d3368339ba 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeInformationalPopover.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeInformationalPopover.ts
@@ -13,9 +13,6 @@ export const useEntityTypeInformationalPopover = (type: CanvasEntityIdentifier['
return 'rasterLayer';
case 'regional_guidance':
return 'regionalGuidanceAndReferenceImage';
- case 'reference_image':
- return 'globalReferenceImage';
-
default:
return undefined;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts
index 7adbbc451b..04bc110fcc 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts
@@ -17,7 +17,6 @@ export const useEntityTypeIsHidden = (type: CanvasEntityIdentifier['type']): boo
return canvas.inpaintMasks.isHidden;
case 'regional_guidance':
return canvas.regionalGuidance.isHidden;
- case 'reference_image':
default:
return false;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts
index 164f737baa..968b9210e6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts
@@ -15,10 +15,6 @@ export const useEntityTypeString = (type: CanvasEntityIdentifier['type'], plural
return plural ? t('controlLayers.inpaintMask_withCount_other') : t('controlLayers.inpaintMask');
case 'regional_guidance':
return plural ? t('controlLayers.regionalGuidance_withCount_other') : t('controlLayers.regionalGuidance');
- case 'reference_image':
- return plural
- ? t('controlLayers.globalReferenceImage_withCount_other')
- : t('controlLayers.globalReferenceImage');
default:
return '';
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts
index def62664c3..f43cbf7961 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts
@@ -21,8 +21,6 @@ export const useEntityTypeTitle = (type: CanvasEntityIdentifier['type']): string
return t('controlLayers.inpaintMasks_withCount', { count, context });
case 'regional_guidance':
return t('controlLayers.regionalGuidance_withCount', { count, context });
- case 'reference_image':
- return t('controlLayers.globalReferenceImages_withCount', { count, context });
default:
return '';
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntityTypeEnabled.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntityTypeEnabled.ts
index 6d9619df66..6984677f3d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntityTypeEnabled.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntityTypeEnabled.ts
@@ -7,7 +7,6 @@ import {
selectIsImagen4,
selectIsSD3,
} from 'features/controlLayers/store/paramsSlice';
-import { selectActiveReferenceImageEntities } from 'features/controlLayers/store/selectors';
import type { CanvasEntityType } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
import type { Equals } from 'tsafe';
@@ -20,15 +19,9 @@ export const useIsEntityTypeEnabled = (entityType: CanvasEntityType) => {
const isImagen4 = useAppSelector(selectIsImagen4);
const isChatGPT4o = useAppSelector(selectIsChatGTP4o);
const isFluxKontext = useAppSelector(selectIsFluxKontext);
- const activeReferenceImageEntities = useAppSelector(selectActiveReferenceImageEntities);
const isEntityTypeEnabled = useMemo(() => {
switch (entityType) {
- case 'reference_image':
- if (isFluxKontext) {
- return activeReferenceImageEntities.length === 0;
- }
- return !isSD3 && !isCogView4 && !isImagen3 && !isImagen4;
case 'regional_guidance':
return !isSD3 && !isCogView4 && !isImagen3 && !isImagen4 && !isFluxKontext && !isChatGPT4o;
case 'control_layer':
@@ -40,7 +33,7 @@ export const useIsEntityTypeEnabled = (entityType: CanvasEntityType) => {
default:
assert>(false);
}
- }, [entityType, isSD3, isCogView4, isImagen3, isImagen4, isFluxKontext, isChatGPT4o, activeReferenceImageEntities]);
+ }, [entityType, isSD3, isCogView4, isImagen3, isImagen4, isFluxKontext, isChatGPT4o]);
return isEntityTypeEnabled;
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts
index 48384eead5..f6328f20b7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts
@@ -1,11 +1,11 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice, selectEntityIdentifierBelowThisOne } from 'features/controlLayers/store/selectors';
-import type { CanvasRenderableEntityIdentifier } from 'features/controlLayers/store/types';
+import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
-export const useEntityIdentifierBelowThisOne = (
+export const useEntityIdentifierBelowThisOne = (
entityIdentifier: T
): T | null => {
const selector = useMemo(
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useVisibleEntityCountByType.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useVisibleEntityCountByType.ts
index 654d1fdf89..5749eb20e7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useVisibleEntityCountByType.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useVisibleEntityCountByType.ts
@@ -4,7 +4,6 @@ import {
selectActiveControlLayerEntities,
selectActiveInpaintMaskEntities,
selectActiveRasterLayerEntities,
- selectActiveReferenceImageEntities,
selectActiveRegionalGuidanceEntities,
} from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
@@ -22,8 +21,6 @@ export const useVisibleEntityCountByType = (type: CanvasEntityIdentifier['type']
return createSelector(selectActiveInpaintMaskEntities, (entities) => entities.length);
case 'regional_guidance':
return createSelector(selectActiveRegionalGuidanceEntities, (entities) => entities.length);
- case 'reference_image':
- return createSelector(selectActiveReferenceImageEntities, (entities) => entities.length);
default:
assert(false, 'Invalid entity type');
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts
index 21099f76b6..dd85b709d2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts
@@ -21,9 +21,9 @@ import {
selectActiveRegionalGuidanceEntities,
} from 'features/controlLayers/store/selectors';
import type {
- CanvasRenderableEntityIdentifier,
- CanvasRenderableEntityState,
- CanvasRenderableEntityType,
+ CanvasEntityIdentifier,
+ CanvasEntityState,
+ CanvasEntityType,
GenerationMode,
Rect,
} from 'features/controlLayers/store/types';
@@ -91,7 +91,7 @@ export class CanvasCompositorModule extends CanvasModuleBase {
* @param type The optional entity type
* @returns The rect
*/
- getVisibleRectOfType = (type?: CanvasRenderableEntityType): Rect => {
+ getVisibleRectOfType = (type?: CanvasEntityType): Rect => {
const rects = [];
for (const adapter of this.manager.getAllAdapters()) {
@@ -139,8 +139,8 @@ export class CanvasCompositorModule extends CanvasModuleBase {
* @param type The entity type
* @returns The adapters for the given entity type that are eligible to be included in a composite
*/
- getVisibleAdaptersOfType = (type: T): CanvasEntityAdapterFromType[] => {
- let entities: CanvasRenderableEntityState[];
+ getVisibleAdaptersOfType = (type: T): CanvasEntityAdapterFromType[] => {
+ let entities: CanvasEntityState[];
switch (type) {
case 'raster_layer':
@@ -327,7 +327,7 @@ export class CanvasCompositorModule extends CanvasModuleBase {
* @param deleteMergedEntities Whether to delete the merged entities after creating the new merged entity
* @returns A promise that resolves to the image DTO, or null if the merge failed
*/
- mergeByEntityIdentifiers = async (
+ mergeByEntityIdentifiers = async (
entityIdentifiers: T[],
deleteMergedEntities: boolean
): Promise => {
@@ -402,8 +402,8 @@ export class CanvasCompositorModule extends CanvasModuleBase {
* @param type The type of entity to merge
* @returns A promise that resolves to the image DTO, or null if the merge failed
*/
- mergeVisibleOfType = (type: CanvasRenderableEntityType): Promise => {
- let entities: CanvasRenderableEntityState[];
+ mergeVisibleOfType = (type: CanvasEntityType): Promise => {
+ let entities: CanvasEntityState[];
switch (type) {
case 'raster_layer':
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 0d2e299cb1..c89f9e376a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts
@@ -26,7 +26,7 @@ import {
} from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
- CanvasRenderableEntityState,
+ CanvasEntityState,
LifecycleCallback,
Rect,
} from 'features/controlLayers/store/types';
@@ -42,7 +42,7 @@ import { assert } from 'tsafe';
import type { Jsonifiable, JsonObject } from 'type-fest';
export abstract class CanvasEntityAdapterBase<
- T extends CanvasRenderableEntityState,
+ T extends CanvasEntityState,
U extends string,
> extends CanvasModuleBase {
readonly type: U;
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer.ts
index 517a4998a8..e33eda59c4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer.ts
@@ -9,7 +9,7 @@ import { addCoords, getKonvaNodeDebugAttrs, getPrefixedId } from 'features/contr
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import type { FilterConfig } from 'features/controlLayers/store/filters';
import { getFilterForModel, IMAGE_FILTERS } from 'features/controlLayers/store/filters';
-import type { CanvasImageState, CanvasRenderableEntityType } from 'features/controlLayers/store/types';
+import type { CanvasImageState, CanvasEntityType } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { toast } from 'features/toast/toast';
import Konva from 'konva';
@@ -373,7 +373,7 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
* Saves the filtered image as a new entity of the given type.
* @param type The type of entity to save the filtered image as.
*/
- saveAs = (type: CanvasRenderableEntityType) => {
+ saveAs = (type: CanvasEntityType) => {
const imageState = this.$imageState.get();
if (!imageState) {
this.log.warn('No image state to apply filter to');
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/types.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/types.ts
index da8402f0f7..47dc71e128 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/types.ts
@@ -2,7 +2,7 @@ import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/kon
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance';
-import type { CanvasRenderableEntityType } from 'features/controlLayers/store/types';
+import type { CanvasEntityType } from 'features/controlLayers/store/types';
export type CanvasEntityAdapter =
| CanvasEntityAdapterRasterLayer
@@ -10,7 +10,7 @@ export type CanvasEntityAdapter =
| CanvasEntityAdapterInpaintMask
| CanvasEntityAdapterRegionalGuidance;
-export type CanvasEntityAdapterFromType = Extract<
+export type CanvasEntityAdapterFromType = Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>;
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
index 52592c8019..b0be88c8a3 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
@@ -16,11 +16,7 @@ import { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/Canvas
import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
-import type {
- CanvasEntityIdentifier,
- CanvasRenderableEntityIdentifier,
- CanvasRenderableEntityType,
-} from 'features/controlLayers/store/types';
+import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
import {
isControlLayerEntityIdentifier,
isInpaintMaskEntityIdentifier,
@@ -135,7 +131,7 @@ export class CanvasManager extends CanvasModuleBase {
this.konva.previewLayer.add(this.tool.konva.group);
}
- getAdapter = (
+ getAdapter = (
entityIdentifier: CanvasEntityIdentifier
): CanvasEntityAdapterFromType | null => {
let adapter: CanvasEntityAdapter | undefined;
@@ -163,7 +159,7 @@ export class CanvasManager extends CanvasModuleBase {
return adapter as CanvasEntityAdapterFromType;
};
- deleteAdapter = (entityIdentifier: CanvasRenderableEntityIdentifier): boolean => {
+ deleteAdapter = (entityIdentifier: CanvasEntityIdentifier): boolean => {
switch (entityIdentifier.type) {
case 'raster_layer':
return this.adapters.rasterLayers.delete(entityIdentifier.id);
@@ -178,7 +174,7 @@ export class CanvasManager extends CanvasModuleBase {
}
};
- getAdapters = (entityIdentifiers: CanvasRenderableEntityIdentifier[]): CanvasEntityAdapter[] => {
+ getAdapters = (entityIdentifiers: CanvasEntityIdentifier[]): CanvasEntityAdapter[] => {
const adapters: CanvasEntityAdapter[] = [];
for (const entityIdentifier of entityIdentifiers) {
const adapter = this.getAdapter(entityIdentifier);
@@ -199,7 +195,7 @@ export class CanvasManager extends CanvasModuleBase {
];
};
- createAdapter = (entityIdentifier: CanvasRenderableEntityIdentifier): CanvasEntityAdapter => {
+ createAdapter = (entityIdentifier: CanvasEntityIdentifier): CanvasEntityAdapter => {
if (isRasterLayerEntityIdentifier(entityIdentifier)) {
const adapter = new CanvasEntityAdapterRasterLayer(entityIdentifier, this);
this.adapters.rasterLayers.set(adapter.id, adapter);
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasSegmentAnythingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasSegmentAnythingModule.ts
index a1963bb971..d8a0e2154c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasSegmentAnythingModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasSegmentAnythingModule.ts
@@ -16,7 +16,7 @@ import {
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import type {
CanvasImageState,
- CanvasRenderableEntityType,
+ CanvasEntityType,
Coordinate,
RgbaColor,
SAMPointLabel,
@@ -703,7 +703,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
* Saves the segmented image as a new entity of the given type.
* @param type The type of entity to save the segmented image as.
*/
- saveAs = (type: CanvasRenderableEntityType) => {
+ saveAs = (type: CanvasEntityType) => {
const imageState = this.$imageState.get();
if (!imageState) {
this.log.error('No image state to save as');
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts
index 60bfd2f309..26fc3d8c24 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts
@@ -48,7 +48,7 @@ import type {
Rect,
RgbaColor,
} from 'features/controlLayers/store/types';
-import { isRenderableEntityIdentifier, RGBA_BLACK } from 'features/controlLayers/store/types';
+import { RGBA_BLACK } from 'features/controlLayers/store/types';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { atom, computed } from 'nanostores';
import type { Logger } from 'roarr';
@@ -576,9 +576,6 @@ export class CanvasStateApiModule extends CanvasModuleBase {
if (!state.selectedEntityIdentifier) {
return null;
}
- if (!isRenderableEntityIdentifier(state.selectedEntityIdentifier)) {
- return null;
- }
return this.manager.getAdapter(state.selectedEntityIdentifier);
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
index b110f97d0d..bc0f88e4c7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
@@ -22,7 +22,6 @@ import type {
Coordinate,
Tool,
} from 'features/controlLayers/store/types';
-import { isRenderableEntityType } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import { atom } from 'nanostores';
@@ -180,7 +179,7 @@ export class CanvasToolModule extends CanvasModuleBase {
this.tools.bbox.syncCursorStyle();
} else if (tool === 'colorPicker') {
this.tools.colorPicker.syncCursorStyle();
- } else if (selectedEntityAdapter && isRenderableEntityType(selectedEntityAdapter.entityIdentifier.type)) {
+ } else if (selectedEntityAdapter) {
if (selectedEntityAdapter.$isDisabled.get()) {
stage.setCursor('not-allowed');
} else if (selectedEntityAdapter.$isEntityTypeHidden.get()) {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index 22eee9b0c2..4715679a65 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -36,10 +36,9 @@ import { ASPECT_RATIO_MAP } from 'features/parameters/components/Bbox/constants'
import { API_BASE_MODELS } from 'features/parameters/types/constants';
import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import type { IRect } from 'konva/lib/types';
-import { isEqual, merge } from 'lodash-es';
+import { merge } from 'lodash-es';
import type { UndoableOptions } from 'redux-undo';
import type {
- ApiModelConfig,
ControlLoRAModelConfig,
ControlNetModelConfig,
FLUXReduxModelConfig,
@@ -47,7 +46,6 @@ import type {
IPAdapterModelConfig,
T2IAdapterModelConfig,
} from 'services/api/types';
-import { assert } from 'tsafe';
import type {
AspectRatioID,
@@ -55,7 +53,6 @@ import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasRasterLayerState,
- CanvasReferenceImageState,
CanvasRegionalGuidanceState,
CanvasState,
CLIPVisionModelV2,
@@ -77,20 +74,16 @@ import {
isChatGPT4oAspectRatioID,
isFluxKontextAspectRatioID,
isImagenAspectRatioID,
- isRenderableEntity,
} from './types';
import {
converters,
getControlLayerState,
getInpaintMaskState,
getRasterLayerState,
- getReferenceImageState,
getRegionalGuidanceState,
imageDTOToImageWithDims,
- initialChatGPT4oReferenceImage,
initialControlLoRA,
initialControlNet,
- initialFluxKontextReferenceImage,
initialFLUXRedux,
initialIPAdapter,
initialT2IAdapter,
@@ -560,204 +553,6 @@ export const canvasSlice = createSlice({
}
layer.withTransparencyEffect = !layer.withTransparencyEffect;
},
- //#region Global Reference Images
- referenceImageAdded: {
- reducer: (
- state,
- action: PayloadAction<{
- id: string;
- overrides?: Partial;
- isSelected?: boolean;
- isBookmarked?: boolean;
- }>
- ) => {
- const { id, overrides, isSelected, isBookmarked } = action.payload;
- const entityState = getReferenceImageState(id, overrides);
-
- state.referenceImages.entities.push(entityState);
- const entityIdentifier = getEntityIdentifier(entityState);
-
- if (isSelected) {
- state.selectedEntityIdentifier = entityIdentifier;
- }
-
- if (isBookmarked) {
- state.bookmarkedEntityIdentifier = entityIdentifier;
- }
- },
- prepare: (payload?: {
- overrides?: Partial;
- isSelected?: boolean;
- isBookmarked?: boolean;
- }) => ({
- payload: { ...payload, id: getPrefixedId('reference_image') },
- }),
- },
- referenceImageRecalled: (state, action: PayloadAction<{ data: CanvasReferenceImageState }>) => {
- const { data } = action.payload;
- state.referenceImages.entities.push(data);
- state.selectedEntityIdentifier = { type: 'reference_image', id: data.id };
- },
- referenceImageIPAdapterImageChanged: (
- state,
- action: PayloadAction>
- ) => {
- const { entityIdentifier, imageDTO } = action.payload;
- const entity = selectEntity(state, entityIdentifier);
- if (!entity) {
- return;
- }
- entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
- },
- referenceImageIPAdapterMethodChanged: (
- state,
- action: PayloadAction>
- ) => {
- const { entityIdentifier, method } = action.payload;
- const entity = selectEntity(state, entityIdentifier);
- if (!entity) {
- return;
- }
- if (entity.ipAdapter.type !== 'ip_adapter') {
- return;
- }
- entity.ipAdapter.method = method;
- },
- referenceImageIPAdapterFLUXReduxImageInfluenceChanged: (
- state,
- action: PayloadAction>
- ) => {
- const { entityIdentifier, imageInfluence } = action.payload;
- const entity = selectEntity(state, entityIdentifier);
- if (!entity) {
- return;
- }
- if (entity.ipAdapter.type !== 'flux_redux') {
- return;
- }
- entity.ipAdapter.imageInfluence = imageInfluence;
- },
- referenceImageIPAdapterModelChanged: (
- state,
- action: PayloadAction<
- EntityIdentifierPayload<
- { modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig | null },
- 'reference_image'
- >
- >
- ) => {
- const { entityIdentifier, modelConfig } = action.payload;
- const entity = selectEntity(state, entityIdentifier);
- if (!entity) {
- return;
- }
-
- const oldModel = entity.ipAdapter.model;
-
- // First set the new model
- entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
-
- if (!entity.ipAdapter.model) {
- return;
- }
-
- if (isEqual(oldModel, entity.ipAdapter.model)) {
- // Nothing changed, so we don't need to do anything
- return;
- }
-
- // The type of ref image depends on the model. When the user switches the model, we rebuild the ref image.
- // When we switch the model, we keep the image the same, but change the other parameters.
-
- if (entity.ipAdapter.model.base === 'chatgpt-4o') {
- // Switching to chatgpt-4o ref image
- entity.ipAdapter = {
- ...initialChatGPT4oReferenceImage,
- image: entity.ipAdapter.image,
- model: entity.ipAdapter.model,
- };
- return;
- }
-
- if (entity.ipAdapter.model.base === 'flux-kontext') {
- // Switching to flux-kontext
- entity.ipAdapter = {
- ...initialFluxKontextReferenceImage,
- image: entity.ipAdapter.image,
- model: entity.ipAdapter.model,
- };
- return;
- }
-
- if (entity.ipAdapter.model.type === 'flux_redux') {
- // Switching to flux_redux
- entity.ipAdapter = {
- ...initialFLUXRedux,
- image: entity.ipAdapter.image,
- model: entity.ipAdapter.model,
- };
- return;
- }
-
- if (entity.ipAdapter.model.type === 'ip_adapter') {
- // Switching to ip_adapter
- entity.ipAdapter = {
- ...initialIPAdapter,
- image: entity.ipAdapter.image,
- model: entity.ipAdapter.model,
- };
- // Ensure that the IP Adapter model is compatible with the CLIP Vision model
- if (entity.ipAdapter.model?.base === 'flux') {
- entity.ipAdapter.clipVisionModel = 'ViT-L';
- } else if (entity.ipAdapter.clipVisionModel === 'ViT-L') {
- // Fall back to ViT-H (ViT-G would also work)
- entity.ipAdapter.clipVisionModel = 'ViT-H';
- }
- return;
- }
- },
- referenceImageIPAdapterCLIPVisionModelChanged: (
- state,
- action: PayloadAction>
- ) => {
- const { entityIdentifier, clipVisionModel } = action.payload;
- const entity = selectEntity(state, entityIdentifier);
- if (!entity) {
- return;
- }
- if (entity.ipAdapter.type !== 'ip_adapter') {
- return;
- }
- entity.ipAdapter.clipVisionModel = clipVisionModel;
- },
- referenceImageIPAdapterWeightChanged: (
- state,
- action: PayloadAction>
- ) => {
- const { entityIdentifier, weight } = action.payload;
- const entity = selectEntity(state, entityIdentifier);
- if (!entity) {
- return;
- }
- if (entity.ipAdapter.type !== 'ip_adapter') {
- return;
- }
- entity.ipAdapter.weight = weight;
- },
- referenceImageIPAdapterBeginEndStepPctChanged: (
- state,
- action: PayloadAction>
- ) => {
- const { entityIdentifier, beginEndStepPct } = action.payload;
- const entity = selectEntity(state, entityIdentifier);
- if (!entity) {
- return;
- }
- if (entity.ipAdapter.type !== 'ip_adapter') {
- return;
- }
- entity.ipAdapter.beginEndStepPct = beginEndStepPct;
- },
//#region Regional Guidance
rgAdded: {
reducer: (
@@ -1466,13 +1261,10 @@ export const canvasSlice = createSlice({
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
- } else if (isRenderableEntity(entity)) {
- entity.isEnabled = true;
- entity.objects = [];
- entity.position = { x: 0, y: 0 };
- } else {
- assert(false, 'Not implemented');
}
+ entity.isEnabled = true;
+ entity.objects = [];
+ entity.position = { x: 0, y: 0 };
},
entityDuplicated: (state, action: PayloadAction) => {
const { entityIdentifier } = action.payload;
@@ -1501,10 +1293,6 @@ export const canvasSlice = createSlice({
}
state.regionalGuidance.entities.push(newEntity);
break;
- case 'reference_image':
- newEntity.id = getPrefixedId('reference_image');
- state.referenceImages.entities.push(newEntity);
- break;
case 'inpaint_mask':
newEntity.id = getPrefixedId('inpaint_mask');
state.inpaintMasks.entities.push(newEntity);
@@ -1558,9 +1346,7 @@ export const canvasSlice = createSlice({
return;
}
- if (isRenderableEntity(entity)) {
- entity.position = position;
- }
+ entity.position = position;
},
entityMovedBy: (state, action: PayloadAction) => {
const { entityIdentifier, offset } = action.payload;
@@ -1569,10 +1355,6 @@ export const canvasSlice = createSlice({
return;
}
- if (!isRenderableEntity(entity)) {
- return;
- }
-
entity.position.x += offset.x;
entity.position.y += offset.y;
},
@@ -1583,11 +1365,9 @@ export const canvasSlice = createSlice({
return;
}
- if (isRenderableEntity(entity)) {
- if (replaceObjects) {
- entity.objects = [imageObject];
- entity.position = position;
- }
+ if (replaceObjects) {
+ entity.objects = [imageObject];
+ entity.position = position;
}
if (isSelected) {
@@ -1601,10 +1381,6 @@ export const canvasSlice = createSlice({
return;
}
- if (!isRenderableEntity(entity)) {
- assert(false, `Cannot add a brush line to a non-drawable entity of type ${entity.type}`);
- }
-
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
// re-render it (reference equality check). I don't like this behaviour.
entity.objects.push({
@@ -1620,10 +1396,6 @@ export const canvasSlice = createSlice({
return;
}
- if (!isRenderableEntity(entity)) {
- assert(false, `Cannot add a eraser line to a non-drawable entity of type ${entity.type}`);
- }
-
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
// re-render it (reference equality check). I don't like this behaviour.
entity.objects.push({
@@ -1639,10 +1411,6 @@ export const canvasSlice = createSlice({
return;
}
- if (!isRenderableEntity(entity)) {
- assert(false, `Cannot add a rect to a non-drawable entity of type ${entity.type}`);
- }
-
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
// re-render it (reference equality check). I don't like this behaviour.
entity.objects.push({ ...rect });
@@ -1673,9 +1441,6 @@ export const canvasSlice = createSlice({
(rg) => rg.id !== entityIdentifier.id
);
break;
- case 'reference_image':
- state.referenceImages.entities = state.referenceImages.entities.filter((rg) => rg.id !== entityIdentifier.id);
- break;
case 'inpaint_mask':
state.inpaintMasks.entities = state.inpaintMasks.entities.filter((rg) => rg.id !== entityIdentifier.id);
break;
@@ -1747,12 +1512,6 @@ export const canvasSlice = createSlice({
entityIdentifiers as CanvasEntityIdentifier<'regional_guidance'>[]
);
break;
- case 'reference_image':
- state.referenceImages.entities = reorderEntities(
- state.referenceImages.entities,
- entityIdentifiers as CanvasEntityIdentifier<'reference_image'>[]
- );
- break;
}
},
entityOpacityChanged: (state, action: PayloadAction>) => {
@@ -1761,9 +1520,6 @@ export const canvasSlice = createSlice({
if (!entity) {
return;
}
- if (entity.type === 'reference_image') {
- return;
- }
entity.opacity = opacity;
},
allEntitiesOfTypeIsHiddenToggled: (state, action: PayloadAction<{ type: CanvasEntityIdentifier['type'] }>) => {
@@ -1782,9 +1538,6 @@ export const canvasSlice = createSlice({
case 'regional_guidance':
state.regionalGuidance.isHidden = !state.regionalGuidance.isHidden;
break;
- case 'reference_image':
- // no-op
- break;
}
},
allEntitiesDeleted: (state) => {
@@ -1794,14 +1547,12 @@ export const canvasSlice = createSlice({
state.controlLayers = initialState.controlLayers;
state.inpaintMasks = initialState.inpaintMasks;
state.regionalGuidance = initialState.regionalGuidance;
- state.referenceImages = initialState.referenceImages;
},
canvasMetadataRecalled: (state, action: PayloadAction) => {
- const { controlLayers, inpaintMasks, rasterLayers, referenceImages, regionalGuidance } = action.payload;
+ const { controlLayers, inpaintMasks, rasterLayers, regionalGuidance } = action.payload;
state.controlLayers.entities = controlLayers;
state.inpaintMasks.entities = inpaintMasks;
state.rasterLayers.entities = rasterLayers;
- state.referenceImages.entities = referenceImages;
state.regionalGuidance.entities = regionalGuidance;
return state;
},
@@ -1928,16 +1679,6 @@ export const {
controlLayerWeightChanged,
controlLayerBeginEndStepPctChanged,
controlLayerWithTransparencyEffectToggled,
- // IP Adapters
- referenceImageAdded,
- // referenceImageRecalled,
- referenceImageIPAdapterImageChanged,
- referenceImageIPAdapterMethodChanged,
- referenceImageIPAdapterModelChanged,
- referenceImageIPAdapterCLIPVisionModelChanged,
- referenceImageIPAdapterWeightChanged,
- referenceImageIPAdapterBeginEndStepPctChanged,
- referenceImageIPAdapterFLUXReduxImageInfluenceChanged,
// Regions
rgAdded,
// rgRecalled,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
new file mode 100644
index 0000000000..8188342c11
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
@@ -0,0 +1,323 @@
+import type { PayloadAction } from '@reduxjs/toolkit';
+import { createSelector, createSlice } from '@reduxjs/toolkit';
+import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
+import type { PersistConfig, RootState } from 'app/store/store';
+import { deepClone } from 'common/util/deepClone';
+import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { canvasMetadataRecalled } from 'features/controlLayers/store/canvasSlice';
+import type { FLUXReduxImageInfluence, RefImagesState } from 'features/controlLayers/store/types';
+import { zModelIdentifierField } from 'features/nodes/types/common';
+import { isEqual } from 'lodash-es';
+import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConfig } from 'services/api/types';
+import { assert } from 'tsafe';
+import type { PartialDeep } from 'type-fest';
+
+import type { CanvasReferenceImageState, CLIPVisionModelV2, IPMethodV2 } from './types';
+import { getInitialRefImagesState } from './types';
+import {
+ getReferenceImageState,
+ imageDTOToImageWithDims,
+ initialChatGPT4oReferenceImage,
+ initialFLUXRedux,
+ initialIPAdapter,
+} from './util';
+
+type PayloadWithId = T extends void
+ ? { id: string }
+ : {
+ id: string;
+ } & T;
+
+export const refImagesSlice = createSlice({
+ name: 'refImages',
+ initialState: getInitialRefImagesState(),
+ reducers: {
+ referenceImageAdded: {
+ reducer: (
+ state,
+ action: PayloadAction<{
+ id: string;
+ overrides?: PartialDeep;
+ isSelected?: boolean;
+ }>
+ ) => {
+ const { id, overrides, isSelected } = action.payload;
+ const entityState = getReferenceImageState(id, overrides);
+
+ state.entities.push(entityState);
+
+ if (isSelected) {
+ state.selectedId = entityState.id;
+ }
+ },
+ prepare: (payload?: { overrides?: PartialDeep; isSelected?: boolean }) => ({
+ payload: { ...payload, id: getPrefixedId('reference_image') },
+ }),
+ },
+ referenceImageRecalled: (state, action: PayloadAction<{ data: CanvasReferenceImageState }>) => {
+ const { data } = action.payload;
+ state.entities.push(data);
+ state.selectedId = data.id;
+ },
+ referenceImageIPAdapterImageChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { id, imageDTO } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
+ },
+ referenceImageIPAdapterMethodChanged: (state, action: PayloadAction>) => {
+ const { id, method } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ if (entity.ipAdapter.type !== 'ip_adapter') {
+ return;
+ }
+ entity.ipAdapter.method = method;
+ },
+ referenceImageIPAdapterFLUXReduxImageInfluenceChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { id, imageInfluence } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ if (entity.ipAdapter.type !== 'flux_redux') {
+ return;
+ }
+ entity.ipAdapter.imageInfluence = imageInfluence;
+ },
+ referenceImageIPAdapterModelChanged: (
+ state,
+ action: PayloadAction<
+ PayloadWithId<{ modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig | null }>
+ >
+ ) => {
+ const { id, modelConfig } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+
+ const oldModel = entity.ipAdapter.model;
+
+ // First set the new model
+ entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
+
+ if (!entity.ipAdapter.model) {
+ return;
+ }
+
+ if (isEqual(oldModel, entity.ipAdapter.model)) {
+ // Nothing changed, so we don't need to do anything
+ return;
+ }
+
+ // The type of ref image depends on the model. When the user switches the model, we rebuild the ref image.
+ // When we switch the model, we keep the image the same, but change the other parameters.
+
+ if (entity.ipAdapter.model.base === 'chatgpt-4o') {
+ // Switching to chatgpt-4o ref image
+ entity.ipAdapter = {
+ ...initialChatGPT4oReferenceImage,
+ image: entity.ipAdapter.image,
+ model: entity.ipAdapter.model,
+ };
+ return;
+ }
+
+ if (entity.ipAdapter.model.type === 'flux_redux') {
+ // Switching to flux_redux
+ entity.ipAdapter = {
+ ...initialFLUXRedux,
+ image: entity.ipAdapter.image,
+ model: entity.ipAdapter.model,
+ };
+ return;
+ }
+
+ if (entity.ipAdapter.model.type === 'ip_adapter') {
+ // Switching to ip_adapter
+ entity.ipAdapter = {
+ ...initialIPAdapter,
+ image: entity.ipAdapter.image,
+ model: entity.ipAdapter.model,
+ };
+ // Ensure that the IP Adapter model is compatible with the CLIP Vision model
+ if (entity.ipAdapter.model?.base === 'flux') {
+ entity.ipAdapter.clipVisionModel = 'ViT-L';
+ } else if (entity.ipAdapter.clipVisionModel === 'ViT-L') {
+ // Fall back to ViT-H (ViT-G would also work)
+ entity.ipAdapter.clipVisionModel = 'ViT-H';
+ }
+ return;
+ }
+ },
+ referenceImageIPAdapterCLIPVisionModelChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { id, clipVisionModel } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ if (entity.ipAdapter.type !== 'ip_adapter') {
+ return;
+ }
+ entity.ipAdapter.clipVisionModel = clipVisionModel;
+ },
+ referenceImageIPAdapterWeightChanged: (state, action: PayloadAction>) => {
+ const { id, weight } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ if (entity.ipAdapter.type !== 'ip_adapter') {
+ return;
+ }
+ entity.ipAdapter.weight = weight;
+ },
+ referenceImageIPAdapterBeginEndStepPctChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { id, beginEndStepPct } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ if (entity.ipAdapter.type !== 'ip_adapter') {
+ return;
+ }
+ entity.ipAdapter.beginEndStepPct = beginEndStepPct;
+ },
+ //#region Shared entity
+ entitySelected: (state, action: PayloadAction<{ id: string }>) => {
+ const { id } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ // Cannot select a non-existent entity
+ return;
+ }
+ state.selectedId = id;
+ },
+ entityNameChanged: (state, action: PayloadAction>) => {
+ const { id, name } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ entity.name = name;
+ },
+ entityDuplicated: (state, action: PayloadAction) => {
+ const { id } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+
+ const newEntity = deepClone(entity);
+ if (newEntity.name) {
+ newEntity.name = `${newEntity.name} (Copy)`;
+ }
+ newEntity.id = getPrefixedId('reference_image');
+ state.entities.push(newEntity);
+
+ state.selectedId = newEntity.id;
+ },
+ entityIsEnabledToggled: (state, action: PayloadAction) => {
+ const { id } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ entity.isEnabled = !entity.isEnabled;
+ },
+ entityIsLockedToggled: (state, action: PayloadAction) => {
+ const { id } = action.payload;
+ const entity = selectRefImageEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ entity.isLocked = !entity.isLocked;
+ },
+ entityDeleted: (state, action: PayloadAction) => {
+ const { id } = action.payload;
+
+ let selectedId: string | null = null;
+ const entities = state.entities;
+ const index = entities.findIndex((entity) => entity.id === id);
+ const nextIndex = entities.length > 1 ? (index + 1) % entities.length : -1;
+ if (nextIndex !== -1) {
+ const nextEntity = entities[nextIndex];
+ if (nextEntity) {
+ selectedId = nextEntity.id;
+ }
+ }
+ state.entities = state.entities.filter((rg) => rg.id !== id);
+ state.selectedId = selectedId;
+ },
+ refImagesReset: () => getInitialRefImagesState(),
+ },
+ extraReducers(builder) {
+ builder.addCase(canvasMetadataRecalled, (state, action) => {
+ const { referenceImages } = action.payload;
+ state.entities = referenceImages;
+ });
+ },
+});
+
+export const {
+ referenceImageAdded,
+ // referenceImageRecalled,
+ referenceImageIPAdapterImageChanged,
+ referenceImageIPAdapterMethodChanged,
+ referenceImageIPAdapterModelChanged,
+ referenceImageIPAdapterCLIPVisionModelChanged,
+ referenceImageIPAdapterWeightChanged,
+ referenceImageIPAdapterBeginEndStepPctChanged,
+ referenceImageIPAdapterFLUXReduxImageInfluenceChanged,
+} = refImagesSlice.actions;
+
+/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+const migrate = (state: any): any => {
+ return state;
+};
+
+export const refImagesPersistConfig: PersistConfig = {
+ name: refImagesSlice.name,
+ initialState: getInitialRefImagesState(),
+ migrate,
+ persistDenylist: [],
+};
+
+export const selectRefImagesSlice = (state: RootState) => state.refImages;
+
+export const selectReferenceImageEntities = createSelector(selectRefImagesSlice, (state) => state.entities);
+export const selectActiveReferenceImageEntities = createSelector(selectReferenceImageEntities, (entities) =>
+ entities.filter((e) => e.isEnabled)
+);
+export const selectRefImageEntityIds = createMemoizedSelector(selectReferenceImageEntities, (entities) =>
+ entities.map((e) => e.id)
+);
+export const selectRefImageEntity = (state: RefImagesState, id: string) =>
+ state.entities.find((entity) => entity.id === id) ?? null;
+
+export function selectRefImageEntityOrThrow(
+ state: RefImagesState,
+ id: string,
+ caller: string
+): CanvasReferenceImageState {
+ const entity = selectRefImageEntity(state, id);
+ assert(entity, `Entity with id ${id} not found in ${caller}`);
+ return entity;
+}
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
index cffa517fd7..4e52436c6f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
@@ -2,17 +2,16 @@ import type { Selector } from '@reduxjs/toolkit';
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasEntityState,
+ CanvasEntityType,
CanvasInpaintMaskState,
CanvasMetadata,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
- CanvasRenderableEntityIdentifier,
- CanvasRenderableEntityState,
- CanvasRenderableEntityType,
CanvasState,
} from 'features/controlLayers/store/types';
import { getGridSize, getOptimalDimension } from 'features/parameters/util/optimalDimension';
@@ -40,14 +39,13 @@ export const createCanvasSelector = (selector: Selector) =>
const selectEntityCountAll = createCanvasSelector((canvas) => {
return (
canvas.regionalGuidance.entities.length +
- canvas.referenceImages.entities.length +
canvas.rasterLayers.entities.length +
canvas.controlLayers.entities.length +
canvas.inpaintMasks.entities.length
);
});
-const isVisibleEntity = (entity: CanvasRenderableEntityState) => entity.isEnabled && entity.objects.length > 0;
+const isVisibleEntity = (entity: CanvasEntityState) => entity.isEnabled && entity.objects.length > 0;
export const selectRasterLayerEntities = createCanvasSelector((canvas) => canvas.rasterLayers.entities);
export const selectActiveRasterLayerEntities = createSelector(selectRasterLayerEntities, (entities) =>
@@ -69,11 +67,6 @@ export const selectActiveRegionalGuidanceEntities = createSelector(selectRegiona
entities.filter(isVisibleEntity)
);
-export const selectReferenceImageEntities = createCanvasSelector((canvas) => canvas.referenceImages.entities);
-export const selectActiveReferenceImageEntities = createSelector(selectReferenceImageEntities, (entities) =>
- entities.filter((e) => e.isEnabled)
-);
-
/**
* Selects the total _active_ canvas entity count:
* - Regions
@@ -89,20 +82,17 @@ export const selectEntityCountActive = createSelector(
selectActiveControlLayerEntities,
selectActiveInpaintMaskEntities,
selectActiveRegionalGuidanceEntities,
- selectActiveReferenceImageEntities,
(
activeRasterLayerEntities,
activeControlLayerEntities,
activeInpaintMaskEntities,
- activeRegionalGuidanceEntities,
- activeIPAdapterEntities
+ activeRegionalGuidanceEntities
) => {
return (
activeRasterLayerEntities.length +
activeControlLayerEntities.length +
activeInpaintMaskEntities.length +
- activeRegionalGuidanceEntities.length +
- activeIPAdapterEntities.length
+ activeRegionalGuidanceEntities.length
);
}
);
@@ -153,9 +143,6 @@ export function selectEntity(
case 'regional_guidance':
entity = state.regionalGuidance.entities.find((entity) => entity.id === id);
break;
- case 'reference_image':
- entity = state.referenceImages.entities.find((entity) => entity.id === id);
- break;
}
// This cast is safe, but TS seems to be unable to infer the type
@@ -165,13 +152,13 @@ export function selectEntity(
/**
* Selects the entity identifier for the entity that is below the given entity in terms of draw order.
*/
-export function selectEntityIdentifierBelowThisOne(
+export function selectEntityIdentifierBelowThisOne(
state: CanvasState,
entityIdentifier: T
): Extract | undefined {
const { id, type } = entityIdentifier;
- let entities: CanvasRenderableEntityState[];
+ let entities: CanvasEntityState[];
switch (type) {
case 'raster_layer': {
@@ -244,9 +231,6 @@ export function selectAllEntitiesOfType(
case 'regional_guidance':
entities = state.regionalGuidance.entities;
break;
- case 'reference_image':
- entities = state.referenceImages.entities;
- break;
}
// This cast is safe, but TS seems to be unable to infer the type
@@ -259,7 +243,6 @@ export function selectAllEntitiesOfType(
export function selectAllEntities(state: CanvasState): CanvasEntityState[] {
// These are in the same order as they are displayed in the list!
return [
- ...state.referenceImages.entities.toReversed(),
...state.inpaintMasks.entities.toReversed(),
...state.regionalGuidance.entities.toReversed(),
...state.controlLayers.entities.toReversed(),
@@ -340,7 +323,7 @@ const selectRegionalGuidanceIsHidden = createCanvasSelector((canvas) => canvas.r
/**
* Returns the hidden selector for the given entity type.
*/
-export const getSelectIsTypeHidden = (type: CanvasRenderableEntityType) => {
+export const getSelectIsTypeHidden = (type: CanvasEntityType) => {
switch (type) {
case 'raster_layer':
return selectRasterLayersIsHidden;
@@ -379,9 +362,6 @@ export const buildSelectHasObjects = (entityIdentifier: CanvasEntityIdentifier)
if (!entity) {
return false;
}
- if (entity.type === 'reference_image') {
- return entity.ipAdapter.image !== null;
- }
return entity.objects.length > 0;
});
};
@@ -397,9 +377,10 @@ export const selectBboxModelBase = createSelector(selectBbox, (bbox) => bbox.mod
export const selectCanvasMetadata = createSelector(
selectCanvasSlice,
- (canvas): { canvas_v2_metadata: CanvasMetadata } => {
+ selectReferenceImageEntities,
+ (canvas, refImageEntities): { canvas_v2_metadata: CanvasMetadata } => {
const canvas_v2_metadata: CanvasMetadata = {
- referenceImages: selectAllEntitiesOfType(canvas, 'reference_image'),
+ referenceImages: refImageEntities,
controlLayers: selectAllEntitiesOfType(canvas, 'control_layer'),
inpaintMasks: selectAllEntitiesOfType(canvas, 'inpaint_mask'),
rasterLayers: selectAllEntitiesOfType(canvas, 'raster_layer'),
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 83912c5b4d..8914e5805c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -408,25 +408,14 @@ const zCanvasEntityState = z.discriminatedUnion('type', [
zCanvasControlLayerState,
zCanvasRegionalGuidanceState,
zCanvasInpaintMaskState,
- zCanvasReferenceImageState,
]);
export type CanvasEntityState = z.infer;
-const zCanvasRenderableEntityState = z.discriminatedUnion('type', [
- zCanvasRasterLayerState,
- zCanvasControlLayerState,
- zCanvasRegionalGuidanceState,
- zCanvasInpaintMaskState,
-]);
-export type CanvasRenderableEntityState = z.infer;
-export type CanvasRenderableEntityType = CanvasRenderableEntityState['type'];
-
const zCanvasEntityType = z.union([
zCanvasRasterLayerState.shape.type,
zCanvasControlLayerState.shape.type,
zCanvasRegionalGuidanceState.shape.type,
zCanvasInpaintMaskState.shape.type,
- zCanvasReferenceImageState.shape.type,
]);
export type CanvasEntityType = z.infer;
@@ -435,7 +424,7 @@ export const zCanvasEntityIdentifer = z.object({
type: zCanvasEntityType,
});
export type CanvasEntityIdentifier = { id: string; type: T };
-export type CanvasRenderableEntityIdentifier = CanvasEntityIdentifier;
+
export type LoRA = {
id: string;
isEnabled: boolean;
@@ -570,9 +559,6 @@ const zRegionalGuidance = z.object({
isHidden: z.boolean(),
entities: z.array(zCanvasRegionalGuidanceState),
});
-const zReferenceImages = z.object({
- entities: z.array(zCanvasReferenceImageState),
-});
const zCanvasState = z.object({
_version: z.literal(3).default(3),
selectedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
@@ -581,7 +567,6 @@ const zCanvasState = z.object({
rasterLayers: zRasterLayers.default({ isHidden: false, entities: [] }),
controlLayers: zControlLayers.default({ isHidden: false, entities: [] }),
regionalGuidance: zRegionalGuidance.default({ isHidden: false, entities: [] }),
- referenceImages: zReferenceImages.default({ entities: [] }),
bbox: zBboxState.default({
rect: { x: 0, y: 0, width: 512, height: 512 },
aspectRatio: DEFAULT_ASPECT_RATIO_CONFIG,
@@ -592,6 +577,14 @@ const zCanvasState = z.object({
});
export type CanvasState = z.infer;
+const zRefImagesState = z.object({
+ selectedId: zId.nullable().default(null),
+ entities: z.array(zCanvasReferenceImageState).default(() => []),
+});
+export type RefImagesState = z.infer;
+const INITIAL_REF_IMAGES_STATE = zRefImagesState.parse({});
+export const getInitialRefImagesState = () => deepClone(INITIAL_REF_IMAGES_STATE);
+
/**
* Gets a fresh canvas initial state with no references in memory to existing objects.
*/
@@ -657,17 +650,6 @@ export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
export type CanvasEntityStateFromType = Extract;
-export function isRenderableEntityType(
- entityType: CanvasEntityState['type']
-): entityType is CanvasRenderableEntityState['type'] {
- return (
- entityType === 'raster_layer' ||
- entityType === 'control_layer' ||
- entityType === 'regional_guidance' ||
- entityType === 'inpaint_mask'
- );
-}
-
export function isRasterLayerEntityIdentifier(
entityIdentifier: CanvasEntityIdentifier
): entityIdentifier is CanvasEntityIdentifier<'raster_layer'> {
@@ -725,16 +707,6 @@ export function isSaveableEntityIdentifier(
return isRasterLayerEntityIdentifier(entityIdentifier) || isControlLayerEntityIdentifier(entityIdentifier);
}
-export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasRenderableEntityState {
- return isRenderableEntityType(entity.type);
-}
-
-export function isRenderableEntityIdentifier(
- entityIdentifier: CanvasEntityIdentifier
-): entityIdentifier is CanvasRenderableEntityIdentifier {
- return isRenderableEntityType(entityIdentifier.type);
-}
-
export const getEntityIdentifier = (
entity: Extract
): CanvasEntityIdentifier => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/util.ts b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
index a4fbfffd02..8cebbe4846 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
@@ -21,6 +21,7 @@ import type {
import { merge } from 'lodash-es';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
+import type { PartialDeep } from 'type-fest';
export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial): CanvasImageState => {
const { width, height, image_name } = imageDTO;
@@ -127,7 +128,7 @@ export const initialControlLoRA: ControlLoRAConfig = {
export const getReferenceImageState = (
id: string,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasReferenceImageState => {
const entityState: CanvasReferenceImageState = {
id,
@@ -143,7 +144,7 @@ export const getReferenceImageState = (
export const getRegionalGuidanceState = (
id: string,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasRegionalGuidanceState => {
const entityState: CanvasRegionalGuidanceState = {
id,
@@ -169,7 +170,7 @@ export const getRegionalGuidanceState = (
export const getControlLayerState = (
id: string,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasControlLayerState => {
const entityState: CanvasControlLayerState = {
id,
@@ -189,7 +190,7 @@ export const getControlLayerState = (
export const getRasterLayerState = (
id: string,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasRasterLayerState => {
const entityState: CanvasRasterLayerState = {
id,
@@ -207,7 +208,7 @@ export const getRasterLayerState = (
export const getInpaintMaskState = (
id: string,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasInpaintMaskState => {
const entityState: CanvasInpaintMaskState = {
id,
@@ -232,7 +233,7 @@ export const getInpaintMaskState = (
const convertRasterLayerToControlLayer = (
newId: string,
rasterLayerState: CanvasRasterLayerState,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasControlLayerState => {
const { name, objects, position } = rasterLayerState;
const controlLayerState = getControlLayerState(newId, {
@@ -247,7 +248,7 @@ const convertRasterLayerToControlLayer = (
const convertRasterLayerToInpaintMask = (
newId: string,
rasterLayerState: CanvasRasterLayerState,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasInpaintMaskState => {
const { name, objects, position } = rasterLayerState;
const inpaintMaskState = getInpaintMaskState(newId, {
@@ -262,7 +263,7 @@ const convertRasterLayerToInpaintMask = (
const convertRasterLayerToRegionalGuidance = (
newId: string,
rasterLayerState: CanvasRasterLayerState,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasRegionalGuidanceState => {
const { name, objects, position } = rasterLayerState;
const regionalGuidanceState = getRegionalGuidanceState(newId, {
@@ -277,7 +278,7 @@ const convertRasterLayerToRegionalGuidance = (
const convertControlLayerToRasterLayer = (
newId: string,
controlLayerState: CanvasControlLayerState,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasRasterLayerState => {
const { name, objects, position } = controlLayerState;
const rasterLayerState = getRasterLayerState(newId, {
@@ -292,7 +293,7 @@ const convertControlLayerToRasterLayer = (
const convertControlLayerToInpaintMask = (
newId: string,
rasterLayerState: CanvasControlLayerState,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasInpaintMaskState => {
const { name, objects, position } = rasterLayerState;
const inpaintMaskState = getInpaintMaskState(newId, {
@@ -307,7 +308,7 @@ const convertControlLayerToInpaintMask = (
const convertControlLayerToRegionalGuidance = (
newId: string,
rasterLayerState: CanvasControlLayerState,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasRegionalGuidanceState => {
const { name, objects, position } = rasterLayerState;
const regionalGuidanceState = getRegionalGuidanceState(newId, {
@@ -322,7 +323,7 @@ const convertControlLayerToRegionalGuidance = (
const convertInpaintMaskToRegionalGuidance = (
newId: string,
inpaintMaskState: CanvasInpaintMaskState,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasRegionalGuidanceState => {
const { name, objects, position } = inpaintMaskState;
const regionalGuidanceState = getRegionalGuidanceState(newId, {
@@ -337,7 +338,7 @@ const convertInpaintMaskToRegionalGuidance = (
const convertRegionalGuidanceToInpaintMask = (
newId: string,
regionalGuidanceState: CanvasRegionalGuidanceState,
- overrides?: Partial
+ overrides?: PartialDeep
): CanvasInpaintMaskState => {
const { name, objects, position } = regionalGuidanceState;
const inpaintMaskState = getInpaintMaskState(newId, {
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
index 1d88a77edc..6f4fd19571 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
+++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
@@ -1,9 +1,14 @@
import { useStore } from '@nanostores/react';
import { getStore, useAppStore } from 'app/store/nanostores/store';
import type { AppDispatch, AppGetState, RootState } from 'app/store/store';
-import { entityDeleted, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
+import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
+import {
+ referenceImageIPAdapterImageChanged,
+ selectReferenceImageEntities,
+ selectRefImagesSlice,
+} from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
-import { type CanvasState, getEntityIdentifier } from 'features/controlLayers/store/types';
+import type { CanvasState, RefImagesState } from 'features/controlLayers/store/types';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { imageSelected } from 'features/gallery/store/gallerySlice';
@@ -145,8 +150,9 @@ const getImageUsageFromImageDTOs = (imageDTOs: ImageDTO[], state: RootState): Im
const nodes = selectNodesSlice(state);
const canvas = selectCanvasSlice(state);
const upscale = selectUpscaleSlice(state);
+ const refImages = selectRefImagesSlice(state);
- return imageDTOs.map(({ image_name }) => getImageUsage(nodes, canvas, upscale, image_name));
+ return imageDTOs.map(({ image_name }) => getImageUsage(nodes, canvas, upscale, refImages, image_name));
};
const getImageUsageSummary = (imageUsage: ImageUsage[]): ImageUsage => ({
@@ -221,9 +227,9 @@ const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image
};
const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
- selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
+ selectReferenceImageEntities(state).forEach((entity) => {
if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
- dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
+ dispatch(referenceImageIPAdapterImageChanged({ id: entity.id, imageDTO: null }));
}
});
};
@@ -243,7 +249,13 @@ const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageD
});
};
-export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: UpscaleState, image_name: string) => {
+export const getImageUsage = (
+ nodes: NodesState,
+ canvas: CanvasState,
+ upscale: UpscaleState,
+ refImages: RefImagesState,
+ image_name: string
+) => {
const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) =>
some(node.data.inputs, (input) => {
if (isImageFieldInputInstance(input)) {
@@ -264,9 +276,7 @@ export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: U
const isUpscaleImage = upscale.upscaleInitialImage?.image_name === image_name;
- const isReferenceImage = canvas.referenceImages.entities.some(
- ({ ipAdapter }) => ipAdapter.image?.image_name === image_name
- );
+ const isReferenceImage = refImages.entities.some(({ ipAdapter }) => ipAdapter.image?.image_name === image_name);
const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts
index 89abd63a77..925a118694 100644
--- a/invokeai/frontend/web/src/features/dnd/dnd.ts
+++ b/invokeai/frontend/web/src/features/dnd/dnd.ts
@@ -1,11 +1,7 @@
import { logger } from 'app/logging/logger';
import type { AppDispatch, RootState } from 'app/store/store';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import type {
- CanvasEntityIdentifier,
- CanvasEntityType,
- CanvasRenderableEntityIdentifier,
-} from 'features/controlLayers/store/types';
+import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common';
import type { BoardId } from 'features/gallery/store/types';
import {
@@ -133,7 +129,7 @@ const _setGlobalReferenceImage = buildTypeAndKey('set-global-reference-image');
export type SetGlobalReferenceImageDndTargetData = DndData<
typeof _setGlobalReferenceImage.type,
typeof _setGlobalReferenceImage.key,
- { entityIdentifier: CanvasEntityIdentifier<'reference_image'> }
+ { id: string }
>;
export const setGlobalReferenceImageDndTarget: DndTarget<
SetGlobalReferenceImageDndTargetData,
@@ -150,8 +146,8 @@ export const setGlobalReferenceImageDndTarget: DndTarget<
},
handler: ({ sourceData, targetData, dispatch }) => {
const { imageDTO } = sourceData.payload;
- const { entityIdentifier } = targetData.payload;
- setGlobalReferenceImage({ entityIdentifier, imageDTO, dispatch });
+ const { id } = targetData.payload;
+ setGlobalReferenceImage({ id, imageDTO, dispatch });
},
};
//#endregion
@@ -352,7 +348,7 @@ type NewCanvasFromImageDndTargetData = DndData<
typeof _newCanvas.type,
typeof _newCanvas.key,
{
- type: CanvasEntityType | 'regional_guidance_with_reference_image';
+ type: CanvasEntityType | 'regional_guidance_with_reference_image' | 'reference_image';
withResize?: boolean;
withInpaintMask?: boolean;
}
@@ -379,7 +375,7 @@ const _replaceCanvasEntityObjectsWithImage = buildTypeAndKey('replace-canvas-ent
export type ReplaceCanvasEntityObjectsWithImageDndTargetData = DndData<
typeof _replaceCanvasEntityObjectsWithImage.type,
typeof _replaceCanvasEntityObjectsWithImage.key,
- { entityIdentifier: CanvasRenderableEntityIdentifier }
+ { entityIdentifier: CanvasEntityIdentifier }
>;
export const replaceCanvasEntityObjectsWithImageDndTarget: DndTarget<
ReplaceCanvasEntityObjectsWithImageDndTargetData,
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
index 3247f7e0a0..fa016a6b46 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
@@ -15,6 +15,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
import { getImageUsage } from 'features/deleteImageModal/store/state';
@@ -54,23 +55,26 @@ const DeleteBoardModal = () => {
const selectImageUsageSummary = useMemo(
() =>
- createMemoizedSelector([selectNodesSlice, selectCanvasSlice, selectUpscaleSlice], (nodes, canvas, upscale) => {
- const allImageUsage = (boardImageNames ?? []).map((imageName) =>
- getImageUsage(nodes, canvas, upscale, imageName)
- );
+ createMemoizedSelector(
+ [selectNodesSlice, selectCanvasSlice, selectUpscaleSlice, selectRefImagesSlice],
+ (nodes, canvas, upscale, refImages) => {
+ const allImageUsage = (boardImageNames ?? []).map((imageName) =>
+ getImageUsage(nodes, canvas, upscale, refImages, imageName)
+ );
- const imageUsageSummary: ImageUsage = {
- isUpscaleImage: some(allImageUsage, (i) => i.isUpscaleImage),
- isRasterLayerImage: some(allImageUsage, (i) => i.isRasterLayerImage),
- isInpaintMaskImage: some(allImageUsage, (i) => i.isInpaintMaskImage),
- isRegionalGuidanceImage: some(allImageUsage, (i) => i.isRegionalGuidanceImage),
- isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
- isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
- isReferenceImage: some(allImageUsage, (i) => i.isReferenceImage),
- };
+ const imageUsageSummary: ImageUsage = {
+ isUpscaleImage: some(allImageUsage, (i) => i.isUpscaleImage),
+ isRasterLayerImage: some(allImageUsage, (i) => i.isRasterLayerImage),
+ isInpaintMaskImage: some(allImageUsage, (i) => i.isInpaintMaskImage),
+ isRegionalGuidanceImage: some(allImageUsage, (i) => i.isRegionalGuidanceImage),
+ isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
+ isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
+ isReferenceImage: some(allImageUsage, (i) => i.isReferenceImage),
+ };
- return imageUsageSummary;
- }),
+ return imageUsageSummary;
+ }
+ ),
[boardImageNames]
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx
index 4038aa4aff..fa20556c5d 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx
@@ -3,6 +3,8 @@ import { useAppStore } from 'app/store/nanostores/store';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
+import { referenceImageAdded } from 'features/controlLayers/store/refImagesSlice';
+import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { sentImageToCanvas } from 'features/gallery/store/actions';
@@ -74,8 +76,8 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
}, [imageDTO, imageViewer, store, t]);
const onClickNewGlobalReferenceImageFromImage = useCallback(() => {
- const { dispatch, getState } = store;
- createNewCanvasEntityFromImage({ imageDTO, type: 'reference_image', dispatch, getState });
+ const { dispatch } = store;
+ dispatch(referenceImageAdded({ overrides: { ipAdapter: { image: imageDTOToImageWithDims(imageDTO) } } }));
dispatch(sentImageToCanvas());
dispatch(setActiveTab('canvas'));
imageViewer.close();
diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts
index b73577bbdf..81fe6e9c44 100644
--- a/invokeai/frontend/web/src/features/imageActions/actions.ts
+++ b/invokeai/frontend/web/src/features/imageActions/actions.ts
@@ -10,23 +10,21 @@ import {
entityRasterized,
inpaintMaskAdded,
rasterLayerAdded,
- referenceImageAdded,
- referenceImageIPAdapterImageChanged,
rgAdded,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasSlice';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { referenceImageAdded, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/refImagesSlice';
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
+ CanvasEntityState,
CanvasEntityType,
CanvasImageState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
- CanvasRenderableEntityIdentifier,
- CanvasRenderableEntityState,
} from 'features/controlLayers/store/types';
import { imageDTOToImageObject, imageDTOToImageWithDims, initialControlNet } from 'features/controlLayers/store/util';
import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
@@ -41,13 +39,9 @@ import type { ImageDTO } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
-export const setGlobalReferenceImage = (arg: {
- imageDTO: ImageDTO;
- entityIdentifier: CanvasEntityIdentifier<'reference_image'>;
- dispatch: AppDispatch;
-}) => {
- const { imageDTO, entityIdentifier, dispatch } = arg;
- dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier, imageDTO }));
+export const setGlobalReferenceImage = (arg: { imageDTO: ImageDTO; id: string; dispatch: AppDispatch }) => {
+ const { imageDTO, id, dispatch } = arg;
+ dispatch(referenceImageIPAdapterImageChanged({ id, imageDTO }));
};
export const setRegionalGuidanceReferenceImage = (arg: {
@@ -84,7 +78,7 @@ export const createNewCanvasEntityFromImage = (arg: {
type: CanvasEntityType | 'regional_guidance_with_reference_image';
dispatch: AppDispatch;
getState: () => RootState;
- overrides?: Partial>;
+ overrides?: Partial>;
}) => {
const { type, imageDTO, dispatch, getState, overrides: _overrides } = arg;
const state = getState();
@@ -117,12 +111,6 @@ export const createNewCanvasEntityFromImage = (arg: {
dispatch(rgAdded({ overrides, isSelected: true }));
break;
}
- case 'reference_image': {
- const ipAdapter = deepClone(selectDefaultRefImageConfig(getState()));
- ipAdapter.image = imageDTOToImageWithDims(imageDTO);
- dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true }));
- break;
- }
case 'regional_guidance_with_reference_image': {
const ipAdapter = deepClone(selectDefaultIPAdapter(getState()));
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
@@ -146,7 +134,7 @@ export const createNewCanvasEntityFromImage = (arg: {
*/
export const newCanvasFromImage = async (arg: {
imageDTO: ImageDTO;
- type: CanvasEntityType | 'regional_guidance_with_reference_image';
+ type: CanvasEntityType | 'regional_guidance_with_reference_image' | 'reference_image';
withResize?: boolean;
withInpaintMask?: boolean;
dispatch: AppDispatch;
@@ -283,7 +271,7 @@ export const newCanvasFromImage = async (arg: {
export const replaceCanvasEntityObjectsWithImage = (arg: {
imageDTO: ImageDTO;
- entityIdentifier: CanvasRenderableEntityIdentifier;
+ entityIdentifier: CanvasEntityIdentifier;
dispatch: AppDispatch;
getState: () => RootState;
}) => {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
index e6a4791587..09a24b9669 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
@@ -3,6 +3,7 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { isChatGPT4oAspectRatioID, isChatGPT4oReferenceImageConfig } from 'features/controlLayers/store/types';
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
@@ -32,6 +33,7 @@ export const buildChatGPT4oGraph = async (
const model = selectMainModelConfig(state);
const canvas = selectCanvasSlice(state);
+ const refImages = selectRefImagesSlice(state);
const { bbox } = canvas;
const { positivePrompt } = selectPresetModifiedPrompts(state);
@@ -41,7 +43,7 @@ export const buildChatGPT4oGraph = async (
assert(isChatGPT4oAspectRatioID(bbox.aspectRatio.id), 'ChatGPT 4o does not support this aspect ratio');
- const validRefImages = canvas.referenceImages.entities
+ const validRefImages = refImages.entities
.filter((entity) => entity.isEnabled)
.filter((entity) => isChatGPT4oReferenceImageConfig(entity.ipAdapter))
.filter((entity) => getGlobalReferenceImageWarnings(entity, model).length === 0)
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
index 2bbfe1d37e..fd97936f67 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
@@ -3,6 +3,7 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addFLUXFill } from 'features/nodes/util/graph/generation/addFLUXFill';
import { addFLUXLoRAs } from 'features/nodes/util/graph/generation/addFLUXLoRAs';
@@ -42,6 +43,7 @@ export const buildFLUXGraph = async (state: RootState, manager?: CanvasManager |
const params = selectParamsSlice(state);
const canvas = selectCanvasSlice(state);
+ const refImages = selectRefImagesSlice(state);
const { bbox } = canvas;
@@ -271,7 +273,7 @@ export const buildFLUXGraph = async (state: RootState, manager?: CanvasManager |
id: getPrefixedId('ip_adapter_collector'),
});
const ipAdapterResult = addIPAdapters({
- entities: canvas.referenceImages.entities,
+ entities: refImages.entities,
g,
collector: ipAdapterCollect,
model,
@@ -284,7 +286,7 @@ export const buildFLUXGraph = async (state: RootState, manager?: CanvasManager |
id: getPrefixedId('ip_adapter_collector'),
});
const fluxReduxResult = addFLUXReduxes({
- entities: canvas.referenceImages.entities,
+ entities: refImages.entities,
g,
collector: fluxReduxCollect,
model,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
index 0bf32670b4..8ba379ec43 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
@@ -3,6 +3,7 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
@@ -37,6 +38,7 @@ export const buildSD1Graph = async (state: RootState, manager?: CanvasManager |
const params = selectParamsSlice(state);
const canvas = selectCanvasSlice(state);
+ const refImages = selectRefImagesSlice(state);
const { bbox } = canvas;
const model = selectMainModelConfig(state);
@@ -265,7 +267,7 @@ export const buildSD1Graph = async (state: RootState, manager?: CanvasManager |
id: getPrefixedId('ip_adapter_collector'),
});
const ipAdapterResult = addIPAdapters({
- entities: canvas.referenceImages.entities,
+ entities: refImages.entities,
g,
collector: ipAdapterCollect,
model,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
index 1960b6ab8b..f091fa4411 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
@@ -3,6 +3,7 @@ import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
@@ -41,6 +42,7 @@ export const buildSDXLGraph = async (state: RootState, manager?: CanvasManager |
const params = selectParamsSlice(state);
const canvas = selectCanvasSlice(state);
+ const refImages = selectRefImagesSlice(state);
const { bbox } = canvas;
@@ -272,7 +274,7 @@ export const buildSDXLGraph = async (state: RootState, manager?: CanvasManager |
id: getPrefixedId('ip_adapter_collector'),
});
const ipAdapterResult = addIPAdapters({
- entities: canvas.referenceImages.entities,
+ entities: refImages.entities,
g,
collector: ipAdapterCollect,
model,
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
index 037c1021fb..f6f71927c8 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
@@ -1,6 +1,7 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
+import { RefImageList } from 'features/controlLayers/components/IPAdapter/IPAdapterList';
import { positivePromptChanged, selectBase, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
@@ -107,6 +108,7 @@ export const ParamPositivePrompt = memo(() => {
label={`${t('parameters.positivePromptPlaceholder')} (${t('stylePresets.preview')})`}
/>
)}
+
);
diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts
index f17542a4df..2355feed84 100644
--- a/invokeai/frontend/web/src/features/queue/store/readiness.ts
+++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts
@@ -9,8 +9,9 @@ import type { AppConfig } from 'app/types/invokeai';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
-import type { CanvasState, ParamsState } from 'features/controlLayers/store/types';
+import type { CanvasState, ParamsState, RefImagesState } from 'features/controlLayers/store/types';
import {
getControlLayerWarnings,
getGlobalReferenceImageWarnings,
@@ -75,6 +76,7 @@ const debouncedUpdateReasons = debounce(
isConnected: boolean,
canvas: CanvasState,
params: ParamsState,
+ refImages: RefImagesState,
dynamicPrompts: DynamicPromptsState,
canvasIsFiltering: boolean,
canvasIsTransforming: boolean,
@@ -97,6 +99,7 @@ const debouncedUpdateReasons = debounce(
model,
canvas,
params,
+ refImages,
dynamicPrompts,
canvasIsFiltering,
canvasIsTransforming,
@@ -138,6 +141,7 @@ export const useReadinessWatcher = () => {
const tab = useAppSelector(selectActiveTab);
const canvas = useAppSelector(selectCanvasSlice);
const params = useAppSelector(selectParamsSlice);
+ const refImages = useAppSelector(selectRefImagesSlice);
const dynamicPrompts = useAppSelector(selectDynamicPromptsSlice);
const nodes = useAppSelector(selectNodesSlice);
const workflowSettings = useAppSelector(selectWorkflowSettingsSlice);
@@ -159,6 +163,7 @@ export const useReadinessWatcher = () => {
isConnected,
canvas,
params,
+ refImages,
dynamicPrompts,
canvasIsFiltering,
canvasIsTransforming,
@@ -177,6 +182,7 @@ export const useReadinessWatcher = () => {
}, [
store,
canvas,
+ refImages,
canvasIsCompositing,
canvasIsFiltering,
canvasIsRasterizing,
@@ -334,6 +340,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
model: MainModelConfig | null | undefined;
canvas: CanvasState;
params: ParamsState;
+ refImages: RefImagesState;
dynamicPrompts: DynamicPromptsState;
canvasIsFiltering: boolean;
canvasIsTransforming: boolean;
@@ -347,6 +354,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
model,
canvas,
params,
+ refImages,
dynamicPrompts,
canvasIsFiltering,
canvasIsTransforming,
@@ -514,24 +522,21 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
}
});
- const enabledGlobalReferenceLayers = canvas.referenceImages.entities.filter(
- (referenceImage) => referenceImage.isEnabled
- );
-
// Flux Kontext only supports 1x Reference Image at a time.
- const referenceImageCount = enabledGlobalReferenceLayers.length;
+ const referenceImageCount = refImages.entities.filter((entity) => entity.isEnabled).length;
if (model?.base === 'flux-kontext' && referenceImageCount > 1) {
reasons.push({ content: i18n.t('parameters.invoke.fluxKontextMultipleReferenceImages') });
}
- canvas.referenceImages.entities
+ refImages.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layer_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
+
const problems = getGlobalReferenceImageWarnings(entity, model);
if (problems.length) {
From 5a2f5c105d658946c181dcc20f66257b396f1349 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 13 Jun 2025 12:22:02 +1000
Subject: [PATCH 087/210] refactor(ui): `refImage.ipAdapter` ->
`refImage.config`
---
.../listeners/modelsLoaded.ts | 34 +--
.../components/IPAdapter/IPAdapterPreview.tsx | 2 +-
.../IPAdapter/IPAdapterSettings.tsx | 36 +--
.../RegionalGuidanceIPAdapterSettings.tsx | 69 +++---
...nalGuidanceIPAdapterSettingsEmptyState.tsx | 4 +-
.../controlLayers/hooks/addLayerHooks.ts | 16 +-
.../controlLayers/hooks/saveCanvasHooks.ts | 22 +-
.../controlLayers/store/canvasSlice.ts | 126 +++++-----
.../controlLayers/store/refImagesSlice.ts | 218 ++++++------------
.../src/features/controlLayers/store/types.ts | 27 ++-
.../src/features/controlLayers/store/util.ts | 15 +-
.../controlLayers/store/validators.ts | 20 +-
.../features/deleteImageModal/store/state.ts | 10 +-
.../ImageMenuItemNewLayerFromImageSubMenu.tsx | 4 +-
.../web/src/features/imageActions/actions.ts | 26 +--
.../web/src/features/metadata/util/parsers.ts | 20 +-
.../util/graph/generation/addFLUXRedux.ts | 17 +-
.../util/graph/generation/addIPAdapters.ts | 17 +-
.../nodes/util/graph/generation/addRegions.ts | 19 +-
.../graph/generation/buildChatGPT4oGraph.ts | 7 +-
.../web/src/features/queue/store/readiness.ts | 26 +--
21 files changed, 312 insertions(+), 423 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
index 8590c35810..35b6f8dac2 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
@@ -1,7 +1,7 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
-import { controlLayerModelChanged, rgIPAdapterModelChanged } from 'features/controlLayers/store/canvasSlice';
+import { controlLayerModelChanged, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
import {
clipEmbedModelSelected,
@@ -11,9 +11,9 @@ import {
t5EncoderModelSelected,
vaeSelected,
} from 'features/controlLayers/store/paramsSlice';
-import { referenceImageIPAdapterModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
+import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
-import { getEntityIdentifier } from 'features/controlLayers/store/types';
+import { getEntityIdentifier, isFLUXReduxConfig, isIPAdapterConfig } from 'features/controlLayers/store/types';
import { modelSelected } from 'features/parameters/store/actions';
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
import {
@@ -208,11 +208,11 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, log)
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
const ipaModels = models.filter(isIPAdapterModelConfig);
selectRefImagesSlice(state).entities.forEach((entity) => {
- if (entity.ipAdapter.type !== 'ip_adapter') {
+ if (!isIPAdapterConfig(entity.config)) {
return;
}
- const selectedIPAdapterModel = entity.ipAdapter.model;
+ const selectedIPAdapterModel = entity.config.model;
// `null` is a valid IP adapter model - no need to do anything.
if (!selectedIPAdapterModel) {
return;
@@ -222,16 +222,16 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
return;
}
log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing');
- dispatch(referenceImageIPAdapterModelChanged({ id: entity.id, modelConfig: null }));
+ dispatch(refImageModelChanged({ id: entity.id, modelConfig: null }));
});
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
- entity.referenceImages.forEach(({ id: referenceImageId, ipAdapter }) => {
- if (ipAdapter.type !== 'ip_adapter') {
+ entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
+ if (!isIPAdapterConfig(config)) {
return;
}
- const selectedIPAdapterModel = ipAdapter.model;
+ const selectedIPAdapterModel = config.model;
// `null` is a valid IP adapter model - no need to do anything.
if (!selectedIPAdapterModel) {
return;
@@ -242,7 +242,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
}
log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing');
dispatch(
- rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null })
+ rgRefImageModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null })
);
});
});
@@ -252,10 +252,10 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
const fluxReduxModels = models.filter(isFluxReduxModelConfig);
selectRefImagesSlice(state).entities.forEach((entity) => {
- if (entity.ipAdapter.type !== 'flux_redux') {
+ if (!isFLUXReduxConfig(entity.config)) {
return;
}
- const selectedFLUXReduxModel = entity.ipAdapter.model;
+ const selectedFLUXReduxModel = entity.config.model;
// `null` is a valid FLUX Redux model - no need to do anything.
if (!selectedFLUXReduxModel) {
return;
@@ -265,16 +265,16 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
return;
}
log.debug({ selectedFLUXReduxModel }, 'Selected FLUX Redux model is not available, clearing');
- dispatch(referenceImageIPAdapterModelChanged({ id: entity.id, modelConfig: null }));
+ dispatch(refImageModelChanged({ id: entity.id, modelConfig: null }));
});
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
- entity.referenceImages.forEach(({ id: referenceImageId, ipAdapter }) => {
- if (ipAdapter.type !== 'flux_redux') {
+ entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
+ if (!isFLUXReduxConfig(config)) {
return;
}
- const selectedFLUXReduxModel = ipAdapter.model;
+ const selectedFLUXReduxModel = config.model;
// `null` is a valid FLUX Redux model - no need to do anything.
if (!selectedFLUXReduxModel) {
return;
@@ -285,7 +285,7 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
}
log.debug({ selectedFLUXReduxModel }, 'Selected FLUX Redux model is not available, clearing');
dispatch(
- rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null })
+ rgRefImageModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null })
);
});
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx
index 58d6db2912..01b85fb5f1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx
@@ -52,7 +52,7 @@ export const RefImagePreview = memo(() => {
-
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
index fe2cd460b7..1e884209b4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
@@ -11,13 +11,13 @@ import { IPAdapterSettingsEmptyState } from 'features/controlLayers/components/I
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import {
- referenceImageIPAdapterBeginEndStepPctChanged,
- referenceImageIPAdapterCLIPVisionModelChanged,
- referenceImageIPAdapterFLUXReduxImageInfluenceChanged,
- referenceImageIPAdapterImageChanged,
- referenceImageIPAdapterMethodChanged,
- referenceImageIPAdapterModelChanged,
- referenceImageIPAdapterWeightChanged,
+ refImageFLUXReduxImageInfluenceChanged,
+ refImageImageChanged,
+ refImageIPAdapterBeginEndStepPctChanged,
+ refImageIPAdapterCLIPVisionModelChanged,
+ refImageIPAdapterMethodChanged,
+ refImageIPAdapterWeightChanged,
+ refImageModelChanged,
selectRefImageEntity,
selectRefImageEntityOrThrow,
selectRefImagesSlice,
@@ -35,64 +35,64 @@ import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConf
import { IPAdapterImagePreview } from './IPAdapterImagePreview';
-const buildSelectIPAdapter = (id: string) =>
+const buildSelectConfig = (id: string) =>
createSelector(
selectRefImagesSlice,
- (refImages) => selectRefImageEntityOrThrow(refImages, id, 'IPAdapterSettings').ipAdapter
+ (refImages) => selectRefImageEntityOrThrow(refImages, id, 'IPAdapterSettings').config
);
const IPAdapterSettingsContent = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const id = useRefImageIdContext();
- const selectIPAdapter = useMemo(() => buildSelectIPAdapter(id), [id]);
+ const selectIPAdapter = useMemo(() => buildSelectConfig(id), [id]);
const ipAdapter = useAppSelector(selectIPAdapter);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
- dispatch(referenceImageIPAdapterBeginEndStepPctChanged({ id, beginEndStepPct }));
+ dispatch(refImageIPAdapterBeginEndStepPctChanged({ id, beginEndStepPct }));
},
[dispatch, id]
);
const onChangeWeight = useCallback(
(weight: number) => {
- dispatch(referenceImageIPAdapterWeightChanged({ id, weight }));
+ dispatch(refImageIPAdapterWeightChanged({ id, weight }));
},
[dispatch, id]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
- dispatch(referenceImageIPAdapterMethodChanged({ id, method }));
+ dispatch(refImageIPAdapterMethodChanged({ id, method }));
},
[dispatch, id]
);
const onChangeFLUXReduxImageInfluence = useCallback(
(imageInfluence: FLUXReduxImageInfluenceType) => {
- dispatch(referenceImageIPAdapterFLUXReduxImageInfluenceChanged({ id, imageInfluence }));
+ dispatch(refImageFLUXReduxImageInfluenceChanged({ id, imageInfluence }));
},
[dispatch, id]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig) => {
- dispatch(referenceImageIPAdapterModelChanged({ id, modelConfig }));
+ dispatch(refImageModelChanged({ id, modelConfig }));
},
[dispatch, id]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
- dispatch(referenceImageIPAdapterCLIPVisionModelChanged({ id, clipVisionModel }));
+ dispatch(refImageIPAdapterCLIPVisionModelChanged({ id, clipVisionModel }));
},
[dispatch, id]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
- dispatch(referenceImageIPAdapterImageChanged({ id, imageDTO }));
+ dispatch(refImageImageChanged({ id, imageDTO }));
},
[dispatch, id]
);
@@ -156,7 +156,7 @@ IPAdapterSettingsContent.displayName = 'IPAdapterSettingsContent';
const buildSelectIPAdapterHasImage = (id: string) =>
createSelector(selectRefImagesSlice, (refImages) => {
const referenceImage = selectRefImageEntity(refImages, id);
- return !!referenceImage && referenceImage.ipAdapter.image !== null;
+ return !!referenceImage && referenceImage.config.image !== null;
});
export const IPAdapterSettings = memo(() => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
index bd5c7c544c..4cc434c439 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
@@ -13,14 +13,14 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import {
- rgIPAdapterBeginEndStepPctChanged,
- rgIPAdapterCLIPVisionModelChanged,
- rgIPAdapterDeleted,
- rgIPAdapterFLUXReduxImageInfluenceChanged,
- rgIPAdapterImageChanged,
- rgIPAdapterMethodChanged,
- rgIPAdapterModelChanged,
- rgIPAdapterWeightChanged,
+ rgRefImageDeleted,
+ rgRefImageFLUXReduxImageInfluenceChanged,
+ rgRefImageImageChanged,
+ rgRefImageIPAdapterBeginEndStepPctChanged,
+ rgRefImageIPAdapterCLIPVisionModelChanged,
+ rgRefImageIPAdapterMethodChanged,
+ rgRefImageIPAdapterWeightChanged,
+ rgRefImageModelChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
import type {
@@ -46,64 +46,64 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onDeleteIPAdapter = useCallback(() => {
- dispatch(rgIPAdapterDeleted({ entityIdentifier, referenceImageId }));
+ dispatch(rgRefImageDeleted({ entityIdentifier, referenceImageId }));
}, [dispatch, entityIdentifier, referenceImageId]);
- const selectIPAdapter = useMemo(
+ const selectConfig = useMemo(
() =>
createSelector(selectCanvasSlice, (canvas) => {
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
assert(referenceImage, `Regional Guidance IP Adapter with id ${referenceImageId} not found`);
- return referenceImage.ipAdapter;
+ return referenceImage.config;
}),
[entityIdentifier, referenceImageId]
);
- const ipAdapter = useAppSelector(selectIPAdapter);
+ const config = useAppSelector(selectConfig);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
- dispatch(rgIPAdapterBeginEndStepPctChanged({ entityIdentifier, referenceImageId, beginEndStepPct }));
+ dispatch(rgRefImageIPAdapterBeginEndStepPctChanged({ entityIdentifier, referenceImageId, beginEndStepPct }));
},
[dispatch, entityIdentifier, referenceImageId]
);
const onChangeWeight = useCallback(
(weight: number) => {
- dispatch(rgIPAdapterWeightChanged({ entityIdentifier, referenceImageId, weight }));
+ dispatch(rgRefImageIPAdapterWeightChanged({ entityIdentifier, referenceImageId, weight }));
},
[dispatch, entityIdentifier, referenceImageId]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
- dispatch(rgIPAdapterMethodChanged({ entityIdentifier, referenceImageId, method }));
+ dispatch(rgRefImageIPAdapterMethodChanged({ entityIdentifier, referenceImageId, method }));
},
[dispatch, entityIdentifier, referenceImageId]
);
const onChangeFLUXReduxImageInfluence = useCallback(
(imageInfluence: FLUXReduxImageInfluenceType) => {
- dispatch(rgIPAdapterFLUXReduxImageInfluenceChanged({ entityIdentifier, referenceImageId, imageInfluence }));
+ dispatch(rgRefImageFLUXReduxImageInfluenceChanged({ entityIdentifier, referenceImageId, imageInfluence }));
},
[dispatch, entityIdentifier, referenceImageId]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig) => {
- dispatch(rgIPAdapterModelChanged({ entityIdentifier, referenceImageId, modelConfig }));
+ dispatch(rgRefImageModelChanged({ entityIdentifier, referenceImageId, modelConfig }));
},
[dispatch, entityIdentifier, referenceImageId]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
- dispatch(rgIPAdapterCLIPVisionModelChanged({ entityIdentifier, referenceImageId, clipVisionModel }));
+ dispatch(rgRefImageIPAdapterCLIPVisionModelChanged({ entityIdentifier, referenceImageId, clipVisionModel }));
},
[dispatch, entityIdentifier, referenceImageId]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
- dispatch(rgIPAdapterImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
+ dispatch(rgRefImageImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
},
[dispatch, entityIdentifier, referenceImageId]
);
@@ -112,9 +112,9 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
() =>
setRegionalGuidanceReferenceImageDndTarget.getData(
{ entityIdentifier, referenceImageId },
- ipAdapter.image?.image_name
+ config.image?.image_name
),
- [entityIdentifier, ipAdapter.image?.image_name, referenceImageId]
+ [entityIdentifier, config.image?.image_name, referenceImageId]
);
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
@@ -140,9 +140,9 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
-
- {ipAdapter.type === 'ip_adapter' && (
-
+
+ {config.type === 'ip_adapter' && (
+
)}
- {ipAdapter.type === 'ip_adapter' && (
+ {config.type === 'ip_adapter' && (
-
-
-
+
+
+
)}
- {ipAdapter.type === 'flux_redux' && (
+ {config.type === 'flux_redux' && (
)}
createSelector(selectCanvasSlice, (canvas) => {
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
- return !!referenceImage && referenceImage.ipAdapter.image !== null;
+ return !!referenceImage && referenceImage.config.image !== null;
});
export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Props) => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
-
- const selectIPAdapterHasImage = useMemo(
+ const selectHasImage = useMemo(
() => buildSelectIPAdapterHasImage(entityIdentifier, referenceImageId),
[entityIdentifier, referenceImageId]
);
- const hasImage = useAppSelector(selectIPAdapterHasImage);
+ const hasImage = useAppSelector(selectHasImage);
if (!hasImage) {
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 e393a97426..e7e3282a64 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState.tsx
@@ -4,7 +4,7 @@ import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
-import { rgIPAdapterDeleted } from 'features/controlLayers/store/canvasSlice';
+import { rgRefImageDeleted } from 'features/controlLayers/store/canvasSlice';
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
@@ -35,7 +35,7 @@ export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImag
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
const onDeleteIPAdapter = useCallback(() => {
- dispatch(rgIPAdapterDeleted({ entityIdentifier, referenceImageId }));
+ dispatch(rgRefImageDeleted({ entityIdentifier, referenceImageId }));
}, [dispatch, entityIdentifier, referenceImageId]);
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
index 5bdbfb5555..41d871436e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
@@ -10,20 +10,20 @@ import {
inpaintMaskNoiseAdded,
rasterLayerAdded,
rgAdded,
- rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
+ rgRefImageAdded,
} from 'features/controlLayers/store/canvasSlice';
import { selectBase, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
-import { referenceImageAdded } from 'features/controlLayers/store/refImagesSlice';
+import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
- CanvasReferenceImageState,
CanvasRegionalGuidanceState,
ControlLoRAConfig,
ControlNetConfig,
IPAdapterConfig,
+ RefImageState,
T2IAdapterConfig,
} from 'features/controlLayers/store/types';
import {
@@ -77,7 +77,7 @@ export const selectDefaultRefImageConfig = createSelector(
selectMainModelConfig,
selectModelConfigsQuery,
selectBase,
- (selectedMainModel, query, base): CanvasReferenceImageState['ipAdapter'] => {
+ (selectedMainModel, query, base): RefImageState['config'] => {
if (selectedMainModel?.base === 'chatgpt-4o') {
const referenceImage = deepClone(initialChatGPT4oReferenceImage);
referenceImage.model = zModelIdentifierField.parse(selectedMainModel);
@@ -179,7 +179,7 @@ export const useAddRegionalReferenceImage = () => {
const func = useCallback(() => {
const overrides: Partial = {
referenceImages: [
- { id: getPrefixedId('regional_guidance_reference_image'), ipAdapter: deepClone(defaultIPAdapter) },
+ { id: getPrefixedId('regional_guidance_reference_image'), config: deepClone(defaultIPAdapter) },
],
};
dispatch(rgAdded({ isSelected: true, overrides }));
@@ -192,8 +192,8 @@ export const useAddGlobalReferenceImage = () => {
const dispatch = useAppDispatch();
const defaultRefImage = useAppSelector(selectDefaultRefImageConfig);
const func = useCallback(() => {
- const overrides = { ipAdapter: deepClone(defaultRefImage) };
- dispatch(referenceImageAdded({ isSelected: true, overrides }));
+ const overrides = { config: deepClone(defaultRefImage) };
+ dispatch(refImageAdded({ isSelected: true, overrides }));
}, [defaultRefImage, dispatch]);
return func;
@@ -203,7 +203,7 @@ export const useAddRegionalGuidanceIPAdapter = (entityIdentifier: CanvasEntityId
const dispatch = useAppDispatch();
const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
const func = useCallback(() => {
- dispatch(rgIPAdapterAdded({ entityIdentifier, overrides: { ipAdapter: deepClone(defaultIPAdapter) } }));
+ dispatch(rgRefImageAdded({ entityIdentifier, overrides: { config: deepClone(defaultIPAdapter) } }));
}, [defaultIPAdapter, dispatch, entityIdentifier]);
return func;
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
index 2e6437cd0b..0e0d2d433d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
@@ -10,7 +10,7 @@ import {
entityRasterized,
rasterLayerAdded,
rgAdded,
- rgIPAdapterImageChanged,
+ rgRefImageImageChanged,
} from 'features/controlLayers/store/canvasSlice';
import {
selectMainModelConfig,
@@ -18,16 +18,16 @@ import {
selectPositivePrompt,
selectSeed,
} from 'features/controlLayers/store/paramsSlice';
-import { referenceImageAdded, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/refImagesSlice';
+import { refImageAdded,refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasMetadata } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasRasterLayerState,
- CanvasReferenceImageState,
CanvasRegionalGuidanceState,
Rect,
- RegionalGuidanceReferenceImageState,
+ RefImageState,
+ RegionalGuidanceRefImageState,
} from 'features/controlLayers/store/types';
import { imageDTOToImageObject, imageDTOToImageWithDims, initialControlNet } from 'features/controlLayers/store/util';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
@@ -172,9 +172,9 @@ export const useNewRegionalReferenceImageFromBbox = () => {
const arg = useMemo(() => {
const onSave = (imageDTO: ImageDTO) => {
- const ipAdapter: RegionalGuidanceReferenceImageState = {
+ const ipAdapter: RegionalGuidanceRefImageState = {
id: getPrefixedId('regional_guidance_reference_image'),
- ipAdapter: {
+ config: {
...deepClone(defaultIPAdapter),
image: imageDTOToImageWithDims(imageDTO),
},
@@ -205,13 +205,13 @@ export const useNewGlobalReferenceImageFromBbox = () => {
const arg = useMemo(() => {
const onSave = (imageDTO: ImageDTO) => {
- const overrides: Partial = {
- ipAdapter: {
+ const overrides: Partial = {
+ config: {
...deepClone(defaultIPAdapter),
image: imageDTOToImageWithDims(imageDTO),
},
};
- dispatch(referenceImageAdded({ overrides, isSelected: true }));
+ dispatch(refImageAdded({ overrides, isSelected: true }));
};
return {
@@ -311,7 +311,7 @@ export const usePullBboxIntoGlobalReferenceImage = (id: string) => {
const arg = useMemo(() => {
const onSave = (imageDTO: ImageDTO, _: Rect) => {
- dispatch(referenceImageIPAdapterImageChanged({ id, imageDTO }));
+ dispatch(refImageImageChanged({ id, imageDTO }));
};
return {
@@ -336,7 +336,7 @@ export const usePullBboxIntoRegionalGuidanceReferenceImage = (
const arg = useMemo(() => {
const onSave = (imageDTO: ImageDTO, _: Rect) => {
- dispatch(rgIPAdapterImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
+ dispatch(rgRefImageImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
};
return {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index 4715679a65..7f967f6a03 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -23,7 +23,7 @@ import type {
EntityMovedByPayload,
FillStyle,
FLUXReduxImageInfluence,
- RegionalGuidanceReferenceImageState,
+ RegionalGuidanceRefImageState,
RgbColor,
} from 'features/controlLayers/store/types';
import {
@@ -38,13 +38,15 @@ import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/par
import type { IRect } from 'konva/lib/types';
import { merge } from 'lodash-es';
import type { UndoableOptions } from 'redux-undo';
-import type {
- ControlLoRAModelConfig,
- ControlNetModelConfig,
- FLUXReduxModelConfig,
- ImageDTO,
- IPAdapterModelConfig,
- T2IAdapterModelConfig,
+import {
+ type ControlLoRAModelConfig,
+ type ControlNetModelConfig,
+ type FLUXReduxModelConfig,
+ type ImageDTO,
+ type IPAdapterModelConfig,
+ isFluxReduxModelConfig,
+ isIPAdapterModelConfig,
+ type T2IAdapterModelConfig,
} from 'services/api/types';
import type {
@@ -73,7 +75,9 @@ import {
getInitialCanvasState,
isChatGPT4oAspectRatioID,
isFluxKontextAspectRatioID,
+ isFLUXReduxConfig,
isImagenAspectRatioID,
+ isIPAdapterConfig,
} from './types';
import {
converters,
@@ -670,12 +674,12 @@ export const canvasSlice = createSlice({
}
rg.autoNegative = !rg.autoNegative;
},
- rgIPAdapterAdded: {
+ rgRefImageAdded: {
reducer: (
state,
action: PayloadAction<
EntityIdentifierPayload<
- { referenceImageId: string; overrides?: Partial },
+ { referenceImageId: string; overrides?: Partial },
'regional_guidance'
>
>
@@ -685,20 +689,17 @@ export const canvasSlice = createSlice({
if (!entity) {
return;
}
- const ipAdapter = { id: referenceImageId, ipAdapter: deepClone(initialIPAdapter) };
- merge(ipAdapter, overrides);
- entity.referenceImages.push(ipAdapter);
+ const config = { id: referenceImageId, config: deepClone(initialIPAdapter) };
+ merge(config, overrides);
+ entity.referenceImages.push(config);
},
prepare: (
- payload: EntityIdentifierPayload<
- { overrides?: Partial },
- 'regional_guidance'
- >
+ payload: EntityIdentifierPayload<{ overrides?: Partial }, 'regional_guidance'>
) => ({
payload: { ...payload, referenceImageId: getPrefixedId('regional_guidance_ip_adapter') },
}),
},
- rgIPAdapterDeleted: (
+ rgRefImageDeleted: (
state,
action: PayloadAction>
) => {
@@ -707,9 +708,9 @@ export const canvasSlice = createSlice({
if (!entity) {
return;
}
- entity.referenceImages = entity.referenceImages.filter((ipAdapter) => ipAdapter.id !== referenceImageId);
+ entity.referenceImages = entity.referenceImages.filter((config) => config.id !== referenceImageId);
},
- rgIPAdapterImageChanged: (
+ rgRefImageImageChanged: (
state,
action: PayloadAction<
EntityIdentifierPayload<{ referenceImageId: string; imageDTO: ImageDTO | null }, 'regional_guidance'>
@@ -720,9 +721,9 @@ export const canvasSlice = createSlice({
if (!referenceImage) {
return;
}
- referenceImage.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
+ referenceImage.config.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
- rgIPAdapterWeightChanged: (
+ rgRefImageIPAdapterWeightChanged: (
state,
action: PayloadAction>
) => {
@@ -731,13 +732,13 @@ export const canvasSlice = createSlice({
if (!referenceImage) {
return;
}
- if (referenceImage.ipAdapter.type !== 'ip_adapter') {
+ if (!isIPAdapterConfig(referenceImage.config)) {
return;
}
- referenceImage.ipAdapter.weight = weight;
+ referenceImage.config.weight = weight;
},
- rgIPAdapterBeginEndStepPctChanged: (
+ rgRefImageIPAdapterBeginEndStepPctChanged: (
state,
action: PayloadAction<
EntityIdentifierPayload<{ referenceImageId: string; beginEndStepPct: [number, number] }, 'regional_guidance'>
@@ -748,13 +749,12 @@ export const canvasSlice = createSlice({
if (!referenceImage) {
return;
}
- if (referenceImage.ipAdapter.type !== 'ip_adapter') {
+ if (!isIPAdapterConfig(referenceImage.config)) {
return;
}
-
- referenceImage.ipAdapter.beginEndStepPct = beginEndStepPct;
+ referenceImage.config.beginEndStepPct = beginEndStepPct;
},
- rgIPAdapterMethodChanged: (
+ rgRefImageIPAdapterMethodChanged: (
state,
action: PayloadAction<
EntityIdentifierPayload<{ referenceImageId: string; method: IPMethodV2 }, 'regional_guidance'>
@@ -765,13 +765,12 @@ export const canvasSlice = createSlice({
if (!referenceImage) {
return;
}
- if (referenceImage.ipAdapter.type !== 'ip_adapter') {
+ if (!isIPAdapterConfig(referenceImage.config)) {
return;
}
-
- referenceImage.ipAdapter.method = method;
+ referenceImage.config.method = method;
},
- rgIPAdapterFLUXReduxImageInfluenceChanged: (
+ rgRefImageFLUXReduxImageInfluenceChanged: (
state,
action: PayloadAction<
EntityIdentifierPayload<
@@ -785,13 +784,13 @@ export const canvasSlice = createSlice({
if (!referenceImage) {
return;
}
- if (referenceImage.ipAdapter.type !== 'flux_redux') {
+ if (!isFLUXReduxConfig(referenceImage.config)) {
return;
}
- referenceImage.ipAdapter.imageInfluence = imageInfluence;
+ referenceImage.config.imageInfluence = imageInfluence;
},
- rgIPAdapterModelChanged: (
+ rgRefImageModelChanged: (
state,
action: PayloadAction<
EntityIdentifierPayload<
@@ -808,43 +807,43 @@ export const canvasSlice = createSlice({
if (!referenceImage) {
return;
}
- referenceImage.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
- if (!referenceImage.ipAdapter.model) {
+ if (!modelConfig) {
+ referenceImage.config.model = null;
return;
}
- if (referenceImage.ipAdapter.type === 'ip_adapter' && referenceImage.ipAdapter.model.type === 'flux_redux') {
+ if (isIPAdapterConfig(referenceImage.config) && isFluxReduxModelConfig(modelConfig)) {
// Switching from ip_adapter to flux_redux
- referenceImage.ipAdapter = {
+ referenceImage.config = {
...initialFLUXRedux,
- image: referenceImage.ipAdapter.image,
- model: referenceImage.ipAdapter.model,
+ image: referenceImage.config.image,
+ model: zModelIdentifierField.parse(modelConfig),
};
return;
}
- if (referenceImage.ipAdapter.type === 'flux_redux' && referenceImage.ipAdapter.model.type === 'ip_adapter') {
+ if (isFLUXReduxConfig(referenceImage.config) && isIPAdapterModelConfig(modelConfig)) {
// Switching from flux_redux to ip_adapter
- referenceImage.ipAdapter = {
+ referenceImage.config = {
...initialIPAdapter,
- image: referenceImage.ipAdapter.image,
- model: referenceImage.ipAdapter.model,
+ image: referenceImage.config.image,
+ model: zModelIdentifierField.parse(modelConfig),
};
return;
}
- if (referenceImage.ipAdapter.type === 'ip_adapter') {
+ if (isIPAdapterConfig(referenceImage.config)) {
// Ensure that the IP Adapter model is compatible with the CLIP Vision model
- if (referenceImage.ipAdapter.model?.base === 'flux') {
- referenceImage.ipAdapter.clipVisionModel = 'ViT-L';
- } else if (referenceImage.ipAdapter.clipVisionModel === 'ViT-L') {
+ if (referenceImage.config.model?.base === 'flux') {
+ referenceImage.config.clipVisionModel = 'ViT-L';
+ } else if (referenceImage.config.clipVisionModel === 'ViT-L') {
// Fall back to ViT-H (ViT-G would also work)
- referenceImage.ipAdapter.clipVisionModel = 'ViT-H';
+ referenceImage.config.clipVisionModel = 'ViT-H';
}
}
},
- rgIPAdapterCLIPVisionModelChanged: (
+ rgRefImageIPAdapterCLIPVisionModelChanged: (
state,
action: PayloadAction<
EntityIdentifierPayload<{ referenceImageId: string; clipVisionModel: CLIPVisionModelV2 }, 'regional_guidance'>
@@ -855,11 +854,10 @@ export const canvasSlice = createSlice({
if (!referenceImage) {
return;
}
- if (referenceImage.ipAdapter.type !== 'ip_adapter') {
+ if (!isIPAdapterConfig(referenceImage.config)) {
return;
}
-
- referenceImage.ipAdapter.clipVisionModel = clipVisionModel;
+ referenceImage.config.clipVisionModel = clipVisionModel;
},
//#region Inpaint mask
inpaintMaskAdded: {
@@ -1686,15 +1684,15 @@ export const {
rgPositivePromptChanged,
rgNegativePromptChanged,
rgAutoNegativeToggled,
- rgIPAdapterAdded,
- rgIPAdapterDeleted,
- rgIPAdapterImageChanged,
- rgIPAdapterWeightChanged,
- rgIPAdapterBeginEndStepPctChanged,
- rgIPAdapterMethodChanged,
- rgIPAdapterModelChanged,
- rgIPAdapterCLIPVisionModelChanged,
- rgIPAdapterFLUXReduxImageInfluenceChanged,
+ rgRefImageAdded,
+ rgRefImageDeleted,
+ rgRefImageImageChanged,
+ rgRefImageIPAdapterWeightChanged,
+ rgRefImageIPAdapterBeginEndStepPctChanged,
+ rgRefImageIPAdapterMethodChanged,
+ rgRefImageModelChanged,
+ rgRefImageIPAdapterCLIPVisionModelChanged,
+ rgRefImageFLUXReduxImageInfluenceChanged,
// Inpaint mask
inpaintMaskAdded,
inpaintMaskConvertedToRegionalGuidance,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
index 8188342c11..c37a6d9f56 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
@@ -2,7 +2,6 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import type { PersistConfig, RootState } from 'app/store/store';
-import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasMetadataRecalled } from 'features/controlLayers/store/canvasSlice';
import type { FLUXReduxImageInfluence, RefImagesState } from 'features/controlLayers/store/types';
@@ -12,8 +11,8 @@ import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConf
import { assert } from 'tsafe';
import type { PartialDeep } from 'type-fest';
-import type { CanvasReferenceImageState, CLIPVisionModelV2, IPMethodV2 } from './types';
-import { getInitialRefImagesState } from './types';
+import type { CLIPVisionModelV2, IPMethodV2, RefImageState } from './types';
+import { getInitialRefImagesState, isFLUXReduxConfig, isIPAdapterConfig } from './types';
import {
getReferenceImageState,
imageDTOToImageWithDims,
@@ -22,84 +21,69 @@ import {
initialIPAdapter,
} from './util';
-type PayloadWithId = T extends void
- ? { id: string }
- : {
- id: string;
- } & T;
+type PayloadActionWithId = T extends void
+ ? PayloadAction<{ id: string }>
+ : PayloadAction<
+ {
+ id: string;
+ } & T
+ >;
export const refImagesSlice = createSlice({
name: 'refImages',
initialState: getInitialRefImagesState(),
reducers: {
- referenceImageAdded: {
- reducer: (
- state,
- action: PayloadAction<{
- id: string;
- overrides?: PartialDeep;
- isSelected?: boolean;
- }>
- ) => {
- const { id, overrides, isSelected } = action.payload;
+ refImageAdded: {
+ reducer: (state, action: PayloadActionWithId<{ overrides?: PartialDeep }>) => {
+ const { id, overrides } = action.payload;
const entityState = getReferenceImageState(id, overrides);
state.entities.push(entityState);
-
- if (isSelected) {
- state.selectedId = entityState.id;
- }
},
- prepare: (payload?: { overrides?: PartialDeep; isSelected?: boolean }) => ({
+ prepare: (payload?: { overrides?: PartialDeep; isSelected?: boolean }) => ({
payload: { ...payload, id: getPrefixedId('reference_image') },
}),
},
- referenceImageRecalled: (state, action: PayloadAction<{ data: CanvasReferenceImageState }>) => {
+ refImageRecalled: (state, action: PayloadAction<{ data: RefImageState }>) => {
const { data } = action.payload;
state.entities.push(data);
- state.selectedId = data.id;
},
- referenceImageIPAdapterImageChanged: (
- state,
- action: PayloadAction>
- ) => {
+ refImageImageChanged: (state, action: PayloadActionWithId<{ imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const entity = selectRefImageEntity(state, id);
if (!entity) {
return;
}
- entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
+ entity.config.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
- referenceImageIPAdapterMethodChanged: (state, action: PayloadAction>) => {
+ refImageIPAdapterMethodChanged: (state, action: PayloadActionWithId<{ method: IPMethodV2 }>) => {
const { id, method } = action.payload;
const entity = selectRefImageEntity(state, id);
if (!entity) {
return;
}
- if (entity.ipAdapter.type !== 'ip_adapter') {
+ if (!isIPAdapterConfig(entity.config)) {
return;
}
- entity.ipAdapter.method = method;
+ entity.config.method = method;
},
- referenceImageIPAdapterFLUXReduxImageInfluenceChanged: (
+ refImageFLUXReduxImageInfluenceChanged: (
state,
- action: PayloadAction>
+ action: PayloadActionWithId<{ imageInfluence: FLUXReduxImageInfluence }>
) => {
const { id, imageInfluence } = action.payload;
const entity = selectRefImageEntity(state, id);
if (!entity) {
return;
}
- if (entity.ipAdapter.type !== 'flux_redux') {
+ if (!isFLUXReduxConfig(entity.config)) {
return;
}
- entity.ipAdapter.imageInfluence = imageInfluence;
+ entity.config.imageInfluence = imageInfluence;
},
- referenceImageIPAdapterModelChanged: (
+ refImageModelChanged: (
state,
- action: PayloadAction<
- PayloadWithId<{ modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig | null }>
- >
+ action: PayloadActionWithId<{ modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig | null }>
) => {
const { id, modelConfig } = action.payload;
const entity = selectRefImageEntity(state, id);
@@ -107,16 +91,16 @@ export const refImagesSlice = createSlice({
return;
}
- const oldModel = entity.ipAdapter.model;
+ const oldModel = entity.config.model;
// First set the new model
- entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
+ entity.config.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
- if (!entity.ipAdapter.model) {
+ if (!entity.config.model) {
return;
}
- if (isEqual(oldModel, entity.ipAdapter.model)) {
+ if (isEqual(oldModel, entity.config.model)) {
// Nothing changed, so we don't need to do anything
return;
}
@@ -124,147 +108,85 @@ export const refImagesSlice = createSlice({
// The type of ref image depends on the model. When the user switches the model, we rebuild the ref image.
// When we switch the model, we keep the image the same, but change the other parameters.
- if (entity.ipAdapter.model.base === 'chatgpt-4o') {
+ if (entity.config.model.base === 'chatgpt-4o') {
// Switching to chatgpt-4o ref image
- entity.ipAdapter = {
+ entity.config = {
...initialChatGPT4oReferenceImage,
- image: entity.ipAdapter.image,
- model: entity.ipAdapter.model,
+ image: entity.config.image,
+ model: entity.config.model,
};
return;
}
- if (entity.ipAdapter.model.type === 'flux_redux') {
+ if (entity.config.model.type === 'flux_redux') {
// Switching to flux_redux
- entity.ipAdapter = {
+ entity.config = {
...initialFLUXRedux,
- image: entity.ipAdapter.image,
- model: entity.ipAdapter.model,
+ image: entity.config.image,
+ model: entity.config.model,
};
return;
}
- if (entity.ipAdapter.model.type === 'ip_adapter') {
+ if (entity.config.model.type === 'ip_adapter') {
// Switching to ip_adapter
- entity.ipAdapter = {
+ entity.config = {
...initialIPAdapter,
- image: entity.ipAdapter.image,
- model: entity.ipAdapter.model,
+ image: entity.config.image,
+ model: entity.config.model,
};
// Ensure that the IP Adapter model is compatible with the CLIP Vision model
- if (entity.ipAdapter.model?.base === 'flux') {
- entity.ipAdapter.clipVisionModel = 'ViT-L';
- } else if (entity.ipAdapter.clipVisionModel === 'ViT-L') {
+ if (entity.config.model?.base === 'flux') {
+ entity.config.clipVisionModel = 'ViT-L';
+ } else if (entity.config.clipVisionModel === 'ViT-L') {
// Fall back to ViT-H (ViT-G would also work)
- entity.ipAdapter.clipVisionModel = 'ViT-H';
+ entity.config.clipVisionModel = 'ViT-H';
}
return;
}
},
- referenceImageIPAdapterCLIPVisionModelChanged: (
+ refImageIPAdapterCLIPVisionModelChanged: (
state,
- action: PayloadAction>
+ action: PayloadActionWithId<{ clipVisionModel: CLIPVisionModelV2 }>
) => {
const { id, clipVisionModel } = action.payload;
const entity = selectRefImageEntity(state, id);
if (!entity) {
return;
}
- if (entity.ipAdapter.type !== 'ip_adapter') {
+ if (!isIPAdapterConfig(entity.config)) {
return;
}
- entity.ipAdapter.clipVisionModel = clipVisionModel;
+ entity.config.clipVisionModel = clipVisionModel;
},
- referenceImageIPAdapterWeightChanged: (state, action: PayloadAction>) => {
+ refImageIPAdapterWeightChanged: (state, action: PayloadActionWithId<{ weight: number }>) => {
const { id, weight } = action.payload;
const entity = selectRefImageEntity(state, id);
if (!entity) {
return;
}
- if (entity.ipAdapter.type !== 'ip_adapter') {
+ if (!isIPAdapterConfig(entity.config)) {
return;
}
- entity.ipAdapter.weight = weight;
+ entity.config.weight = weight;
},
- referenceImageIPAdapterBeginEndStepPctChanged: (
+ refImageIPAdapterBeginEndStepPctChanged: (
state,
- action: PayloadAction>
+ action: PayloadActionWithId<{ beginEndStepPct: [number, number] }>
) => {
const { id, beginEndStepPct } = action.payload;
const entity = selectRefImageEntity(state, id);
if (!entity) {
return;
}
- if (entity.ipAdapter.type !== 'ip_adapter') {
+ if (!isIPAdapterConfig(entity.config)) {
return;
}
- entity.ipAdapter.beginEndStepPct = beginEndStepPct;
+ entity.config.beginEndStepPct = beginEndStepPct;
},
- //#region Shared entity
- entitySelected: (state, action: PayloadAction<{ id: string }>) => {
+ refImageDeleted: (state, action: PayloadActionWithId) => {
const { id } = action.payload;
- const entity = selectRefImageEntity(state, id);
- if (!entity) {
- // Cannot select a non-existent entity
- return;
- }
- state.selectedId = id;
- },
- entityNameChanged: (state, action: PayloadAction>) => {
- const { id, name } = action.payload;
- const entity = selectRefImageEntity(state, id);
- if (!entity) {
- return;
- }
- entity.name = name;
- },
- entityDuplicated: (state, action: PayloadAction) => {
- const { id } = action.payload;
- const entity = selectRefImageEntity(state, id);
- if (!entity) {
- return;
- }
-
- const newEntity = deepClone(entity);
- if (newEntity.name) {
- newEntity.name = `${newEntity.name} (Copy)`;
- }
- newEntity.id = getPrefixedId('reference_image');
- state.entities.push(newEntity);
-
- state.selectedId = newEntity.id;
- },
- entityIsEnabledToggled: (state, action: PayloadAction) => {
- const { id } = action.payload;
- const entity = selectRefImageEntity(state, id);
- if (!entity) {
- return;
- }
- entity.isEnabled = !entity.isEnabled;
- },
- entityIsLockedToggled: (state, action: PayloadAction) => {
- const { id } = action.payload;
- const entity = selectRefImageEntity(state, id);
- if (!entity) {
- return;
- }
- entity.isLocked = !entity.isLocked;
- },
- entityDeleted: (state, action: PayloadAction) => {
- const { id } = action.payload;
-
- let selectedId: string | null = null;
- const entities = state.entities;
- const index = entities.findIndex((entity) => entity.id === id);
- const nextIndex = entities.length > 1 ? (index + 1) % entities.length : -1;
- if (nextIndex !== -1) {
- const nextEntity = entities[nextIndex];
- if (nextEntity) {
- selectedId = nextEntity.id;
- }
- }
state.entities = state.entities.filter((rg) => rg.id !== id);
- state.selectedId = selectedId;
},
refImagesReset: () => getInitialRefImagesState(),
},
@@ -277,15 +199,14 @@ export const refImagesSlice = createSlice({
});
export const {
- referenceImageAdded,
- // referenceImageRecalled,
- referenceImageIPAdapterImageChanged,
- referenceImageIPAdapterMethodChanged,
- referenceImageIPAdapterModelChanged,
- referenceImageIPAdapterCLIPVisionModelChanged,
- referenceImageIPAdapterWeightChanged,
- referenceImageIPAdapterBeginEndStepPctChanged,
- referenceImageIPAdapterFLUXReduxImageInfluenceChanged,
+ refImageAdded,
+ refImageImageChanged,
+ refImageIPAdapterMethodChanged,
+ refImageModelChanged,
+ refImageIPAdapterCLIPVisionModelChanged,
+ refImageIPAdapterWeightChanged,
+ refImageIPAdapterBeginEndStepPctChanged,
+ refImageFLUXReduxImageInfluenceChanged,
} = refImagesSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -303,20 +224,13 @@ export const refImagesPersistConfig: PersistConfig = {
export const selectRefImagesSlice = (state: RootState) => state.refImages;
export const selectReferenceImageEntities = createSelector(selectRefImagesSlice, (state) => state.entities);
-export const selectActiveReferenceImageEntities = createSelector(selectReferenceImageEntities, (entities) =>
- entities.filter((e) => e.isEnabled)
-);
export const selectRefImageEntityIds = createMemoizedSelector(selectReferenceImageEntities, (entities) =>
entities.map((e) => e.id)
);
export const selectRefImageEntity = (state: RefImagesState, id: string) =>
state.entities.find((entity) => entity.id === id) ?? null;
-export function selectRefImageEntityOrThrow(
- state: RefImagesState,
- id: string,
- caller: string
-): CanvasReferenceImageState {
+export function selectRefImageEntityOrThrow(state: RefImagesState, id: string, caller: string): RefImageState {
const entity = selectRefImageEntity(state, id);
assert(entity, `Entity with id ${id} not found in ${caller}`);
return entity;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 8914e5805c..dacaff07ca 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -300,25 +300,25 @@ const zCanvasEntityBase = z.object({
isLocked: z.boolean(),
});
-const zCanvasReferenceImageState = zCanvasEntityBase.extend({
- type: z.literal('reference_image'),
+const zRefImageState = z.object({
+ id: zId,
// This should be named `referenceImage` but we need to keep it as `ipAdapter` for backwards compatibility
- ipAdapter: z.discriminatedUnion('type', [
+ config: z.discriminatedUnion('type', [
zIPAdapterConfig,
zFLUXReduxConfig,
zChatGPT4oReferenceImageConfig,
zFluxKontextReferenceImageConfig,
]),
});
-export type CanvasReferenceImageState = z.infer;
+export type RefImageState = z.infer;
-export const isIPAdapterConfig = (config: CanvasReferenceImageState['ipAdapter']): config is IPAdapterConfig =>
+export const isIPAdapterConfig = (config: RefImageState['config']): config is IPAdapterConfig =>
config.type === 'ip_adapter';
-export const isFLUXReduxConfig = (config: CanvasReferenceImageState['ipAdapter']): config is FLUXReduxConfig =>
+export const isFLUXReduxConfig = (config: RefImageState['config']): config is FLUXReduxConfig =>
config.type === 'flux_redux';
export const isChatGPT4oReferenceImageConfig = (
- config: CanvasReferenceImageState['ipAdapter']
+ config: RefImageState['config']
): config is ChatGPT4oReferenceImageConfig => config.type === 'chatgpt_4o_reference_image';
export const isFluxKontextReferenceImageConfig = (
config: CanvasReferenceImageState['ipAdapter']
@@ -329,11 +329,11 @@ export type FillStyle = z.infer;
export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success;
const zFill = z.object({ style: zFillStyle, color: zRgbColor });
-const zRegionalGuidanceReferenceImageState = z.object({
+const zRegionalGuidanceRefImageState = z.object({
id: zId,
- ipAdapter: z.discriminatedUnion('type', [zIPAdapterConfig, zFLUXReduxConfig]),
+ config: z.discriminatedUnion('type', [zIPAdapterConfig, zFLUXReduxConfig]),
});
-export type RegionalGuidanceReferenceImageState = z.infer;
+export type RegionalGuidanceRefImageState = z.infer;
const zCanvasRegionalGuidanceState = zCanvasEntityBase.extend({
type: z.literal('regional_guidance'),
@@ -343,7 +343,7 @@ const zCanvasRegionalGuidanceState = zCanvasEntityBase.extend({
fill: zFill,
positivePrompt: zParameterPositivePrompt.nullable(),
negativePrompt: zParameterNegativePrompt.nullable(),
- referenceImages: z.array(zRegionalGuidanceReferenceImageState),
+ referenceImages: z.array(zRegionalGuidanceRefImageState),
autoNegative: z.boolean(),
});
export type CanvasRegionalGuidanceState = z.infer;
@@ -578,8 +578,7 @@ const zCanvasState = z.object({
export type CanvasState = z.infer;
const zRefImagesState = z.object({
- selectedId: zId.nullable().default(null),
- entities: z.array(zCanvasReferenceImageState).default(() => []),
+ entities: z.array(zRefImageState).default(() => []),
});
export type RefImagesState = z.infer;
const INITIAL_REF_IMAGES_STATE = zRefImagesState.parse({});
@@ -596,7 +595,7 @@ export const zCanvasMetadata = z.object({
rasterLayers: z.array(zCanvasRasterLayerState),
controlLayers: z.array(zCanvasControlLayerState),
regionalGuidance: z.array(zCanvasRegionalGuidanceState),
- referenceImages: z.array(zCanvasReferenceImageState),
+ referenceImages: z.array(zRefImageState),
});
export type CanvasMetadata = z.infer;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/util.ts b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
index 8cebbe4846..bcc6f7943c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
@@ -5,7 +5,6 @@ import type {
CanvasImageState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
- CanvasReferenceImageState,
CanvasRegionalGuidanceState,
ChatGPT4oReferenceImageConfig,
ControlLoRAConfig,
@@ -15,6 +14,7 @@ import type {
FLUXReduxConfig,
ImageWithDims,
IPAdapterConfig,
+ RefImageState,
RgbColor,
T2IAdapterConfig,
} from 'features/controlLayers/store/types';
@@ -126,17 +126,10 @@ export const initialControlLoRA: ControlLoRAConfig = {
weight: 0.75,
};
-export const getReferenceImageState = (
- id: string,
- overrides?: PartialDeep
-): CanvasReferenceImageState => {
- const entityState: CanvasReferenceImageState = {
+export const getReferenceImageState = (id: string, overrides?: PartialDeep): RefImageState => {
+ const entityState: RefImageState = {
id,
- type: 'reference_image',
- name: null,
- isLocked: false,
- isEnabled: true,
- ipAdapter: deepClone(initialIPAdapter),
+ config: deepClone(initialIPAdapter),
};
merge(entityState, overrides);
return entityState;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
index 425dab23ff..534c9a337a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
@@ -2,8 +2,8 @@ import type {
CanvasControlLayerState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
- CanvasReferenceImageState,
CanvasRegionalGuidanceState,
+ RefImageState,
} from 'features/controlLayers/store/types';
import type { MainModelConfig } from 'services/api/types';
@@ -58,16 +58,16 @@ export const getRegionalGuidanceWarnings = (
}
}
- entity.referenceImages.forEach(({ ipAdapter }) => {
- if (!ipAdapter.model) {
+ entity.referenceImages.forEach(({ config }) => {
+ if (!config.model) {
// No model selected
warnings.push(WARNINGS.IP_ADAPTER_NO_MODEL_SELECTED);
- } else if (ipAdapter.model.base !== model.base) {
+ } else if (config.model.base !== model.base) {
// Supported model architecture but doesn't match
warnings.push(WARNINGS.IP_ADAPTER_INCOMPATIBLE_BASE_MODEL);
}
- if (!ipAdapter.image) {
+ if (!config.image) {
// No image selected
warnings.push(WARNINGS.IP_ADAPTER_NO_IMAGE_SELECTED);
}
@@ -78,7 +78,7 @@ export const getRegionalGuidanceWarnings = (
};
export const getGlobalReferenceImageWarnings = (
- entity: CanvasReferenceImageState,
+ entity: RefImageState,
model: MainModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -90,17 +90,17 @@ export const getGlobalReferenceImageWarnings = (
return warnings;
}
- const { ipAdapter } = entity;
+ const { config } = entity;
- if (!ipAdapter.model) {
+ if (!config.model) {
// No model selected
warnings.push(WARNINGS.IP_ADAPTER_NO_MODEL_SELECTED);
- } else if (ipAdapter.model.base !== model.base) {
+ } else if (config.model.base !== model.base) {
// Supported model architecture but doesn't match
warnings.push(WARNINGS.IP_ADAPTER_INCOMPATIBLE_BASE_MODEL);
}
- if (!entity.ipAdapter.image) {
+ if (!entity.config.image) {
// No image selected
warnings.push(WARNINGS.IP_ADAPTER_NO_IMAGE_SELECTED);
}
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
index 6f4fd19571..8dec87f5e7 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
+++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
@@ -3,7 +3,7 @@ import { getStore, useAppStore } from 'app/store/nanostores/store';
import type { AppDispatch, AppGetState, RootState } from 'app/store/store';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import {
- referenceImageIPAdapterImageChanged,
+ refImageImageChanged,
selectReferenceImageEntities,
selectRefImagesSlice,
} from 'features/controlLayers/store/refImagesSlice';
@@ -228,8 +228,8 @@ const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image
const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
selectReferenceImageEntities(state).forEach((entity) => {
- if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
- dispatch(referenceImageIPAdapterImageChanged({ id: entity.id, imageDTO: null }));
+ if (entity.config.image?.image_name === imageDTO.image_name) {
+ dispatch(refImageImageChanged({ id: entity.id, imageDTO: null }));
}
});
};
@@ -276,7 +276,7 @@ export const getImageUsage = (
const isUpscaleImage = upscale.upscaleInitialImage?.image_name === image_name;
- const isReferenceImage = refImages.entities.some(({ ipAdapter }) => ipAdapter.image?.image_name === image_name);
+ const isReferenceImage = refImages.entities.some(({ config }) => config.image?.image_name === image_name);
const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
@@ -291,7 +291,7 @@ export const getImageUsage = (
);
const isRegionalGuidanceImage = canvas.regionalGuidance.entities.some(({ referenceImages }) =>
- referenceImages.some(({ ipAdapter }) => ipAdapter.image?.image_name === image_name)
+ referenceImages.some(({ config }) => config.image?.image_name === image_name)
);
const imageUsage: ImageUsage = {
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx
index fa20556c5d..7b3971ba39 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx
@@ -3,7 +3,7 @@ import { useAppStore } from 'app/store/nanostores/store';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
-import { referenceImageAdded } from 'features/controlLayers/store/refImagesSlice';
+import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
@@ -77,7 +77,7 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
const onClickNewGlobalReferenceImageFromImage = useCallback(() => {
const { dispatch } = store;
- dispatch(referenceImageAdded({ overrides: { ipAdapter: { image: imageDTOToImageWithDims(imageDTO) } } }));
+ dispatch(refImageAdded({ overrides: { config: { image: imageDTOToImageWithDims(imageDTO) } } }));
dispatch(sentImageToCanvas());
dispatch(setActiveTab('canvas'));
imageViewer.close();
diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts
index 81fe6e9c44..babbdd35a2 100644
--- a/invokeai/frontend/web/src/features/imageActions/actions.ts
+++ b/invokeai/frontend/web/src/features/imageActions/actions.ts
@@ -11,10 +11,10 @@ import {
inpaintMaskAdded,
rasterLayerAdded,
rgAdded,
- rgIPAdapterImageChanged,
+ rgRefImageImageChanged,
} from 'features/controlLayers/store/canvasSlice';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { referenceImageAdded, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/refImagesSlice';
+import { refImageAdded, refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
@@ -41,7 +41,7 @@ import { assert } from 'tsafe';
export const setGlobalReferenceImage = (arg: { imageDTO: ImageDTO; id: string; dispatch: AppDispatch }) => {
const { imageDTO, id, dispatch } = arg;
- dispatch(referenceImageIPAdapterImageChanged({ id, imageDTO }));
+ dispatch(refImageImageChanged({ id, imageDTO }));
};
export const setRegionalGuidanceReferenceImage = (arg: {
@@ -51,7 +51,7 @@ export const setRegionalGuidanceReferenceImage = (arg: {
dispatch: AppDispatch;
}) => {
const { imageDTO, entityIdentifier, referenceImageId, dispatch } = arg;
- dispatch(rgIPAdapterImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
+ dispatch(rgRefImageImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
};
export const setUpscaleInitialImage = (arg: { imageDTO: ImageDTO; dispatch: AppDispatch }) => {
@@ -112,9 +112,9 @@ export const createNewCanvasEntityFromImage = (arg: {
break;
}
case 'regional_guidance_with_reference_image': {
- const ipAdapter = deepClone(selectDefaultIPAdapter(getState()));
- ipAdapter.image = imageDTOToImageWithDims(imageDTO);
- const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }];
+ const config = deepClone(selectDefaultIPAdapter(getState()));
+ config.image = imageDTOToImageWithDims(imageDTO);
+ const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), config }];
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
break;
}
@@ -242,10 +242,10 @@ export const newCanvasFromImage = async (arg: {
break;
}
case 'reference_image': {
- const ipAdapter = deepClone(selectDefaultRefImageConfig(getState()));
- ipAdapter.image = imageDTOToImageWithDims(imageDTO);
+ const config = deepClone(selectDefaultRefImageConfig(getState()));
+ config.image = imageDTOToImageWithDims(imageDTO);
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
- dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true }));
+ dispatch(refImageAdded({ overrides: { config }, isSelected: true }));
if (withInpaintMask) {
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
}
@@ -253,9 +253,9 @@ export const newCanvasFromImage = async (arg: {
break;
}
case 'regional_guidance_with_reference_image': {
- const ipAdapter = deepClone(selectDefaultIPAdapter(getState()));
- ipAdapter.image = imageDTOToImageWithDims(imageDTO);
- const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }];
+ const config = deepClone(selectDefaultIPAdapter(getState()));
+ config.image = imageDTOToImageWithDims(imageDTO);
+ const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), config }];
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
if (withInpaintMask) {
diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts
index 16ede40209..4504c05667 100644
--- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts
+++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts
@@ -5,9 +5,9 @@ import type {
CanvasInpaintMaskState,
CanvasMetadata,
CanvasRasterLayerState,
- CanvasReferenceImageState,
CanvasRegionalGuidanceState,
LoRA,
+ RefImageState,
} from 'features/controlLayers/store/types';
import { zCanvasMetadata, zCanvasRasterLayerState } from 'features/controlLayers/store/types';
import {
@@ -417,7 +417,7 @@ const parseAllIPAdapters: MetadataParseFunc = async (
const parseLayer: MetadataParseFunc<
| CanvasRasterLayerState
| CanvasControlLayerState
- | CanvasReferenceImageState
+ | RefImageState
| CanvasRegionalGuidanceState
| CanvasInpaintMaskState
> = (metadataItem) => zCanvasRasterLayerState.parseAsync(metadataItem);
@@ -431,7 +431,7 @@ const parseLayers: MetadataParseFunc<
(
| CanvasRasterLayerState
| CanvasControlLayerState
- | CanvasReferenceImageState
+ | RefImageState
| CanvasRegionalGuidanceState
| CanvasInpaintMaskState
)[]
@@ -444,7 +444,7 @@ const parseLayers: MetadataParseFunc<
const layers: (
| CanvasRasterLayerState
| CanvasControlLayerState
- | CanvasReferenceImageState
+ | RefImageState
| CanvasRegionalGuidanceState
| CanvasInpaintMaskState
)[] = [];
@@ -493,7 +493,7 @@ const parseLayers: MetadataParseFunc<
ipAdaptersRaw.map(async (cn) => await parseIPAdapterToIPAdapterLayer(cn))
);
const ipAdaptersAsLayers = ipAdaptersParseResults
- .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled')
+ .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled')
.map((result) => result.value);
layers.push(...ipAdaptersAsLayers);
} catch {
@@ -598,7 +598,7 @@ const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => {
+const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async (metadataItem) => {
const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model');
const key = await getModelKey(ip_adapter_model, 'ip_adapter');
const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig);
@@ -630,13 +630,9 @@ const parseIPAdapterToIPAdapterLayer: MetadataParseFunc;
model: MainModelConfig;
@@ -22,19 +18,18 @@ type AddFLUXReduxArg = {
export const addFLUXReduxes = ({ entities, g, collector, model }: AddFLUXReduxArg): AddFLUXReduxResult => {
const validFLUXReduxes = entities
- .filter((entity) => entity.isEnabled)
- .filter((entity) => isFLUXReduxConfig(entity.ipAdapter))
+ .filter((entity) => isFLUXReduxConfig(entity.config))
.filter((entity) => getGlobalReferenceImageWarnings(entity, model).length === 0);
const result: AddFLUXReduxResult = {
addedFLUXReduxes: 0,
};
- for (const { id, ipAdapter } of validFLUXReduxes) {
- assert(isFLUXReduxConfig(ipAdapter), 'This should have been filtered out');
+ for (const { id, config } of validFLUXReduxes) {
+ assert(isFLUXReduxConfig(config), 'This should have been filtered out');
result.addedFLUXReduxes++;
- addFLUXRedux(id, ipAdapter, g, collector);
+ addFLUXRedux(id, config, g, collector);
}
return result;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts
index f063afbb29..49e94aa03f 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts
@@ -1,8 +1,4 @@
-import {
- type CanvasReferenceImageState,
- type IPAdapterConfig,
- isIPAdapterConfig,
-} from 'features/controlLayers/store/types';
+import { type IPAdapterConfig, isIPAdapterConfig,type RefImageState } from 'features/controlLayers/store/types';
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import type { Invocation, MainModelConfig } from 'services/api/types';
@@ -13,7 +9,7 @@ type AddIPAdaptersResult = {
};
type AddIPAdaptersArg = {
- entities: CanvasReferenceImageState[];
+ entities: RefImageState[];
g: Graph;
collector: Invocation<'collect'>;
model: MainModelConfig;
@@ -21,19 +17,18 @@ type AddIPAdaptersArg = {
export const addIPAdapters = ({ entities, g, collector, model }: AddIPAdaptersArg): AddIPAdaptersResult => {
const validIPAdapters = entities
- .filter((entity) => entity.isEnabled)
- .filter((entity) => isIPAdapterConfig(entity.ipAdapter))
+ .filter((entity) => isIPAdapterConfig(entity.config))
.filter((entity) => getGlobalReferenceImageWarnings(entity, model).length === 0);
const result: AddIPAdaptersResult = {
addedIPAdapters: 0,
};
- for (const { id, ipAdapter } of validIPAdapters) {
- assert(isIPAdapterConfig(ipAdapter), 'This should have been filtered out');
+ for (const { id, config } of validIPAdapters) {
+ assert(isIPAdapterConfig(config), 'This should have been filtered out');
result.addedIPAdapters++;
- addIPAdapter(id, ipAdapter, g, collector);
+ addIPAdapter(id, config, g, collector);
}
return result;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts
index d36591bdae..7ab6c7e7fa 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts
@@ -3,7 +3,12 @@ import { deepClone } from 'common/util/deepClone';
import { withResultAsync } from 'common/util/result';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import type { CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types';
+import {
+ type CanvasRegionalGuidanceState,
+ isFLUXReduxConfig,
+ isIPAdapterConfig,
+ type Rect,
+} from 'features/controlLayers/store/types';
import { getRegionalGuidanceWarnings } from 'features/controlLayers/store/validators';
import { IMAGE_INFLUENCE_TO_SETTINGS } from 'features/nodes/util/graph/generation/addFLUXRedux';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
@@ -273,12 +278,12 @@ export const addRegions = async ({
}
}
- for (const { id, ipAdapter } of region.referenceImages) {
- if (ipAdapter.type === 'ip_adapter') {
+ for (const { id, config } of region.referenceImages) {
+ if (isIPAdapterConfig(config)) {
assert(!isFLUX, 'Regional IP adapters are not supported for FLUX.');
result.addedIPAdapters++;
- const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter;
+ const { weight, model, clipVisionModel, method, beginEndStepPct, image } = config;
assert(model, 'IP Adapter model is required');
assert(image, 'IP Adapter image is required');
@@ -299,11 +304,11 @@ export const addRegions = async ({
// Connect the mask to the conditioning
g.addEdge(maskToTensor, 'mask', ipAdapterNode, 'mask');
g.addEdge(ipAdapterNode, 'ip_adapter', ipAdapterCollect, 'item');
- } else if (ipAdapter.type === 'flux_redux') {
+ } else if (isFLUXReduxConfig(config)) {
assert(isFLUX, 'Regional FLUX Redux requires FLUX.');
assert(fluxReduxCollect !== null, 'FLUX Redux collector is required.');
result.addedFLUXReduxes++;
- const { model: fluxReduxModel, image } = ipAdapter;
+ const { model: fluxReduxModel, image } = config;
assert(fluxReduxModel, 'FLUX Redux model is required');
assert(image, 'FLUX Redux image is required');
@@ -314,7 +319,7 @@ export const addRegions = async ({
image: {
image_name: image.image_name,
},
- ...IMAGE_INFLUENCE_TO_SETTINGS[ipAdapter.imageInfluence ?? 'highest'],
+ ...IMAGE_INFLUENCE_TO_SETTINGS[config.imageInfluence ?? 'highest'],
});
// Connect the mask to the conditioning
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
index 09a24b9669..2f389d059a 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts
@@ -44,8 +44,7 @@ export const buildChatGPT4oGraph = async (
assert(isChatGPT4oAspectRatioID(bbox.aspectRatio.id), 'ChatGPT 4o does not support this aspect ratio');
const validRefImages = refImages.entities
- .filter((entity) => entity.isEnabled)
- .filter((entity) => isChatGPT4oReferenceImageConfig(entity.ipAdapter))
+ .filter((entity) => isChatGPT4oReferenceImageConfig(entity.config))
.filter((entity) => getGlobalReferenceImageWarnings(entity, model).length === 0)
.toReversed(); // sends them in order they are displayed in the list
@@ -54,9 +53,9 @@ export const buildChatGPT4oGraph = async (
if (validRefImages.length > 0) {
reference_images = [];
for (const entity of validRefImages) {
- assert(entity.ipAdapter.image, 'Image is required for reference image');
+ assert(entity.config.image, 'Image is required for reference image');
reference_images.push({
- image_name: entity.ipAdapter.image.image_name,
+ image_name: entity.config.image.image_name,
});
}
}
diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts
index 2355feed84..1d8e8a5444 100644
--- a/invokeai/frontend/web/src/features/queue/store/readiness.ts
+++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts
@@ -523,27 +523,23 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
});
// Flux Kontext only supports 1x Reference Image at a time.
- const referenceImageCount = refImages.entities.filter((entity) => entity.isEnabled).length;
+ const referenceImageCount = refImages.entities.length;
if (model?.base === 'flux-kontext' && referenceImageCount > 1) {
reasons.push({ content: i18n.t('parameters.invoke.fluxKontextMultipleReferenceImages') });
}
- refImages.entities
- .filter((entity) => entity.isEnabled)
- .forEach((entity, i) => {
- const layerLiteral = i18n.t('controlLayers.layer_one');
- const layerNumber = i + 1;
- const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
- const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
+ refImages.entities.forEach((entity, i) => {
+ const layerNumber = i + 1;
+ const refImageLiteral = i18n.t(LAYER_TYPE_TO_TKEY['reference_image']);
+ const prefix = `${refImageLiteral} #${layerNumber}`;
+ const problems = getGlobalReferenceImageWarnings(entity, model);
- const problems = getGlobalReferenceImageWarnings(entity, model);
-
- if (problems.length) {
- const content = upperFirst(problems.map((p) => i18n.t(p)).join(', '));
- reasons.push({ prefix, content });
- }
- });
+ if (problems.length) {
+ const content = upperFirst(problems.map((p) => i18n.t(p)).join(', '));
+ reasons.push({ prefix, content });
+ }
+ });
canvas.regionalGuidance.entities
.filter((entity) => entity.isEnabled)
From 48e2e7e4a11e33ef58d0d80529646fdde46127fe Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 13 Jun 2025 13:08:03 +1000
Subject: [PATCH 088/210] refactor(ui): ref images (WIP)
---
.../components/CanvasAddEntityButtons.tsx | 4 +-
...nvasContextMenuSelectedEntityMenuItems.tsx | 2 +-
.../EntityListGlobalActionBarAddLayerMenu.tsx | 4 +-
.../components/IPAdapter/IPAdapter.tsx | 29 ---
.../{IPAdapter => RefImage}/IPAdapterList.tsx | 4 +-
.../IPAdapterMenuItemPullBbox.tsx | 0
.../IPAdapterMenuItems.tsx | 2 +-
.../IPAdapterMethod.tsx | 0
.../IPAdapterSettings.tsx | 52 +++---
.../RefImage.tsx} | 11 +-
.../RefImageImage.tsx} | 4 +-
.../RefImageModel.tsx} | 4 +-
.../RefImageNoImageState.tsx} | 4 +-
...onalGuidanceAddPromptsIPAdapterButtons.tsx | 12 +-
.../RegionalGuidanceIPAdapterSettings.tsx | 10 +-
...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 12 +-
.../components/common/CanvasEntityHeader.tsx | 2 +-
...Model.tsx => IPAdapterCLIPVisionModel.tsx} | 4 +-
.../controlLayers/hooks/addLayerHooks.ts | 171 +++++++++---------
.../controlLayers/hooks/saveCanvasHooks.ts | 23 +--
invokeai/frontend/web/src/features/dnd/dnd.ts | 6 +-
.../ImageMenuItemNewLayerFromImageSubMenu.tsx | 22 ---
.../ImageMenuItemUseAsRefImage.tsx | 39 ++++
.../SingleSelectionMenuItems.tsx | 2 +
.../web/src/features/imageActions/actions.ts | 19 +-
.../components/Core/ParamPositivePrompt.tsx | 2 +-
.../src/services/api/hooks/modelsByType.ts | 20 +-
.../services/events/onInvocationComplete.tsx | 4 +-
.../services/events/onModelInstallError.tsx | 4 +-
29 files changed, 235 insertions(+), 237 deletions(-)
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter => RefImage}/IPAdapterList.tsx (88%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter => RefImage}/IPAdapterMenuItemPullBbox.tsx (100%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter => RefImage}/IPAdapterMenuItems.tsx (94%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter => RefImage}/IPAdapterMethod.tsx (100%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter => RefImage}/IPAdapterSettings.tsx (77%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter/IPAdapterPreview.tsx => RefImage/RefImage.tsx} (89%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter/IPAdapterImagePreview.tsx => RefImage/RefImageImage.tsx} (96%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter/GlobalReferenceImageModel.tsx => RefImage/RefImageModel.tsx} (93%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPAdapter/IPAdapterSettingsEmptyState.tsx => RefImage/RefImageNoImageState.tsx} (95%)
rename invokeai/frontend/web/src/features/controlLayers/components/common/{CLIPVisionModel.tsx => IPAdapterCLIPVisionModel.tsx} (92%)
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
index 6027ed14d2..d3475a385c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
@@ -5,7 +5,7 @@ import {
useAddInpaintMask,
useAddRasterLayer,
useAddRegionalGuidance,
- useAddRegionalReferenceImage,
+ useAddNewRegionalGuidanceWithARefImage,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled';
import { memo } from 'react';
@@ -18,7 +18,7 @@ export const CanvasAddEntityButtons = memo(() => {
const addRegionalGuidance = useAddRegionalGuidance();
const addRasterLayer = useAddRasterLayer();
const addControlLayer = useAddControlLayer();
- const addRegionalReferenceImage = useAddRegionalReferenceImage();
+ const addRegionalReferenceImage = useAddNewRegionalGuidanceWithARefImage();
const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance');
const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer');
const isInpaintLayerEnabled = useIsEntityTypeEnabled('inpaint_mask');
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx
index 015e1d9238..8d150e0bb7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx
@@ -2,7 +2,7 @@ import { MenuGroup } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
-import { IPAdapterMenuItems } from 'features/controlLayers/components/IPAdapter/IPAdapterMenuItems';
+import { IPAdapterMenuItems } from 'features/controlLayers/components/RefImage/IPAdapterMenuItems';
import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems';
import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx
index 9ffdc58bf6..04994f200e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx
@@ -4,7 +4,7 @@ import {
useAddInpaintMask,
useAddRasterLayer,
useAddRegionalGuidance,
- useAddRegionalReferenceImage,
+ useAddNewRegionalGuidanceWithARefImage,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled';
@@ -17,7 +17,7 @@ export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
const isBusy = useCanvasIsBusy();
const addInpaintMask = useAddInpaintMask();
const addRegionalGuidance = useAddRegionalGuidance();
- const addRegionalReferenceImage = useAddRegionalReferenceImage();
+ const addRegionalReferenceImage = useAddNewRegionalGuidanceWithARefImage();
const addRasterLayer = useAddRasterLayer();
const addControlLayer = useAddControlLayer();
const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance');
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx
deleted file mode 100644
index a31841a716..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Spacer } from '@invoke-ai/ui-library';
-import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
-import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
-import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
-import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
-import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings';
-import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
-import { memo } from 'react';
-
-type Props = {
- id: string;
-};
-
-export const IPAdapter = memo(({ id }: Props) => {
- return (
-
-
-
-
-
-
-
-
-
-
- );
-});
-
-IPAdapter.displayName = 'IPAdapter';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterList.tsx
similarity index 88%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterList.tsx
index c803bae13c..6cda069930 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterList.tsx
@@ -2,7 +2,7 @@
import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
-import { RefImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterPreview';
+import { RefImage } from 'features/controlLayers/components/RefImage/RefImage';
import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
import { selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
import { memo } from 'react';
@@ -27,7 +27,7 @@ export const RefImageList = memo((props: FlexProps) => {
{ids.map((id) => (
-
+
))}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItemPullBbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterMenuItemPullBbox.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItemPullBbox.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterMenuItemPullBbox.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterMenuItems.tsx
similarity index 94%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterMenuItems.tsx
index f174bdf004..94d30ad5d2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterMenuItems.tsx
@@ -3,7 +3,7 @@ import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
-import { IPAdapterMenuItemPullBbox } from 'features/controlLayers/components/IPAdapter/IPAdapterMenuItemPullBbox';
+import { IPAdapterMenuItemPullBbox } from 'features/controlLayers/components/RefImage/IPAdapterMenuItemPullBbox';
import { memo } from 'react';
export const IPAdapterMenuItems = memo(() => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterMethod.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMethod.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterMethod.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterSettings.tsx
similarity index 77%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterSettings.tsx
index 1e884209b4..8afacb0d2d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterSettings.tsx
@@ -2,12 +2,12 @@ import { Flex } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
-import { CLIPVisionModel } from 'features/controlLayers/components/common/CLIPVisionModel';
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/common/FLUXReduxImageInfluence';
+import { IPAdapterCLIPVisionModel } from 'features/controlLayers/components/common/IPAdapterCLIPVisionModel';
import { Weight } from 'features/controlLayers/components/common/Weight';
-import { GlobalReferenceImageModel } from 'features/controlLayers/components/IPAdapter/GlobalReferenceImageModel';
-import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
-import { IPAdapterSettingsEmptyState } from 'features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState';
+import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAdapterMethod';
+import { RefImageModel } from 'features/controlLayers/components/RefImage/RefImageModel';
+import { RefImageNoImageState } from 'features/controlLayers/components/RefImage/RefImageNoImageState';
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import {
@@ -22,10 +22,12 @@ import {
selectRefImageEntityOrThrow,
selectRefImagesSlice,
} from 'features/controlLayers/store/refImagesSlice';
-import type {
- CLIPVisionModelV2,
- FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
- IPMethodV2,
+import {
+ type CLIPVisionModelV2,
+ type FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
+ type IPMethodV2,
+ isFLUXReduxConfig,
+ isIPAdapterConfig,
} from 'features/controlLayers/store/types';
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
@@ -33,7 +35,7 @@ import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConfig } from 'services/api/types';
-import { IPAdapterImagePreview } from './IPAdapterImagePreview';
+import { RefImageImage } from './RefImageImage';
const buildSelectConfig = (id: string) =>
createSelector(
@@ -45,8 +47,8 @@ const IPAdapterSettingsContent = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const id = useRefImageIdContext();
- const selectIPAdapter = useMemo(() => buildSelectConfig(id), [id]);
- const ipAdapter = useAppSelector(selectIPAdapter);
+ const selectConfig = useMemo(() => buildSelectConfig(id), [id]);
+ const config = useAppSelector(selectConfig);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
@@ -98,8 +100,8 @@ const IPAdapterSettingsContent = memo(() => {
);
const dndTargetData = useMemo(
- () => setGlobalReferenceImageDndTarget.getData({ id }, ipAdapter.image?.image_name),
- [id, ipAdapter.image?.image_name]
+ () => setGlobalReferenceImageDndTarget.getData({ id }, config.image?.image_name),
+ [id, config.image?.image_name]
);
// const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
// const isBusy = useCanvasIsBusy();
@@ -109,9 +111,9 @@ const IPAdapterSettingsContent = memo(() => {
return (
-
- {ipAdapter.type === 'ip_adapter' && (
-
+
+ {isIPAdapterConfig(config) && (
+
)}
{/* {
/> */}
- {ipAdapter.type === 'ip_adapter' && (
+ {isIPAdapterConfig(config) && (
- {!isFLUX && }
-
-
+ {!isFLUX && }
+
+
)}
- {ipAdapter.type === 'flux_redux' && (
+ {isFLUXReduxConfig(config) && (
)}
- {
const hasImage = useAppSelector(selectIPAdapterHasImage);
if (!hasImage) {
- return ;
+ return ;
}
return ;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
similarity index 89%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
index 01b85fb5f1..1212fbe3fa 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterPreview.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
@@ -14,7 +14,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector } from 'app/store/storeHooks';
import { useDisclosure } from 'common/hooks/useBoolean';
import { useFilterableOutsideClick } from 'common/hooks/useFilterableOutsideClick';
-import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings';
+import { IPAdapterSettings } from 'features/controlLayers/components/RefImage/IPAdapterSettings';
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
import { selectRefImageEntityOrThrow, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import type { ImageWithDims } from 'features/controlLayers/store/types';
@@ -34,15 +34,12 @@ const sx: SystemStyleObject = {
transitionDuration: '0.2s',
};
-export const RefImagePreview = memo(() => {
+export const RefImage = memo(() => {
const id = useRefImageIdContext();
const ref = useRef(null);
const disclosure = useDisclosure(false);
const selectEntity = useMemo(
- () =>
- createSelector(selectRefImagesSlice, (refImages) =>
- selectRefImageEntityOrThrow(refImages, id, 'RefImagePreview')
- ),
+ () => createSelector(selectRefImagesSlice, (refImages) => selectRefImageEntityOrThrow(refImages, id, 'RefImage')),
[id]
);
const entity = useAppSelector(selectEntity);
@@ -66,7 +63,7 @@ export const RefImagePreview = memo(() => {
);
});
-RefImagePreview.displayName = 'RefImagePreview';
+RefImage.displayName = 'RefImage';
const Thumbnail = memo(({ image }: { image: ImageWithDims | null }) => {
const { data: imageDTO } = useGetImageDTOQuery(image?.image_name ?? skipToken);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx
similarity index 96%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx
index 57d1a3f3f4..979b40f5e1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx
@@ -21,7 +21,7 @@ type Props;
};
-export const IPAdapterImagePreview = memo(
+export const RefImageImage = memo(
({
image,
onChangeImage,
@@ -77,4 +77,4 @@ export const IPAdapterImagePreview = memo(
}
);
-IPAdapterImagePreview.displayName = 'IPAdapterImagePreview';
+RefImageImage.displayName = 'RefImageImage';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/GlobalReferenceImageModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageModel.tsx
similarity index 93%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/GlobalReferenceImageModel.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageModel.tsx
index a0b6b4ad4e..8e6e0749aa 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/GlobalReferenceImageModel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageModel.tsx
@@ -12,7 +12,7 @@ type Props = {
onChangeModel: (modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig) => void;
};
-export const GlobalReferenceImageModel = memo(({ modelKey, onChangeModel }: Props) => {
+export const RefImageModel = memo(({ modelKey, onChangeModel }: Props) => {
const { t } = useTranslation();
const currentBaseModel = useAppSelector(selectBase);
const [modelConfigs, { isLoading }] = useGlobalReferenceImageModels();
@@ -60,4 +60,4 @@ export const GlobalReferenceImageModel = memo(({ modelKey, onChangeModel }: Prop
);
});
-GlobalReferenceImageModel.displayName = 'GlobalReferenceImageModel';
+RefImageModel.displayName = 'RefImageModel';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageState.tsx
similarity index 95%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageState.tsx
index 3bf6744d99..3a82fd16f9 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageState.tsx
@@ -13,7 +13,7 @@ import { memo, useCallback, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { ImageDTO } from 'services/api/types';
-export const IPAdapterSettingsEmptyState = memo(() => {
+export const RefImageNoImageState = memo(() => {
const { t } = useTranslation();
const id = useRefImageIdContext();
const dispatch = useAppDispatch();
@@ -66,4 +66,4 @@ export const IPAdapterSettingsEmptyState = memo(() => {
);
});
-IPAdapterSettingsEmptyState.displayName = 'IPAdapterSettingsEmptyState';
+RefImageNoImageState.displayName = 'RefImageNoImageState';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx
index 059135b507..cb88c281f7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx
@@ -3,9 +3,9 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
buildSelectValidRegionalGuidanceActions,
- useAddRegionalGuidanceIPAdapter,
- useAddRegionalGuidanceNegativePrompt,
- useAddRegionalGuidancePositivePrompt,
+ useAddRefImageToExistingRegionalGuidance,
+ useAddNegativePromptToExistingRegionalGuidance,
+ useAddPositivePromptToExistingRegionalGuidance,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -14,9 +14,9 @@ import { PiPlusBold } from 'react-icons/pi';
export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
- const addRegionalGuidanceIPAdapter = useAddRegionalGuidanceIPAdapter(entityIdentifier);
- const addRegionalGuidancePositivePrompt = useAddRegionalGuidancePositivePrompt(entityIdentifier);
- const addRegionalGuidanceNegativePrompt = useAddRegionalGuidanceNegativePrompt(entityIdentifier);
+ const addRegionalGuidanceIPAdapter = useAddRefImageToExistingRegionalGuidance(entityIdentifier);
+ const addRegionalGuidancePositivePrompt = useAddPositivePromptToExistingRegionalGuidance(entityIdentifier);
+ const addRegionalGuidanceNegativePrompt = useAddNegativePromptToExistingRegionalGuidance(entityIdentifier);
const selectValidActions = useMemo(
() => buildSelectValidRegionalGuidanceActions(entityIdentifier),
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
index 4cc434c439..70445b484b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
@@ -2,11 +2,11 @@ import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
-import { CLIPVisionModel } from 'features/controlLayers/components/common/CLIPVisionModel';
+import { IPAdapterCLIPVisionModel } from 'features/controlLayers/components/common/IPAdapterCLIPVisionModel';
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/common/FLUXReduxImageInfluence';
import { Weight } from 'features/controlLayers/components/common/Weight';
-import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview';
-import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
+import { RefImageImage } from 'features/controlLayers/components/RefImage/RefImageImage';
+import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAdapterMethod';
import { RegionalGuidanceIPAdapterSettingsEmptyState } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState';
import { RegionalReferenceImageModel } from 'features/controlLayers/components/RegionalGuidance/RegionalReferenceImageModel';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
@@ -142,7 +142,7 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
{config.type === 'ip_adapter' && (
-
+
)}
)}
- {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const isBusy = useCanvasIsBusy();
- const addRegionalGuidanceIPAdapter = useAddRegionalGuidanceIPAdapter(entityIdentifier);
- const addRegionalGuidancePositivePrompt = useAddRegionalGuidancePositivePrompt(entityIdentifier);
- const addRegionalGuidanceNegativePrompt = useAddRegionalGuidanceNegativePrompt(entityIdentifier);
+ const addRegionalGuidanceIPAdapter = useAddRefImageToExistingRegionalGuidance(entityIdentifier);
+ const addRegionalGuidancePositivePrompt = useAddPositivePromptToExistingRegionalGuidance(entityIdentifier);
+ const addRegionalGuidanceNegativePrompt = useAddNegativePromptToExistingRegionalGuidance(entityIdentifier);
const selectValidActions = useMemo(
() => buildSelectValidRegionalGuidanceActions(entityIdentifier),
[entityIdentifier]
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx
index 89a924ad6c..8c8e07cdc9 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx
@@ -2,7 +2,7 @@ import type { FlexProps } from '@invoke-ai/ui-library';
import { ContextMenu, Flex, MenuList } from '@invoke-ai/ui-library';
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
-import { IPAdapterMenuItems } from 'features/controlLayers/components/IPAdapter/IPAdapterMenuItems';
+import { IPAdapterMenuItems } from 'features/controlLayers/components/RefImage/IPAdapterMenuItems';
import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems';
import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CLIPVisionModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/IPAdapterCLIPVisionModel.tsx
similarity index 92%
rename from invokeai/frontend/web/src/features/controlLayers/components/common/CLIPVisionModel.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/common/IPAdapterCLIPVisionModel.tsx
index 6023fd579f..dd7917d39a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CLIPVisionModel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/IPAdapterCLIPVisionModel.tsx
@@ -22,7 +22,7 @@ type Props = {
onChange: (clipVisionModel: CLIPVisionModelV2) => void;
};
-export const CLIPVisionModel = memo(({ model, onChange }: Props) => {
+export const IPAdapterCLIPVisionModel = memo(({ model, onChange }: Props) => {
const { t } = useTranslation();
const _onChangeCLIPVisionModel = useCallback(
@@ -58,4 +58,4 @@ export const CLIPVisionModel = memo(({ model, onChange }: Props) => {
);
});
-CLIPVisionModel.displayName = 'CLIPVisionModel';
+IPAdapterCLIPVisionModel.displayName = 'IPAdapterCLIPVisionModel';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
index 41d871436e..f8860e210c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
@@ -1,6 +1,8 @@
import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useAppStore } from 'app/store/nanostores/store';
+import type { AppGetState } from 'app/store/store';
+import { useAppDispatch } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import {
@@ -20,10 +22,11 @@ import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/se
import type {
CanvasEntityIdentifier,
CanvasRegionalGuidanceState,
+ ChatGPT4oReferenceImageConfig,
ControlLoRAConfig,
ControlNetConfig,
+ FluxKontextReferenceImageConfig,
IPAdapterConfig,
- RefImageState,
T2IAdapterConfig,
} from 'features/controlLayers/store/types';
import {
@@ -36,13 +39,9 @@ import {
import { zModelIdentifierField } from 'features/nodes/types/common';
import { useCallback } from 'react';
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
-import type {
- ControlLoRAModelConfig,
- ControlNetModelConfig,
- IPAdapterModelConfig,
- T2IAdapterModelConfig,
-} from 'services/api/types';
-import { isControlLayerModelConfig, isIPAdapterModelConfig } from 'services/api/types';
+import { selectIPAdapterModels } from 'services/api/hooks/modelsByType';
+import type { ControlLoRAModelConfig, ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
+import { isControlLayerModelConfig } from 'services/api/types';
/**
* Selects the default control adapter configuration based on the model configurations and the base.
@@ -73,67 +72,69 @@ export const selectDefaultControlAdapter = createSelector(
}
);
-export const selectDefaultRefImageConfig = createSelector(
- selectMainModelConfig,
- selectModelConfigsQuery,
- selectBase,
- (selectedMainModel, query, base): RefImageState['config'] => {
- if (selectedMainModel?.base === 'chatgpt-4o') {
- const referenceImage = deepClone(initialChatGPT4oReferenceImage);
- referenceImage.model = zModelIdentifierField.parse(selectedMainModel);
- return referenceImage;
- }
+export const getDefaultRefImageConfig = (
+ getState: AppGetState
+): IPAdapterConfig | ChatGPT4oReferenceImageConfig | FluxKontextReferenceImageConfig => {
+ const state = getState();
- if (selectedMainModel?.base === 'flux-kontext') {
- const referenceImage = deepClone(initialFluxKontextReferenceImage);
- referenceImage.model = zModelIdentifierField.parse(selectedMainModel);
- return referenceImage;
- }
+ const mainModelConfig = selectMainModelConfig(state);
+ const ipAdapterModelConfigs = selectIPAdapterModels(state);
- const { data } = query;
- let model: IPAdapterModelConfig | null = null;
- if (data) {
- const modelConfigs = modelConfigsAdapterSelectors.selectAll(data).filter(isIPAdapterModelConfig);
- const compatibleModels = modelConfigs.filter((m) => (base ? m.base === base : true));
- model = compatibleModels[0] ?? modelConfigs[0] ?? null;
- }
- const ipAdapter = deepClone(initialIPAdapter);
- if (model) {
- ipAdapter.model = zModelIdentifierField.parse(model);
- if (model.base === 'flux') {
- ipAdapter.clipVisionModel = 'ViT-L';
- }
- }
- return ipAdapter;
+ const base = mainModelConfig?.base;
+
+ // For ChatGPT-4o, the ref image model is the model itself.
+ if (base === 'chatgpt-4o') {
+ const config = deepClone(initialChatGPT4oReferenceImage);
+ config.model = zModelIdentifierField.parse(mainModelConfig);
+ return config;
}
-);
-/**
- * Selects the default IP adapter configuration based on the model configurations and the base.
- *
- * Be sure to clone the output of this selector before modifying it!
- */
-export const selectDefaultIPAdapter = createSelector(
- selectModelConfigsQuery,
- selectBase,
- (query, base): IPAdapterConfig => {
- const { data } = query;
- let model: IPAdapterModelConfig | null = null;
- if (data) {
- const modelConfigs = modelConfigsAdapterSelectors.selectAll(data).filter(isIPAdapterModelConfig);
- const compatibleModels = modelConfigs.filter((m) => (base ? m.base === base : true));
- model = compatibleModels[0] ?? modelConfigs[0] ?? null;
- }
- const ipAdapter = deepClone(initialIPAdapter);
- if (model) {
- ipAdapter.model = zModelIdentifierField.parse(model);
- if (model.base === 'flux') {
- ipAdapter.clipVisionModel = 'ViT-L';
- }
- }
- return ipAdapter;
+ if (base === 'flux-kontext') {
+ const config = deepClone(initialFluxKontextReferenceImage);
+ config.model = zModelIdentifierField.parse(mainModelConfig);
+ return config;
}
-);
+
+ // Otherwise, find the first compatible IP Adapter model.
+ const modelConfig = ipAdapterModelConfigs.find((m) => m.base === base);
+
+ // Clone the initial IP Adapter config and set the model if available.
+ const config = deepClone(initialIPAdapter);
+
+ if (modelConfig) {
+ config.model = zModelIdentifierField.parse(modelConfig);
+ // FLUX models use a different vision model.
+ if (modelConfig.base === 'flux') {
+ config.clipVisionModel = 'ViT-L';
+ }
+ }
+ return config;
+};
+
+export const getDefaultRegionalGuidanceRefImageConfig = (getState: AppGetState): IPAdapterConfig => {
+ // Regional guidance ref images do not support ChatGPT-4o, so we always return the IP Adapter config.
+ const state = getState();
+
+ const mainModelConfig = selectMainModelConfig(state);
+ const ipAdapterModelConfigs = selectIPAdapterModels(state);
+
+ const base = mainModelConfig?.base;
+
+ // Find the first compatible IP Adapter model.
+ const modelConfig = ipAdapterModelConfigs.find((m) => m.base === base);
+
+ // Clone the initial IP Adapter config and set the model if available.
+ const config = deepClone(initialIPAdapter);
+
+ if (modelConfig) {
+ config.model = zModelIdentifierField.parse(modelConfig);
+ // FLUX models use a different vision model.
+ if (modelConfig.base === 'flux') {
+ config.clipVisionModel = 'ViT-L';
+ }
+ }
+ return config;
+};
export const useAddControlLayer = () => {
const dispatch = useAppDispatch();
@@ -172,44 +173,46 @@ export const useAddRegionalGuidance = () => {
return func;
};
-export const useAddRegionalReferenceImage = () => {
- const dispatch = useAppDispatch();
- const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
+export const useAddNewRegionalGuidanceWithARefImage = () => {
+ const { dispatch, getState } = useAppStore();
const func = useCallback(() => {
+ const config = getDefaultRegionalGuidanceRefImageConfig(getState);
const overrides: Partial = {
- referenceImages: [
- { id: getPrefixedId('regional_guidance_reference_image'), config: deepClone(defaultIPAdapter) },
- ],
+ referenceImages: [{ id: getPrefixedId('regional_guidance_reference_image'), config }],
};
dispatch(rgAdded({ isSelected: true, overrides }));
- }, [defaultIPAdapter, dispatch]);
+ }, [dispatch, getState]);
return func;
};
export const useAddGlobalReferenceImage = () => {
- const dispatch = useAppDispatch();
- const defaultRefImage = useAppSelector(selectDefaultRefImageConfig);
+ const { dispatch, getState } = useAppStore();
const func = useCallback(() => {
- const overrides = { config: deepClone(defaultRefImage) };
+ const config = getDefaultRefImageConfig(getState);
+ const overrides = { config };
dispatch(refImageAdded({ isSelected: true, overrides }));
- }, [defaultRefImage, dispatch]);
+ }, [dispatch, getState]);
return func;
};
-export const useAddRegionalGuidanceIPAdapter = (entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>) => {
- const dispatch = useAppDispatch();
- const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
+export const useAddRefImageToExistingRegionalGuidance = (
+ entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>
+) => {
+ const { dispatch, getState } = useAppStore();
const func = useCallback(() => {
- dispatch(rgRefImageAdded({ entityIdentifier, overrides: { config: deepClone(defaultIPAdapter) } }));
- }, [defaultIPAdapter, dispatch, entityIdentifier]);
+ const config = getDefaultRegionalGuidanceRefImageConfig(getState);
+ dispatch(rgRefImageAdded({ entityIdentifier, overrides: { config } }));
+ }, [dispatch, entityIdentifier, getState]);
return func;
};
-export const useAddRegionalGuidancePositivePrompt = (entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>) => {
+export const useAddPositivePromptToExistingRegionalGuidance = (
+ entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>
+) => {
const dispatch = useAppDispatch();
const func = useCallback(() => {
dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' }));
@@ -218,7 +221,9 @@ export const useAddRegionalGuidancePositivePrompt = (entityIdentifier: CanvasEnt
return func;
};
-export const useAddRegionalGuidanceNegativePrompt = (entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>) => {
+export const useAddNegativePromptToExistingRegionalGuidance = (
+ entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>
+) => {
const dispatch = useAppDispatch();
const runc = useCallback(() => {
dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' }));
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
index 0e0d2d433d..395768585f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
@@ -1,9 +1,12 @@
import { logger } from 'app/logging/logger';
-import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
+import { useAppDispatch, useAppStore } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
import { withResultAsync } from 'common/util/result';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectDefaultIPAdapter, selectDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
+import {
+ getDefaultRefImageConfig,
+ getDefaultRegionalGuidanceRefImageConfig,
+} from 'features/controlLayers/hooks/addLayerHooks';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import {
controlLayerAdded,
@@ -18,7 +21,7 @@ import {
selectPositivePrompt,
selectSeed,
} from 'features/controlLayers/store/paramsSlice';
-import { refImageAdded,refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
+import { refImageAdded, refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasMetadata } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
@@ -167,15 +170,14 @@ export const useSaveBboxToGallery = () => {
export const useNewRegionalReferenceImageFromBbox = () => {
const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
+ const { dispatch, getState } = useAppStore();
const arg = useMemo(() => {
const onSave = (imageDTO: ImageDTO) => {
const ipAdapter: RegionalGuidanceRefImageState = {
id: getPrefixedId('regional_guidance_reference_image'),
config: {
- ...deepClone(defaultIPAdapter),
+ ...getDefaultRegionalGuidanceRefImageConfig(getState),
image: imageDTOToImageWithDims(imageDTO),
},
};
@@ -193,21 +195,20 @@ export const useNewRegionalReferenceImageFromBbox = () => {
toastOk: t('controlLayers.newRegionalReferenceImageOk'),
toastError: t('controlLayers.newRegionalReferenceImageError'),
};
- }, [defaultIPAdapter, dispatch, t]);
+ }, [dispatch, getState, t]);
const func = useSaveCanvas(arg);
return func;
};
export const useNewGlobalReferenceImageFromBbox = () => {
const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const defaultIPAdapter = useAppSelector(selectDefaultRefImageConfig);
+ const { dispatch, getState } = useAppStore();
const arg = useMemo(() => {
const onSave = (imageDTO: ImageDTO) => {
const overrides: Partial = {
config: {
- ...deepClone(defaultIPAdapter),
+ ...getDefaultRefImageConfig(getState),
image: imageDTOToImageWithDims(imageDTO),
},
};
@@ -221,7 +222,7 @@ export const useNewGlobalReferenceImageFromBbox = () => {
toastOk: t('controlLayers.newGlobalReferenceImageOk'),
toastError: t('controlLayers.newGlobalReferenceImageError'),
};
- }, [defaultIPAdapter, dispatch, t]);
+ }, [dispatch, getState, t]);
const func = useSaveCanvas(arg);
return func;
};
diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts
index 925a118694..bb401727d3 100644
--- a/invokeai/frontend/web/src/features/dnd/dnd.ts
+++ b/invokeai/frontend/web/src/features/dnd/dnd.ts
@@ -1,5 +1,5 @@
import { logger } from 'app/logging/logger';
-import type { AppDispatch, RootState } from 'app/store/store';
+import type { AppDispatch, AppGetState } from 'app/store/store';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common';
@@ -114,13 +114,13 @@ type DndTarget = {
sourceData: RecordUnknown;
targetData: TargetData;
dispatch: AppDispatch;
- getState: () => RootState;
+ getState: AppGetState;
}) => boolean;
handler: (arg: {
sourceData: SourceData;
targetData: TargetData;
dispatch: AppDispatch;
- getState: () => RootState;
+ getState: AppGetState;
}) => void;
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx
index 7b3971ba39..4fc9934f07 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx
@@ -3,8 +3,6 @@ import { useAppStore } from 'app/store/nanostores/store';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
-import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
-import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { sentImageToCanvas } from 'features/gallery/store/actions';
@@ -75,19 +73,6 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
});
}, [imageDTO, imageViewer, store, t]);
- const onClickNewGlobalReferenceImageFromImage = useCallback(() => {
- const { dispatch } = store;
- dispatch(refImageAdded({ overrides: { config: { image: imageDTOToImageWithDims(imageDTO) } } }));
- dispatch(sentImageToCanvas());
- dispatch(setActiveTab('canvas'));
- imageViewer.close();
- toast({
- id: 'SENT_TO_CANVAS',
- title: t('toast.sentToCanvas'),
- status: 'success',
- });
- }, [imageDTO, imageViewer, store, t]);
-
const onClickNewRegionalReferenceImageFromImage = useCallback(() => {
const { dispatch, getState } = store;
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance_with_reference_image', dispatch, getState });
@@ -127,13 +112,6 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
>
{t('controlLayers.referenceImageRegional')}
- }
- onClickCapture={onClickNewGlobalReferenceImageFromImage}
- isDisabled={isBusy}
- >
- {t('controlLayers.referenceImageGlobal')}
-
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage.tsx
new file mode 100644
index 0000000000..344f81ef7f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage.tsx
@@ -0,0 +1,39 @@
+import { MenuItem } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
+import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
+import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
+import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
+import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
+import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
+import { toast } from 'features/toast/toast';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiImageBold } from 'react-icons/pi';
+
+export const ImageMenuItemUseAsRefImage = memo(() => {
+ const { t } = useTranslation();
+ const store = useAppStore();
+ const imageDTO = useImageDTOContext();
+ const imageViewer = useImageViewer();
+
+ const onClickNewGlobalReferenceImageFromImage = useCallback(() => {
+ const { dispatch, getState } = store;
+ const config = getDefaultRefImageConfig(getState);
+ config.image = imageDTOToImageWithDims(imageDTO);
+ dispatch(refImageAdded({ overrides: { config } }));
+ imageViewer.close();
+ toast({
+ id: 'SENT_TO_CANVAS',
+ title: t('toast.sentToCanvas'),
+ status: 'success',
+ });
+ }, [imageDTO, imageViewer, store, t]);
+
+ return (
+ } onClickCapture={onClickNewGlobalReferenceImageFromImage}>
+ Use as Reference Image
+
+ );
+});
+
+ImageMenuItemUseAsRefImage.displayName = 'ImageMenuItemUseAsRefImage';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
index bac272d6c1..368d265735 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
@@ -13,6 +13,7 @@ import { ImageMenuItemOpenInViewer } from 'features/gallery/components/ImageCont
import { ImageMenuItemSelectForCompare } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare';
import { ImageMenuItemSendToUpscale } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSendToUpscale';
import { ImageMenuItemStarUnstar } from 'features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar';
+import { ImageMenuItemUseAsRefImage } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage';
import { ImageDTOContextProvider } from 'features/gallery/contexts/ImageDTOContext';
import { memo } from 'react';
import type { ImageDTO } from 'services/api/types';
@@ -37,6 +38,7 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
+
diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts
index babbdd35a2..52db1a7c09 100644
--- a/invokeai/frontend/web/src/features/imageActions/actions.ts
+++ b/invokeai/frontend/web/src/features/imageActions/actions.ts
@@ -1,6 +1,9 @@
-import type { AppDispatch, RootState } from 'app/store/store';
+import type { AppDispatch, AppGetState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { selectDefaultIPAdapter, selectDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
+import {
+ getDefaultRefImageConfig,
+ getDefaultRegionalGuidanceRefImageConfig,
+} from 'features/controlLayers/hooks/addLayerHooks';
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import {
@@ -77,7 +80,7 @@ export const createNewCanvasEntityFromImage = (arg: {
imageDTO: ImageDTO;
type: CanvasEntityType | 'regional_guidance_with_reference_image';
dispatch: AppDispatch;
- getState: () => RootState;
+ getState: AppGetState;
overrides?: Partial>;
}) => {
const { type, imageDTO, dispatch, getState, overrides: _overrides } = arg;
@@ -112,7 +115,7 @@ export const createNewCanvasEntityFromImage = (arg: {
break;
}
case 'regional_guidance_with_reference_image': {
- const config = deepClone(selectDefaultIPAdapter(getState()));
+ const config = getDefaultRegionalGuidanceRefImageConfig(getState);
config.image = imageDTOToImageWithDims(imageDTO);
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), config }];
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
@@ -138,7 +141,7 @@ export const newCanvasFromImage = async (arg: {
withResize?: boolean;
withInpaintMask?: boolean;
dispatch: AppDispatch;
- getState: () => RootState;
+ getState: AppGetState;
}) => {
const { type, imageDTO, withResize = false, withInpaintMask = false, dispatch, getState } = arg;
const state = getState();
@@ -242,7 +245,7 @@ export const newCanvasFromImage = async (arg: {
break;
}
case 'reference_image': {
- const config = deepClone(selectDefaultRefImageConfig(getState()));
+ const config = deepClone(getDefaultRefImageConfig(getState));
config.image = imageDTOToImageWithDims(imageDTO);
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(refImageAdded({ overrides: { config }, isSelected: true }));
@@ -253,7 +256,7 @@ export const newCanvasFromImage = async (arg: {
break;
}
case 'regional_guidance_with_reference_image': {
- const config = deepClone(selectDefaultIPAdapter(getState()));
+ const config = getDefaultRegionalGuidanceRefImageConfig(getState);
config.image = imageDTOToImageWithDims(imageDTO);
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), config }];
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
@@ -273,7 +276,7 @@ export const replaceCanvasEntityObjectsWithImage = (arg: {
imageDTO: ImageDTO;
entityIdentifier: CanvasEntityIdentifier;
dispatch: AppDispatch;
- getState: () => RootState;
+ getState: AppGetState;
}) => {
const { imageDTO, entityIdentifier, dispatch, getState } = arg;
const imageObject = imageDTOToImageObject(imageDTO);
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
index f6f71927c8..aa65205470 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
@@ -1,7 +1,7 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
-import { RefImageList } from 'features/controlLayers/components/IPAdapter/IPAdapterList';
+import { RefImageList } from 'features/controlLayers/components/RefImage/IPAdapterList';
import { positivePromptChanged, selectBase, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
diff --git a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
index fc2c782003..553d441005 100644
--- a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
+++ b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
@@ -101,15 +101,15 @@ export const useImagen4Models = buildModelsHook(isImagen4ModelConfig);
export const useChatGPT4oModels = buildModelsHook(isChatGPT4oModelConfig);
export const useFluxKontextModels = buildModelsHook(isFluxKontextModelConfig);
-// const buildModelsSelector =
-// (typeGuard: (config: AnyModelConfig) => config is T): Selector =>
-// (state) => {
-// const result = selectModelConfigsQuery(state);
-// if (!result.data) {
-// return EMPTY_ARRAY;
-// }
-// return modelConfigsAdapterSelectors.selectAll(result.data).filter(typeGuard);
-// };
+const buildModelsSelector =
+ (typeGuard: (config: AnyModelConfig) => config is T): Selector =>
+ (state) => {
+ const result = selectModelConfigsQuery(state);
+ if (!result.data) {
+ return EMPTY_ARRAY;
+ }
+ return modelConfigsAdapterSelectors.selectAll(result.data).filter(typeGuard);
+ };
// export const selectSDMainModels = buildModelsSelector(isNonRefinerNonFluxMainModelConfig);
// export const selectMainModels = buildModelsSelector(isNonRefinerMainModelConfig);
// export const selectNonSDXLMainModels = buildModelsSelector(isNonSDXLMainModelConfig);
@@ -123,7 +123,7 @@ export const useFluxKontextModels = buildModelsHook(isFluxKontextModelConfig);
// export const selectT5EncoderModels = buildModelsSelector(isT5EncoderModelConfig);
// export const selectClipEmbedModels = buildModelsSelector(isClipEmbedModelConfig);
// export const selectSpandrelImageToImageModels = buildModelsSelector(isSpandrelImageToImageModelConfig);
-// export const selectIPAdapterModels = buildModelsSelector(isIPAdapterModelConfig);
+export const selectIPAdapterModels = buildModelsSelector(isIPAdapterModelConfig);
// export const selectEmbeddingModels = buildModelsSelector(isTIModelConfig);
// export const selectVAEModels = buildModelsSelector(isVAEModelConfig);
// export const selectFluxVAEModels = buildModelsSelector(isFluxVAEModelConfig);
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index 8e299202e9..c40d3fb5a0 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -1,5 +1,5 @@
import { logger } from 'app/logging/logger';
-import type { AppDispatch, RootState } from 'app/store/store';
+import type { AppDispatch, AppGetState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
@@ -20,7 +20,7 @@ const log = logger('events');
const nodeTypeDenylist = ['load_image', 'image'];
-export const buildOnInvocationComplete = (getState: () => RootState, dispatch: AppDispatch) => {
+export const buildOnInvocationComplete = (getState: AppGetState, dispatch: AppDispatch) => {
const addImagesToGallery = async (data: S['InvocationCompleteEvent']) => {
if (nodeTypeDenylist.includes(data.invocation.type)) {
log.trace(`Skipping denylisted node type (${data.invocation.type})`);
diff --git a/invokeai/frontend/web/src/services/events/onModelInstallError.tsx b/invokeai/frontend/web/src/services/events/onModelInstallError.tsx
index 3ae11de353..36c6762323 100644
--- a/invokeai/frontend/web/src/services/events/onModelInstallError.tsx
+++ b/invokeai/frontend/web/src/services/events/onModelInstallError.tsx
@@ -1,7 +1,7 @@
import { Button, ExternalLink, Spinner, Text } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { logger } from 'app/logging/logger';
-import type { AppDispatch, RootState } from 'app/store/store';
+import type { AppDispatch, AppGetState } from 'app/store/store';
import { useAppDispatch } from 'app/store/storeHooks';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@@ -41,7 +41,7 @@ const getHFTokenStatus = async (dispatch: AppDispatch): Promise RootState, dispatch: AppDispatch) => {
+export const buildOnModelInstallError = (getState: AppGetState, dispatch: AppDispatch) => {
return async (data: S['ModelInstallErrorEvent']) => {
log.error({ data }, 'Model install error');
From 8d1ab0a2e565708dcbab8fee1ed09e946ccea888 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 13 Jun 2025 15:19:02 +1000
Subject: [PATCH 089/210] refactor(ui): ref images (WIP)
---
invokeai/frontend/web/public/locales/en.json | 3 +-
.../web/src/common/hooks/useBoolean.ts | 2 +-
.../common/hooks/useFilterableOutsideClick.ts | 20 ++-
.../components/RefImage/IPAdapterList.tsx | 37 -----
.../components/RefImage/RefImage.tsx | 137 +++++++++++++-----
.../components/RefImage/RefImageHeader.tsx | 41 ++++++
.../components/RefImage/RefImageList.tsx | 77 ++++++++++
.../RefImage/RefImageNoImageState.tsx | 18 +--
.../RefImageNoImageStateWithCanvasOptions.tsx | 69 +++++++++
...apterSettings.tsx => RefImageSettings.tsx} | 57 +++++---
.../components/RefImage/useRefImageEntity.ts | 16 ++
...nalGuidanceIPAdapterSettingsEmptyState.tsx | 2 +-
.../common/PullBboxIntoRefImageIconButton.tsx | 28 ++++
.../controlLayers/hooks/addLayerHooks.ts | 2 +-
.../controlLayers/store/refImagesSlice.ts | 3 +-
.../web/src/features/imageActions/actions.ts | 2 +-
.../components/Core/ParamPositivePrompt.tsx | 2 -
.../parameters/components/Prompts/Prompts.tsx | 2 +
.../features/ui/components/TabMountGate.tsx | 3 +
19 files changed, 394 insertions(+), 127 deletions(-)
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterList.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageHeader.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions.tsx
rename invokeai/frontend/web/src/features/controlLayers/components/RefImage/{IPAdapterSettings.tsx => RefImageSettings.tsx} (80%)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RefImage/useRefImageEntity.ts
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/PullBboxIntoRefImageIconButton.tsx
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 0375f0f456..ce14674926 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -2017,7 +2017,8 @@
"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.",
- "referenceImageEmptyState": "Upload an image, drag an image from the gallery onto this layer, or pull the bounding box into this layer 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",
diff --git a/invokeai/frontend/web/src/common/hooks/useBoolean.ts b/invokeai/frontend/web/src/common/hooks/useBoolean.ts
index ec68457ecd..9b571d31bc 100644
--- a/invokeai/frontend/web/src/common/hooks/useBoolean.ts
+++ b/invokeai/frontend/web/src/common/hooks/useBoolean.ts
@@ -73,7 +73,7 @@ export const useBoolean = (initialValue: boolean): UseBoolean => {
};
};
-type UseDisclosure = {
+export type UseDisclosure = {
isOpen: boolean;
open: () => void;
close: () => void;
diff --git a/invokeai/frontend/web/src/common/hooks/useFilterableOutsideClick.ts b/invokeai/frontend/web/src/common/hooks/useFilterableOutsideClick.ts
index 6b45cb8554..4317fc01d9 100644
--- a/invokeai/frontend/web/src/common/hooks/useFilterableOutsideClick.ts
+++ b/invokeai/frontend/web/src/common/hooks/useFilterableOutsideClick.ts
@@ -22,6 +22,8 @@
import { useCallback, useEffect, useRef } from 'react';
+type FilterFunction = (el: HTMLElement | SVGElement) => boolean;
+
export function useCallbackRef any>(
callback: T | undefined,
deps: React.DependencyList = []
@@ -54,10 +56,17 @@ export interface UseOutsideClickProps {
*
* If omitted, a default filter function that ignores clicks in Chakra UI portals and react-select components is used.
*/
- filter?: (el: HTMLElement) => boolean;
+ filter?: FilterFunction;
}
-const DEFAULT_FILTER = (el: HTMLElement) => el.className.includes('chakra-portal') || el.id.includes('react-select');
+export const DEFAULT_FILTER: FilterFunction = (el) => {
+ if (el instanceof SVGElement) {
+ // SVGElement's type appears to be incorrect. Its className is not a string, which causes `includes` to fail.
+ // Let's assume that SVG elements with a class name are not part of the portal and should not be filtered.
+ return false;
+ }
+ return el.className.includes('chakra-portal') || el.id.includes('react-select');
+};
/**
* Example, used in components like Dialogs and Popovers, so they can close
@@ -119,11 +128,7 @@ export function useFilterableOutsideClick(props: UseOutsideClickProps) {
}, [handler, ref, savedHandler, state, enabled, filter]);
}
-function isValidEvent(
- event: Event,
- ref: React.RefObject,
- filter?: (el: HTMLElement) => boolean
-): boolean {
+function isValidEvent(event: Event, ref: React.RefObject, filter?: FilterFunction): boolean {
const target = (event.composedPath?.()[0] ?? event.target) as HTMLElement;
if (target) {
@@ -137,6 +142,7 @@ function isValidEvent(
return false;
}
+ // This is the main logic change from the original hook.
if (filter) {
// Check if the click is inside an element matching the filter.
// This is used for portal-awareness or other general exclusion cases.
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterList.tsx
deleted file mode 100644
index 6cda069930..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterList.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable i18next/no-literal-string */
-import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
-import { Flex } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
-import { RefImage } from 'features/controlLayers/components/RefImage/RefImage';
-import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
-import { selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
-import { memo } from 'react';
-
-const sx: SystemStyleObject = {
- opacity: 0.3,
- _hover: {
- opacity: 1,
- },
- transitionProperty: 'opacity',
- transitionDuration: '0.2s',
-};
-
-export const RefImageList = memo((props: FlexProps) => {
- const ids = useAppSelector(selectRefImageEntityIds);
-
- if (ids.length === 0) {
- return null;
- }
-
- return (
-
- {ids.map((id) => (
-
-
-
- ))}
-
- );
-});
-
-RefImageList.displayName = 'RefImageList';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
index 1212fbe3fa..ca21d6a84a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
@@ -1,62 +1,77 @@
-import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import type {
+ SystemStyleObject} from '@invoke-ai/ui-library';
import {
+ Divider,
Flex,
+ IconButton,
Image,
Popover,
PopoverAnchor,
PopoverArrow,
PopoverBody,
PopoverContent,
- Portal,
+ Portal
} from '@invoke-ai/ui-library';
-import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/query';
-import { useAppSelector } from 'app/store/storeHooks';
+import { POPPER_MODIFIERS } from 'common/components/InformationalPopover/constants';
+import type { UseDisclosure } from 'common/hooks/useBoolean';
import { useDisclosure } from 'common/hooks/useBoolean';
-import { useFilterableOutsideClick } from 'common/hooks/useFilterableOutsideClick';
-import { IPAdapterSettings } from 'features/controlLayers/components/RefImage/IPAdapterSettings';
+import { DEFAULT_FILTER, useFilterableOutsideClick } from 'common/hooks/useFilterableOutsideClick';
+import { RefImageHeader } from 'features/controlLayers/components/RefImage/RefImageHeader';
+import { RefImageSettings } from 'features/controlLayers/components/RefImage/RefImageSettings';
+import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
-import { selectRefImageEntityOrThrow, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
-import type { ImageWithDims } from 'features/controlLayers/store/types';
-import { memo, useMemo, useRef } from 'react';
+import { memo, useCallback, useRef } from 'react';
+import { PiImageBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
-const sx: SystemStyleObject = {
- opacity: 0.5,
- _hover: {
- opacity: 1,
- },
- "&[data-is-open='true']": {
- opacity: 1,
- pointerEvents: 'none',
- },
- transitionProperty: 'opacity',
- transitionDuration: '0.2s',
-};
+// There is some awkwardness here with closing the popover when clicking outside of it, related to Chakra's
+// handling of refs, portals, outside clicks, and a race condition with framer-motion animations that can leave
+// the popover closed when its internal state is still open.
+//
+// We have to manually manage the popover open state to work around the race condition, and then have to do special
+// handling to close the popover when clicking outside of it.
+
+// We have to reach outside react to identify the popover trigger element instead of using refs, thanks to how Chakra
+// handles refs for PopoverAnchor internally. Maybe there is some way to merge them but I couldn't figure it out.
+const getRefImagePopoverTriggerId = (id: string) => `ref-image-popover-trigger-${id}`;
export const RefImage = memo(() => {
const id = useRefImageIdContext();
const ref = useRef(null);
const disclosure = useDisclosure(false);
- const selectEntity = useMemo(
- () => createSelector(selectRefImagesSlice, (refImages) => selectRefImageEntityOrThrow(refImages, id, 'RefImage')),
+ // This filter prevents the popover from closing when clicking on a sibling portal element, like the dropdown menu
+ // inside the ref image settings popover. It also prevents the popover from closing when clicking on the popover's
+ // own trigger element.
+ const filter = useCallback(
+ (el: HTMLElement | SVGElement) => {
+ return DEFAULT_FILTER(el) || el.id === getRefImagePopoverTriggerId(id);
+ },
[id]
);
- const entity = useAppSelector(selectEntity);
- useFilterableOutsideClick({ ref, handler: disclosure.close });
+ useFilterableOutsideClick({ ref, handler: disclosure.close, filter });
return (
-
-
-
-
-
-
+
+
-
+
-
+
+
+
+
+
@@ -65,8 +80,58 @@ export const RefImage = memo(() => {
});
RefImage.displayName = 'RefImage';
-const Thumbnail = memo(({ image }: { image: ImageWithDims | null }) => {
- const { data: imageDTO } = useGetImageDTOQuery(image?.image_name ?? skipToken);
- return ;
+const imageSx: SystemStyleObject = {
+ opacity: 0.5,
+ _hover: {
+ opacity: 1,
+ },
+ "&[data-is-open='true']": {
+ opacity: 1,
+ },
+ transitionProperty: 'opacity',
+ transitionDuration: '0.2s',
+};
+
+const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
+ const id = useRefImageIdContext();
+ const entity = useRefImageEntity(id);
+ const { data: imageDTO } = useGetImageDTOQuery(entity.config.image?.image_name ?? skipToken);
+
+ if (!imageDTO || !entity.config.image) {
+ return (
+
+ }
+ colorScheme="error"
+ onClick={disclosure.toggle}
+ />
+
+ );
+ }
+ return (
+
+
+
+ );
});
Thumbnail.displayName = 'Thumbnail';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageHeader.tsx
new file mode 100644
index 0000000000..1a2eb27310
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageHeader.tsx
@@ -0,0 +1,41 @@
+import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
+import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
+import { refImageDeleted } from 'features/controlLayers/store/refImagesSlice';
+import { memo, useCallback } from 'react';
+import { PiTrashBold } from 'react-icons/pi';
+
+export const RefImageHeader = memo(() => {
+ const id = useRefImageIdContext();
+ const dispatch = useAppDispatch();
+ const entity = useRefImageEntity(id);
+ const deleteRefImage = useCallback(() => {
+ dispatch(refImageDeleted({ id }));
+ }, [dispatch, id]);
+
+ return (
+
+ {entity.config.image !== null && (
+
+ Reference Image
+
+ )}
+ {entity.config.image === null && (
+
+ Reference Image - No Image Selected
+
+ )}
+ }
+ onClick={deleteRefImage}
+ aria-label="Delete reference image"
+ colorScheme="error"
+ />
+
+ );
+});
+RefImageHeader.displayName = 'RefImageHeader';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
new file mode 100644
index 0000000000..723237fee5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
@@ -0,0 +1,77 @@
+/* eslint-disable i18next/no-literal-string */
+import type { FlexProps } from '@invoke-ai/ui-library';
+import { Button, Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { RefImage } from 'features/controlLayers/components/RefImage/RefImage';
+import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
+import { useAddGlobalReferenceImage } from 'features/controlLayers/hooks/addLayerHooks';
+import { selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
+import { memo } from 'react';
+import { PiPlusBold } from 'react-icons/pi';
+
+export const RefImageList = memo((props: FlexProps) => {
+ const ids = useAppSelector(selectRefImageEntityIds);
+ const addRefImage = useAddGlobalReferenceImage();
+ return (
+
+ {ids.map((id) => (
+
+
+
+ ))}
+
+ }
+ onClick={addRefImage}
+ isDisabled={ids.length >= 5} // Limit to 5 reference images
+ >
+ Ref Image
+
+
+ );
+});
+
+RefImageList.displayName = 'RefImageList';
+
+const AddRefImageIconButton = memo(() => {
+ const addRefImage = useAddGlobalReferenceImage();
+ return (
+ }
+ />
+ );
+});
+AddRefImageIconButton.displayName = 'AddRefImageIconButton';
+
+const AddRefImageButton = memo((props) => {
+ const addRefImage = useAddGlobalReferenceImage();
+ return (
+ }
+ onClick={addRefImage}
+ >
+ Ref Image
+
+ );
+});
+AddRefImageButton.displayName = 'AddRefImageButton';
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 3a82fd16f9..81aec80fec 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageState.tsx
@@ -2,8 +2,6 @@ import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
-import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
-import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
@@ -17,7 +15,6 @@ export const RefImageNoImageState = memo(() => {
const { t } = useTranslation();
const id = useRefImageIdContext();
const dispatch = useAppDispatch();
- const isBusy = useCanvasIsBusy();
const onUpload = useCallback(
(imageDTO: ImageDTO) => {
setGlobalReferenceImage({ imageDTO, id, dispatch });
@@ -28,7 +25,6 @@ export const RefImageNoImageState = memo(() => {
const onClickGalleryButton = useCallback(() => {
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
- const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
const dndTargetData = useMemo(
() => setGlobalReferenceImageDndTarget.getData({ id }),
@@ -37,17 +33,10 @@ export const RefImageNoImageState = memo(() => {
const components = useMemo(
() => ({
- UploadButton: (
-
- ),
- GalleryButton: (
-
- ),
- PullBboxButton: (
-
- ),
+ UploadButton: ,
+ GalleryButton: ,
}),
- [isBusy, onClickGalleryButton, pullBboxIntoIPAdapter, uploadApi]
+ [onClickGalleryButton, uploadApi]
);
return (
@@ -60,7 +49,6 @@ export const RefImageNoImageState = memo(() => {
dndTarget={setGlobalReferenceImageDndTarget}
dndTargetData={dndTargetData}
label={t('controlLayers.useImage')}
- isDisabled={isBusy}
/>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions.tsx
new file mode 100644
index 0000000000..329c6411b2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions.tsx
@@ -0,0 +1,69 @@
+import { Button, Flex, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
+import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
+import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
+import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
+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';
+
+export const RefImageNoImageStateWithCanvasOptions = memo(() => {
+ const { t } = useTranslation();
+ const id = useRefImageIdContext();
+ const dispatch = useAppDispatch();
+ const isBusy = useCanvasIsBusy();
+ const onUpload = useCallback(
+ (imageDTO: ImageDTO) => {
+ setGlobalReferenceImage({ imageDTO, id, dispatch });
+ },
+ [dispatch, id]
+ );
+ const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
+ const onClickGalleryButton = useCallback(() => {
+ dispatch(activeTabCanvasRightPanelChanged('gallery'));
+ }, [dispatch]);
+ const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
+
+ const dndTargetData = useMemo(
+ () => setGlobalReferenceImageDndTarget.getData({ id }),
+ [id]
+ );
+
+ const components = useMemo(
+ () => ({
+ UploadButton: (
+
+ ),
+ GalleryButton: (
+
+ ),
+ PullBboxButton: (
+
+ ),
+ }),
+ [isBusy, onClickGalleryButton, pullBboxIntoIPAdapter, uploadApi]
+ );
+
+ return (
+
+
+
+
+
+
+
+ );
+});
+
+RefImageNoImageStateWithCanvasOptions.displayName = 'RefImageNoImageStateWithCanvasOptions';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx
similarity index 80%
rename from invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterSettings.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx
index 8afacb0d2d..540a9723e0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/IPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx
@@ -4,10 +4,16 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/common/FLUXReduxImageInfluence';
import { IPAdapterCLIPVisionModel } from 'features/controlLayers/components/common/IPAdapterCLIPVisionModel';
+import { PullBboxIntoRefImageIconButton } from 'features/controlLayers/components/common/PullBboxIntoRefImageIconButton';
import { Weight } from 'features/controlLayers/components/common/Weight';
import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAdapterMethod';
import { RefImageModel } from 'features/controlLayers/components/RefImage/RefImageModel';
import { RefImageNoImageState } from 'features/controlLayers/components/RefImage/RefImageNoImageState';
+import { RefImageNoImageStateWithCanvasOptions } from 'features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions';
+import {
+ CanvasManagerProviderGate,
+ useCanvasManagerSafe,
+} from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import {
@@ -22,17 +28,16 @@ import {
selectRefImageEntityOrThrow,
selectRefImagesSlice,
} from 'features/controlLayers/store/refImagesSlice';
-import {
- type CLIPVisionModelV2,
- type FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
- type IPMethodV2,
- isFLUXReduxConfig,
- isIPAdapterConfig,
+import type {
+ CLIPVisionModelV2,
+ FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
+ IPMethodV2,
} from 'features/controlLayers/store/types';
+import { isFLUXReduxConfig, isIPAdapterConfig } from 'features/controlLayers/store/types';
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { RefImageImage } from './RefImageImage';
@@ -43,12 +48,12 @@ const buildSelectConfig = (id: string) =>
(refImages) => selectRefImageEntityOrThrow(refImages, id, 'IPAdapterSettings').config
);
-const IPAdapterSettingsContent = memo(() => {
- const { t } = useTranslation();
+const RefImageSettingsContent = memo(() => {
const dispatch = useAppDispatch();
const id = useRefImageIdContext();
const selectConfig = useMemo(() => buildSelectConfig(id), [id]);
const config = useAppSelector(selectConfig);
+ const tab = useAppSelector(selectActiveTab);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
@@ -103,8 +108,6 @@ const IPAdapterSettingsContent = memo(() => {
() => setGlobalReferenceImageDndTarget.getData({ id }, config.image?.image_name),
[id, config.image?.image_name]
);
- // const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
- // const isBusy = useCanvasIsBusy();
const isFLUX = useAppSelector(selectIsFLUX);
@@ -115,14 +118,11 @@ const IPAdapterSettingsContent = memo(() => {
{isIPAdapterConfig(config) && (
)}
- {/* }
- /> */}
+ {tab === 'canvas' && (
+
+
+
+ )}
{isIPAdapterConfig(config) && (
@@ -153,7 +153,7 @@ const IPAdapterSettingsContent = memo(() => {
);
});
-IPAdapterSettingsContent.displayName = 'IPAdapterSettingsContent';
+RefImageSettingsContent.displayName = 'RefImageSettingsContent';
const buildSelectIPAdapterHasImage = (id: string) =>
createSelector(selectRefImagesSlice, (refImages) => {
@@ -161,17 +161,26 @@ const buildSelectIPAdapterHasImage = (id: string) =>
return !!referenceImage && referenceImage.config.image !== null;
});
-export const IPAdapterSettings = memo(() => {
+export const RefImageSettings = memo(() => {
const id = useRefImageIdContext();
-
+ const tab = useAppSelector(selectActiveTab);
+ const canvasManager = useCanvasManagerSafe();
const selectIPAdapterHasImage = useMemo(() => buildSelectIPAdapterHasImage(id), [id]);
const hasImage = useAppSelector(selectIPAdapterHasImage);
+ if (!hasImage && canvasManager && tab === 'canvas') {
+ return (
+
+
+
+ );
+ }
+
if (!hasImage) {
return ;
}
- return ;
+ return ;
});
-IPAdapterSettings.displayName = 'IPAdapterSettings';
+RefImageSettings.displayName = 'RefImageSettings';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/useRefImageEntity.ts b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/useRefImageEntity.ts
new file mode 100644
index 0000000000..eda05ecee6
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/useRefImageEntity.ts
@@ -0,0 +1,16 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectRefImageEntityOrThrow, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
+import { useMemo } from 'react';
+
+export const useRefImageEntity = (id: string) => {
+ const selectEntity = useMemo(
+ () =>
+ createSelector(selectRefImagesSlice, (refImages) =>
+ selectRefImageEntityOrThrow(refImages, id, `useRefImageState(${id})`)
+ ),
+ [id]
+ );
+ const entity = useAppSelector(selectEntity);
+ return entity;
+};
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 e7e3282a64..7e00664f03 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState.tsx
@@ -83,7 +83,7 @@ export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImag
-
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/PullBboxIntoRefImageIconButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/PullBboxIntoRefImageIconButton.tsx
new file mode 100644
index 0000000000..f14b87d5fc
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/PullBboxIntoRefImageIconButton.tsx
@@ -0,0 +1,28 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
+import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
+import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiBoundingBoxBold } from 'react-icons/pi';
+
+export const PullBboxIntoRefImageIconButton = memo(() => {
+ const { t } = useTranslation();
+ const id = useRefImageIdContext();
+
+ const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
+ const isBusy = useCanvasIsBusy();
+
+ return (
+ }
+ />
+ );
+});
+
+PullBboxIntoRefImageIconButton.displayName = 'PullBboxIntoRefImageIconButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
index f8860e210c..3c4eacc1f8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
@@ -192,7 +192,7 @@ export const useAddGlobalReferenceImage = () => {
const func = useCallback(() => {
const config = getDefaultRefImageConfig(getState);
const overrides = { config };
- dispatch(refImageAdded({ isSelected: true, overrides }));
+ dispatch(refImageAdded({ overrides }));
}, [dispatch, getState]);
return func;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
index c37a6d9f56..a2a73645fa 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
@@ -40,7 +40,7 @@ export const refImagesSlice = createSlice({
state.entities.push(entityState);
},
- prepare: (payload?: { overrides?: PartialDeep; isSelected?: boolean }) => ({
+ prepare: (payload?: { overrides?: PartialDeep }) => ({
payload: { ...payload, id: getPrefixedId('reference_image') },
}),
},
@@ -200,6 +200,7 @@ export const refImagesSlice = createSlice({
export const {
refImageAdded,
+ refImageDeleted,
refImageImageChanged,
refImageIPAdapterMethodChanged,
refImageModelChanged,
diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts
index 52db1a7c09..65335fb55f 100644
--- a/invokeai/frontend/web/src/features/imageActions/actions.ts
+++ b/invokeai/frontend/web/src/features/imageActions/actions.ts
@@ -248,7 +248,7 @@ export const newCanvasFromImage = async (arg: {
const config = deepClone(getDefaultRefImageConfig(getState));
config.image = imageDTOToImageWithDims(imageDTO);
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
- dispatch(refImageAdded({ overrides: { config }, isSelected: true }));
+ dispatch(refImageAdded({ overrides: { config } }));
if (withInpaintMask) {
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
}
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
index aa65205470..037c1021fb 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
@@ -1,7 +1,6 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
-import { RefImageList } from 'features/controlLayers/components/RefImage/IPAdapterList';
import { positivePromptChanged, selectBase, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
@@ -108,7 +107,6 @@ export const ParamPositivePrompt = memo(() => {
label={`${t('parameters.positivePromptPlaceholder')} (${t('stylePresets.preview')})`}
/>
)}
-
);
diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx
index c16268a5c6..c0f4f2959b 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx
@@ -1,5 +1,6 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
+import { RefImageList } from 'features/controlLayers/components/RefImage/RefImageList';
import { createParamsSelector, selectIsChatGTP4o, selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
@@ -21,6 +22,7 @@ export const Prompts = memo(() => {
{withStylePrompts && }
+
{!isFLUX && !isChatGPT4o && }
{withStylePrompts && }
diff --git a/invokeai/frontend/web/src/features/ui/components/TabMountGate.tsx b/invokeai/frontend/web/src/features/ui/components/TabMountGate.tsx
index 5d8224e8f5..ff4bbcd4ca 100644
--- a/invokeai/frontend/web/src/features/ui/components/TabMountGate.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/TabMountGate.tsx
@@ -5,6 +5,9 @@ import type { TabName } from 'features/ui/store/uiTypes';
import type { PropsWithChildren } from 'react';
import { memo, useMemo } from 'react';
+/**
+ * TabMountGate is a component that conditionally renders its children based on whether the specified tab is enabled.
+ */
export const TabMountGate = memo(({ tab, children }: PropsWithChildren<{ tab: TabName }>) => {
const selectIsTabEnabled = useMemo(
() => createSelector(selectConfigSlice, (config) => !config.disabledTabs.includes(tab)),
From 3bb446c08fba27ff610254700f33b3902ad843be Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 12 Jun 2025 14:24:57 +1000
Subject: [PATCH 090/210] experiment(ui): add generate tab
---
.../web/src/app/hooks/useStudioInitAction.ts | 14 +++---
.../listeners/enqueueRequestedLinear.ts | 26 ++++++++--
.../NewSessionConfirmationAlertDialog.tsx | 6 +--
.../components/SimpleSession/InitialState.tsx | 13 ++++-
.../InitialStateButtonGridItem.tsx | 49 ++++++++++---------
.../SimpleSession/StagingAreaHeader.tsx | 2 -
.../StagingAreaToolbarAcceptButton.tsx | 4 +-
.../StagingAreaToolbarDiscardAllButton.tsx | 11 +++--
...tagingAreaToolbarDiscardSelectedButton.tsx | 11 +++--
.../components/StartOverButton.tsx | 7 +--
.../components/Toolbar/CanvasToolbar.tsx | 2 -
.../controlLayers/store/canvasSlice.ts | 2 -
.../store/canvasStagingAreaSlice.ts | 48 +++++++++++-------
.../controlLayers/store/lorasSlice.ts | 4 +-
.../web/src/features/hrf/store/hrfSlice.ts | 4 +-
.../web/src/features/imageActions/actions.ts | 14 +++---
.../nodes/util/graph/graphBuilderUtils.ts | 8 +--
.../web/src/features/queue/hooks/useInvoke.ts | 2 +-
.../stylePresets/store/stylePresetSlice.ts | 4 +-
.../components/FloatingLeftPanelButtons.tsx | 4 +-
.../ui/components/LeftPanelContent.tsx | 1 +
.../ui/components/MainPanelContent.tsx | 15 +++++-
.../ui/components/RightPanelContent.tsx | 4 +-
.../src/features/ui/components/TabButton.tsx | 8 +++
.../features/ui/components/VerticalNavBar.tsx | 5 +-
.../web/src/features/ui/store/uiSlice.ts | 18 +++++--
.../web/src/features/ui/store/uiTypes.ts | 2 +-
27 files changed, 181 insertions(+), 107 deletions(-)
diff --git a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
index 29db2ff0b3..b08d40417a 100644
--- a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
+++ b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts
@@ -2,8 +2,9 @@ import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { withResultAsync } from 'common/util/result';
+import { canvasReset } from 'features/controlLayers/store/actions';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
@@ -90,9 +91,8 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
const overrides: Partial = {
objects: [imageObject],
};
- store.dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ store.dispatch(canvasReset());
store.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
- store.dispatch(setActiveTab('canvas'));
store.dispatch(sentImageToCanvas());
$imageViewer.set(false);
toast({
@@ -116,9 +116,9 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
return;
}
const metadata = getImageMetadataResult.value;
+ store.dispatch(canvasReset());
// This shows a toast
await parseAndRecallAllMetadata(metadata, true);
- store.dispatch(setActiveTab('canvas'));
},
[store, t]
);
@@ -162,15 +162,13 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
switch (destination) {
case 'generation':
// Go to the canvas tab, open the image viewer, and enable send-to-gallery mode
- store.dispatch(canvasSessionTypeChanged({ type: 'simple' }));
- store.dispatch(setActiveTab('canvas'));
+ store.dispatch(paramsReset());
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
$imageViewer.set(true);
break;
case 'canvas':
// Go to the canvas tab, close the image viewer, and disable send-to-gallery mode
- store.dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
- store.dispatch(setActiveTab('canvas'));
+ store.dispatch(canvasReset());
$imageViewer.set(false);
break;
case 'workflows':
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
index 8deabbe967..d763884030 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
@@ -6,8 +6,10 @@ import { extractMessageFromAssertionError } from 'common/util/extractMessageFrom
import { withResult, withResultAsync } from 'common/util/result';
import { parseify } from 'common/util/serialize';
import {
- canvasSessionGenerationStarted,
+ canvasSessionIdCreated,
+ generateSessionIdCreated,
selectCanvasSessionId,
+ selectGenerateSessionId,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
@@ -22,6 +24,7 @@ import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Grap
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { toast } from 'features/toast/toast';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { serializeError } from 'serialize-error';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
import { assert, AssertionError } from 'tsafe';
@@ -36,12 +39,27 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
effect: async (action, { getState, dispatch }) => {
log.debug('Enqueue requested');
- if (!selectCanvasSessionId(getState())) {
- dispatch(canvasSessionGenerationStarted());
+ const tab = selectActiveTab(getState());
+ let sessionId = null;
+ if (tab === 'generate') {
+ sessionId = selectGenerateSessionId(getState());
+ if (!sessionId) {
+ dispatch(generateSessionIdCreated());
+ sessionId = selectGenerateSessionId(getState());
+ }
+ } else if (tab === 'canvas') {
+ sessionId = selectCanvasSessionId(getState());
+ if (!sessionId) {
+ dispatch(canvasSessionIdCreated());
+ sessionId = selectCanvasSessionId(getState());
+ }
+ } else {
+ log.warn(`Enqueue requested in unsupported tab ${tab}`);
+ return;
}
const state = getState();
- const destination = state.canvasSession.id;
+ const destination = sessionId;
assert(destination !== null);
const { prepend } = action.payload;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
index 84466bf0cf..5aa618e389 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx
@@ -2,7 +2,7 @@ import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text }
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
selectSystemShouldConfirmOnNewSession,
shouldConfirmOnNewSessionToggled,
@@ -20,7 +20,7 @@ export const useNewGallerySession = () => {
const newSessionDialog = useNewGallerySessionDialog();
const newGallerySessionImmediate = useCallback(() => {
- dispatch(canvasSessionTypeChanged({ type: 'simple' }));
+ dispatch(generateSessionReset());
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
@@ -41,7 +41,7 @@ export const useNewCanvasSession = () => {
const newSessionDialog = useNewCanvasSessionDialog();
const newCanvasSessionImmediate = useCallback(() => {
- dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ dispatch(canvasSessionReset());
dispatch(activeTabCanvasRightPanelChanged('layers'));
}, [dispatch]);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
index 2e8cf7a29b..2c8f92af6f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -6,13 +6,22 @@ import { InitialStateAddAStyleReference } from 'features/controlLayers/component
import { InitialStateEditImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateEditImageCard';
import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
import { InitialStateUseALayoutImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { toast } from 'features/toast/toast';
+import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
export const InitialState = memo(() => {
const dispatch = useAppDispatch();
const newCanvasSession = useCallback(() => {
- dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ dispatch(setActiveTab('canvas'));
+ toast({
+ title: 'Switched to Canvas',
+ description: 'You are in advanced mode yadda yadda.',
+ status: 'info',
+ position: 'top',
+ // isClosable: false,
+ duration: 5000,
+ });
}, [dispatch]);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
index 4861e4f475..b53e059843 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
@@ -1,28 +1,31 @@
import type { GridItemProps } from '@invoke-ai/ui-library';
-import { Button, GridItem } from '@invoke-ai/ui-library';
+import { Button, forwardRef, GridItem } from '@invoke-ai/ui-library';
import { memo } from 'react';
-export const InitialStateButtonGridItem = memo(({ children, ...rest }: GridItemProps) => {
- return (
-
- {children}
-
- );
-});
+export const InitialStateButtonGridItem = memo(
+ forwardRef(({ children, ...rest }: GridItemProps, ref) => {
+ return (
+
+ {children}
+
+ );
+ })
+);
InitialStateButtonGridItem.displayName = 'InitialStateButtonGridItem';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
index 0f4828c93f..480b9f1659 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
@@ -1,6 +1,5 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Heading, Spacer } from '@invoke-ai/ui-library';
-import { StartOverButton } from 'features/controlLayers/components/StartOverButton';
import { memo } from 'react';
export const StagingAreaHeader = memo(() => {
@@ -8,7 +7,6 @@ export const StagingAreaHeader = memo(() => {
Review Session
-
);
});
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 8718385567..1eb8451bb4 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 { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionReset } 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';
@@ -40,7 +40,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
};
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
- dispatch(canvasSessionGenerationFinished());
+ dispatch(canvasSessionReset());
deleteQueueItemsByDestination.trigger(ctx.session.id);
}, [
selectedItemImageDTO,
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 9c7f653898..7982385e7c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteQueueItemsByDestination } from 'features/queue/hooks/useDeleteQueueItemsByDestination';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -15,8 +15,13 @@ export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisa
const discardAll = useCallback(() => {
deleteQueueItemsByDestination.trigger(ctx.session.id);
- dispatch(canvasSessionGenerationFinished());
- }, [deleteQueueItemsByDestination, ctx.session.id, dispatch]);
+ if (ctx.session.type === 'advanced') {
+ dispatch(canvasSessionReset());
+ } else {
+ // ctx.session.type === 'simple'
+ dispatch(generateSessionReset());
+ }
+ }, [deleteQueueItemsByDestination, ctx.session.id, ctx.session.type, dispatch]);
return (
{
const dispatch = useAppDispatch();
const startOver = useCallback(() => {
- dispatch(canvasSessionTypeChanged({ type: 'simple' }));
- }, [dispatch]);
+ // dispatch(canvasSessionTypeChanged({ type: 'simple' }));
+ $simpleId.set(null);
+ }, []);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
index 8939df125b..91b02351c3 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
@@ -1,7 +1,6 @@
/* eslint-disable i18next/no-literal-string */
import { Divider, Flex, Heading } from '@invoke-ai/ui-library';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
-import { StartOverButton } from 'features/controlLayers/components/StartOverButton';
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
import { CanvasToolbarFitBboxToLayersButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton';
@@ -51,7 +50,6 @@ export const CanvasToolbar = memo(() => {
-
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index 7f967f6a03..300aa4129d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -6,7 +6,6 @@ import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
import {
selectAllEntities,
@@ -1595,7 +1594,6 @@ export const canvasSlice = createSlice({
syncScaledSize(state);
}
});
- builder.addCase(canvasSessionTypeChanged, (state) => resetState(state));
},
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index 3bb050a4bb..cb5e30aa4e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -5,13 +5,13 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
type CanvasStagingAreaState = {
- type: 'simple' | 'advanced';
- id: string | null;
+ generateSessionId: string | null;
+ canvasSessionId: string | null;
};
const INITIAL_STATE: CanvasStagingAreaState = {
- type: 'simple',
- id: null,
+ generateSessionId: null,
+ canvasSessionId: null,
};
const getInitialState = (): CanvasStagingAreaState => deepClone(INITIAL_STATE);
@@ -20,30 +20,39 @@ export const canvasSessionSlice = createSlice({
name: 'canvasSession',
initialState: getInitialState(),
reducers: {
- canvasSessionTypeChanged: (state, action: PayloadAction<{ type: CanvasStagingAreaState['type'] }>) => {
- const { type } = action.payload;
- state.type = type;
- state.id = null;
- },
- canvasSessionGenerationStarted: {
+ generateSessionIdCreated: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
- state.id = id;
+ state.generateSessionId = id;
+ },
+ prepare: () => ({
+ payload: { id: getPrefixedId('generate') },
+ }),
+ },
+ generateSessionReset: (state) => {
+ state.generateSessionId = null;
+ },
+ canvasSessionIdCreated: {
+ reducer: (state, action: PayloadAction<{ id: string }>) => {
+ const { id } = action.payload;
+ state.canvasSessionId = id;
},
prepare: () => ({
payload: { id: getPrefixedId('canvas') },
}),
},
- canvasSessionGenerationFinished: (state) => {
- state.id = null;
+ canvasSessionReset: (state) => {
+ state.canvasSessionId = null;
},
},
extraReducers(builder) {
- builder.addCase(canvasReset, () => getInitialState());
+ builder.addCase(canvasReset, (state) => {
+ state.canvasSessionId = null;
+ });
},
});
-export const { canvasSessionTypeChanged, canvasSessionGenerationStarted, canvasSessionGenerationFinished } =
+export const { generateSessionIdCreated, generateSessionReset, canvasSessionIdCreated, canvasSessionReset } =
canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -60,6 +69,9 @@ export const canvasStagingAreaPersistConfig: PersistConfig s[canvasSessionSlice.name];
-export const selectIsStaging = createSelector(selectCanvasSessionSlice, ({ id }) => id !== null);
-export const selectCanvasSessionType = createSelector(selectCanvasSessionSlice, ({ type }) => type);
-export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ id }) => id);
+export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ canvasSessionId }) => canvasSessionId);
+export const selectGenerateSessionId = createSelector(
+ selectCanvasSessionSlice,
+ ({ generateSessionId }) => generateSessionId
+);
+export const selectIsStaging = createSelector(selectCanvasSessionId, (canvasSessionId) => canvasSessionId !== null);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts
index ea34668a84..869cb93ed3 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts
@@ -1,7 +1,7 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import type { LoRA } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { LoRAModelConfig } from 'services/api/types';
@@ -64,7 +64,7 @@ export const lorasSlice = createSlice({
},
},
extraReducers(builder) {
- builder.addCase(canvasSessionTypeChanged, () => {
+ builder.addCase(paramsReset, () => {
// When a new session is requested, clear all LoRAs
return deepClone(initialState);
});
diff --git a/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts b/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts
index 0bfe797ad1..4780ed28f3 100644
--- a/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts
+++ b/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts
@@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import type { ParameterHRFMethod, ParameterStrength } from 'features/parameters/types/parameterSchemas';
interface HRFState {
@@ -34,7 +34,7 @@ export const hrfSlice = createSlice({
},
},
extraReducers(builder) {
- builder.addCase(canvasSessionTypeChanged, () => {
+ builder.addCase(paramsReset, () => {
return deepClone(initialHRFState);
});
},
diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts
index 65335fb55f..2d91dbc177 100644
--- a/invokeai/frontend/web/src/features/imageActions/actions.ts
+++ b/invokeai/frontend/web/src/features/imageActions/actions.ts
@@ -6,6 +6,7 @@ import {
} from 'features/controlLayers/hooks/addLayerHooks';
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer';
import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { canvasReset } from 'features/controlLayers/store/actions';
import {
bboxChangedFromCanvas,
canvasClearHistory,
@@ -16,7 +17,6 @@ import {
rgAdded,
rgRefImageImageChanged,
} from 'features/controlLayers/store/canvasSlice';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { refImageAdded, refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
import type {
@@ -185,7 +185,7 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
@@ -202,7 +202,7 @@ export const newCanvasFromImage = async (arg: {
controlAdapter: deepClone(initialControlNet),
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(controlLayerAdded({ overrides, isSelected: true }));
@@ -218,7 +218,7 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
@@ -234,7 +234,7 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial;
addFitOnLayerInitCallback(overrides.id);
- dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rgAdded({ overrides, isSelected: true }));
@@ -247,7 +247,7 @@ export const newCanvasFromImage = async (arg: {
case 'reference_image': {
const config = deepClone(getDefaultRefImageConfig(getState));
config.image = imageDTOToImageWithDims(imageDTO);
- dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ dispatch(canvasReset());
dispatch(refImageAdded({ overrides: { config } }));
if (withInpaintMask) {
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
@@ -259,7 +259,7 @@ export const newCanvasFromImage = async (arg: {
const config = getDefaultRegionalGuidanceRefImageConfig(getState);
config.image = imageDTOToImageWithDims(imageDTO);
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), config }];
- dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
+ dispatch(canvasReset());
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
if (withInpaintMask) {
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
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 a34b7b95b2..04edd1bf56 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -1,13 +1,13 @@
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import type { CanvasState, ParamsState } from 'features/controlLayers/store/types';
import type { BoardField } from 'features/nodes/types/common';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import { selectStylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { pick } from 'lodash-es';
import { selectListStylePresetsRequestState } from 'services/api/endpoints/stylePresets';
import type { Invocation, S } from 'services/api/types';
@@ -36,9 +36,9 @@ export const getBoardField = (state: RootState): BoardField | undefined => {
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 type = selectCanvasSessionType(state);
- const is_intermediate = type === 'advanced';
- const board = type === 'advanced' ? undefined : getBoardField(state);
+ const tab = selectActiveTab(state);
+ const is_intermediate = tab === 'canvas';
+ const board = tab === 'canvas' ? undefined : getBoardField(state);
return {
is_intermediate,
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts
index 99e4419aca..4419a1e4f3 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts
@@ -44,7 +44,7 @@ export const useInvoke = () => {
return;
}
- if (tabName === 'canvas') {
+ if (tabName === 'canvas' || tabName === 'generate') {
dispatch(enqueueRequestedCanvas({ prepend }));
return;
}
diff --git a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts
index 11a775c26d..ba996defc7 100644
--- a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts
+++ b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts
@@ -2,7 +2,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
-import { canvasSessionGenerationStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import { atom } from 'nanostores';
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
@@ -29,7 +29,7 @@ export const stylePresetSlice = createSlice({
},
},
extraReducers(builder) {
- builder.addCase(canvasSessionGenerationStarted, () => {
+ builder.addCase(paramsReset, () => {
return deepClone(initialState);
});
builder.addMatcher(stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled, (state, action) => {
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
index 8630476015..789dda66c2 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
@@ -2,7 +2,6 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
@@ -22,11 +21,10 @@ import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) => {
const tab = useAppSelector(selectActiveTab);
- const type = useAppSelector(selectCanvasSessionType);
return (
- {tab === 'canvas' && type === 'advanced' && (
+ {tab === 'canvas' && (
diff --git a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
index f6190ed58c..a603d540e7 100644
--- a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
@@ -15,6 +15,7 @@ export const LeftPanelContent = memo(() => {
+ {tab === 'generate' && }
{tab === 'canvas' && }
{tab === 'upscaling' && }
{tab === 'workflows' && }
diff --git a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
index 48c812e6a1..58010ba503 100644
--- a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
@@ -1,19 +1,30 @@
import { useAppSelector } from 'app/store/storeHooks';
-import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
+import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
+import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
+import { selectCanvasSessionId, selectGenerateSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
import QueueTab from 'features/ui/components/tabs/QueueTab';
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
+import { atom } from 'nanostores';
import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
+export const $simpleId = atom(null);
+export const $advancedId = atom(null);
+
export const MainPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
+ const generateId = useAppSelector(selectGenerateSessionId);
+ const canvasId = useAppSelector(selectCanvasSessionId);
+ if (tab === 'generate') {
+ return ;
+ }
if (tab === 'canvas') {
- return ;
+ return ;
}
if (tab === 'upscaling') {
return ;
diff --git a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
index 6eb9eb761f..6625ce0175 100644
--- a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
@@ -4,7 +4,6 @@ import { useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
import { Gallery } from 'features/gallery/components/Gallery';
import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
@@ -29,7 +28,6 @@ export const RightPanelContent = memo(() => {
const boardSearchText = useAppSelector(selectBoardSearchText);
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const imperativePanelGroupRef = useRef(null);
- const type = useAppSelector(selectCanvasSessionType);
const tab = useAppSelector(selectActiveTab);
const boardsListPanelOptions = useMemo(
@@ -79,7 +77,7 @@ export const RightPanelContent = memo(() => {
- {tab === 'canvas' && type === 'advanced' && (
+ {tab === 'canvas' && (
<>
diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
index d59f51f3ed..fb29cd6744 100644
--- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
@@ -1,3 +1,4 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -5,6 +6,12 @@ import { setActiveTab } from 'features/ui/store/uiSlice';
import type { TabName } from 'features/ui/store/uiTypes';
import { forwardRef, memo, type ReactElement, useCallback } from 'react';
+const sx: SystemStyleObject = {
+ '&[data-selected=true]': {
+ svg: { fill: 'invokeYellow.300' },
+ },
+};
+
export const TabButton = memo(
forwardRef(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }, ref) => {
const dispatch = useAppDispatch();
@@ -26,6 +33,7 @@ export const TabButton = memo(
data-selected={activeTabName === tab}
aria-label={label}
data-testid={label}
+ sx={sx}
/>
);
diff --git a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
index a5925d8de5..29cde774d0 100644
--- a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
@@ -8,7 +8,7 @@ import { VideosModalButton } from 'features/system/components/VideosModal/Videos
import { TabMountGate } from 'features/ui/components/TabMountGate';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiBoundingBoxBold, PiCubeBold, PiFlowArrowBold, PiFrameCornersBold, PiQueueBold } from 'react-icons/pi';
+import { PiBoundingBoxBold, PiCubeBold, PiFlowArrowBold, PiFrameCornersBold, PiQueueBold, PiTextAaBold } from 'react-icons/pi';
import { Notifications } from './Notifications';
import { TabButton } from './TabButton';
@@ -21,6 +21,9 @@ export const VerticalNavBar = memo(() => {
+
+ } label="Generate" />
+
} label={t('ui.tabs.canvas')} />
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
index aa4b3ec0e0..92af3b2ec8 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
@@ -1,7 +1,8 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
-import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { canvasReset } from 'features/controlLayers/store/actions';
+import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { Dimensions } from 'features/controlLayers/store/types';
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
import { atom } from 'nanostores';
@@ -56,9 +57,18 @@ export const uiSlice = createSlice({
builder.addCase(workflowLoaded, (state) => {
state.activeTab = 'workflows';
});
- builder.addCase(canvasSessionTypeChanged, (state) => {
+ builder.addCase(canvasReset, (state) => {
state.activeTab = 'canvas';
});
+ builder.addCase(canvasSessionReset, (state) => {
+ state.activeTab = 'canvas';
+ });
+ builder.addCase(generateSessionReset, (state) => {
+ state.activeTab = 'generate';
+ });
+ // builder.addCase(canvasSessionTypeChanged, (state) => {
+ // state.activeTab = 'canvas';
+ // });
},
});
@@ -98,12 +108,12 @@ export const uiPersistConfig: PersistConfig = {
persistDenylist: ['shouldShowImageDetails'],
};
-const TABS_WITH_LEFT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows'] as const;
+const TABS_WITH_LEFT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
export const LEFT_PANEL_MIN_SIZE_PX = 400;
export const $isLeftPanelOpen = atom(true);
export const selectWithLeftPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_LEFT_PANEL.includes(ui.activeTab));
-const TABS_WITH_RIGHT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows'] as const;
+const TABS_WITH_RIGHT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
export const RIGHT_PANEL_MIN_SIZE_PX = 390;
export const $isRightPanelOpen = atom(true);
export const selectWithRightPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_RIGHT_PANEL.includes(ui.activeTab));
diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
index fe278384ab..3702c58ec5 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
@@ -1,6 +1,6 @@
import type { Dimensions } from 'features/controlLayers/store/types';
-export type TabName = 'canvas' | 'upscaling' | 'workflows' | 'models' | 'queue';
+export type TabName = 'generate' | 'canvas' | 'upscaling' | 'workflows' | 'models' | 'queue';
export type CanvasRightPanelTabName = 'layers' | 'gallery';
export interface UIState {
From 45b1ef623160146b7673fe4d551c223b0c694961 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 13 Jun 2025 15:39:57 +1000
Subject: [PATCH 091/210] tweak(ui): ref image header
---
.../controlLayers/components/RefImage/RefImageHeader.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageHeader.tsx
index 1a2eb27310..7d75307095 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageHeader.tsx
@@ -23,7 +23,7 @@ export const RefImageHeader = memo(() => {
)}
{entity.config.image === null && (
- Reference Image - No Image Selected
+ No Reference Image Selected
)}
Date: Wed, 11 Jun 2025 23:21:56 +1000
Subject: [PATCH 092/210] fix(ui): hack to close chakra tooltips on drag
---
.../src/app/components/GlobalHookIsolator.tsx | 2 ++
.../hooks/useCloseChakraTooltipsOnDragFix.ts | 19 +++++++++++++++++++
2 files changed, 21 insertions(+)
create mode 100644 invokeai/frontend/web/src/common/hooks/useCloseChakraTooltipsOnDragFix.ts
diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
index ef45771119..17bb4f4ce4 100644
--- a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
@@ -8,6 +8,7 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { PartialAppConfig } from 'app/types/invokeai';
import { useFocusRegionWatcher } from 'common/hooks/focus';
+import { useCloseChakraTooltipsOnDragFix } from 'common/hooks/useCloseChakraTooltipsOnDragFix';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
import { toggleImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
@@ -43,6 +44,7 @@ export const GlobalHookIsolator = memo(
useGlobalHotkeys();
useGetOpenAPISchemaQuery();
useSyncLoggingConfig();
+ useCloseChakraTooltipsOnDragFix();
// Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
// and/or in progress canvas sessions.
diff --git a/invokeai/frontend/web/src/common/hooks/useCloseChakraTooltipsOnDragFix.ts b/invokeai/frontend/web/src/common/hooks/useCloseChakraTooltipsOnDragFix.ts
new file mode 100644
index 0000000000..79a92398d7
--- /dev/null
+++ b/invokeai/frontend/web/src/common/hooks/useCloseChakraTooltipsOnDragFix.ts
@@ -0,0 +1,19 @@
+import { useEffect } from 'react';
+
+// Chakra tooltips sometimes open during a drag operation. We can fix it by dispatching an event that chakra listens
+// for to close tooltips. It's reaching into the internals but it seems to work.
+
+const closeEventName = 'chakra-ui:close-tooltip';
+
+export const useCloseChakraTooltipsOnDragFix = () => {
+ useEffect(() => {
+ const closeTooltips = () => {
+ document.dispatchEvent(new window.CustomEvent(closeEventName));
+ };
+ document.addEventListener('drag', closeTooltips);
+
+ return () => {
+ document.removeEventListener('drag', closeTooltips);
+ };
+ }, []);
+};
From 1b1e1983d9d40108d1cddf74cfea86acfca5ad67 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 11 Jun 2025 23:22:35 +1000
Subject: [PATCH 093/210] fix(ui): update queue item preview images on init of
queue items context
---
.../features/controlLayers/components/SimpleSession/context.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 7529c6995d..94357c5e0f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -329,7 +329,7 @@ export const CanvasSessionContextProvider = memo(
});
// Clean up the progress data when a queue item is discarded.
- const unsubCleanUpProgressData = $items.listen(async (items) => {
+ const unsubCleanUpProgressData = $items.subscribe(async (items) => {
const progressData = $progressData.get();
const toDelete: number[] = [];
From a28c15d5456ff36f562a3c2ab1006c3d879acc8c Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 13 Jun 2025 21:22:30 +1000
Subject: [PATCH 094/210] chore: bump version to v6.0.0a2
---
invokeai/version/invokeai_version.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py
index fce95e672c..c7f18a7bc8 100644
--- a/invokeai/version/invokeai_version.py
+++ b/invokeai/version/invokeai_version.py
@@ -1 +1 @@
-__version__ = "6.0.0a1"
+__version__ = "6.0.0a2"
From 450a0bf142eeb5358bd42bc716541f6a038fa26c Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 16 Jun 2025 16:34:29 +1000
Subject: [PATCH 095/210] fix(ui): remove old isSelected from refImageAdded
call
---
.../web/src/features/controlLayers/hooks/saveCanvasHooks.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
index 395768585f..f164b1f2f1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts
@@ -212,7 +212,7 @@ export const useNewGlobalReferenceImageFromBbox = () => {
image: imageDTOToImageWithDims(imageDTO),
},
};
- dispatch(refImageAdded({ overrides, isSelected: true }));
+ dispatch(refImageAdded({ overrides }));
};
return {
From 0f1a69a0c38c61dc089750cd5918f37e36f78075 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 16 Jun 2025 17:03:19 +1000
Subject: [PATCH 096/210] feat(ui): toggleable negative prompt
---
.../hooks/useIsEntityTypeEnabled.ts | 4 +-
.../controlLayers/store/paramsSlice.ts | 14 +++++--
.../src/features/controlLayers/store/types.ts | 3 +-
.../features/gallery/hooks/useImageActions.ts | 6 +--
.../nodes/util/graph/graphBuilderUtils.ts | 5 ++-
.../components/Bbox/BboxAspectRatioSelect.tsx | 4 +-
.../Core/NegativePromptToggleButton.tsx | 42 +++++++++++++++++++
.../components/Core/ParamNegativePrompt.tsx | 7 +++-
.../components/Core/ParamPositivePrompt.tsx | 12 +++++-
.../parameters/components/Prompts/Prompts.tsx | 14 ++++---
.../parameters/hooks/useIsApiModel.ts | 4 +-
.../parameters/types/parameterSchemas.ts | 2 +-
.../hooks/usePresetModifiedPrompts.ts | 4 +-
13 files changed, 94 insertions(+), 27 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntityTypeEnabled.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntityTypeEnabled.ts
index 6984677f3d..e53291ebfd 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntityTypeEnabled.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntityTypeEnabled.ts
@@ -1,6 +1,6 @@
import { useAppSelector } from 'app/store/storeHooks';
import {
- selectIsChatGTP4o,
+ selectIsChatGPT4o,
selectIsCogView4,
selectIsFluxKontext,
selectIsImagen3,
@@ -17,8 +17,8 @@ export const useIsEntityTypeEnabled = (entityType: CanvasEntityType) => {
const isCogView4 = useAppSelector(selectIsCogView4);
const isImagen3 = useAppSelector(selectIsImagen3);
const isImagen4 = useAppSelector(selectIsImagen4);
- const isChatGPT4o = useAppSelector(selectIsChatGTP4o);
const isFluxKontext = useAppSelector(selectIsFluxKontext);
+ const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
const isEntityTypeEnabled = useMemo(() => {
switch (entityType) {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index 5fb816e709..68ca5bf1c1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -14,6 +14,8 @@ import type {
ParameterControlLoRAModel,
ParameterGuidance,
ParameterModel,
+ ParameterNegativePrompt,
+ ParameterPositivePrompt,
ParameterPrecision,
ParameterScheduler,
ParameterSDXLRefinerModel,
@@ -124,10 +126,10 @@ export const paramsSlice = createSlice({
shouldUseCpuNoiseChanged: (state, action: PayloadAction) => {
state.shouldUseCpuNoise = action.payload;
},
- positivePromptChanged: (state, action: PayloadAction) => {
+ positivePromptChanged: (state, action: PayloadAction) => {
state.positivePrompt = action.payload;
},
- negativePromptChanged: (state, action: PayloadAction) => {
+ negativePromptChanged: (state, action: PayloadAction) => {
state.negativePrompt = action.payload;
},
positivePrompt2Changed: (state, action: PayloadAction) => {
@@ -273,8 +275,8 @@ export const selectIsSD3 = createParamsSelector((params) => params.model?.base =
export const selectIsCogView4 = createParamsSelector((params) => params.model?.base === 'cogview4');
export const selectIsImagen3 = createParamsSelector((params) => params.model?.base === 'imagen3');
export const selectIsImagen4 = createParamsSelector((params) => params.model?.base === 'imagen4');
-export const selectIsChatGTP4o = createParamsSelector((params) => params.model?.base === 'chatgpt-4o');
export const selectIsFluxKontext = createParamsSelector((params) => params.model?.base === 'flux-kontext');
+export const selectIsChatGPT4o = createParamsSelector((params) => params.model?.base === 'chatgpt-4o');
export const selectModel = createParamsSelector((params) => params.model);
export const selectModelKey = createParamsSelector((params) => params.model?.key);
@@ -306,6 +308,12 @@ export const selectImg2imgStrength = createParamsSelector((params) => params.img
export const selectOptimizedDenoisingEnabled = createParamsSelector((params) => params.optimizedDenoisingEnabled);
export const selectPositivePrompt = createParamsSelector((params) => params.positivePrompt);
export const selectNegativePrompt = createParamsSelector((params) => params.negativePrompt);
+export const selectNegativePromptWithFallback = createParamsSelector((params) => params.negativePrompt ?? '');
+export const selectHasNegativePrompt = createParamsSelector((params) => params.negativePrompt !== null);
+export const selectModelSupportsNegativePrompt = createSelector(
+ [selectIsFLUX, selectIsChatGPT4o],
+ (isFLUX, isChatGPT4o) => !isFLUX && !isChatGPT4o
+);
export const selectPositivePrompt2 = createParamsSelector((params) => params.positivePrompt2);
export const selectNegativePrompt2 = createParamsSelector((params) => params.negativePrompt2);
export const selectShouldConcatPrompts = createParamsSelector((params) => params.shouldConcatPrompts);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index dacaff07ca..558ac423a0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -522,7 +522,8 @@ const zParamsState = z.object({
clipSkip: z.number().default(0),
shouldUseCpuNoise: z.boolean().default(true),
positivePrompt: zParameterPositivePrompt.default(''),
- negativePrompt: zParameterNegativePrompt.default(''),
+ // Negative prompt may be disabled, in which case it will be null
+ negativePrompt: zParameterNegativePrompt.default(null),
positivePrompt2: zParameterPositiveStylePromptSDXL.default(''),
negativePrompt2: zParameterNegativeStylePromptSDXL.default(''),
shouldConcatPrompts: z.boolean().default(true),
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
index b63da55d89..87f0b6aba2 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
@@ -121,8 +121,8 @@ export const useImageActions = (imageDTO: ImageDTO) => {
if (!metadata) {
return;
}
- let positivePrompt;
- let negativePrompt;
+ let positivePrompt: string;
+ let negativePrompt: string;
try {
positivePrompt = await handlers.positivePrompt.parse(metadata);
@@ -130,7 +130,7 @@ export const useImageActions = (imageDTO: ImageDTO) => {
positivePrompt = '';
}
try {
- negativePrompt = await handlers.negativePrompt.parse(metadata);
+ negativePrompt = (await handlers.negativePrompt.parse(metadata)) ?? '';
} catch (error) {
negativePrompt = '';
}
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 04edd1bf56..345fc664d5 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -56,7 +56,8 @@ export const selectPresetModifiedPrompts = createSelector(
selectStylePresetSlice,
selectListStylePresetsRequestState,
(params, stylePresetSlice, listStylePresetsRequestState) => {
- const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = params;
+ const negativePrompt = params.negativePrompt ?? '';
+ const { positivePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = params;
const { activeStylePresetId } = stylePresetSlice;
if (activeStylePresetId) {
@@ -72,7 +73,7 @@ export const selectPresetModifiedPrompts = createSelector(
const presetModifiedNegativePrompt = buildPresetModifiedPrompt(
activeStylePreset.preset_data.negative_prompt,
- negativePrompt
+ negativePrompt ?? ''
);
return {
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 e5b0a5210c..c2c2cc30fb 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
@@ -4,7 +4,7 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
- selectIsChatGTP4o,
+ selectIsChatGPT4o,
selectIsFluxKontext,
selectIsImagen3,
selectIsImagen4,
@@ -28,7 +28,7 @@ export const BboxAspectRatioSelect = memo(() => {
const id = useAppSelector(selectAspectRatioID);
const isStaging = useAppSelector(selectIsStaging);
const isImagen3 = useAppSelector(selectIsImagen3);
- const isChatGPT4o = useAppSelector(selectIsChatGTP4o);
+ const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
const isImagen4 = useAppSelector(selectIsImagen4);
const isFluxKontext = useAppSelector(selectIsFluxKontext);
const options = useMemo(() => {
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx
new file mode 100644
index 0000000000..7283c76b25
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx
@@ -0,0 +1,42 @@
+import { IconButton, Tooltip } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { negativePromptChanged, selectHasNegativePrompt } from 'features/controlLayers/store/paramsSlice';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiPlusMinusBold } from 'react-icons/pi';
+
+export const NegativePromptToggleButton = memo(() => {
+ const hasNegativePrompt = useAppSelector(selectHasNegativePrompt);
+
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+
+ const onClick = useCallback(() => {
+ if (hasNegativePrompt) {
+ dispatch(negativePromptChanged(null));
+ } else {
+ dispatch(negativePromptChanged(''));
+ }
+ }, [dispatch, hasNegativePrompt]);
+
+ const label = useMemo(
+ () => (hasNegativePrompt ? 'Remove Negative Prompt' : 'Add Negative Prompt'),
+ [hasNegativePrompt]
+ );
+
+ return (
+
+ }
+ variant="promptOverlay"
+ fontSize={12}
+ px={0.5}
+ colorScheme={hasNegativePrompt ? 'invokeBlue' : 'base'}
+ />
+
+ );
+});
+
+NegativePromptToggleButton.displayName = 'NegativePromptToggleButton';
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx
index 853d1326ef..94faaaa0cb 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx
@@ -1,7 +1,10 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
-import { negativePromptChanged, selectNegativePrompt } from 'features/controlLayers/store/paramsSlice';
+import {
+ negativePromptChanged,
+ selectNegativePromptWithFallback,
+} from 'features/controlLayers/store/paramsSlice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
@@ -23,7 +26,7 @@ const persistOptions: Parameters[2] = {
export const ParamNegativePrompt = memo(() => {
const dispatch = useAppDispatch();
- const prompt = useAppSelector(selectNegativePrompt);
+ const prompt = useAppSelector(selectNegativePromptWithFallback);
const viewMode = useAppSelector(selectStylePresetViewMode);
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
index 037c1021fb..808ae55a1e 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx
@@ -1,8 +1,14 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
-import { positivePromptChanged, selectBase, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
+import {
+ positivePromptChanged,
+ selectBase,
+ selectModelSupportsNegativePrompt,
+ selectPositivePrompt,
+} from 'features/controlLayers/store/paramsSlice';
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
+import { NegativePromptToggleButton } from 'features/parameters/components/Core/NegativePromptToggleButton';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
@@ -32,6 +38,7 @@ export const ParamPositivePrompt = memo(() => {
const baseModel = useAppSelector(selectBase);
const viewMode = useAppSelector(selectStylePresetViewMode);
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
+ const modelSupportsNegativePrompt = useAppSelector(selectModelSupportsNegativePrompt);
const textareaRef = useRef(null);
usePersistedTextAreaSize('positive_prompt', textareaRef, persistOptions);
@@ -98,8 +105,9 @@ export const ParamPositivePrompt = memo(() => {
{baseModel === 'sdxl' && }
+ {modelSupportsNegativePrompt && }
-
+
{viewMode && (
{
export const Prompts = memo(() => {
const withStylePrompts = useAppSelector(selectWithStylePrompts);
- const isFLUX = useAppSelector(selectIsFLUX);
- const isChatGPT4o = useAppSelector(selectIsChatGTP4o);
+ const modelSupportsNegativePrompt = useAppSelector(selectModelSupportsNegativePrompt);
+ const hasNegativePrompt = useAppSelector(selectHasNegativePrompt);
return (
{withStylePrompts && }
-
- {!isFLUX && !isChatGPT4o && }
+ {modelSupportsNegativePrompt && hasNegativePrompt && }
{withStylePrompts && }
+
);
});
diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useIsApiModel.ts b/invokeai/frontend/web/src/features/parameters/hooks/useIsApiModel.ts
index 90bcb6b391..86865a8067 100644
--- a/invokeai/frontend/web/src/features/parameters/hooks/useIsApiModel.ts
+++ b/invokeai/frontend/web/src/features/parameters/hooks/useIsApiModel.ts
@@ -1,6 +1,6 @@
import { useAppSelector } from 'app/store/storeHooks';
import {
- selectIsChatGTP4o,
+ selectIsChatGPT4o,
selectIsFluxKontext,
selectIsImagen3,
selectIsImagen4,
@@ -9,8 +9,8 @@ import {
export const useIsApiModel = () => {
const isImagen3 = useAppSelector(selectIsImagen3);
const isImagen4 = useAppSelector(selectIsImagen4);
- const isChatGPT4o = useAppSelector(selectIsChatGTP4o);
const isFluxKontext = useAppSelector(selectIsFluxKontext);
+ const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
return isImagen3 || isImagen4 || isChatGPT4o || isFluxKontext;
};
diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts
index 3daf1f4137..01d2788b45 100644
--- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts
+++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts
@@ -29,7 +29,7 @@ export type ParameterPositivePrompt = z.infer;
// #endregion
// #region Negative prompt
-export const [zParameterNegativePrompt, isParameterNegativePrompt] = buildParameter(z.string());
+export const [zParameterNegativePrompt, isParameterNegativePrompt] = buildParameter(z.string().nullable());
export type ParameterNegativePrompt = z.infer;
// #endregion
diff --git a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts
index 16d5a3502d..cefac18d9f 100644
--- a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts
+++ b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts
@@ -1,5 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks';
-import { selectNegativePrompt, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
+import { selectNegativePromptWithFallback, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { selectStylePresetActivePresetId } from 'features/stylePresets/store/stylePresetSlice';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
@@ -13,7 +13,7 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s
export const usePresetModifiedPrompts = () => {
const positivePrompt = useAppSelector(selectPositivePrompt);
- const negativePrompt = useAppSelector(selectNegativePrompt);
+ const negativePrompt = useAppSelector(selectNegativePromptWithFallback);
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
From ed05bf2df3e5102a54ec8077aac387f1f135911b Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 16 Jun 2025 17:33:17 +1000
Subject: [PATCH 097/210] feat(ui): refine ref images UI
---
.../components/RefImage/RefImage.tsx | 29 ++---
.../components/RefImage/RefImageList.tsx | 108 ++++++++++--------
invokeai/frontend/web/src/features/dnd/dnd.ts | 32 ++++++
3 files changed, 107 insertions(+), 62 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
index ca21d6a84a..f184f204a6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
@@ -1,5 +1,3 @@
-import type {
- SystemStyleObject} from '@invoke-ai/ui-library';
import {
Divider,
Flex,
@@ -10,7 +8,8 @@ import {
PopoverArrow,
PopoverBody,
PopoverContent,
- Portal
+ Portal,
+ Skeleton,
} from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { POPPER_MODIFIERS } from 'common/components/InformationalPopover/constants';
@@ -80,24 +79,12 @@ export const RefImage = memo(() => {
});
RefImage.displayName = 'RefImage';
-const imageSx: SystemStyleObject = {
- opacity: 0.5,
- _hover: {
- opacity: 1,
- },
- "&[data-is-open='true']": {
- opacity: 1,
- },
- transitionProperty: 'opacity',
- transitionDuration: '0.2s',
-};
-
const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
const id = useRefImageIdContext();
const entity = useRefImageEntity(id);
const { data: imageDTO } = useGetImageDTOQuery(entity.config.image?.image_name ?? skipToken);
- if (!imageDTO || !entity.config.image) {
+ if (!entity.config.image) {
return (
{
icon={}
colorScheme="error"
onClick={disclosure.toggle}
+ flexShrink={0}
/>
);
@@ -120,14 +108,21 @@ const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
return (
}
maxW="full"
maxH="full"
borderRadius="base"
onClick={disclosure.toggle}
+ flexShrink={0}
// sx={imageSx}
// data-is-open={disclosure.isOpen}
/>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
index 723237fee5..d7efcb3d0b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
@@ -1,17 +1,22 @@
/* eslint-disable i18next/no-literal-string */
import type { FlexProps } from '@invoke-ai/ui-library';
-import { Button, Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
+import { Button, Flex } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { RefImage } from 'features/controlLayers/components/RefImage/RefImage';
import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
-import { useAddGlobalReferenceImage } from 'features/controlLayers/hooks/addLayerHooks';
-import { selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
-import { memo } from 'react';
-import { PiPlusBold } from 'react-icons/pi';
+import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
+import { refImageAdded, selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
+import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
+import { addGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { memo, useMemo } from 'react';
+import { PiUploadBold } from 'react-icons/pi';
+import type { ImageDTO } from 'services/api/types';
export const RefImageList = memo((props: FlexProps) => {
const ids = useAppSelector(selectRefImageEntityIds);
- const addRefImage = useAddGlobalReferenceImage();
return (
{ids.map((id) => (
@@ -19,59 +24,72 @@ export const RefImageList = memo((props: FlexProps) => {
))}
-
- }
- onClick={addRefImage}
- isDisabled={ids.length >= 5} // Limit to 5 reference images
- >
- Ref Image
-
+ {ids.length < 5 && }
+ {ids.length >= 5 && }
);
});
RefImageList.displayName = 'RefImageList';
-const AddRefImageIconButton = memo(() => {
- const addRefImage = useAddGlobalReferenceImage();
- return (
- }
- />
- );
-});
-AddRefImageIconButton.displayName = 'AddRefImageIconButton';
+const dndTargetData = addGlobalReferenceImageDndTarget.getData();
-const AddRefImageButton = memo((props) => {
- const addRefImage = useAddGlobalReferenceImage();
+const MaxRefImages = memo(() => {
return (
}
- onClick={addRefImage}
+ isDisabled
>
- Ref Image
+ Max Ref Images
);
});
-AddRefImageButton.displayName = 'AddRefImageButton';
+MaxRefImages.displayName = 'MaxRefImages';
+
+const AddRefImageDropTargetAndButton = memo(() => {
+ const { dispatch, getState } = useAppStore();
+
+ const uploadOptions = useMemo(
+ () =>
+ ({
+ onUpload: (imageDTO: ImageDTO) => {
+ const config = getDefaultRefImageConfig(getState);
+ config.image = imageDTOToImageWithDims(imageDTO);
+ dispatch(refImageAdded({ overrides: { config } }));
+ },
+ allowMultiple: false,
+ }) as const,
+ [dispatch, getState]
+ );
+
+ const uploadApi = useImageUploadButton(uploadOptions);
+
+ return (
+ <>
+ }
+ {...uploadApi.getUploadButtonProps()}
+ >
+ Reference Image
+
+
+
+ >
+ );
+});
+AddRefImageDropTargetAndButton.displayName = 'AddRefImageDropTargetAndButton';
diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts
index bb401727d3..df768f4ec6 100644
--- a/invokeai/frontend/web/src/features/dnd/dnd.ts
+++ b/invokeai/frontend/web/src/features/dnd/dnd.ts
@@ -1,7 +1,10 @@
import { logger } from 'app/logging/logger';
import type { AppDispatch, AppGetState } from 'app/store/store';
+import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
+import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common';
import type { BoardId } from 'features/gallery/store/types';
import {
@@ -152,6 +155,34 @@ export const setGlobalReferenceImageDndTarget: DndTarget<
};
//#endregion
+//#region Add Global Reference Image
+const _addGlobalReferenceImage = buildTypeAndKey('add-global-reference-image');
+export type AddGlobalReferenceImageDndTargetData = DndData<
+ typeof _addGlobalReferenceImage.type,
+ typeof _addGlobalReferenceImage.key
+>;
+export const addGlobalReferenceImageDndTarget: DndTarget<
+ AddGlobalReferenceImageDndTargetData,
+ SingleImageDndSourceData
+> = {
+ ..._addGlobalReferenceImage,
+ typeGuard: buildTypeGuard(_addGlobalReferenceImage.key),
+ getData: buildGetData(_addGlobalReferenceImage.key, _addGlobalReferenceImage.type),
+ isValid: ({ sourceData }) => {
+ if (singleImageDndSource.typeGuard(sourceData)) {
+ return true;
+ }
+ return false;
+ },
+ handler: ({ sourceData, dispatch, getState }) => {
+ const { imageDTO } = sourceData.payload;
+ const config = getDefaultRefImageConfig(getState);
+ config.image = imageDTOToImageWithDims(imageDTO);
+ dispatch(refImageAdded({ overrides: { config } }));
+ },
+};
+//#endregion
+
//#region Set Regional Guidance Reference Image
const _setRegionalGuidanceReferenceImage = buildTypeAndKey('set-regional-guidance-reference-image');
export type SetRegionalGuidanceReferenceImageDndTargetData = DndData<
@@ -496,6 +527,7 @@ export const dndTargets = [
addImageToBoardDndTarget,
removeImageFromBoardDndTarget,
newCanvasFromImageDndTarget,
+ addGlobalReferenceImageDndTarget,
// Single or Multiple Image
addImageToBoardDndTarget,
removeImageFromBoardDndTarget,
From 2e0824a799bff3c5166f5297d0430febf0b8fcce Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 16 Jun 2025 17:41:47 +1000
Subject: [PATCH 098/210] feat(ui): make autoswitch on/off
When the invocation cache is used, we might skip all progress images. This can prevent auto-switch-on-first-progress from working, as we don't get any of those events.
It's much easier to only support auto-switch on complete.
---
.../components/SimpleSession/context.tsx | 14 +++-----------
.../StagingAreaToolbarMenuAutoSwitch.tsx | 14 +++++---------
2 files changed, 8 insertions(+), 20 deletions(-)
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 94357c5e0f..88f6f4f2d0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -13,10 +13,6 @@ import { queueApi } from 'services/api/endpoints/queue';
import type { ImageDTO, S } from 'services/api/types';
import { $socket } from 'services/events/stores';
import { assert } from 'tsafe';
-import { z } from 'zod';
-
-export const zAutoSwitchMode = z.enum(['off', 'first_progress', 'completed']);
-export type AutoSwitchMode = z.infer;
export type ProgressData = {
itemId: number;
@@ -91,7 +87,7 @@ type CanvasSessionContextValue = {
$selectedItem: Atom;
$selectedItemIndex: Atom;
$selectedItemOutputImageDTO: Atom;
- $autoSwitch: WritableAtom;
+ $autoSwitch: WritableAtom;
$lastLoadedItemId: WritableAtom;
selectNext: () => void;
selectPrev: () => void;
@@ -126,7 +122,7 @@ export const CanvasSessionContextProvider = memo(
/**
* Whether auto-switch is enabled.
*/
- const $autoSwitch = useState(() => atom('first_progress'))[0];
+ const $autoSwitch = useState(() => atom(true))[0];
/**
* An internal flag used to work around race conditions with auto-switch switching to queue items before their
@@ -273,11 +269,7 @@ export const CanvasSessionContextProvider = memo(
if (data.destination !== session.id) {
return;
}
- const isFirstProgressImage = !$progressData.get()[data.item_id]?.progressImage && !!data.image;
setProgress($progressData, data);
- if ($autoSwitch.get() === 'first_progress' && isFirstProgressImage) {
- $selectedItemId.set(data.item_id);
- }
};
socket.on('invocation_progress', onProgress);
@@ -413,7 +405,7 @@ export const CanvasSessionContextProvider = memo(
if (lastLoadedItemId === null) {
return;
}
- if ($autoSwitch.get() === 'completed') {
+ if ($autoSwitch.get()) {
$selectedItemId.set(lastLoadedItemId);
}
$lastLoadedItemId.set(null);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx
index 567e6beb5e..ff8a83ff87 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx
@@ -1,7 +1,7 @@
/* eslint-disable i18next/no-literal-string */
import { MenuItemOption, MenuOptionGroup } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { useCanvasSessionContext, zAutoSwitchMode } from 'features/controlLayers/components/SimpleSession/context';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { memo, useCallback } from 'react';
export const StagingAreaToolbarMenuAutoSwitch = memo(() => {
@@ -10,22 +10,18 @@ export const StagingAreaToolbarMenuAutoSwitch = memo(() => {
const onChange = useCallback(
(val: string | string[]) => {
- const newAutoSwitch = zAutoSwitchMode.parse(val);
- ctx.$autoSwitch.set(newAutoSwitch);
+ ctx.$autoSwitch.set(val === 'on');
},
[ctx.$autoSwitch]
);
return (
-
+
Off
-
- First Progress
-
-
- Completed
+
+ On
);
From 893f7a87441d7b4e93bb32a340bd780954ec73cd Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 16 Jun 2025 18:25:37 +1000
Subject: [PATCH 099/210] fix(ui): progress image fixes
---
.../SimpleSession/QueueItemPreviewFull.tsx | 19 ++--
.../SimpleSession/QueueItemPreviewMini.tsx | 19 ++--
.../SimpleSession/QueueItemStatusLabel.tsx | 28 ++++--
.../components/SimpleSession/context.tsx | 91 +++++++++++++------
4 files changed, 104 insertions(+), 53 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
index ba45a9f2ba..a39d8f64bb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
@@ -1,6 +1,10 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
-import { useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/context';
+import {
+ useCanvasSessionContext,
+ useOutputImageDTO,
+ useProgressData,
+} from 'features/controlLayers/components/SimpleSession/context';
import { ImageActions } from 'features/controlLayers/components/SimpleSession/ImageActions';
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
@@ -8,7 +12,7 @@ import { QueueItemProgressImage } from 'features/controlLayers/components/Simple
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
import { DndImage } from 'features/dnd/DndImage';
-import { memo, useCallback, useState } from 'react';
+import { memo } from 'react';
import type { S } from 'services/api/types';
type Props = {
@@ -27,17 +31,14 @@ const sx = {
} satisfies SystemStyleObject;
export const QueueItemPreviewFull = memo(({ item, number }: Props) => {
+ const ctx = useCanvasSessionContext();
const imageDTO = useOutputImageDTO(item);
- const [imageLoaded, setImageLoaded] = useState(false);
-
- const onLoad = useCallback(() => {
- setImageLoaded(true);
- }, []);
+ const { imageLoaded } = useProgressData(ctx.$progressData, item.item_id);
return (
-
- {imageDTO && }
+
+ {imageDTO && }
{!imageLoaded && }
{imageDTO && }
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 cd69cd1bdb..420abee061 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -1,13 +1,17 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
-import { useCanvasSessionContext, useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/context';
+import {
+ useCanvasSessionContext,
+ useOutputImageDTO,
+ useProgressData,
+} from 'features/controlLayers/components/SimpleSession/context';
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
import { DndImage } from 'features/dnd/DndImage';
-import { memo, useCallback, useState } from 'react';
+import { memo, useCallback } from 'react';
import type { S } from 'services/api/types';
const sx = {
@@ -35,7 +39,7 @@ type Props = {
export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) => {
const ctx = useCanvasSessionContext();
- const [imageLoaded, setImageLoaded] = useState(false);
+ const { imageLoaded } = useProgressData(ctx.$progressData, item.item_id);
const imageDTO = useOutputImageDTO(item);
const onClick = useCallback(() => {
@@ -43,15 +47,12 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
}, [ctx.$selectedItemId, item.item_id]);
const onLoad = useCallback(() => {
- setImageLoaded(true);
- if (ctx.$progressData.get()[item.item_id]) {
- ctx.$lastLoadedItemId.set(item.item_id);
- }
- }, [ctx.$lastLoadedItemId, ctx.$progressData, item.item_id]);
+ ctx.onImageLoad(item.item_id);
+ }, [ctx, item.item_id]);
return (
-
+
{imageDTO && }
{!imageLoaded && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
index 56cb4c92c0..13364cac00 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
@@ -1,27 +1,35 @@
/* eslint-disable i18next/no-literal-string */
import type { TextProps } from '@invoke-ai/ui-library';
import { Text } from '@invoke-ai/ui-library';
+import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { memo } from 'react';
import type { S } from 'services/api/types';
-type Props = { status: S['SessionQueueItem']['status'] } & TextProps;
+type Props = { item: S['SessionQueueItem'] } & TextProps;
-export const QueueItemStatusLabel = memo(({ status, ...rest }: Props) => {
- if (status === 'pending') {
+export const QueueItemStatusLabel = memo(({ item, ...rest }: Props) => {
+ const ctx = useCanvasSessionContext();
+ const { progressImage, imageLoaded } = useProgressData(ctx.$progressData, item.item_id);
+
+ if (progressImage || imageLoaded) {
+ return null;
+ }
+
+ if (item.status === 'pending') {
return (
Pending
);
}
- if (status === 'canceled') {
+ if (item.status === 'canceled') {
return (
Canceled
);
}
- if (status === 'failed') {
+ if (item.status === 'failed') {
return (
Failed
@@ -29,7 +37,7 @@ export const QueueItemStatusLabel = memo(({ status, ...rest }: Props) => {
);
}
- if (status === 'in_progress') {
+ if (item.status === 'in_progress') {
return (
In Progress
@@ -37,6 +45,14 @@ export const QueueItemStatusLabel = memo(({ status, ...rest }: Props) => {
);
}
+ if (item.status === 'completed') {
+ return (
+
+ Completed
+
+ );
+ }
+
return null;
});
QueueItemStatusLabel.displayName = 'QueueItemStatusLabel';
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 88f6f4f2d0..8d9a77b163 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -4,21 +4,22 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppStore } from 'app/store/nanostores/store';
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
import type { ProgressImage } from 'features/nodes/types/common';
-import type { Atom, WritableAtom } from 'nanostores';
-import { atom, computed, effect } from 'nanostores';
+import type { Atom, MapStore, StoreValue, WritableAtom } from 'nanostores';
+import { atom, computed, effect, map, subscribeKeys } from 'nanostores';
import type { PropsWithChildren } from 'react';
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { getImageDTOSafe } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
import type { ImageDTO, S } from 'services/api/types';
import { $socket } from 'services/events/stores';
-import { assert } from 'tsafe';
+import { assert, objectEntries } from 'tsafe';
export type ProgressData = {
itemId: number;
progressEvent: S['InvocationProgressEvent'] | null;
progressImage: ProgressImage | null;
imageDTO: ImageDTO | null;
+ imageLoaded: boolean;
};
const getInitialProgressData = (itemId: number): ProgressData => ({
@@ -26,17 +27,17 @@ const getInitialProgressData = (itemId: number): ProgressData => ({
progressEvent: null,
progressImage: null,
imageDTO: null,
+ imageLoaded: false,
});
-export const useProgressData = (
- $progressData: WritableAtom>,
- itemId: number
-): ProgressData => {
- const [value, setValue] = useState(() => {
- return $progressData.get()[itemId] ?? getInitialProgressData(itemId);
- });
+export const useProgressData = ($progressData: ProgressDataMap, itemId: number): ProgressData => {
+ const getInitialValue = useCallback(
+ () => $progressData.get()[itemId] ?? getInitialProgressData(itemId),
+ [$progressData, itemId]
+ );
+ const [value, setValue] = useState(getInitialValue);
useEffect(() => {
- const unsub = $progressData.subscribe((data) => {
+ const unsub = subscribeKeys($progressData, [itemId], (data) => {
const progressData = data[itemId];
if (!progressData) {
return;
@@ -51,7 +52,7 @@ export const useProgressData = (
return value;
};
-const setProgress = ($progressData: WritableAtom>, data: S['InvocationProgressEvent']) => {
+const setProgress = ($progressData: ProgressDataMap, data: S['InvocationProgressEvent']) => {
const progressData = $progressData.get();
const current = progressData[data.item_id];
if (current) {
@@ -72,27 +73,30 @@ const setProgress = ($progressData: WritableAtom>,
progressEvent: data,
progressImage: data.image ?? null,
imageDTO: null,
+ imageLoaded: false,
},
});
}
};
+type ProgressDataMap = MapStore>;
+
type CanvasSessionContextValue = {
session: { id: string; type: 'simple' | 'advanced' };
$items: Atom;
$itemCount: Atom;
$hasItems: Atom;
- $progressData: WritableAtom>;
+ $progressData: ProgressDataMap;
$selectedItemId: WritableAtom;
$selectedItem: Atom;
$selectedItemIndex: Atom;
$selectedItemOutputImageDTO: Atom;
$autoSwitch: WritableAtom;
- $lastLoadedItemId: WritableAtom;
selectNext: () => void;
selectPrev: () => void;
selectFirst: () => void;
selectLast: () => void;
+ onImageLoad: (itemId: number) => void;
};
const CanvasSessionContext = createContext(null);
@@ -112,6 +116,7 @@ export const CanvasSessionContextProvider = memo(
const store = useAppStore();
const socket = useStore($socket);
+ const $lastCompletedItemId = useState(() => atom(null))[0];
/**
* Manually-synced atom containing queue items for the current session. This is populated from the RTK Query cache
@@ -133,7 +138,7 @@ export const CanvasSessionContextProvider = memo(
/**
* An ephemeral store of progress events and images for all items in the current session.
*/
- const $progressData = useState(() => atom>({}))[0];
+ const $progressData = useState(() => map>({}))[0];
/**
* The currently selected queue item's ID, or null if one is not selected.
@@ -259,6 +264,27 @@ export const CanvasSessionContextProvider = memo(
$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) {
+ $selectedItemId.set(itemId);
+ $lastCompletedItemId.set(null);
+ }
+ },
+ [$lastCompletedItemId, $progressData, $selectedItemId]
+ );
+
// Set up socket listeners
useEffect(() => {
if (!socket) {
@@ -272,12 +298,23 @@ export const CanvasSessionContextProvider = memo(
setProgress($progressData, data);
};
+ const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
+ if (data.destination !== session.id) {
+ return;
+ }
+ if (data.status === 'completed') {
+ $lastCompletedItemId.set(data.item_id);
+ }
+ };
+
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);
};
- }, [$autoSwitch, $progressData, $selectedItemId, session.id, socket]);
+ }, [$autoSwitch, $lastCompletedItemId, $progressData, $selectedItemId, session.id, socket]);
// Set up state subscriptions and effects
useEffect(() => {
@@ -327,7 +364,11 @@ export const CanvasSessionContextProvider = memo(
const toDelete: number[] = [];
const toUpdate: ProgressData[] = [];
- for (const datum of Object.values(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);
@@ -376,21 +417,13 @@ export const CanvasSessionContextProvider = memo(
}
}
- if (toDelete.length === 0 && toUpdate.length === 0) {
- return;
- }
-
- const newProgressData = { ...progressData };
-
for (const itemId of toDelete) {
- delete newProgressData[itemId];
+ $progressData.setKey(itemId, undefined);
}
for (const datum of toUpdate) {
- newProgressData[datum.itemId] = datum;
+ $progressData.setKey(datum.itemId, datum);
}
-
- $progressData.set(newProgressData);
});
// We only want to auto-switch to completed queue items once their images have fully loaded to prevent flashes
@@ -440,19 +473,18 @@ export const CanvasSessionContextProvider = memo(
$autoSwitch,
$selectedItem,
$selectedItemIndex,
- $lastLoadedItemId,
$selectedItemOutputImageDTO,
$itemCount,
selectNext,
selectPrev,
selectFirst,
selectLast,
+ onImageLoad,
}),
[
$autoSwitch,
$items,
$hasItems,
- $lastLoadedItemId,
$progressData,
$selectedItem,
$selectedItemId,
@@ -464,6 +496,7 @@ export const CanvasSessionContextProvider = memo(
selectPrev,
selectFirst,
selectLast,
+ onImageLoad,
]
);
From c31cb0b106ec5022d0aa96f888c10fae19d19bb6 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 16 Jun 2025 18:27:29 +1000
Subject: [PATCH 100/210] fix(ui): invoke button tooltip on generate tab
---
.../components/InvokeButtonTooltip/InvokeButtonTooltip.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx
index 68992a84f4..04e9d30441 100644
--- a/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx
@@ -35,7 +35,7 @@ export const InvokeButtonTooltip = ({ prepend, children, ...rest }: PropsWithChi
const TooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => {
const activeTab = useAppSelector(selectActiveTab);
- if (activeTab === 'canvas') {
+ if (activeTab === 'canvas' || activeTab === 'generate') {
return ;
}
From 161624c722f95cc7e5121d7772278eb6d507ef6e Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 16 Jun 2025 18:59:01 +1000
Subject: [PATCH 101/210] feat(ui): rework simple session initial state
---
.../components/SimpleSession/InitialState.tsx | 52 +++++++++----------
.../InitialStateAddAStyleReference.tsx | 41 +++++++++------
.../InitialStateButtonGridItem.tsx | 14 +++--
.../InitialStateGenerateFromText.tsx | 10 ++--
.../InitialStateMainModelPicker.tsx | 48 +++++++++++++++++
5 files changed, 109 insertions(+), 56 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
index 2c8f92af6f..d8cf7aa2c6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -1,12 +1,10 @@
/* eslint-disable i18next/no-literal-string */
-import { Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
+import { Alert, Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
-import { InitialStateEditImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateEditImageCard';
import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
-import { InitialStateUseALayoutImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard';
-import { toast } from 'features/toast/toast';
+import { InitialStateMainModelPicker } from 'features/controlLayers/components/SimpleSession/InitialStateMainModelPicker';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
@@ -14,14 +12,6 @@ export const InitialState = memo(() => {
const dispatch = useAppDispatch();
const newCanvasSession = useCallback(() => {
dispatch(setActiveTab('canvas'));
- toast({
- title: 'Switched to Canvas',
- description: 'You are in advanced mode yadda yadda.',
- status: 'info',
- position: 'top',
- // isClosable: false,
- duration: 5000,
- });
}, [dispatch]);
return (
@@ -31,24 +21,30 @@ export const InitialState = memo(() => {
- Choose a starting method.
-
- Drag an image onto a card or click the upload icon.
-
-
-
+ Get started with Invoke.
+
+
+
+
+
+ Want to learn what prompts work best for each model?{' '}
+
+ Check our our Model Guide.
+
+
+
+
-
-
-
-
-
- or{' '}
-
- start from a blank canvas.
-
-
+
+
+ Looking to get more control, edit, and iterate on your images?
+
+
+ Navigate to Canvas for more capabilities.
+
+
+
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
index 0b97e5637e..9e23714002 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
@@ -4,34 +4,43 @@ import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
-import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
+import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
+import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
+import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
+import { addGlobalReferenceImageDndTarget, newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
-import { newCanvasFromImage } from 'features/imageActions/actions';
-import { memo, useCallback } from 'react';
+import { memo, useMemo } from 'react';
import { PiUploadBold, PiUserCircleGearBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
-const NEW_CANVAS_OPTIONS = { type: 'reference_image' } as const;
-
-const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
+const dndTargetData = addGlobalReferenceImageDndTarget.getData();
export const InitialStateAddAStyleReference = memo(() => {
- const { getState, dispatch } = useAppStore();
+ const { dispatch, getState } = useAppStore();
- const onUpload = useCallback(
- (imageDTO: ImageDTO) => {
- newCanvasFromImage({ imageDTO, getState, dispatch, ...NEW_CANVAS_OPTIONS });
- },
+ const uploadOptions = useMemo(
+ () =>
+ ({
+ onUpload: (imageDTO: ImageDTO) => {
+ const config = getDefaultRefImageConfig(getState);
+ config.image = imageDTOToImageWithDims(imageDTO);
+ dispatch(refImageAdded({ overrides: { config } }));
+ },
+ allowMultiple: false,
+ }) as const,
[dispatch, getState]
);
- const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
+
+ const uploadApi = useImageUploadButton(uploadOptions);
return (
-
+
- Add a Style Reference
- Add an image to transfer its look.
-
+
+ Add a Style Reference
+ Add an image to transfer its look.
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
index b53e059843..c7b85239d9 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
@@ -1,21 +1,19 @@
-import type { GridItemProps } from '@invoke-ai/ui-library';
-import { Button, forwardRef, GridItem } from '@invoke-ai/ui-library';
+import type { ButtonProps } from '@invoke-ai/ui-library';
+import { Button, forwardRef } from '@invoke-ai/ui-library';
import { memo } from 'react';
export const InitialStateButtonGridItem = memo(
- forwardRef(({ children, ...rest }: GridItemProps, ref) => {
+ forwardRef(({ children, ...rest }: ButtonProps, ref) => {
return (
-
{children}
-
+
);
})
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
index 67048c383f..d51b06cd5a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
@@ -15,11 +15,13 @@ const focusOnPrompt = () => {
export const InitialStateGenerateFromText = memo(() => {
return (
-
+
- Generate from Text
- Enter a prompt and Invoke.
-
+
+ Generate from Text
+ Enter a prompt and Invoke.
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx
new file mode 100644
index 0000000000..421b275cd2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx
@@ -0,0 +1,48 @@
+/* eslint-disable i18next/no-literal-string */
+import { Flex, FormControl, FormLabel, Icon } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
+import { ModelPicker } from 'features/parameters/components/ModelPicker';
+import { modelSelected } from 'features/parameters/store/actions';
+import { memo, useCallback, useMemo } from 'react';
+import { MdMoneyOff } from 'react-icons/md';
+import { useMainModels } from 'services/api/hooks/modelsByType';
+import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
+import { type AnyModelConfig, isCheckpointMainModelConfig } from 'services/api/types';
+
+export const InitialStateMainModelPicker = memo(() => {
+ const dispatch = useAppDispatch();
+ const [modelConfigs] = useMainModels();
+ const selectedModelConfig = useSelectedModelConfig();
+ const onChange = useCallback(
+ (modelConfig: AnyModelConfig) => {
+ dispatch(modelSelected(modelConfig));
+ },
+ [dispatch]
+ );
+
+ const isFluxDevSelected = useMemo(
+ () =>
+ selectedModelConfig &&
+ isCheckpointMainModelConfig(selectedModelConfig) &&
+ selectedModelConfig.config_path === 'flux-dev',
+ [selectedModelConfig]
+ );
+
+ return (
+
+
+ Select your Model
+
+ {isFluxDevSelected && (
+
+
+
+
+
+ )}
+
+
+ );
+});
+InitialStateMainModelPicker.displayName = 'InitialStateMainModelPicker';
From 5ac5115269cd81a66862a51ef487b37a79ef2fc1 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 16 Jun 2025 19:02:01 +1000
Subject: [PATCH 102/210] chore(ui): lint
---
invokeai/frontend/web/.eslintrc.js | 3 ++-
.../AdvancedSession/AdvancedSession.tsx | 1 -
.../components/CanvasAddEntityButtons.tsx | 2 +-
...nvasContextMenuSelectedEntityMenuItems.tsx | 2 +-
.../EntityListGlobalActionBarAddLayerMenu.tsx | 2 +-
.../components/CanvasMainPanelContent.tsx | 23 -------------------
.../components/RefImage/RefImageList.tsx | 1 -
...onalGuidanceAddPromptsIPAdapterButtons.tsx | 2 +-
.../RegionalGuidanceIPAdapterSettings.tsx | 4 ++--
...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 2 +-
.../components/SimpleSession/ImageActions.tsx | 1 -
.../components/SimpleSession/InitialState.tsx | 2 --
.../InitialStateAddAStyleReference.tsx | 2 --
.../InitialStateEditImageCard.tsx | 2 --
.../InitialStateGenerateFromText.tsx | 2 --
.../InitialStateMainModelPicker.tsx | 1 -
.../InitialStateUseALayoutImageCard.tsx | 2 --
.../QueueItemProgressMessage.tsx | 1 -
.../SimpleSession/QueueItemStatusLabel.tsx | 1 -
.../components/SimpleSession/StagingArea.tsx | 2 --
.../SimpleSession/StagingAreaContent.tsx | 1 -
.../SimpleSession/StagingAreaHeader.tsx | 1 -
.../SimpleSession/StagingAreaItemsList.tsx | 1 -
.../SimpleSession/StagingAreaNoItems.tsx | 2 --
.../SimpleSession/StagingAreaSelectedItem.tsx | 1 -
.../components/SimpleSession/context.tsx | 2 +-
.../StagingAreaToolbarMenuAutoSwitch.tsx | 1 -
.../components/StartOverButton.tsx | 4 ----
.../components/Toolbar/CanvasToolbar.tsx | 1 -
.../components/common/CanvasEntityHeader.tsx | 2 +-
.../useNextRenderableEntityIdentifier.ts | 4 +---
.../CanvasEntity/CanvasEntityAdapterBase.ts | 5 +---
.../CanvasEntity/CanvasEntityFilterer.ts | 2 +-
.../konva/CanvasSegmentAnythingModule.ts | 2 +-
.../konva/CanvasStagingAreaModule.ts | 9 +++-----
.../util/graph/generation/addIPAdapters.ts | 2 +-
.../Core/NegativePromptToggleButton.tsx | 2 --
.../components/Core/ParamNegativePrompt.tsx | 5 +---
.../components/InvokeAILogoComponent.tsx | 1 -
.../features/ui/components/VerticalNavBar.tsx | 9 +++++++-
40 files changed, 28 insertions(+), 87 deletions(-)
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
diff --git a/invokeai/frontend/web/.eslintrc.js b/invokeai/frontend/web/.eslintrc.js
index ed08009e66..3e6498af4c 100644
--- a/invokeai/frontend/web/.eslintrc.js
+++ b/invokeai/frontend/web/.eslintrc.js
@@ -9,7 +9,8 @@ module.exports = {
// https://github.com/qdanik/eslint-plugin-path
'path/no-relative-imports': ['error', { maxDepth: 0 }],
// https://github.com/edvardchen/eslint-plugin-i18next/blob/HEAD/docs/rules/no-literal-string.md
- 'i18next/no-literal-string': 'error',
+ // TODO: ENABLE THIS RULE BEFORE v6.0.0
+ // 'i18next/no-literal-string': 'error',
// https://eslint.org/docs/latest/rules/no-console
'no-console': 'error',
// https://eslint.org/docs/latest/rules/no-promise-executor-return
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
index 88fc045919..1628b25697 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
index d3475a385c..88b5b6d37e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
@@ -3,9 +3,9 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
import {
useAddControlLayer,
useAddInpaintMask,
+ useAddNewRegionalGuidanceWithARefImage,
useAddRasterLayer,
useAddRegionalGuidance,
- useAddNewRegionalGuidanceWithARefImage,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled';
import { memo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx
index 8d150e0bb7..049853e3f2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx
@@ -2,8 +2,8 @@ import { MenuGroup } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
-import { IPAdapterMenuItems } from 'features/controlLayers/components/RefImage/IPAdapterMenuItems';
import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems';
+import { IPAdapterMenuItems } from 'features/controlLayers/components/RefImage/IPAdapterMenuItems';
import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx
index 04994f200e..4a7e6fa375 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx
@@ -2,9 +2,9 @@ import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@in
import {
useAddControlLayer,
useAddInpaintMask,
+ useAddNewRegionalGuidanceWithARefImage,
useAddRasterLayer,
useAddRegionalGuidance,
- useAddNewRegionalGuidanceWithARefImage,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
deleted file mode 100644
index b42a2f716a..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useAppSelector } from 'app/store/storeHooks';
-import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
-import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
-import { selectCanvasSessionId, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { memo } from 'react';
-import type { Equals } from 'tsafe';
-import { assert } from 'tsafe';
-
-export const CanvasMainPanelContent = memo(() => {
- const type = useAppSelector(selectCanvasSessionType);
- const id = useAppSelector(selectCanvasSessionId);
-
- if (type === 'simple') {
- return ;
- }
-
- if (type === 'advanced') {
- return ;
- }
-
- assert>(false, 'Unexpected session type');
-});
-CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
index d7efcb3d0b..76b1624b11 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import type { FlexProps } from '@invoke-ai/ui-library';
import { Button, Flex } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx
index cb88c281f7..2ffd494e16 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx
@@ -3,9 +3,9 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
buildSelectValidRegionalGuidanceActions,
- useAddRefImageToExistingRegionalGuidance,
useAddNegativePromptToExistingRegionalGuidance,
useAddPositivePromptToExistingRegionalGuidance,
+ useAddRefImageToExistingRegionalGuidance,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
index 70445b484b..c49a5a1658 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
@@ -2,11 +2,11 @@ import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
-import { IPAdapterCLIPVisionModel } from 'features/controlLayers/components/common/IPAdapterCLIPVisionModel';
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/common/FLUXReduxImageInfluence';
+import { IPAdapterCLIPVisionModel } from 'features/controlLayers/components/common/IPAdapterCLIPVisionModel';
import { Weight } from 'features/controlLayers/components/common/Weight';
-import { RefImageImage } from 'features/controlLayers/components/RefImage/RefImageImage';
import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAdapterMethod';
+import { RefImageImage } from 'features/controlLayers/components/RefImage/RefImageImage';
import { RegionalGuidanceIPAdapterSettingsEmptyState } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState';
import { RegionalReferenceImageModel } from 'features/controlLayers/components/RegionalGuidance/RegionalReferenceImageModel';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx
index de80b387fc..c8c43089b8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx
@@ -3,9 +3,9 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
buildSelectValidRegionalGuidanceActions,
- useAddRefImageToExistingRegionalGuidance,
useAddNegativePromptToExistingRegionalGuidance,
useAddPositivePromptToExistingRegionalGuidance,
+ useAddRefImageToExistingRegionalGuidance,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { memo, useMemo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx
index 7e877afce4..2bfeec992e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/ImageActions.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import type { ButtonGroupProps } from '@invoke-ai/ui-library';
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
index d8cf7aa2c6..6229654c6c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
-
import { Alert, Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
index 9e23714002..9a1c53a0a4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
-
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx
index b6448f41d0..c5d566fb82 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateEditImageCard.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
-
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
index d51b06cd5a..b684ba60ec 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
-
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
import { memo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx
index 421b275cd2..ac875b8232 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import { Flex, FormControl, FormLabel, Icon } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
index 955ace441f..93e2e3b03d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
-
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx
index fc60a93acf..2699383666 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import type { TextProps } from '@invoke-ai/ui-library';
import { Text } from '@invoke-ai/ui-library';
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
index 13364cac00..3cf9194b9d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import type { TextProps } from '@invoke-ai/ui-library';
import { Text } from '@invoke-ai/ui-library';
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingArea.tsx
index d00f6a4ec8..dbaf22649a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingArea.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingArea.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
-
import { Divider, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
index 0e5c230204..741619ee95 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaContent.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import { Divider, Flex } from '@invoke-ai/ui-library';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaSelectedItem } from 'features/controlLayers/components/SimpleSession/StagingAreaSelectedItem';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
index 480b9f1659..64d59855fd 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import { Flex, Heading, Spacer } from '@invoke-ai/ui-library';
import { memo } from 'react';
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 36c6d21ec8..519898d2cd 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaNoItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaNoItems.tsx
index 7c85c52c7a..087650c74c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaNoItems.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaNoItems.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
-
import { Flex, Text } from '@invoke-ai/ui-library';
import { memo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaSelectedItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaSelectedItem.tsx
index 67f8143529..49dd40a84c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaSelectedItem.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaSelectedItem.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import { Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
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 8d9a77b163..3f55bfe252 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -79,7 +79,7 @@ const setProgress = ($progressData: ProgressDataMap, data: S['InvocationProgress
}
};
-type ProgressDataMap = MapStore>;
+export type ProgressDataMap = MapStore>;
type CanvasSessionContextValue = {
session: { id: string; type: 'simple' | 'advanced' };
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx
index ff8a83ff87..b67eefa9cf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import { MenuItemOption, MenuOptionGroup } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StartOverButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StartOverButton.tsx
index 3557f91590..ae3ef6c84e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StartOverButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StartOverButton.tsx
@@ -1,12 +1,8 @@
-/* eslint-disable i18next/no-literal-string */
import { Button } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
import { $simpleId } from 'features/ui/components/MainPanelContent';
import { memo, useCallback } from 'react';
export const StartOverButton = memo(() => {
- const dispatch = useAppDispatch();
-
const startOver = useCallback(() => {
// dispatch(canvasSessionTypeChanged({ type: 'simple' }));
$simpleId.set(null);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
index 91b02351c3..a233a14f13 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import { Divider, Flex, Heading } from '@invoke-ai/ui-library';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx
index 8c8e07cdc9..86d76fa58e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx
@@ -2,8 +2,8 @@ import type { FlexProps } from '@invoke-ai/ui-library';
import { ContextMenu, Flex, MenuList } from '@invoke-ai/ui-library';
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
-import { IPAdapterMenuItems } from 'features/controlLayers/components/RefImage/IPAdapterMenuItems';
import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems';
+import { IPAdapterMenuItems } from 'features/controlLayers/components/RefImage/IPAdapterMenuItems';
import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { memo, useCallback } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts
index f6328f20b7..6552f4f2e5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts
@@ -5,9 +5,7 @@ import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
-export const useEntityIdentifierBelowThisOne = (
- entityIdentifier: T
-): T | null => {
+export const useEntityIdentifierBelowThisOne = (entityIdentifier: T): T | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
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 c89f9e376a..9ca0e7772a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts
@@ -41,10 +41,7 @@ import stableHash from 'stable-hash';
import { assert } from 'tsafe';
import type { Jsonifiable, JsonObject } from 'type-fest';
-export abstract class CanvasEntityAdapterBase<
- T extends CanvasEntityState,
- U extends string,
-> extends CanvasModuleBase {
+export abstract class CanvasEntityAdapterBase extends CanvasModuleBase {
readonly type: U;
readonly id: string;
readonly path: string[];
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer.ts
index e33eda59c4..38c7e664e1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer.ts
@@ -9,7 +9,7 @@ import { addCoords, getKonvaNodeDebugAttrs, getPrefixedId } from 'features/contr
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import type { FilterConfig } from 'features/controlLayers/store/filters';
import { getFilterForModel, IMAGE_FILTERS } from 'features/controlLayers/store/filters';
-import type { CanvasImageState, CanvasEntityType } from 'features/controlLayers/store/types';
+import type { CanvasEntityType, CanvasImageState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { toast } from 'features/toast/toast';
import Konva from 'konva';
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasSegmentAnythingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasSegmentAnythingModule.ts
index d8a0e2154c..593bd235c4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasSegmentAnythingModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasSegmentAnythingModule.ts
@@ -15,8 +15,8 @@ import {
} from 'features/controlLayers/konva/util';
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import type {
- CanvasImageState,
CanvasEntityType,
+ CanvasImageState,
Coordinate,
RgbaColor,
SAMPointLabel,
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
index 8262c70886..709fb2cbe4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts
@@ -1,5 +1,5 @@
import { Mutex } from 'async-mutex';
-import type { ProgressData } from 'features/controlLayers/components/SimpleSession/context';
+import type { ProgressDataMap } from 'features/controlLayers/components/SimpleSession/context';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
@@ -7,7 +7,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { CanvasImageState } from 'features/controlLayers/store/types';
import Konva from 'konva';
-import type { Atom, WritableAtom } from 'nanostores';
+import type { Atom } from 'nanostores';
import { atom, effect } from 'nanostores';
import type { Logger } from 'roarr';
@@ -135,10 +135,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
};
- connectToSession = (
- $selectedItemId: Atom,
- $progressData: WritableAtom>
- ) =>
+ connectToSession = ($selectedItemId: Atom, $progressData: ProgressDataMap) =>
effect([$selectedItemId, $progressData], (selectedItemId, progressData) => {
if (!selectedItemId) {
this.$imageSrc.set(null);
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts
index 49e94aa03f..ab5fb74465 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts
@@ -1,4 +1,4 @@
-import { type IPAdapterConfig, isIPAdapterConfig,type RefImageState } from 'features/controlLayers/store/types';
+import { type IPAdapterConfig, isIPAdapterConfig, type RefImageState } from 'features/controlLayers/store/types';
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import type { Invocation, MainModelConfig } from 'services/api/types';
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx
index 7283c76b25..9733abf712 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx
@@ -2,14 +2,12 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { negativePromptChanged, selectHasNegativePrompt } from 'features/controlLayers/store/paramsSlice';
import { memo, useCallback, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
import { PiPlusMinusBold } from 'react-icons/pi';
export const NegativePromptToggleButton = memo(() => {
const hasNegativePrompt = useAppSelector(selectHasNegativePrompt);
const dispatch = useAppDispatch();
- const { t } = useTranslation();
const onClick = useCallback(() => {
if (hasNegativePrompt) {
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx
index 94faaaa0cb..f18d1238ea 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx
@@ -1,10 +1,7 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
-import {
- negativePromptChanged,
- selectNegativePromptWithFallback,
-} from 'features/controlLayers/store/paramsSlice';
+import { negativePromptChanged, selectNegativePromptWithFallback } from 'features/controlLayers/store/paramsSlice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
diff --git a/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx b/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx
index 91d4382a52..f5817fc658 100644
--- a/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx
+++ b/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable i18next/no-literal-string */
import { Image, Text, Tooltip } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $logo } from 'app/store/nanostores/logo';
diff --git a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
index 29cde774d0..c2eacf45fa 100644
--- a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
@@ -8,7 +8,14 @@ import { VideosModalButton } from 'features/system/components/VideosModal/Videos
import { TabMountGate } from 'features/ui/components/TabMountGate';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiBoundingBoxBold, PiCubeBold, PiFlowArrowBold, PiFrameCornersBold, PiQueueBold, PiTextAaBold } from 'react-icons/pi';
+import {
+ PiBoundingBoxBold,
+ PiCubeBold,
+ PiFlowArrowBold,
+ PiFrameCornersBold,
+ PiQueueBold,
+ PiTextAaBold,
+} from 'react-icons/pi';
import { Notifications } from './Notifications';
import { TabButton } from './TabButton';
From 2f9ea9189694d085ef9240f0f6e295521069c342 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 17 Jun 2025 11:32:29 +1000
Subject: [PATCH 103/210] feat(ui): tweak splash screen layout
---
.../components/SimpleSession/InitialState.tsx | 12 +++++------
.../InitialStateMainModelPicker.tsx | 20 +++++++++----------
2 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
index 6229654c6c..dc968e5a51 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -13,17 +13,17 @@ export const InitialState = memo(() => {
}, [dispatch]);
return (
-
-
+
+
Get Started
-
+
Get started with Invoke.
-
-
+
+
-
+
Want to learn what prompts work best for each model?{' '}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx
index ac875b8232..c6e13357cf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateMainModelPicker.tsx
@@ -30,16 +30,16 @@ export const InitialStateMainModelPicker = memo(() => {
return (
-
- Select your Model
-
- {isFluxDevSelected && (
-
-
-
-
-
- )}
+
+ Select your Model{' '}
+ {isFluxDevSelected && (
+
+
+
+
+
+ )}
+
);
From 32aa3e6d480a637cfc4568ecd81cfe149557a47e Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 17 Jun 2025 12:09:27 +1000
Subject: [PATCH 104/210] feat(ui): switch tab on drag over tab button
---
.../common/hooks/useCallbackOnDragEnter.ts | 29 ++++++++++
.../src/common/hooks/useTimeoutCallback.ts | 21 +++++++
.../src/features/ui/components/TabButton.tsx | 56 ++++++++++---------
3 files changed, 79 insertions(+), 27 deletions(-)
create mode 100644 invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts
create mode 100644 invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts
diff --git a/invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts b/invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts
new file mode 100644
index 0000000000..f0a1c2ed74
--- /dev/null
+++ b/invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts
@@ -0,0 +1,29 @@
+import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
+import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
+import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
+import { useTimeoutCallback } from 'common/hooks/useTimeoutCallback';
+import type { RefObject } from 'react';
+import { useEffect } from 'react';
+
+export const useCallbackOnDragEnter = (cb: () => void, ref: RefObject, delay = 300) => {
+ const [run, cancel] = useTimeoutCallback(cb, delay);
+
+ useEffect(() => {
+ const element = ref.current;
+ if (!element) {
+ return;
+ }
+ return combine(
+ dropTargetForElements({
+ element,
+ onDragEnter: run,
+ onDragLeave: cancel,
+ }),
+ dropTargetForExternal({
+ element,
+ onDragEnter: run,
+ onDragLeave: cancel,
+ })
+ );
+ }, [cancel, ref, run]);
+};
diff --git a/invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts b/invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts
new file mode 100644
index 0000000000..0406d3eae5
--- /dev/null
+++ b/invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts
@@ -0,0 +1,21 @@
+import { useCallback, useMemo, useRef } from 'react';
+
+export const useTimeoutCallback = (callback: () => void, delay: number, onCancel?: () => void) => {
+ const timeoutRef = useRef(null);
+ const cancel = useCallback(() => {
+ if (timeoutRef.current !== null) {
+ window.clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ onCancel?.();
+ }
+ }, [onCancel]);
+ const callWithTimeout = useCallback(() => {
+ cancel();
+ timeoutRef.current = window.setTimeout(() => {
+ callback();
+ timeoutRef.current = null;
+ }, delay);
+ }, [callback, cancel, delay]);
+ const api = useMemo(() => [callWithTimeout, cancel] as const, [callWithTimeout, cancel]);
+ return api;
+};
diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
index fb29cd6744..8974a9c326 100644
--- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
@@ -1,10 +1,12 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { setActiveTab } from 'features/ui/store/uiSlice';
import type { TabName } from 'features/ui/store/uiTypes';
-import { forwardRef, memo, type ReactElement, useCallback } from 'react';
+import type { ReactElement } from 'react';
+import { memo, useCallback, useRef } from 'react';
const sx: SystemStyleObject = {
'&[data-selected=true]': {
@@ -12,32 +14,32 @@ const sx: SystemStyleObject = {
},
};
-export const TabButton = memo(
- forwardRef(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }, ref) => {
- const dispatch = useAppDispatch();
- const activeTabName = useAppSelector(selectActiveTab);
- const onClick = useCallback(() => {
- dispatch(setActiveTab(tab));
- }, [dispatch, tab]);
+export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }) => {
+ const dispatch = useAppDispatch();
+ const ref = useRef(null);
+ const activeTabName = useAppSelector(selectActiveTab);
+ const selectTab = useCallback(() => {
+ dispatch(setActiveTab(tab));
+ }, [dispatch, tab]);
+ useCallbackOnDragEnter(selectTab, ref, 300);
- return (
-
-
-
- );
- })
-);
+ return (
+
+
+
+ );
+});
TabButton.displayName = 'TabButton';
From a7e45731eca7285bebcab0258614b8e8c32c38d1 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 17 Jun 2025 12:17:19 +1000
Subject: [PATCH 105/210] feat(ui): ref images feel more like buttons
---
.../components/RefImage/RefImage.tsx | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
index f184f204a6..3a21e19bf8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
@@ -1,3 +1,4 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
import {
Divider,
Flex,
@@ -79,6 +80,18 @@ export const RefImage = memo(() => {
});
RefImage.displayName = 'RefImage';
+const imageSx: SystemStyleObject = {
+ opacity: 0.7,
+ transitionProperty: 'opacity',
+ transitionDuration: 'normal',
+ _hover: {
+ opacity: 1,
+ },
+ '&[data-is-open="true"]': {
+ opacity: 1,
+ },
+};
+
const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
const id = useRefImageIdContext();
const entity = useRefImageEntity(id);
@@ -123,8 +136,8 @@ const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
borderRadius="base"
onClick={disclosure.toggle}
flexShrink={0}
- // sx={imageSx}
- // data-is-open={disclosure.isOpen}
+ sx={imageSx}
+ data-is-open={disclosure.isOpen}
/>
);
From a5baf0c102175403694a1d19fb019f8df1c0c8ec Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 17 Jun 2025 12:20:08 +1000
Subject: [PATCH 106/210] fix(ui): overflow on ref image model
---
.../controlLayers/components/RefImage/RefImageModel.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageModel.tsx
index 8e6e0749aa..650cda390b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageModel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageModel.tsx
@@ -47,7 +47,7 @@ export const RefImageModel = memo(({ modelKey, onChangeModel }: Props) => {
return (
-
+
Date: Tue, 17 Jun 2025 12:57:21 +1000
Subject: [PATCH 107/210] feat(ui): represent IP adapter weight in ref image
thumbnail
---
.../components/RefImage/RefImage.tsx | 101 +++++++++++++++---
1 file changed, 87 insertions(+), 14 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
index 3a21e19bf8..2eeb2b908a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
@@ -11,6 +11,7 @@ import {
PopoverContent,
Portal,
Skeleton,
+ Text,
} from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { POPPER_MODIFIERS } from 'common/components/InformationalPopover/constants';
@@ -21,7 +22,9 @@ import { RefImageHeader } from 'features/controlLayers/components/RefImage/RefIm
import { RefImageSettings } from 'features/controlLayers/components/RefImage/RefImageSettings';
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
-import { memo, useCallback, useRef } from 'react';
+import { isIPAdapterConfig } from 'features/controlLayers/store/types';
+import { round } from 'lodash-es';
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { PiImageBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
@@ -80,10 +83,11 @@ export const RefImage = memo(() => {
});
RefImage.displayName = 'RefImage';
-const imageSx: SystemStyleObject = {
+const baseSx: SystemStyleObject = {
opacity: 0.7,
transitionProperty: 'opacity',
transitionDuration: 'normal',
+ position: 'relative',
_hover: {
opacity: 1,
},
@@ -92,11 +96,58 @@ const imageSx: SystemStyleObject = {
},
};
+const weightDisplaySx: SystemStyleObject = {
+ pointerEvents: 'none',
+ transitionProperty: 'opacity',
+ transitionDuration: 'normal',
+ opacity: 0,
+ '&[data-visible="true"]': {
+ opacity: 1,
+ },
+};
+
+const getImageSxWithWeight = (weight: number): SystemStyleObject => {
+ const fillPercentage = Math.max(0, Math.min(100, weight * 100));
+
+ return {
+ ...baseSx,
+ _after: {
+ content: '""',
+ position: 'absolute',
+ inset: 0,
+ background: `linear-gradient(to top, transparent ${fillPercentage}%, rgba(0, 0, 0, 0.8) ${fillPercentage}%)`,
+ pointerEvents: 'none',
+ borderRadius: 'base',
+ },
+ };
+};
+
const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
const id = useRefImageIdContext();
const entity = useRefImageEntity(id);
+ const [showWeightDisplay, setShowWeightDisplay] = useState(false);
const { data: imageDTO } = useGetImageDTOQuery(entity.config.image?.image_name ?? skipToken);
+ const sx = useMemo(() => {
+ if (!isIPAdapterConfig(entity.config)) {
+ return baseSx;
+ }
+ return getImageSxWithWeight(entity.config.weight);
+ }, [entity.config]);
+
+ useEffect(() => {
+ if (!isIPAdapterConfig(entity.config)) {
+ return;
+ }
+ setShowWeightDisplay(true);
+ const timeout = window.setTimeout(() => {
+ setShowWeightDisplay(false);
+ }, 1000);
+ return () => {
+ window.clearTimeout(timeout);
+ };
+ }, [entity.config]);
+
if (!entity.config.image) {
return (
@@ -120,25 +171,47 @@ const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
}
return (
- }
maxW="full"
maxH="full"
- borderRadius="base"
- onClick={disclosure.toggle}
flexShrink={0}
- sx={imageSx}
+ sx={sx}
data-is-open={disclosure.isOpen}
- />
+ id={getRefImagePopoverTriggerId(id)}
+ role="button"
+ onClick={disclosure.toggle}
+ cursor="pointer"
+ >
+ }
+ maxW="full"
+ maxH="full"
+ borderRadius="base"
+ />
+
+
+ {`${round(entity.config.weight * 100, 2)}%`}
+
+
+
);
});
From 18775e8b6774206ff0147888473d916be7f816d6 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 17 Jun 2025 12:59:45 +1000
Subject: [PATCH 108/210] fix(ui): only show weight for IP adapters
---
.../components/RefImage/RefImage.tsx | 30 ++++++++++---------
1 file changed, 16 insertions(+), 14 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
index 2eeb2b908a..2f66b80390 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
@@ -197,20 +197,22 @@ const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
maxH="full"
borderRadius="base"
/>
-
-
- {`${round(entity.config.weight * 100, 2)}%`}
-
-
+ {isIPAdapterConfig(entity.config) && (
+
+
+ {`${round(entity.config.weight * 100, 2)}%`}
+
+
+ )}
);
From d5c238e7c2a0962affeee4b90628dfa8d1600b77 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 17 Jun 2025 13:04:31 +1000
Subject: [PATCH 109/210] feat(ui): port UI slice to zod
---
.../web/src/features/ui/store/uiSlice.ts | 62 ++++++++++-------
.../web/src/features/ui/store/uiTypes.ts | 69 ++++++++-----------
2 files changed, 66 insertions(+), 65 deletions(-)
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
index 92af3b2ec8..c96be420bb 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
@@ -3,55 +3,67 @@ import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { canvasReset } from 'features/controlLayers/store/actions';
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import type { Dimensions } from 'features/controlLayers/store/types';
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
import { atom } from 'nanostores';
-import type { CanvasRightPanelTabName, TabName, UIState } from './uiTypes';
-
-const initialUIState: UIState = {
- _version: 3,
- activeTab: 'canvas',
- activeTabCanvasRightPanel: 'gallery',
- shouldShowImageDetails: false,
- shouldShowProgressInViewer: true,
- accordions: {},
- expanders: {},
- textAreaSizes: {},
- shouldShowNotificationV2: true,
-};
+import type { TabName, UIState } from './uiTypes';
+import { getInitialUIState } from './uiTypes';
export const uiSlice = createSlice({
name: 'ui',
- initialState: initialUIState,
+ initialState: getInitialUIState(),
reducers: {
- setActiveTab: (state, action: PayloadAction) => {
+ setActiveTab: (state, action: PayloadAction) => {
state.activeTab = action.payload;
},
- activeTabCanvasRightPanelChanged: (state, action: PayloadAction) => {
+ activeTabCanvasRightPanelChanged: (state, action: PayloadAction) => {
state.activeTabCanvasRightPanel = action.payload;
},
- setShouldShowImageDetails: (state, action: PayloadAction) => {
+ setShouldShowImageDetails: (state, action: PayloadAction) => {
state.shouldShowImageDetails = action.payload;
},
- setShouldShowProgressInViewer: (state, action: PayloadAction) => {
+ setShouldShowProgressInViewer: (state, action: PayloadAction) => {
state.shouldShowProgressInViewer = action.payload;
},
- accordionStateChanged: (state, action: PayloadAction<{ id: string; isOpen: boolean }>) => {
+ accordionStateChanged: (
+ state,
+ action: PayloadAction<{
+ id: keyof UIState['accordions'];
+ isOpen: UIState['accordions'][keyof UIState['accordions']];
+ }>
+ ) => {
const { id, isOpen } = action.payload;
state.accordions[id] = isOpen;
},
- expanderStateChanged: (state, action: PayloadAction<{ id: string; isOpen: boolean }>) => {
+ expanderStateChanged: (
+ state,
+ action: PayloadAction<{
+ id: keyof UIState['expanders'];
+ isOpen: UIState['expanders'][keyof UIState['expanders']];
+ }>
+ ) => {
const { id, isOpen } = action.payload;
state.expanders[id] = isOpen;
},
- textAreaSizesStateChanged: (state, action: PayloadAction<{ id: string; size: Partial }>) => {
+ textAreaSizesStateChanged: (
+ state,
+ action: PayloadAction<{
+ id: keyof UIState['textAreaSizes'];
+ size: UIState['textAreaSizes'][keyof UIState['textAreaSizes']];
+ }>
+ ) => {
const { id, size } = action.payload;
state.textAreaSizes[id] = size;
},
- shouldShowNotificationChanged: (state, action: PayloadAction) => {
+ shouldShowNotificationChanged: (state, action: PayloadAction) => {
state.shouldShowNotificationV2 = action.payload;
},
+ showGenerateTabSplashScreenChanged: (state, action: PayloadAction) => {
+ state.showGenerateTabSplashScreen = action.payload;
+ },
+ showCanvasTabSplashScreenChanged: (state, action: PayloadAction) => {
+ state.showCanvasTabSplashScreen = action.payload;
+ },
},
extraReducers(builder) {
builder.addCase(workflowLoaded, (state) => {
@@ -81,6 +93,8 @@ export const {
expanderStateChanged,
shouldShowNotificationChanged,
textAreaSizesStateChanged,
+ showGenerateTabSplashScreenChanged,
+ showCanvasTabSplashScreenChanged,
} = uiSlice.actions;
export const selectUiSlice = (state: RootState) => state.ui;
@@ -103,7 +117,7 @@ const migrateUIState = (state: any): any => {
export const uiPersistConfig: PersistConfig = {
name: uiSlice.name,
- initialState: initialUIState,
+ initialState: getInitialUIState(),
migrate: migrateUIState,
persistDenylist: ['shouldShowImageDetails'],
};
diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
index 3702c58ec5..6c0ab9023a 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
@@ -1,43 +1,30 @@
-import type { Dimensions } from 'features/controlLayers/store/types';
+import { deepClone } from 'common/util/deepClone';
+import { z } from 'zod';
-export type TabName = 'generate' | 'canvas' | 'upscaling' | 'workflows' | 'models' | 'queue';
-export type CanvasRightPanelTabName = 'layers' | 'gallery';
+const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']);
+export type TabName = z.infer;
+const zCanvasRightPanelTabName = z.enum(['layers', 'gallery']);
+export type CanvasRightPanelTabName = z.infer;
-export interface UIState {
- /**
- * Slice schema version.
- */
- _version: 3;
- /**
- * The currently active tab.
- */
- activeTab: TabName;
- /**
- * The currently active right panel canvas tab
- */
- activeTabCanvasRightPanel: CanvasRightPanelTabName;
- /**
- * Whether or not to show image details, e.g. metadata, workflow, etc.
- */
- shouldShowImageDetails: boolean;
- /**
- * Whether or not to show progress in the viewer.
- */
- shouldShowProgressInViewer: boolean;
- /**
- * The state of accordions. The key is the id of the accordion, and the value is a boolean representing the open state.
- */
- accordions: Record;
- /**
- * The state of expanders. The key is the id of the expander, and the value is a boolean representing the open state.
- */
- expanders: Record;
- /**
- * The size of textareas. The key is the id of the text area, and the value is an object representing its width and/or height.
- */
- textAreaSizes: Record>;
- /**
- * Whether or not to show the user the open notification. Bump version to reset users who may have closed previous version.
- */
- shouldShowNotificationV2: boolean;
-}
+const zPartialDimensions = z.object({
+ width: z.number().optional(),
+ height: z.number().optional(),
+});
+export type PartialDimensions = z.infer;
+
+export const zUIState = z.object({
+ _version: z.literal(3).default(3),
+ activeTab: zTabName.default('canvas'),
+ activeTabCanvasRightPanel: zCanvasRightPanelTabName.default('gallery'),
+ shouldShowImageDetails: z.boolean().default(false),
+ shouldShowProgressInViewer: z.boolean().default(true),
+ accordions: z.record(z.string(), z.boolean()).default(() => ({})),
+ expanders: z.record(z.string(), z.boolean()).default(() => ({})),
+ textAreaSizes: z.record(z.string(), zPartialDimensions).default({}),
+ shouldShowNotificationV2: z.boolean().default(true),
+ showGenerateTabSplashScreen: z.boolean().default(true),
+ showCanvasTabSplashScreen: z.boolean().default(true),
+});
+const INITIAL_STATE = zUIState.parse({});
+export type UIState = z.infer;
+export const getInitialUIState = (): UIState => deepClone(INITIAL_STATE);
From abaa33e22c3787476142ad78933857461b357b05 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 11 Jun 2025 23:21:56 +1000
Subject: [PATCH 110/210] wip
---
invokeai/frontend/web/package.json | 1 +
invokeai/frontend/web/pnpm-lock.yaml | 16 +
.../src/common/components/Loading/Loading.tsx | 1 +
.../AdvancedSession/AdvancedSession.tsx | 194 ++++++----
.../components/SimpleSession/InitialState.tsx | 8 +-
.../SimpleSession/SimpleSession.tsx | 49 ++-
.../SimpleSession/SimpleSessionNoId.tsx | 15 +
.../Boards/BoardsList/BoardsSearch.tsx | 3 +
.../components/BoardsListPanelContent.tsx | 35 +-
.../features/gallery/components/Gallery.tsx | 2 +-
.../gallery/components/GalleryTopBar.tsx | 106 +++---
.../ImageGrid/useGallerySearchTerm.ts | 3 +-
.../ImageViewer/CurrentImagePreview2.tsx | 110 ++++++
.../components/ImageViewer/ImageViewer2.tsx | 125 +++++++
.../components/ImageViewer/ProgressImage2.tsx | 56 +++
.../components/ImageViewer/ViewerToolbar2.tsx | 18 +
.../gallery/hooks/useGalleryHotkeys.ts | 3 +-
.../features/gallery/hooks/useImageActions.ts | 12 +-
.../AdvancedSettingsAccordion.tsx | 62 ++--
.../UpscaleTabAdvancedSettingsAccordion.tsx | 90 +++++
.../GenerationSettingsAccordion.tsx | 17 +-
.../UpscaleTabGenerationSettingsAccordion.tsx | 92 +++++
.../src/features/ui/components/AppContent.tsx | 62 ++--
.../ui/components/LeftPanelContent.tsx | 2 +-
.../ui/components/MainPanelContent.tsx | 9 +-
.../src/features/ui/components/TabButton.tsx | 5 +-
.../features/ui/components/VerticalNavBar.tsx | 2 +-
.../src/features/ui/layouts/AutoLayout.tsx | 45 +++
.../ui/layouts/TabWithoutCloseButton.tsx | 35 ++
.../ui/layouts/auto-layout-context.tsx | 14 +
.../ui/layouts/canvas-tab-auto-layout.tsx | 339 ++++++++++++++++++
.../ui/layouts/generate-tab-auto-layout.tsx | 175 +++++++++
.../layouts/use-collapsible-gridview-panel.ts | 98 +++++
.../web/src/features/ui/store/uiSelectors.ts | 2 +
.../web/src/features/ui/store/uiSlice.ts | 4 +-
.../ui/styles/dockview-theme-invoke.css | 65 ++++
.../web/src/features/ui/styles/theme.ts | 6 +
37 files changed, 1602 insertions(+), 279 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx
create mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx
create mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts
create mode 100644 invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css
create mode 100644 invokeai/frontend/web/src/features/ui/styles/theme.ts
diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index f7c2513edb..c5e580c049 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -67,6 +67,7 @@
"chakra-react-select": "^4.9.2",
"cmdk": "^1.1.1",
"compare-versions": "^6.1.1",
+ "dockview": "^4.3.1",
"filesize": "^10.1.6",
"fracturedjsonjs": "^4.1.0",
"framer-motion": "^11.10.0",
diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml
index 79de0c48c1..61fba0669c 100644
--- a/invokeai/frontend/web/pnpm-lock.yaml
+++ b/invokeai/frontend/web/pnpm-lock.yaml
@@ -50,6 +50,9 @@ dependencies:
compare-versions:
specifier: ^6.1.1
version: 6.1.1
+ dockview:
+ specifier: ^4.3.1
+ version: 4.3.1(react@18.3.1)
filesize:
specifier: ^10.1.6
version: 10.1.6
@@ -4492,6 +4495,19 @@ packages:
resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==}
dev: false
+ /dockview-core@4.3.1:
+ resolution: {integrity: sha512-cjGIXKc1wtHHkeKisuDLNt3HSHCVzvabxm1K9Auna27A9T3QR7ISOiTJyEUKUPllkcztFYBut0vwnnvwLnPAuQ==}
+ dev: false
+
+ /dockview@4.3.1(react@18.3.1):
+ resolution: {integrity: sha512-D4SvZPs1GJxGUBPkrehlKNGsWlSDaBiPuSYI+IEXnZ7b2bCUs1/h954sVs7xyykqEW3r6TkPKLWdTR/47Q7/QQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ dependencies:
+ dockview-core: 4.3.1
+ react: 18.3.1
+ dev: false
+
/doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
diff --git a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
index 7caf12952a..b8bbcf668a 100644
--- a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
+++ b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
@@ -17,6 +17,7 @@ const Loading = () => {
right={0}
bottom={0}
left={0}
+ zIndex={99999}
>
{
}, []);
return (
-
-
-
-
-
-
- renderMenu={renderMenu} withLongPress={false}>
- {(ref) => (
-
-
+
+
+ Welcome
+ Workspace
+ Viewer
+
+
+
+
+
+
+
+
-
- {showHUD && }
-
-
-
-
-
-
- } colorScheme="base" />
-
-
-
+
+
+
+ renderMenu={renderMenu} withLongPress={false}>
+ {(ref) => (
+
+
+
+
+ {showHUD && }
+
+
+
+
+
+
+ } colorScheme="base" />
+
+
+
+
+
+ )}
+
+ {id !== null && (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
- )}
-
- {id !== null && (
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
});
AdvancedSession.displayName = 'AdvancedSession';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
index dc968e5a51..73d867cff2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -1,4 +1,4 @@
-import { Alert, Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
+import { Alert, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
@@ -14,11 +14,7 @@ export const InitialState = memo(() => {
return (
-
- Get Started
-
-
-
+
Get started with Invoke.
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
index 750066526e..54b0be8e1a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
@@ -1,16 +1,45 @@
-import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
+import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
-import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea';
-import { memo } from 'react';
+import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
+import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
+import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
+import { selectShowGenerateTabSplashScreen } from 'features/ui/store/uiSelectors';
+import { showGenerateTabSplashScreenChanged } from 'features/ui/store/uiSlice';
+import { memo, useCallback } from 'react';
+
+export const SimpleSession = memo(() => {
+ const showGenerateTabSplashScreen = useAppSelector(selectShowGenerateTabSplashScreen);
+ const dispatch = useAppDispatch();
+
+ const showSplashScreen = useCallback(() => {
+ dispatch(showGenerateTabSplashScreenChanged(true));
+ }, [dispatch]);
-export const SimpleSession = memo(({ id }: { id: string | null }) => {
- if (id === null) {
- return ;
- }
return (
-
-
-
+
+
+ Launchpad
+ Viewer
+ Generation Progress
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
});
SimpleSession.displayName = 'SimpleSession';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx
new file mode 100644
index 0000000000..e7b3657a95
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx
@@ -0,0 +1,15 @@
+import { Divider, Flex } from '@invoke-ai/ui-library';
+import { StagingAreaHeader } from 'features/controlLayers/components/SimpleSession/StagingAreaHeader';
+import { StagingAreaNoItems } from 'features/controlLayers/components/SimpleSession/StagingAreaNoItems';
+import { memo } from 'react';
+
+export const SimpleSessionNoId = memo(() => {
+ return (
+
+
+
+
+
+ );
+});
+SimpleSessionNoId.displayName = 'StSimpleSessionNoIdagingArea';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
index 5c578a5539..3b66a41c63 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
@@ -1,5 +1,6 @@
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { buildUseDisclosure } from 'common/hooks/useBoolean';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
import type { ChangeEvent, KeyboardEvent } from 'react';
@@ -7,6 +8,8 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
+export const [useBoardSearchDisclosure, $boardSearchIsOpen] = buildUseDisclosure(false);
+
export const BoardsSearch = memo(() => {
const dispatch = useAppDispatch();
const boardSearchText = useAppSelector(selectBoardSearchText);
diff --git a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
index b456cbaaa7..bb80d8e160 100644
--- a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
@@ -1,25 +1,26 @@
-import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
import { Box, Collapse, Divider, Flex } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { BoardsListWrapper } from 'features/gallery/components/Boards/BoardsList/BoardsListWrapper';
-import { BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
+import { $boardSearchIsOpen, BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
+import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
import type { CSSProperties } from 'react';
import { memo } from 'react';
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
-export const BoardsListPanelContent = memo(
- ({ boardSearchDisclosure }: { boardSearchDisclosure: UseDisclosureReturn }) => {
- return (
-
-
-
-
-
-
-
-
-
- );
- }
-);
+export const BoardsListPanelContent = memo(() => {
+ const boardSearchDisclosure = useStore($boardSearchIsOpen);
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+});
BoardsListPanelContent.displayName = 'BoardsListPanelContent';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
index 785bc53af8..0fcfa7dd82 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
@@ -70,7 +70,7 @@ export const Gallery = memo(() => {
const boardName = useBoardName(selectedBoardId);
return (
-
+
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
index e84e733c58..a5ff519384 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
@@ -1,67 +1,67 @@
-import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
import { Button, Flex, IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useBoardSearchDisclosure } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
import { BoardsSettingsPopover } from 'features/gallery/components/Boards/BoardsSettingsPopover';
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
-import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
+import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
+import { useCollapsibleGridviewPanel } from 'features/ui/layouts/use-collapsible-gridview-panel';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
-export const GalleryTopBar = memo(
- ({
- boardsListPanel,
- boardSearchDisclosure,
- }: {
- boardsListPanel: UsePanelReturn;
- boardSearchDisclosure: UseDisclosureReturn;
- }) => {
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const boardSearchText = useAppSelector(selectBoardSearchText);
+export const GalleryTopBar = memo(() => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const boardSearchText = useAppSelector(selectBoardSearchText);
+ const boardSearchDisclosure = useBoardSearchDisclosure();
+ const api = useAutoLayoutContext();
+ const boardsPanel = useCollapsibleGridviewPanel(api, 'boards', 'vertical', 256);
+ const isBoardsPanelCollapsed = useStore(boardsPanel.$isCollapsed);
- const onClickBoardSearch = useCallback(() => {
- if (boardSearchText.length) {
- dispatch(boardSearchTextChanged(''));
- }
- boardSearchDisclosure.onToggle();
- boardsListPanel.expand();
- }, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
+ const onClickBoardSearch = useCallback(() => {
+ if (boardSearchText.length) {
+ dispatch(boardSearchTextChanged(''));
+ }
+ if (!boardSearchDisclosure.isOpen && boardsPanel.$isCollapsed.get()) {
+ boardsPanel.expand();
+ }
+ boardSearchDisclosure.toggle();
+ }, [boardSearchText.length, boardSearchDisclosure, dispatch, boardsPanel]);
- return (
-
-
- : }
- >
- {boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
-
-
-
-
-
-
-
- }
- colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
- />
-
+ return (
+
+
+ : }
+ >
+ {isBoardsPanelCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
+
- );
- }
-);
+
+
+
+
+
+ }
+ colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
+ />
+
+
+ );
+});
GalleryTopBar.displayName = 'GalleryTopBar';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts
index b9651e3d8e..6071d8a8f8 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts
@@ -1,5 +1,4 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { selectSearchTerm } from 'features/gallery/store/gallerySelectors';
import { searchTermChanged } from 'features/gallery/store/gallerySlice';
import { debounce } from 'lodash-es';
@@ -7,7 +6,7 @@ import { useCallback, useMemo, useState } from 'react';
export const useGallerySearchTerm = () => {
// Highlander!
- useAssertSingleton('gallery-search-state');
+ // useAssertSingleton('gallery-search-state');
const dispatch = useAppDispatch();
const searchTerm = useAppSelector(selectSearchTerm);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx
new file mode 100644
index 0000000000..9e323bd920
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx
@@ -0,0 +1,110 @@
+import { Box, Flex } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppSelector } from 'app/store/storeHooks';
+import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
+import { DndImage } from 'features/dnd/DndImage';
+import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
+import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
+import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
+import type { AnimationProps } from 'framer-motion';
+import { AnimatePresence, motion } from 'framer-motion';
+import { memo, useCallback, useRef, useState } from 'react';
+import type { ImageDTO } from 'services/api/types';
+import { $hasLastProgressImage } from 'services/events/stores';
+
+import { NoContentForViewer } from './NoContentForViewer';
+
+export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
+ const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
+
+ // Show and hide the next/prev buttons on mouse move
+ const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState(false);
+ const timeoutId = useRef(0);
+ const onMouseOver = useCallback(() => {
+ setShouldShowNextPrevButtons(true);
+ window.clearTimeout(timeoutId.current);
+ }, []);
+ const onMouseOut = useCallback(() => {
+ timeoutId.current = window.setTimeout(() => {
+ setShouldShowNextPrevButtons(false);
+ }, 500);
+ }, []);
+
+ return (
+
+
+
+
+
+ {shouldShowImageDetails && imageDTO && (
+
+
+
+ )}
+
+ {shouldShowNextPrevButtons && imageDTO && (
+
+
+
+ )}
+
+
+ );
+});
+CurrentImagePreview.displayName = 'CurrentImagePreview';
+
+const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
+ const hasProgressImage = useStore($hasLastProgressImage);
+ const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
+
+ if (!imageDTO) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+});
+ImageContent.displayName = 'ImageContent';
+
+const initial: AnimationProps['initial'] = {
+ opacity: 0,
+};
+const animateArrows: AnimationProps['animate'] = {
+ opacity: 1,
+ transition: { duration: 0.07 },
+};
+const exit: AnimationProps['exit'] = {
+ opacity: 0,
+ transition: { duration: 0.07 },
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx
new file mode 100644
index 0000000000..07cab441af
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx
@@ -0,0 +1,125 @@
+import { Box, Flex, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { useAppSelector } from 'app/store/storeHooks';
+import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
+import { selectImageToCompare } from 'features/gallery/components/ImageViewer/common';
+import { CurrentImagePreview } from 'features/gallery/components/ImageViewer/CurrentImagePreview2';
+import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
+import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
+import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
+import { memo, useRef } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
+import { useTranslation } from 'react-i18next';
+import { PiXBold } from 'react-icons/pi';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+
+import { useImageViewer } from './useImageViewer';
+
+// type Props = {
+// closeButton?: ReactNode;
+// };
+
+// const useFocusRegionOptions = {
+// focusOnMount: true,
+// };
+
+// const FOCUS_REGION_STYLES: SystemStyleObject = {
+// display: 'flex',
+// width: 'full',
+// height: 'full',
+// position: 'absolute',
+// flexDirection: 'column',
+// inset: 0,
+// alignItems: 'center',
+// justifyContent: 'center',
+// overflow: 'hidden',
+// };
+
+export const ImageViewer = memo(() => {
+ const lastSelectedImageName = useAppSelector(selectLastSelectedImageName);
+ const { data: lastSelectedImageDTO } = useGetImageDTOQuery(lastSelectedImageName ?? skipToken);
+ const comparisonImageDTO = useAppSelector(selectImageToCompare);
+
+ if (lastSelectedImageDTO && comparisonImageDTO) {
+ return ;
+ }
+
+ return ;
+});
+
+ImageViewer.displayName = 'ImageViewer';
+
+const imageViewerContainerSx: SystemStyleObject = {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ transition: 'opacity 0.15s ease',
+ opacity: 1,
+ pointerEvents: 'auto',
+ '&[data-hidden="true"]': {
+ opacity: 0,
+ pointerEvents: 'none',
+ },
+ backdropFilter: 'blur(10px) brightness(70%)',
+};
+
+export const ImageViewerModal = memo(() => {
+ const ref = useRef(null);
+ const imageViewer = useImageViewer();
+ useOutsideClick({
+ ref,
+ handler: imageViewer.close,
+ });
+
+ useHotkeys(
+ 'esc',
+ imageViewer.close,
+ {
+ preventDefault: true,
+ enabled: imageViewer.isOpen,
+ },
+ [imageViewer.isOpen]
+ );
+
+ return (
+
+
+
+
+
+
+ );
+});
+
+ImageViewerModal.displayName = 'GatedImageViewer';
+
+const ImageViewerCloseButton = memo(() => {
+ const { t } = useTranslation();
+ const imageViewer = useImageViewer();
+ useAssertSingleton('ImageViewerCloseButton');
+ useHotkeys('esc', imageViewer.close);
+ return (
+ }
+ variant="link"
+ alignSelf="stretch"
+ onClick={imageViewer.close}
+ />
+ );
+});
+
+ImageViewerCloseButton.displayName = 'ImageViewerCloseButton';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx
new file mode 100644
index 0000000000..850ebc63e1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx
@@ -0,0 +1,56 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex, Image } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import { selectSystemSlice } from 'features/system/store/systemSlice';
+import { memo, useMemo } from 'react';
+import { PiPulseBold } from 'react-icons/pi';
+import { $lastProgressImage } from 'services/events/stores';
+
+const selectShouldAntialiasProgressImage = createSelector(
+ selectSystemSlice,
+ (system) => system.shouldAntialiasProgressImage
+);
+
+export const ProgressImage = memo(() => {
+ const progressImage = useStore($lastProgressImage);
+ const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
+
+ const sx = useMemo(
+ () => ({
+ imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
+ }),
+ [shouldAntialiasProgressImage]
+ );
+
+ if (!progressImage) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+});
+
+ProgressImage.displayName = 'ProgressImage';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx
new file mode 100644
index 0000000000..f8dc34d654
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx
@@ -0,0 +1,18 @@
+import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
+import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
+import { memo } from 'react';
+
+import CurrentImageButtons from './CurrentImageButtons';
+
+export const ViewerToolbar = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+
+ViewerToolbar.displayName = 'ViewerToolbar';
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
index ab6da024fd..0f2a175c1c 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
@@ -1,6 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
-import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
@@ -14,7 +13,7 @@ import { useListImagesQuery } from 'services/api/endpoints/images';
* Registers gallery hotkeys. This hook is a singleton.
*/
export const useGalleryHotkeys = () => {
- useAssertSingleton('useGalleryHotkeys');
+ // useAssertSingleton('useGalleryHotkeys');
const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
const selection = useAppSelector((s) => s.gallery.selection);
const queryArgs = useAppSelector(selectListImagesQueryArgs);
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
index 87f0b6aba2..cb37c2c11c 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
@@ -1,6 +1,7 @@
import { useStore } from '@nanostores/react';
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useAppStore } from 'app/store/nanostores/store';
+import { useAppSelector } from 'app/store/storeHooks';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import {
@@ -24,11 +25,10 @@ import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { ImageDTO } from 'services/api/types';
export const useImageActions = (imageDTO: ImageDTO) => {
- const dispatch = useAppDispatch();
+ const { dispatch, getState } = useAppStore();
const { t } = useTranslation();
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
const isStaging = useAppSelector(selectIsStaging);
- const activeTabName = useAppSelector(selectActiveTab);
const { metadata } = useDebouncedMetadata(imageDTO.image_name);
const [hasMetadata, setHasMetadata] = useState(false);
const [hasSeed, setHasSeed] = useState(false);
@@ -82,18 +82,20 @@ export const useImageActions = (imageDTO: ImageDTO) => {
if (!metadata) {
return;
}
+ const activeTabName = selectActiveTab(getState());
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', isStaging ? ['width', 'height'] : []);
clearStylePreset();
- }, [metadata, activeTabName, isStaging, clearStylePreset]);
+ }, [metadata, getState, isStaging, clearStylePreset]);
const remix = useCallback(() => {
if (!metadata) {
return;
}
+ const activeTabName = selectActiveTab(getState());
// Recalls all metadata parameters except seed
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', ['seed']);
clearStylePreset();
- }, [activeTabName, metadata, clearStylePreset]);
+ }, [metadata, getState, clearStylePreset]);
const recallSeed = useCallback(() => {
if (!metadata) {
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
index 00dc4754d6..37475ad6aa 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
@@ -12,12 +12,10 @@ import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip
import ParamT5EncoderModelSelect from 'features/parameters/components/Advanced/ParamT5EncoderModelSelect';
import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis';
import ParamSeamlessYAxis from 'features/parameters/components/Seamless/ParamSeamlessYAxis';
-import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed';
import ParamFLUXVAEModelSelect from 'features/parameters/components/VAEModel/ParamFLUXVAEModelSelect';
import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect';
import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetModelConfigQuery } from 'services/api/endpoints/models';
@@ -33,7 +31,6 @@ const formLabelProps2: FormLabelProps = {
export const AdvancedSettingsAccordion = memo(() => {
const vaeKey = useAppSelector(selectVAEKey);
const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken);
- const activeTabName = useAppSelector(selectActiveTab);
const isFLUX = useAppSelector(selectIsFLUX);
const isSD3 = useAppSelector(selectIsSD3);
@@ -68,19 +65,16 @@ export const AdvancedSettingsAccordion = memo(() => {
if (params.seamlessXAxis || params.seamlessYAxis) {
badges.push('seamless');
}
- if (activeTabName === 'upscaling' && !params.shouldRandomizeSeed) {
- badges.push('Manual Seed');
- }
}
return badges;
}),
- [vaeConfig, activeTabName]
+ [vaeConfig]
);
const badges = useAppSelector(selectBadges);
const { t } = useTranslation();
const { isOpen, onToggle } = useStandaloneAccordionToggle({
- id: `'advanced-settings-${activeTabName}`,
+ id: `'advanced-settings-generate`,
defaultIsOpen: false,
});
@@ -91,39 +85,33 @@ export const AdvancedSettingsAccordion = memo(() => {
{isFLUX ? : }
{!isFLUX && !isSD3 && }
- {activeTabName === 'upscaling' ? (
-
- ) : (
+ {!isFLUX && !isSD3 && (
<>
- {!isFLUX && !isSD3 && (
- <>
-
-
-
-
-
-
-
-
-
-
- >
- )}
- {isFLUX && (
-
-
-
+
+
+
+
+
+
+
+
- )}
- {isSD3 && (
-
-
-
-
-
- )}
+
>
)}
+ {isFLUX && (
+
+
+
+
+ )}
+ {isSD3 && (
+
+
+
+
+
+ )}
);
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx
new file mode 100644
index 0000000000..04b55e80c6
--- /dev/null
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx
@@ -0,0 +1,90 @@
+import type { FormLabelProps } from '@invoke-ai/ui-library';
+import { Flex, StandaloneAccordion } from '@invoke-ai/ui-library';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectIsFLUX, selectIsSD3, selectParamsSlice, selectVAEKey } from 'features/controlLayers/store/paramsSlice';
+import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed';
+import ParamFLUXVAEModelSelect from 'features/parameters/components/VAEModel/ParamFLUXVAEModelSelect';
+import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect';
+import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision';
+import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
+import { memo, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useGetModelConfigQuery } from 'services/api/endpoints/models';
+
+const formLabelProps: FormLabelProps = {
+ minW: '9.2rem',
+};
+
+const formLabelProps2: FormLabelProps = {
+ flexGrow: 1,
+};
+
+export const AdvancedSettingsAccordion = memo(() => {
+ const vaeKey = useAppSelector(selectVAEKey);
+ const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken);
+ const isFLUX = useAppSelector(selectIsFLUX);
+ const isSD3 = useAppSelector(selectIsSD3);
+
+ const selectBadges = useMemo(
+ () =>
+ createMemoizedSelector([selectParamsSlice, selectIsFLUX], (params, isFLUX) => {
+ const badges: (string | number)[] = [];
+ if (isFLUX) {
+ if (vaeConfig) {
+ let vaeBadge = vaeConfig.name;
+ if (params.vaePrecision === 'fp16') {
+ vaeBadge += ` ${params.vaePrecision}`;
+ }
+ badges.push(vaeBadge);
+ }
+ } else {
+ if (vaeConfig) {
+ let vaeBadge = vaeConfig.name;
+ if (params.vaePrecision === 'fp16') {
+ vaeBadge += ` ${params.vaePrecision}`;
+ }
+ badges.push(vaeBadge);
+ } else if (params.vaePrecision === 'fp16') {
+ badges.push(`VAE ${params.vaePrecision}`);
+ }
+ if (params.clipSkip) {
+ badges.push(`Skip ${params.clipSkip}`);
+ }
+ if (params.cfgRescaleMultiplier) {
+ badges.push(`Rescale ${params.cfgRescaleMultiplier}`);
+ }
+ if (params.seamlessXAxis || params.seamlessYAxis) {
+ badges.push('seamless');
+ }
+ if (!params.shouldRandomizeSeed) {
+ badges.push('Manual Seed');
+ }
+ }
+
+ return badges;
+ }),
+ [vaeConfig]
+ );
+ const badges = useAppSelector(selectBadges);
+ const { t } = useTranslation();
+ const { isOpen, onToggle } = useStandaloneAccordionToggle({
+ id: `'advanced-settings-upscaling`,
+ defaultIsOpen: false,
+ });
+
+ return (
+
+
+
+ {isFLUX ? : }
+ {!isFLUX && !isSD3 && }
+
+
+
+
+ );
+});
+
+AdvancedSettingsAccordion.displayName = 'AdvancedSettingsAccordion';
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
index 0be0a0c602..1e5e29884b 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
@@ -12,14 +12,11 @@ import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
import ParamScheduler from 'features/parameters/components/Core/ParamScheduler';
import ParamSteps from 'features/parameters/components/Core/ParamSteps';
import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning';
-import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale';
-import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler';
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
import { API_BASE_MODELS } from 'features/parameters/types/constants';
import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker';
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
@@ -32,16 +29,12 @@ const formLabelProps: FormLabelProps = {
export const GenerationSettingsAccordion = memo(() => {
const { t } = useTranslation();
const modelConfig = useSelectedModelConfig();
- const activeTabName = useAppSelector(selectActiveTab);
const isFLUX = useAppSelector(selectIsFLUX);
const isSD3 = useAppSelector(selectIsSD3);
const isCogView4 = useAppSelector(selectIsCogView4);
const isApiModel = useIsApiModel();
- const isUpscaling = useMemo(() => {
- return activeTabName === 'upscaling';
- }, [activeTabName]);
const selectBadges = useMemo(
() =>
createMemoizedSelector(selectLoRAsSlice, (loras) => {
@@ -63,8 +56,8 @@ export const GenerationSettingsAccordion = memo(() => {
defaultIsOpen: false,
});
const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({
- id: `generation-settings-${activeTabName}`,
- defaultIsOpen: activeTabName !== 'upscaling',
+ id: `generation-settings-generate`,
+ defaultIsOpen: true,
});
return (
@@ -85,12 +78,10 @@ export const GenerationSettingsAccordion = memo(() => {
- {!isFLUX && !isSD3 && !isCogView4 && !isUpscaling && }
- {isUpscaling && }
+ {!isFLUX && !isSD3 && !isCogView4 && }
{isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && }
- {isUpscaling && }
- {!isFLUX && !isUpscaling && }
+ {!isFLUX && }
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx
new file mode 100644
index 0000000000..31221105e1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx
@@ -0,0 +1,92 @@
+import type { FormLabelProps } from '@invoke-ai/ui-library';
+import { Box, Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library';
+import { EMPTY_ARRAY } from 'app/store/constants';
+import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
+import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
+import { LoRAList } from 'features/lora/components/LoRAList';
+import LoRASelect from 'features/lora/components/LoRASelect';
+import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
+import ParamSteps from 'features/parameters/components/Core/ParamSteps';
+import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning';
+import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale';
+import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler';
+import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
+import { API_BASE_MODELS } from 'features/parameters/types/constants';
+import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker';
+import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
+import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
+import { memo, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
+import { isFluxFillMainModelModelConfig } from 'services/api/types';
+
+const formLabelProps: FormLabelProps = {
+ minW: '4rem',
+};
+
+export const UpscaleTabGenerationSettingsAccordion = memo(() => {
+ const { t } = useTranslation();
+ const modelConfig = useSelectedModelConfig();
+ const isFLUX = useAppSelector(selectIsFLUX);
+
+ const isApiModel = useIsApiModel();
+
+ const selectBadges = useMemo(
+ () =>
+ createMemoizedSelector(selectLoRAsSlice, (loras) => {
+ const enabledLoRAsCount = loras.loras.filter((l) => l.isEnabled).length;
+ const loraTabBadges = enabledLoRAsCount ? [`${enabledLoRAsCount} ${t('models.concepts')}`] : EMPTY_ARRAY;
+ const accordionBadges =
+ modelConfig && API_BASE_MODELS.includes(modelConfig.base)
+ ? [modelConfig.name]
+ : modelConfig
+ ? [modelConfig.name, modelConfig.base]
+ : EMPTY_ARRAY;
+ return { loraTabBadges, accordionBadges };
+ }),
+ [modelConfig, t]
+ );
+ const { loraTabBadges, accordionBadges } = useAppSelector(selectBadges);
+ const { isOpen: isOpenExpander, onToggle: onToggleExpander } = useExpanderToggle({
+ id: 'generation-settings-advanced',
+ defaultIsOpen: false,
+ });
+ const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({
+ id: `generation-settings-upscaling`,
+ defaultIsOpen: false,
+ });
+
+ return (
+
+
+
+
+
+ {!isApiModel && }
+ {!isApiModel && }
+
+ {!isApiModel && (
+
+
+
+
+
+ {isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && }
+
+
+
+
+ )}
+
+
+ );
+});
+
+UpscaleTabGenerationSettingsAccordion.displayName = 'UpscaleTabGenerationSettingsAccordion';
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 199e5d8c43..74bcdc6119 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -1,15 +1,16 @@
-import { Flex } from '@invoke-ai/ui-library';
+import 'dockview/dist/styles/dockview.css';
+import 'features/ui/styles/dockview-theme-invoke.css';
+
+import { TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
-import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
-import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
-import { LeftPanelContent } from 'features/ui/components/LeftPanelContent';
-import { MainPanelContent } from 'features/ui/components/MainPanelContent';
-import { RightPanelContent } from 'features/ui/components/RightPanelContent';
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel';
+import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
+import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import {
$isLeftPanelOpen,
$isRightPanelOpen,
@@ -21,9 +22,6 @@ import {
import type { CSSProperties } from 'react';
import { memo, useMemo, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
-import { Panel, PanelGroup } from 'react-resizable-panels';
-
-import { VerticalResizeHandle } from './tabs/ResizeHandle';
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
@@ -31,6 +29,7 @@ const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCo
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
export const AppContent = memo(() => {
+ const tab = useAppSelector(selectActiveTab);
const imperativePanelGroupRef = useRef(null);
useDndMonitor();
@@ -108,38 +107,19 @@ export const AppContent = memo(() => {
});
return (
-
-
-
- {withLeftPanel && (
- <>
-
-
-
-
- >
- )}
-
-
- {withLeftPanel && }
- {withRightPanel && }
-
- {withRightPanel && (
- <>
-
-
-
-
- >
- )}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
});
AppContent.displayName = 'AppContent';
diff --git a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
index a603d540e7..e30de8bf17 100644
--- a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
@@ -12,7 +12,7 @@ export const LeftPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
return (
-
+
{tab === 'generate' && }
diff --git a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
index 58010ba503..ec5e41deff 100644
--- a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
@@ -1,27 +1,22 @@
import { useAppSelector } from 'app/store/storeHooks';
import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
-import { selectCanvasSessionId, selectGenerateSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
import QueueTab from 'features/ui/components/tabs/QueueTab';
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
-import { atom } from 'nanostores';
import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
-export const $simpleId = atom(null);
-export const $advancedId = atom(null);
-
export const MainPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
- const generateId = useAppSelector(selectGenerateSessionId);
const canvasId = useAppSelector(selectCanvasSessionId);
if (tab === 'generate') {
- return ;
+ return ;
}
if (tab === 'canvas') {
return ;
diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
index 8974a9c326..1a8850f309 100644
--- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { IconButton, Tooltip } from '@invoke-ai/ui-library';
+import { IconButton, Tab, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -25,7 +25,8 @@ export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: React
return (
- {
const customNavComponent = useStore($customNavComponent);
return (
-
+
diff --git a/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx b/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
new file mode 100644
index 0000000000..a1feebb13f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
@@ -0,0 +1,45 @@
+import { useAppSelector } from 'app/store/storeHooks';
+import type { GridviewApi, IGridviewReactProps } from 'dockview';
+import { GridviewReact, Orientation } from 'dockview';
+import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { canvasTabComponents, initializeCanvasTabLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
+import { generateTabComponents, initializeGenerateTabLayout } from 'features/ui/layouts/generate-tab-auto-layout';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
+import type { TabName } from 'features/ui/store/uiTypes';
+import { memo, useCallback, useEffect, useState } from 'react';
+
+const components: IGridviewReactProps['components'] = {
+ ...generateTabComponents,
+ ...canvasTabComponents,
+};
+
+export const AutoLayout = memo(() => {
+ const tab = useAppSelector(selectActiveTab);
+ const [api, setApi] = useState(null);
+ const syncLayout = useCallback((tab: TabName, api: GridviewApi) => {
+ if (tab === 'generate') {
+ initializeGenerateTabLayout(api);
+ } else if (tab === 'canvas') {
+ initializeCanvasTabLayout(api);
+ }
+ }, []);
+ const onReady = useCallback((event) => {
+ setApi(event.api);
+ }, []);
+ useEffect(() => {
+ if (api) {
+ syncLayout(tab, api);
+ }
+ }, [api, syncLayout, tab]);
+ return (
+
+
+
+ );
+});
+AutoLayout.displayName = 'AutoLayout';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
new file mode 100644
index 0000000000..77e469a462
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
@@ -0,0 +1,35 @@
+import { Flex, Text } from '@invoke-ai/ui-library';
+import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
+import type { IDockviewPanelHeaderProps } from 'dockview';
+import { useCallback, useEffect, useId, useRef } from 'react';
+
+export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => {
+ const id = useId();
+ const ref = useRef(null);
+ const setActive = useCallback(() => {
+ if (!props.api.isActive) {
+ props.api.setActive();
+ }
+ }, [props.api]);
+
+ useCallbackOnDragEnter(setActive, ref, 300);
+
+ useEffect(() => {
+ const el = document.querySelector(`[data-id="${id}"]`);
+ if (!el) {
+ return;
+ }
+ const parentTab = el.closest('.dv-tab');
+ if (!parentTab) {
+ return;
+ }
+ parentTab.setAttribute('draggable', 'false');
+ }, [id]);
+
+ return (
+
+ {props.api.title ?? props.api.id}
+
+ );
+};
+TabWithoutCloseButton.displayName = 'TabWithoutCloseButton';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
new file mode 100644
index 0000000000..d86ae4bee1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
@@ -0,0 +1,14 @@
+import type { GridviewApi } from 'dockview';
+import type { PropsWithChildren } from 'react';
+import { createContext, useContext } from 'react';
+
+const AutoLayoutContext = createContext(null);
+
+export const AutoLayoutProvider = (props: PropsWithChildren<{ api: GridviewApi | null }>) => {
+ return {props.children};
+};
+
+export const useAutoLayoutContext = () => {
+ const api = useContext(AutoLayoutContext);
+ return api;
+};
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
new file mode 100644
index 0000000000..43ea4dc4e1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx
@@ -0,0 +1,339 @@
+import { Box, ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
+import { DockviewReact, GridviewReact, Orientation } from 'dockview';
+import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
+import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
+import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
+import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
+import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
+import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
+import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
+import { Filter } from 'features/controlLayers/components/Filters/Filter';
+import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
+import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
+import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
+import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
+import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
+import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
+import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
+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 { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
+import { Gallery } from 'features/gallery/components/Gallery';
+import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
+import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
+import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
+import QueueControls from 'features/queue/components/QueueControls';
+import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
+import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
+import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
+import { dockviewTheme } from 'features/ui/styles/theme';
+import { memo, useCallback, useState } from 'react';
+import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
+
+const MenuContent = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+MenuContent.displayName = 'MenuContent';
+
+const canvasBgSx = {
+ position: 'relative',
+ w: 'full',
+ h: 'full',
+ borderRadius: 'base',
+ overflow: 'hidden',
+ bg: 'base.900',
+ '&[data-dynamic-grid="true"]': {
+ bg: 'base.850',
+ },
+};
+
+export const CanvasPanel = memo(() => {
+ const dynamicGrid = useAppSelector(selectDynamicGrid);
+ const showHUD = useAppSelector(selectShowHUD);
+ const canvasId = useAppSelector(selectCanvasSessionId);
+
+ const renderMenu = useCallback(() => {
+ return ;
+ }, []);
+
+ return (
+
+
+
+
+
+ renderMenu={renderMenu} withLongPress={false}>
+ {(ref) => (
+
+
+
+
+ {showHUD && }
+
+
+
+
+
+
+ } colorScheme="base" />
+
+
+
+
+
+ )}
+
+ {canvasId !== null && (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+CanvasPanel.displayName = 'CanvasPanel';
+
+const LayersPanelContent = memo(() => (
+
+
+
+));
+LayersPanelContent.displayName = 'LayersPanelContent';
+
+const ViewerPanelContent = memo(() => (
+
+
+
+
+
+));
+ViewerPanelContent.displayName = 'ViewerPanelContent';
+
+const ProgressPanelContent = memo(() => (
+
+
+
+));
+ProgressPanelContent.displayName = 'ProgressPanelContent';
+
+const mainPanelComponents: IDockviewReactProps['components'] = {
+ welcome: InitialState,
+ canvas: CanvasPanel,
+ viewer: ViewerPanelContent,
+ progress: ProgressPanelContent,
+};
+
+const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
+ const { api } = event;
+ api.addPanel({
+ id: 'welcome',
+ component: 'welcome',
+ title: 'Launchpad',
+ });
+ api.addPanel({
+ id: 'canvas',
+ component: 'canvas',
+ title: 'Canvas',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+ api.addPanel({
+ id: 'viewer',
+ component: 'viewer',
+ title: 'Image Viewer',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+ api.addPanel({
+ id: 'progress',
+ component: 'progress',
+ title: 'Generation Progress',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+
+ const disposables = [
+ api.onWillShowOverlay((e) => {
+ if (e.kind === 'header_space' || e.kind === 'tab') {
+ return;
+ }
+ e.preventDefault();
+ }),
+ ];
+
+ return () => {
+ disposables.forEach((disposable) => {
+ disposable.dispose();
+ });
+ };
+};
+
+const MainPanel = memo(() => {
+ return (
+
+
+
+ );
+});
+MainPanel.displayName = 'MainPanel';
+
+const Left = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+Left.displayName = 'Left';
+
+export const canvasTabComponents: IGridviewReactProps['components'] = {
+ left: Left,
+ main: MainPanel,
+ boards: BoardsListPanelContent,
+ gallery: Gallery,
+ layers: LayersPanelContent,
+};
+
+export const initializeCanvasTabLayout = (api: GridviewApi) => {
+ const main = api.addPanel({
+ id: 'main',
+ component: 'main',
+ minimumWidth: 256,
+ });
+ const left = api.addPanel({
+ id: 'left',
+ component: 'left',
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: 'main',
+ },
+ });
+ api.addPanel({
+ id: 'gallery',
+ component: 'gallery',
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ position: {
+ direction: 'right',
+ referencePanel: 'main',
+ },
+ });
+ api.addPanel({
+ id: 'layers',
+ component: 'layers',
+ minimumHeight: 256,
+ position: {
+ direction: 'below',
+ referencePanel: 'gallery',
+ },
+ });
+ const boards = api.addPanel({
+ id: 'boards',
+ component: 'boards',
+ minimumHeight: 36,
+ position: {
+ direction: 'above',
+ referencePanel: 'gallery',
+ },
+ });
+ left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+};
+
+export const CanvasTabAutoLayout = memo(() => {
+ const [api, setApi] = useState(null);
+ const onReady = useCallback((event) => {
+ setApi(event.api);
+ initializeCanvasTabLayout(event.api);
+ }, []);
+ return (
+
+
+
+ );
+});
+CanvasTabAutoLayout.displayName = 'CanvasTabAutoLayout';
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
new file mode 100644
index 0000000000..f97a404652
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx
@@ -0,0 +1,175 @@
+import { Box, Divider, Flex } from '@invoke-ai/ui-library';
+import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
+import { DockviewReact, GridviewReact, Orientation } from 'dockview';
+import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
+import { Gallery } from 'features/gallery/components/Gallery';
+import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
+import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
+import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
+import QueueControls from 'features/queue/components/QueueControls';
+import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
+import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
+import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
+import { dockviewTheme } from 'features/ui/styles/theme';
+import { memo, useCallback, useState } from 'react';
+
+const ViewerPanelContent = memo(() => (
+
+
+
+
+
+));
+ViewerPanelContent.displayName = 'ViewerPanelContent';
+
+const ProgressPanelContent = memo(() => (
+
+
+
+));
+ProgressPanelContent.displayName = 'ProgressPanelContent';
+
+const mainPanelComponents: IDockviewReactProps['components'] = {
+ welcome: InitialState,
+ viewer: ViewerPanelContent,
+ progress: ProgressPanelContent,
+};
+
+const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
+ const { api } = event;
+ api.addPanel({
+ id: 'welcome',
+ component: 'welcome',
+ title: 'Launchpad',
+ });
+ api.addPanel({
+ id: 'viewer',
+ component: 'viewer',
+ title: 'Image Viewer',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+ api.addPanel({
+ id: 'progress',
+ component: 'progress',
+ title: 'Generation Progress',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+
+ const disposables = [
+ api.onWillShowOverlay((e) => {
+ if (e.kind === 'header_space' || e.kind === 'tab') {
+ return;
+ }
+ e.preventDefault();
+ }),
+ ];
+
+ return () => {
+ disposables.forEach((disposable) => {
+ disposable.dispose();
+ });
+ };
+};
+
+const MainPanel = memo(() => {
+ return (
+
+
+
+ );
+});
+MainPanel.displayName = 'MainPanel';
+
+const Left = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+Left.displayName = 'Left';
+
+export const generateTabComponents: IGridviewReactProps['components'] = {
+ left: Left,
+ main: MainPanel,
+ boards: BoardsListPanelContent,
+ gallery: Gallery,
+};
+
+export const initializeGenerateTabLayout = (api: GridviewApi) => {
+ const main = api.addPanel({
+ id: 'main',
+ component: 'main',
+ minimumWidth: 256,
+ });
+ const left = api.addPanel({
+ id: 'left',
+ component: 'left',
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: 'main',
+ },
+ });
+ api.addPanel({
+ id: 'gallery',
+ component: 'gallery',
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ position: {
+ direction: 'right',
+ referencePanel: 'main',
+ },
+ });
+ const boards = api.addPanel({
+ id: 'boards',
+ component: 'boards',
+ minimumHeight: 36,
+ position: {
+ direction: 'above',
+ referencePanel: 'gallery',
+ },
+ });
+ left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+};
+
+export const GenerateTabAutoLayout = memo(() => {
+ const [api, setApi] = useState(null);
+ const onReady = useCallback((event) => {
+ console.log('GenerateTabAutoLayout onReady');
+ setApi(event.api);
+ initializeGenerateTabLayout(event.api);
+ }, []);
+ return (
+
+
+
+ );
+});
+GenerateTabAutoLayout.displayName = 'GenerateTabAutoLayout';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts
new file mode 100644
index 0000000000..7d6bd82989
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts
@@ -0,0 +1,98 @@
+import type { GridviewApi, GridviewPanelApi, IGridviewPanel } from 'dockview';
+import { atom } from 'nanostores';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+const getIsCollapsed = (
+ panel: IGridviewPanel,
+ orientation: 'vertical' | 'horizontal',
+ collapsedSize?: number
+) => {
+ if (orientation === 'vertical') {
+ return panel.height <= (collapsedSize ?? panel.minimumHeight);
+ }
+ return panel.width <= (collapsedSize ?? panel.minimumWidth);
+};
+
+export const useCollapsibleGridviewPanel = (
+ api: GridviewApi | null,
+ panelId: string,
+ orientation: 'horizontal' | 'vertical',
+ defaultSize: number,
+ collapsedSize?: number
+) => {
+ const $isCollapsed = useState(() => atom(false))[0];
+ const collapse = useCallback(() => {
+ if (!api) {
+ return;
+ }
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+ if (orientation === 'vertical') {
+ panel.api.setSize({ height: collapsedSize ?? panel.minimumHeight });
+ } else {
+ panel.api.setSize({ width: collapsedSize ?? panel.minimumWidth });
+ }
+ }, [api, collapsedSize, orientation, panelId]);
+
+ const expand = useCallback(() => {
+ if (!api) {
+ return;
+ }
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+ if (orientation === 'vertical') {
+ panel.api.setSize({ height: defaultSize });
+ } else {
+ panel.api.setSize({ width: defaultSize });
+ }
+ }, [api, defaultSize, orientation, panelId]);
+
+ const toggle = useCallback(() => {
+ if (!api) {
+ return;
+ }
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+ const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
+ if (isCollapsed) {
+ expand();
+ } else {
+ collapse();
+ }
+ }, [api, panelId, orientation, collapsedSize, expand, collapse]);
+
+ useEffect(() => {
+ if (!api) {
+ return;
+ }
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+
+ const disposable = panel.api.onDidDimensionsChange(() => {
+ const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
+ $isCollapsed.set(isCollapsed);
+ });
+
+ return () => {
+ disposable.dispose();
+ };
+ }, [$isCollapsed, api, collapsedSize, orientation, panelId]);
+
+ return useMemo(
+ () => ({
+ $isCollapsed,
+ expand,
+ collapse,
+ toggle,
+ }),
+ [$isCollapsed, collapse, expand, toggle]
+ );
+};
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
index 3990c28aa2..4b11ca8d6a 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
@@ -5,3 +5,5 @@ export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTa
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
export const selectActiveTabCanvasRightPanel = createSelector(selectUiSlice, (ui) => ui.activeTabCanvasRightPanel);
+export const selectShowGenerateTabSplashScreen = createSelector(selectUiSlice, (ui) => ui.showGenerateTabSplashScreen);
+export const selectShowCanvasTabSplashScreen = createSelector(selectUiSlice, (ui) => ui.showCanvasTabSplashScreen);
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
index c96be420bb..2637b58d89 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
@@ -123,11 +123,11 @@ export const uiPersistConfig: PersistConfig = {
};
const TABS_WITH_LEFT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
-export const LEFT_PANEL_MIN_SIZE_PX = 400;
+export const LEFT_PANEL_MIN_SIZE_PX = 420;
export const $isLeftPanelOpen = atom(true);
export const selectWithLeftPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_LEFT_PANEL.includes(ui.activeTab));
const TABS_WITH_RIGHT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
-export const RIGHT_PANEL_MIN_SIZE_PX = 390;
+export const RIGHT_PANEL_MIN_SIZE_PX = 420;
export const $isRightPanelOpen = atom(true);
export const selectWithRightPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_RIGHT_PANEL.includes(ui.activeTab));
diff --git a/invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css b/invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css
new file mode 100644
index 0000000000..06a1e99c58
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css
@@ -0,0 +1,65 @@
+.dockview-theme-invoke {
+ --dv-paneview-active-outline-color: var(--invoke-colors-invokeBlue-300);
+ --dv-tabs-and-actions-container-font-size: var(--invoke-fontSizes-sm);
+ --dv-tabs-and-actions-container-height: var(--invoke-sizes-8);
+ --dv-drag-over-background-color: var(--invoke-colors-baseAlpha-400);
+ --dv-drag-over-border-color: var(--invoke-colors-base-300);
+ --dv-tabs-container-scrollbar-color: #888;
+ --dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
+ --dv-floating-box-shadow: none;
+ --dv-overlay-z-index: 999;
+
+ --dv-tab-font-size: inherit;
+ --dv-border-radius: 0;
+ --dv-tab-margin: 0;
+ --dv-sash-color: transparent;
+ --dv-active-sash-color: var(--invoke-colors-base-700);
+ --dv-active-sash-transition-duration: 0.15s;
+ --dv-active-sash-transition-delay: 0.1s;
+
+ --dv-group-view-background-color: var(--invoke-colors-base-900);
+
+ --dv-tabs-and-actions-container-background-color: var(--invoke-colors-base-850);
+
+ --dv-activegroup-visiblepanel-tab-color: var(--invoke-colors-base-50);
+ --dv-activegroup-visiblepanel-tab-background-color: var(--invoke-colors-base-700);
+
+ --dv-activegroup-hiddenpanel-tab-color: var(--invoke-colors-base-300);
+ --dv-activegroup-hiddenpanel-tab-background-color: var(--invoke-colors-base-850);
+
+ --dv-inactivegroup-visiblepanel-tab-color: var(--invoke-colors-base-500);
+ --dv-inactivegroup-visiblepanel-tab-background-color: var(--invoke-colors-base-800);
+
+ --dv-inactivegroup-hiddenpanel-tab-color: var(--invoke-colors-base-600);
+ --dv-inactivegroup-hiddenpanel-tab-background-color: var(--invoke-colors-base-850);
+
+ --dv-tab-divider-color: var(--invoke-colors-base-700);
+ --dv-inactivegroup-tab-divider-color: var(--invoke-colors-base-800);
+
+ --dv-separator-border: var(--invoke-colors-base-750);
+ --dv-paneview-header-border-color: rgba(204, 204, 204, 0.2);
+}
+
+.dv-default-tab-content {
+ margin-right: 0px !important;
+}
+
+.dv-groupview-floating {
+ border-radius: var(--invoke-space-2);
+ border-width: 1px;
+ border-color: var(--invoke-colors-base-800);
+ filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.4)) drop-shadow(5px 5px 10px rgba(0, 0, 0, 0.6));
+}
+
+.dv-resize-container {
+ border: none;
+}
+
+.dv-tab {
+ /* margin-right: 2px; */
+}
+
+.dv-inactive-group .dv-tabs-container.dv-horizontal .dv-tab:not(:first-child)::before {
+ /* this is the tab divider */
+ background-color: var(--dv-inactivegroup-tab-divider-color);
+}
diff --git a/invokeai/frontend/web/src/features/ui/styles/theme.ts b/invokeai/frontend/web/src/features/ui/styles/theme.ts
new file mode 100644
index 0000000000..032bc48e34
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/styles/theme.ts
@@ -0,0 +1,6 @@
+import type { DockviewTheme } from 'dockview';
+
+export const dockviewTheme: DockviewTheme = {
+ name: 'invoke',
+ className: 'dockview-theme-invoke',
+};
From 7f44da49020cba2f7e767bd0207525334b972f1b Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 18 Jun 2025 12:28:48 +1000
Subject: [PATCH 111/210] fix(ui): wonky stage sizing on first visibility
---
.../controlLayers/konva/CanvasStageModule.ts | 84 +++++++++++--------
1 file changed, 50 insertions(+), 34 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts
index 9015017c22..ec9dd69faf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts
@@ -154,36 +154,36 @@ export class CanvasStageModule extends CanvasModuleBase {
// If the stage _had_ no size just before this function was called, that means we've just mounted the stage or
// maybe un-hidden it. In that case, the user is about to see the stage for the first time, so we should fit the
// layers to the stage. If we don't do this, the layers will not be centered.
- const shouldFitLayersAfterFittingStage = this.konva.stage.width() === 0 || this.konva.stage.height() === 0;
+ if (this.konva.stage.width() === 0 || this.konva.stage.height() === 0) {
+ // This fit must happen before the stage size is set, else we can end up with a brief flash of an incorrectly
+ // sized and scaled stage.
+ this.fitLayersToStage({ animate: false, targetWidth: containerWidth, targetHeight: containerHeight });
+ }
this.konva.stage.width(containerWidth);
this.konva.stage.height(containerHeight);
this.syncStageAttrs();
-
- if (shouldFitLayersAfterFittingStage) {
- this.fitLayersToStage();
- }
};
/**
* Fits the bbox to the stage. This will center the bbox and scale it to fit the stage with some padding.
*/
- fitBboxToStage = (): void => {
+ fitBboxToStage = (options?: { animate?: boolean; targetWidth?: number; targetHeight?: number }): void => {
const { rect } = this.manager.stateApi.getBbox();
this.log.trace({ rect }, 'Fitting bbox to stage');
- this.fitRect(rect);
+ this.fitRect(rect, options);
};
/**
* Fits the visible canvas to the stage. This will center the canvas and scale it to fit the stage with some padding.
*/
- fitLayersToStage = (): void => {
+ fitLayersToStage = (options?: { animate?: boolean; targetWidth?: number; targetHeight?: number }): void => {
const rect = this.manager.compositor.getVisibleRectOfType();
if (rect.width === 0 || rect.height === 0) {
- this.fitBboxToStage();
+ this.fitBboxToStage(options);
} else {
this.log.trace({ rect }, 'Fitting layers to stage');
- this.fitRect(rect);
+ this.fitRect(rect, options);
}
};
@@ -191,12 +191,12 @@ export class CanvasStageModule extends CanvasModuleBase {
* Fits the bbox and layers to the stage. The union of the bbox and the visible layers will be centered and scaled
* to fit the stage with some padding.
*/
- fitBboxAndLayersToStage = (): void => {
+ fitBboxAndLayersToStage = (options?: { animate?: boolean; targetWidth?: number; targetHeight?: number }): void => {
const layersRect = this.manager.compositor.getVisibleRectOfType();
const bboxRect = this.manager.stateApi.getBbox().rect;
const unionRect = getRectUnion(layersRect, bboxRect);
this.log.trace({ bboxRect, layersRect, unionRect }, 'Fitting bbox and layers to stage');
- this.fitRect(unionRect);
+ this.fitRect(unionRect, options);
};
/**
@@ -204,16 +204,22 @@ export class CanvasStageModule extends CanvasModuleBase {
*
* The max scale is 1, but the stage can be scaled down to fit the rect.
*/
- fitRect = (rect: Rect): void => {
- const { width, height } = this.getSize();
+ fitRect = (rect: Rect, options?: { animate?: boolean; targetWidth?: number; targetHeight?: number }): void => {
+ const size = this.getSize();
+ const { animate, targetWidth, targetHeight } = {
+ animate: true,
+ targetWidth: size.width,
+ targetHeight: size.height,
+ ...options,
+ };
// If the stage has no size, we can't fit anything to it
- if (width === 0 || height === 0) {
+ if (targetWidth === 0 || targetHeight === 0) {
return;
}
- const availableWidth = width - this.config.FIT_LAYERS_TO_STAGE_PADDING_PX * 2;
- const availableHeight = height - this.config.FIT_LAYERS_TO_STAGE_PADDING_PX * 2;
+ const availableWidth = targetWidth - this.config.FIT_LAYERS_TO_STAGE_PADDING_PX * 2;
+ const availableHeight = targetHeight - this.config.FIT_LAYERS_TO_STAGE_PADDING_PX * 2;
// Make sure we don't accidentally set the scale to something nonsensical, like a negative number, 0 or something
// outside the valid range
@@ -231,23 +237,33 @@ export class CanvasStageModule extends CanvasModuleBase {
this._intendedScale = scale;
this._activeSnapPoint = null;
- const tween = new Konva.Tween({
- node: this.konva.stage,
- duration: 0.15,
- x,
- y,
- scaleX: scale,
- scaleY: scale,
- easing: Konva.Easings.EaseInOut,
- onUpdate: () => {
- this.syncStageAttrs();
- },
- onFinish: () => {
- this.syncStageAttrs();
- tween.destroy();
- },
- });
- tween.play();
+ if (animate) {
+ const tween = new Konva.Tween({
+ node: this.konva.stage,
+ duration: 0.15,
+ x,
+ y,
+ scaleX: scale,
+ scaleY: scale,
+ easing: Konva.Easings.EaseInOut,
+ onUpdate: () => {
+ this.syncStageAttrs();
+ },
+ onFinish: () => {
+ this.syncStageAttrs();
+ tween.destroy();
+ },
+ });
+ tween.play();
+ } else {
+ this.konva.stage.setAttrs({
+ x,
+ y,
+ scaleX: scale,
+ scaleY: scale,
+ });
+ this.syncStageAttrs();
+ }
};
/**
From 6eecdca56cdd912927b5368f819a3f4c39af467f Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 19 Jun 2025 12:42:42 +1000
Subject: [PATCH 112/210] wip
---
.../frontend/web/src/app/components/App.tsx | 6 +-
.../app/store/nanostores/globalIsLoading.ts | 13 +
.../AdvancedSession/AdvancedSession.tsx | 4 +-
.../components/CanvasLayersPanelContent.tsx | 17 +-
.../components/SimpleSession/InitialState.tsx | 4 +-
.../SimpleSession/SimpleSession.tsx | 4 +-
.../components/BoardsListPanelContent.tsx | 4 +-
.../features/gallery/components/Gallery.tsx | 4 +-
.../gallery/components/GalleryTopBar.tsx | 5 +-
.../ImageViewer/GenerationProgressPanel.tsx | 10 +
.../ImageViewer/ImageViewerPanel.tsx | 13 +
.../src/features/ui/components/AppContent.tsx | 183 +++++------
.../ui/components/RightPanelContent.tsx | 12 +-
.../src/features/ui/components/TabButton.tsx | 5 +-
.../src/features/ui/layouts/AutoLayout.tsx | 304 ++++++++++++++++--
.../ui/layouts/CanvasWorkspacePanel.tsx | 139 ++++++++
.../ui/layouts/TabWithoutCloseButton.tsx | 16 +-
.../ui/layouts/auto-layout-context.tsx | 10 +-
.../ui/layouts/canvas-tab-auto-layout.tsx | 42 +--
.../web/src/features/ui/layouts/components.ts | 29 ++
.../ui/layouts/generate-tab-auto-layout.tsx | 22 +-
21 files changed, 641 insertions(+), 205 deletions(-)
create mode 100644 invokeai/frontend/web/src/app/store/nanostores/globalIsLoading.ts
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/GenerationProgressPanel.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/components.ts
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index 96ce9888da..0cb811fec6 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
import { GlobalHookIsolator } from 'app/components/GlobalHookIsolator';
import { GlobalModalIsolator } from 'app/components/GlobalModalIsolator';
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
-import { $didStudioInit } from 'app/hooks/useStudioInitAction';
+import { $globalIsLoading } from 'app/store/nanostores/globalIsLoading';
import type { PartialAppConfig } from 'app/types/invokeai';
import Loading from 'common/components/Loading/Loading';
import { useClearStorage } from 'common/hooks/useClearStorage';
@@ -20,7 +20,7 @@ interface Props {
}
const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
- const didStudioInit = useStore($didStudioInit);
+ const globalIsLoading = useStore($globalIsLoading);
const clearStorage = useClearStorage();
const handleReset = useCallback(() => {
@@ -33,7 +33,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
- {!didStudioInit && }
+ {globalIsLoading && }
diff --git a/invokeai/frontend/web/src/app/store/nanostores/globalIsLoading.ts b/invokeai/frontend/web/src/app/store/nanostores/globalIsLoading.ts
new file mode 100644
index 0000000000..e875df4e52
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/nanostores/globalIsLoading.ts
@@ -0,0 +1,13 @@
+import { $didStudioInit } from 'app/hooks/useStudioInitAction';
+import { atom, computed } from 'nanostores';
+import { flushSync } from 'react-dom';
+
+export const $isLayoutLoading = atom(false);
+export const setIsLayoutLoading = (isLoading: boolean) => {
+ flushSync(() => {
+ $isLayoutLoading.set(isLoading);
+ });
+};
+export const $globalIsLoading = computed([$didStudioInit, $isLayoutLoading], (didStudioInit, isLayoutLoading) => {
+ return !didStudioInit || isLayoutLoading;
+});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
index 7a978ca905..fd21523c73 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
@@ -26,7 +26,7 @@ import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
-import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
@@ -84,7 +84,7 @@ export const AdvancedSession = memo(({ id }: { id: string | null }) => {
-
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx
index 6de28bbfa5..3b28be2799 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx
@@ -1,24 +1,19 @@
-import { Divider, Flex, type SystemStyleObject } from '@invoke-ai/ui-library';
+import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
-import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
+import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { ParamDenoisingStrength } from './ParamDenoisingStrength';
-const FOCUS_REGION_STYLES: SystemStyleObject = {
- width: 'full',
- height: 'full',
-};
-
-export const CanvasLayersPanelContent = memo(() => {
+export const CanvasLayersPanel = memo(() => {
const hasEntities = useAppSelector(selectHasEntities);
return (
-
+
@@ -27,8 +22,8 @@ export const CanvasLayersPanelContent = memo(() => {
{!hasEntities && }
{hasEntities && }
-
+
);
});
-CanvasLayersPanelContent.displayName = 'CanvasLayersPanelContent';
+CanvasLayersPanel.displayName = 'CanvasLayersPanel';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
index 73d867cff2..16c6510417 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -6,7 +6,7 @@ import { InitialStateMainModelPicker } from 'features/controlLayers/components/S
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
-export const InitialState = memo(() => {
+export const GenerateLaunchpadPanel = memo(() => {
const dispatch = useAppDispatch();
const newCanvasSession = useCallback(() => {
dispatch(setActiveTab('canvas'));
@@ -43,4 +43,4 @@ export const InitialState = memo(() => {
);
});
-InitialState.displayName = 'InitialState';
+GenerateLaunchpadPanel.displayName = 'InitialState';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
index 54b0be8e1a..5d5dd3b79a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
@@ -1,6 +1,6 @@
import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
@@ -25,7 +25,7 @@ export const SimpleSession = memo(() => {
-
+
diff --git a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
index bb80d8e160..edf5ca25ec 100644
--- a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
@@ -8,7 +8,7 @@ import { memo } from 'react';
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
-export const BoardsListPanelContent = memo(() => {
+export const BoardsPanel = memo(() => {
const boardSearchDisclosure = useStore($boardSearchIsOpen);
return (
@@ -23,4 +23,4 @@ export const BoardsListPanelContent = memo(() => {
);
});
-BoardsListPanelContent.displayName = 'BoardsListPanelContent';
+BoardsPanel.displayName = 'BoardsPanel';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
index 0fcfa7dd82..1679cc1fb1 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
@@ -46,7 +46,7 @@ const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '10
const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView);
const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
-export const Gallery = memo(() => {
+export const GalleryPanel = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const galleryView = useAppSelector(selectGalleryView);
@@ -117,4 +117,4 @@ export const Gallery = memo(() => {
);
});
-Gallery.displayName = 'Gallery';
+GalleryPanel.displayName = 'Gallery';
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
index a5ff519384..6a05236929 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
@@ -17,8 +17,9 @@ export const GalleryTopBar = memo(() => {
const dispatch = useAppDispatch();
const boardSearchText = useAppSelector(selectBoardSearchText);
const boardSearchDisclosure = useBoardSearchDisclosure();
- const api = useAutoLayoutContext();
- const boardsPanel = useCollapsibleGridviewPanel(api, 'boards', 'vertical', 256);
+ const $api = useAutoLayoutContext();
+ const api = useStore($api);
+ const boardsPanel = useCollapsibleGridviewPanel(api, 'Boards', 'vertical', 256);
const isBoardsPanelCollapsed = useStore(boardsPanel.$isCollapsed);
const onClickBoardSearch = useCallback(() => {
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/GenerationProgressPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/GenerationProgressPanel.tsx
new file mode 100644
index 0000000000..bfcda19650
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/GenerationProgressPanel.tsx
@@ -0,0 +1,10 @@
+import { Flex } from '@invoke-ai/ui-library';
+import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
+import { memo } from 'react';
+
+export const GenerationProgressPanel = memo(() => (
+
+
+
+));
+GenerationProgressPanel.displayName = 'GenerationProgressPanel';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx
new file mode 100644
index 0000000000..0fd32cfa54
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx
@@ -0,0 +1,13 @@
+import { Divider, Flex } from '@invoke-ai/ui-library';
+import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
+import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
+import { memo } from 'react';
+
+export const ImageViewerPanel = memo(() => (
+
+
+
+
+
+));
+ImageViewerPanel.displayName = 'ImageViewerPanel';
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 74bcdc6119..c66fdfb6c8 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -1,27 +1,13 @@
import 'dockview/dist/styles/dockview.css';
import 'features/ui/styles/dockview-theme-invoke.css';
-import { TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
+import { Flex } from '@invoke-ai/ui-library';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
-import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
-import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
-import { usePanel } from 'features/ui/hooks/usePanel';
-import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
-import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
-import {
- $isLeftPanelOpen,
- $isRightPanelOpen,
- LEFT_PANEL_MIN_SIZE_PX,
- RIGHT_PANEL_MIN_SIZE_PX,
- selectWithLeftPanel,
- selectWithRightPanel,
-} from 'features/ui/store/uiSlice';
+import { AutoLayout } from 'features/ui/layouts/AutoLayout';
+import { $isLeftPanelOpen, $isRightPanelOpen } from 'features/ui/store/uiSlice';
import type { CSSProperties } from 'react';
-import { memo, useMemo, useRef } from 'react';
-import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
+import { memo } from 'react';
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
@@ -29,97 +15,88 @@ const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCo
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
export const AppContent = memo(() => {
- const tab = useAppSelector(selectActiveTab);
- const imperativePanelGroupRef = useRef(null);
+ // const tab = useAppSelector(selectActiveTab);
+ // const imperativePanelGroupRef = useRef(null);
useDndMonitor();
- const withLeftPanel = useAppSelector(selectWithLeftPanel);
- const leftPanelUsePanelOptions = useMemo(
- () => ({
- id: 'left-panel',
- minSizePx: LEFT_PANEL_MIN_SIZE_PX,
- defaultSizePx: LEFT_PANEL_MIN_SIZE_PX,
- imperativePanelGroupRef,
- panelGroupDirection: 'horizontal',
- onCollapse: onLeftPanelCollapse,
- }),
- []
- );
- const leftPanel = usePanel(leftPanelUsePanelOptions);
- useRegisteredHotkeys({
- id: 'toggleLeftPanel',
- category: 'app',
- callback: leftPanel.toggle,
- options: { enabled: withLeftPanel },
- dependencies: [leftPanel.toggle, withLeftPanel],
- });
+ // const withLeftPanel = useAppSelector(selectWithLeftPanel);
+ // const leftPanelUsePanelOptions = useMemo(
+ // () => ({
+ // id: 'left-panel',
+ // minSizePx: LEFT_PANEL_MIN_SIZE_PX,
+ // defaultSizePx: LEFT_PANEL_MIN_SIZE_PX,
+ // imperativePanelGroupRef,
+ // panelGroupDirection: 'horizontal',
+ // onCollapse: onLeftPanelCollapse,
+ // }),
+ // []
+ // );
+ // const leftPanel = usePanel(leftPanelUsePanelOptions);
+ // useRegisteredHotkeys({
+ // id: 'toggleLeftPanel',
+ // category: 'app',
+ // callback: leftPanel.toggle,
+ // options: { enabled: withLeftPanel },
+ // dependencies: [leftPanel.toggle, withLeftPanel],
+ // });
- const withRightPanel = useAppSelector(selectWithRightPanel);
- const rightPanelUsePanelOptions = useMemo(
- () => ({
- id: 'right-panel',
- minSizePx: RIGHT_PANEL_MIN_SIZE_PX,
- defaultSizePx: RIGHT_PANEL_MIN_SIZE_PX,
- imperativePanelGroupRef,
- panelGroupDirection: 'horizontal',
- onCollapse: onRightPanelCollapse,
- }),
- []
- );
- const rightPanel = usePanel(rightPanelUsePanelOptions);
- useRegisteredHotkeys({
- id: 'toggleRightPanel',
- category: 'app',
- callback: rightPanel.toggle,
- options: { enabled: withRightPanel },
- dependencies: [rightPanel.toggle, withRightPanel],
- });
+ // const withRightPanel = useAppSelector(selectWithRightPanel);
+ // const rightPanelUsePanelOptions = useMemo(
+ // () => ({
+ // id: 'right-panel',
+ // minSizePx: RIGHT_PANEL_MIN_SIZE_PX,
+ // defaultSizePx: RIGHT_PANEL_MIN_SIZE_PX,
+ // imperativePanelGroupRef,
+ // panelGroupDirection: 'horizontal',
+ // onCollapse: onRightPanelCollapse,
+ // }),
+ // []
+ // );
+ // const rightPanel = usePanel(rightPanelUsePanelOptions);
+ // useRegisteredHotkeys({
+ // id: 'toggleRightPanel',
+ // category: 'app',
+ // callback: rightPanel.toggle,
+ // options: { enabled: withRightPanel },
+ // dependencies: [rightPanel.toggle, withRightPanel],
+ // });
- useRegisteredHotkeys({
- id: 'resetPanelLayout',
- category: 'app',
- callback: () => {
- leftPanel.reset();
- rightPanel.reset();
- },
- dependencies: [leftPanel.reset, rightPanel.reset],
- });
- useRegisteredHotkeys({
- id: 'togglePanels',
- category: 'app',
- callback: () => {
- if (leftPanel.isCollapsed || rightPanel.isCollapsed) {
- leftPanel.expand();
- rightPanel.expand();
- } else {
- leftPanel.collapse();
- rightPanel.collapse();
- }
- },
- dependencies: [
- leftPanel.isCollapsed,
- rightPanel.isCollapsed,
- leftPanel.expand,
- rightPanel.expand,
- leftPanel.collapse,
- rightPanel.collapse,
- ],
- });
+ // useRegisteredHotkeys({
+ // id: 'resetPanelLayout',
+ // category: 'app',
+ // callback: () => {
+ // leftPanel.reset();
+ // rightPanel.reset();
+ // },
+ // dependencies: [leftPanel.reset, rightPanel.reset],
+ // });
+ // useRegisteredHotkeys({
+ // id: 'togglePanels',
+ // category: 'app',
+ // callback: () => {
+ // if (leftPanel.isCollapsed || rightPanel.isCollapsed) {
+ // leftPanel.expand();
+ // rightPanel.expand();
+ // } else {
+ // leftPanel.collapse();
+ // rightPanel.collapse();
+ // }
+ // },
+ // dependencies: [
+ // leftPanel.isCollapsed,
+ // rightPanel.isCollapsed,
+ // leftPanel.expand,
+ // rightPanel.expand,
+ // leftPanel.collapse,
+ // rightPanel.collapse,
+ // ],
+ // });
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
});
AppContent.displayName = 'AppContent';
diff --git a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
index 6625ce0175..04bf65154d 100644
--- a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx
@@ -2,10 +2,10 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
-import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
+import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
-import { Gallery } from 'features/gallery/components/Gallery';
+import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
+import { GalleryPanel } from 'features/gallery/components/Gallery';
import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
@@ -71,18 +71,18 @@ export const RightPanelContent = memo(() => {
-
+
-
+
{tab === 'canvas' && (
<>
-
+
>
diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
index 1a8850f309..8974a9c326 100644
--- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { IconButton, Tab, Tooltip } from '@invoke-ai/ui-library';
+import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -25,8 +25,7 @@ export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: React
return (
- {
+ return (
+ api.getPanel('GenerateLaunchpad') ||
+ api.addPanel({
+ id: 'GenerateLaunchpad',
+ component: 'GenerateLaunchpad',
+ title: 'Launchpad',
+ })
+ );
+};
+
+const getCanvasLaunchpadPanel = (api: DockviewApi) => {
+ return (
+ api.getPanel('CanvasLaunchpad') ||
+ api.addPanel({
+ id: 'CanvasLaunchpad',
+ component: 'CanvasLaunchpad',
+ title: 'Launchpad',
+ })
+ );
+};
+
+const getCanvasWorkspacePanel = (api: DockviewApi) => {
+ return (
+ api.getPanel('CanvasWorkspace') ||
+ api.addPanel({
+ id: 'CanvasWorkspace',
+ component: 'CanvasWorkspace',
+ title: 'Canvas',
+ })
+ );
+};
+
+const getImageViewerPanel = (api: DockviewApi) => {
+ return (
+ api.getPanel('ImageViewer') ||
+ api.addPanel({
+ id: 'ImageViewer',
+ component: 'ImageViewer',
+ title: 'Image Viewer',
+ })
+ );
+};
+
+const getGenerationProgressPanel = (api: DockviewApi) => {
+ return (
+ api.getPanel('GenerationProgress') ||
+ api.addPanel({
+ id: 'GenerationProgress',
+ component: 'GenerationProgress',
+ title: 'Generation Progress',
+ })
+ );
+};
+
+const syncMainPanelLayout = (tab: TabName, api: DockviewApi) => {
+ if (tab === 'generate') {
+ const GenerateLaunchpad = getGenerateLaunchpadPanel(api);
+ const ImageViewer = getImageViewerPanel(api);
+ const GenerationProgress = getGenerationProgressPanel(api);
+ const panelsToKeep = [GenerateLaunchpad.id, ImageViewer.id, GenerationProgress.id];
+ for (const panel of api.panels) {
+ if (!panelsToKeep.includes(panel.id)) {
+ api.removePanel(panel);
+ }
+ }
+ } else if (tab === 'canvas') {
+ const CanvasLaunchpad = getCanvasLaunchpadPanel(api);
+ const CanvasWorkspace = getCanvasWorkspacePanel(api);
+ const ImageViewer = getImageViewerPanel(api);
+ const GenerationProgress = getGenerationProgressPanel(api);
+ const panelsToKeep = [CanvasLaunchpad.id, CanvasWorkspace.id, ImageViewer.id, GenerationProgress.id];
+ for (const panel of api.panels) {
+ if (!panelsToKeep.includes(panel.id)) {
+ api.removePanel(panel);
+ }
+ }
+ }
+};
+
+const MainPanel = memo(() => {
+ const tab = useAppSelector(selectActiveTab);
+ const [api, setApi] = useState(null);
+ const onReady = useCallback((event) => {
+ console.log('MainPanel onReady', event.api);
+ setApi(event.api);
+ }, []);
+
+ useEffect(() => {
+ if (api) {
+ syncMainPanelLayout(tab, api);
+ }
+ }, [api, tab]);
+
+ return (
+
+
+
+ );
+});
+MainPanel.displayName = 'MainPanel';
+
+export const gridviewComponents: IGridviewReactProps['components'] = {
+ // Shared components
+ Gallery: GalleryPanel,
+ Boards: BoardsPanel,
+ Main: MainPanel,
+ GenerateLeft: GenerateLeftPanel,
+ CanvasLeft: GenerateLeftPanel,
+ CanvasLayers: CanvasLayersPanel,
+};
+
+const syncGridviewLayout = (tab: TabName, api: GridviewApi) => {
+ if (tab === 'generate') {
+ const MainPanel =
+ api.getPanel('Main') ??
+ api.addPanel({
+ id: 'Main',
+ component: 'Main',
+ });
+
+ const GenerateLeftPanel =
+ api.getPanel('GenerateLeft') ??
+ api.addPanel({
+ id: 'GenerateLeft',
+ component: 'GenerateLeft',
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: MainPanel.id,
+ },
+ });
+
+ const GalleryPanel =
+ api.getPanel('Gallery') ??
+ api.addPanel({
+ id: 'Gallery',
+ component: 'Gallery',
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ position: {
+ direction: 'right',
+ referencePanel: MainPanel.id,
+ },
+ });
+
+ const BoardsPanel =
+ api.getPanel('Boards') ??
+ api.addPanel({
+ id: 'Boards',
+ component: 'Boards',
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 36,
+ position: {
+ direction: 'above',
+ referencePanel: GalleryPanel.id,
+ },
+ });
+
+ const panelsToKeep = [MainPanel.id, GenerateLeftPanel.id, GalleryPanel.id, BoardsPanel.id];
+ for (const panel of api.panels) {
+ if (!panelsToKeep.includes(panel.id)) {
+ api.removePanel(panel);
+ }
+ }
+ } else if (tab === 'canvas') {
+ const MainPanel =
+ api.getPanel('Main') ??
+ api.addPanel({
+ id: 'Main',
+ component: 'Main',
+ });
+
+ const CanvasLeftPanel =
+ api.getPanel('CanvasLeft') ??
+ api.addPanel({
+ id: 'CanvasLeft',
+ component: 'CanvasLeft',
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: MainPanel.id,
+ },
+ });
+
+ const GalleryPanel =
+ api.getPanel('Gallery') ??
+ api.addPanel({
+ id: 'Gallery',
+ component: 'Gallery',
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ position: {
+ direction: 'right',
+ referencePanel: MainPanel.id,
+ },
+ });
+
+ const BoardsPanel =
+ api.getPanel('Boards') ??
+ api.addPanel({
+ id: 'Boards',
+ component: 'Boards',
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 36,
+ position: {
+ direction: 'above',
+ referencePanel: GalleryPanel.id,
+ },
+ });
+
+ const CanvasLayersPanel =
+ api.getPanel('CanvasLayers') ??
+ api.addPanel({
+ id: 'CanvasLayers',
+ component: 'CanvasLayers',
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ position: {
+ direction: 'below',
+ referencePanel: GalleryPanel.id,
+ },
+ });
+
+ const panelsToKeep = [MainPanel.id, CanvasLeftPanel.id, GalleryPanel.id, BoardsPanel.id, CanvasLayersPanel.id];
+ for (const panel of api.panels) {
+ if (!panelsToKeep.includes(panel.id)) {
+ api.removePanel(panel);
+ }
+ }
+ }
};
export const AutoLayout = memo(() => {
const tab = useAppSelector(selectActiveTab);
- const [api, setApi] = useState(null);
- const syncLayout = useCallback((tab: TabName, api: GridviewApi) => {
- if (tab === 'generate') {
- initializeGenerateTabLayout(api);
- } else if (tab === 'canvas') {
- initializeCanvasTabLayout(api);
- }
- }, []);
- const onReady = useCallback((event) => {
- setApi(event.api);
- }, []);
+ const $api = useState(() => atom(null))[0];
+ const onReady = useCallback(
+ (event) => {
+ $api.set(event.api);
+ },
+ [$api]
+ );
useEffect(() => {
+ const api = $api.get();
if (api) {
- syncLayout(tab, api);
+ syncGridviewLayout(tab, api);
}
- }, [api, syncLayout, tab]);
+ }, [$api, tab]);
return (
-
+
diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx
new file mode 100644
index 0000000000..1a2a592ddd
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx
@@ -0,0 +1,139 @@
+import { ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
+import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
+import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
+import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
+import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
+import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
+import { Filter } from 'features/controlLayers/components/Filters/Filter';
+import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
+import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
+import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
+import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
+import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
+import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
+import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
+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';
+
+const MenuContent = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+MenuContent.displayName = 'MenuContent';
+
+const canvasBgSx = {
+ position: 'relative',
+ w: 'full',
+ h: 'full',
+ borderRadius: 'base',
+ overflow: 'hidden',
+ bg: 'base.900',
+ '&[data-dynamic-grid="true"]': {
+ bg: 'base.850',
+ },
+};
+
+export const CanvasWorkspacePanel = memo(() => {
+ const dynamicGrid = useAppSelector(selectDynamicGrid);
+ const showHUD = useAppSelector(selectShowHUD);
+ const canvasId = useAppSelector(selectCanvasSessionId);
+
+ const renderMenu = useCallback(() => {
+ return ;
+ }, []);
+
+ return (
+
+
+
+
+
+ renderMenu={renderMenu} withLongPress={false}>
+ {(ref) => (
+
+
+
+
+ {showHUD && }
+
+
+
+
+
+
+ } colorScheme="base" />
+
+
+
+
+
+ )}
+
+ {canvasId !== null && (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+CanvasWorkspacePanel.displayName = 'CanvasPanel';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
index 77e469a462..0d4803adca 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
@@ -1,10 +1,9 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import type { IDockviewPanelHeaderProps } from 'dockview';
-import { useCallback, useEffect, useId, useRef } from 'react';
+import { useCallback, useRef } from 'react';
export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => {
- const id = useId();
const ref = useRef(null);
const setActive = useCallback(() => {
if (!props.api.isActive) {
@@ -13,18 +12,7 @@ export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => {
}, [props.api]);
useCallbackOnDragEnter(setActive, ref, 300);
-
- useEffect(() => {
- const el = document.querySelector(`[data-id="${id}"]`);
- if (!el) {
- return;
- }
- const parentTab = el.closest('.dv-tab');
- if (!parentTab) {
- return;
- }
- parentTab.setAttribute('draggable', 'false');
- }, [id]);
+ console.log(props.api.title);
return (
diff --git a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
index d86ae4bee1..87ad53faf8 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
@@ -1,14 +1,18 @@
import type { GridviewApi } from 'dockview';
+import type { Atom } from 'nanostores';
import type { PropsWithChildren } from 'react';
import { createContext, useContext } from 'react';
-const AutoLayoutContext = createContext(null);
+const AutoLayoutContext = createContext | null>(null);
-export const AutoLayoutProvider = (props: PropsWithChildren<{ api: GridviewApi | null }>) => {
- return {props.children};
+export const AutoLayoutProvider = (props: PropsWithChildren<{ $api: Atom }>) => {
+ return {props.children};
};
export const useAutoLayoutContext = () => {
const api = useContext(AutoLayoutContext);
+ if (!api) {
+ throw new Error('useAutoLayoutContext must be used within an AutoLayoutProvider');
+ }
return api;
};
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 43ea4dc4e1..ed2cc14b8f 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
@@ -1,4 +1,5 @@
import { Box, ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
+import { $isLayoutLoading } from 'app/store/nanostores/globalIsLoading';
import { useAppSelector } from 'app/store/storeHooks';
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
@@ -8,13 +9,13 @@ import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/compone
import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
-import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
+import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { Filter } from 'features/controlLayers/components/Filters/Filter';
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
-import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
@@ -22,14 +23,15 @@ 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 { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
-import { Gallery } from 'features/gallery/components/Gallery';
+import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
+import { GalleryPanel } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
import QueueControls from 'features/queue/components/QueueControls';
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { components } from 'features/ui/layouts/components';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import { dockviewTheme } from 'features/ui/styles/theme';
@@ -60,7 +62,7 @@ const canvasBgSx = {
},
};
-export const CanvasPanel = memo(() => {
+export const CanvasWorkspacePanel = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
const canvasId = useAppSelector(selectCanvasSessionId);
@@ -151,11 +153,11 @@ export const CanvasPanel = memo(() => {
);
});
-CanvasPanel.displayName = 'CanvasPanel';
+CanvasWorkspacePanel.displayName = 'CanvasPanel';
const LayersPanelContent = memo(() => (
-
+
));
LayersPanelContent.displayName = 'LayersPanelContent';
@@ -177,8 +179,8 @@ const ProgressPanelContent = memo(() => (
ProgressPanelContent.displayName = 'ProgressPanelContent';
const mainPanelComponents: IDockviewReactProps['components'] = {
- welcome: InitialState,
- canvas: CanvasPanel,
+ canvasLaunchpad: GenerateLaunchpadPanel,
+ canvas: CanvasWorkspacePanel,
viewer: ViewerPanelContent,
progress: ProgressPanelContent,
};
@@ -186,9 +188,9 @@ const mainPanelComponents: IDockviewReactProps['components'] = {
const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const { api } = event;
api.addPanel({
- id: 'welcome',
- component: 'welcome',
- title: 'Launchpad',
+ id: 'canvasLaunchpad',
+ component: 'canvasLaunchpad',
+ title: 'canvasLaunchpad',
});
api.addPanel({
id: 'canvas',
@@ -196,7 +198,7 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
title: 'Canvas',
position: {
direction: 'within',
- referencePanel: 'welcome',
+ referencePanel: 'canvasLaunchpad',
},
});
api.addPanel({
@@ -205,7 +207,7 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
title: 'Image Viewer',
position: {
direction: 'within',
- referencePanel: 'welcome',
+ referencePanel: 'canvasLaunchpad',
},
});
api.addPanel({
@@ -214,7 +216,7 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
title: 'Generation Progress',
position: {
direction: 'within',
- referencePanel: 'welcome',
+ referencePanel: 'canvasLaunchpad',
},
});
@@ -243,7 +245,7 @@ const MainPanel = memo(() => {
disableFloatingGroups={true}
dndEdges={false}
defaultTabComponent={TabWithoutCloseButton}
- components={mainPanelComponents}
+ components={components}
onReady={onReadyMainPanel}
theme={dockviewTheme}
/>
@@ -264,11 +266,13 @@ const Left = memo(() => {
});
Left.displayName = 'Left';
+const Null = () => null;
+
export const canvasTabComponents: IGridviewReactProps['components'] = {
left: Left,
main: MainPanel,
- boards: BoardsListPanelContent,
- gallery: Gallery,
+ boards: BoardsPanel,
+ gallery: GalleryPanel,
layers: LayersPanelContent,
};
@@ -322,8 +326,10 @@ export const initializeCanvasTabLayout = (api: GridviewApi) => {
export const CanvasTabAutoLayout = memo(() => {
const [api, setApi] = useState(null);
const onReady = useCallback((event) => {
+ $isLayoutLoading.set(true);
setApi(event.api);
initializeCanvasTabLayout(event.api);
+ $isLayoutLoading.set(false);
}, []);
return (
diff --git a/invokeai/frontend/web/src/features/ui/layouts/components.ts b/invokeai/frontend/web/src/features/ui/layouts/components.ts
new file mode 100644
index 0000000000..becb52fc3b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/components.ts
@@ -0,0 +1,29 @@
+import type { IDockviewReactProps, IGridviewReactProps } from 'dockview';
+import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
+import { GalleryPanel } from 'features/gallery/components/Gallery';
+import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
+import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
+import { CanvasWorkspacePanel } from 'features/ui/layouts/canvas-tab-auto-layout';
+import { GenerateLeftPanel } from 'features/ui/layouts/generate-tab-auto-layout';
+
+export const components: IDockviewReactProps['components'] & IGridviewReactProps['components'] = {
+ // Shared components
+ Gallery: GalleryPanel,
+ Boards: BoardsPanel,
+ ImageViewer: ImageViewerPanel,
+ GenerationProgress: GenerationProgressPanel,
+ // Generate tab
+ GenerateLaunchpad: GenerateLaunchpadPanel,
+ GenerateLeft: GenerateLeftPanel,
+ // Upscaling tab
+ UpscalingLaunchpad: GenerateLaunchpadPanel,
+ // Workflows tab
+ WorkflowsLaunchpad: GenerateLaunchpadPanel,
+ // Canvas tab
+ CanvasLaunchpad: GenerateLaunchpadPanel,
+ CanvasLayers: CanvasLayersPanel,
+ CanvasWorkspace: CanvasWorkspacePanel,
+ CanvasLeft: GenerateLeftPanel,
+};
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 f97a404652..26ade5fe55 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
@@ -1,9 +1,10 @@
import { Box, Divider, Flex } from '@invoke-ai/ui-library';
+import { $isLayoutLoading } from 'app/store/nanostores/globalIsLoading';
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
-import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
-import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
-import { Gallery } from 'features/gallery/components/Gallery';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
+import { GalleryPanel } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
@@ -32,7 +33,7 @@ const ProgressPanelContent = memo(() => (
ProgressPanelContent.displayName = 'ProgressPanelContent';
const mainPanelComponents: IDockviewReactProps['components'] = {
- welcome: InitialState,
+ welcome: GenerateLaunchpadPanel,
viewer: ViewerPanelContent,
progress: ProgressPanelContent,
};
@@ -97,7 +98,7 @@ const MainPanel = memo(() => {
});
MainPanel.displayName = 'MainPanel';
-const Left = memo(() => {
+export const GenerateLeftPanel = memo(() => {
return (
@@ -107,13 +108,13 @@ const Left = memo(() => {
);
});
-Left.displayName = 'Left';
+GenerateLeftPanel.displayName = 'GenerateLeftPanel';
export const generateTabComponents: IGridviewReactProps['components'] = {
- left: Left,
+ left: GenerateLeftPanel,
main: MainPanel,
- boards: BoardsListPanelContent,
- gallery: Gallery,
+ boards: BoardsPanel,
+ gallery: GalleryPanel,
};
export const initializeGenerateTabLayout = (api: GridviewApi) => {
@@ -157,9 +158,10 @@ export const initializeGenerateTabLayout = (api: GridviewApi) => {
export const GenerateTabAutoLayout = memo(() => {
const [api, setApi] = useState(null);
const onReady = useCallback((event) => {
- console.log('GenerateTabAutoLayout onReady');
+ $isLayoutLoading.set(true);
setApi(event.api);
initializeGenerateTabLayout(event.api);
+ $isLayoutLoading.set(false);
}, []);
return (
From fcaeba290eb95562158dc99a86945a84accc7d59 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 19 Jun 2025 19:28:45 +1000
Subject: [PATCH 113/210] feat(ui): canvas launchpad
---
.../AdvancedSession/AdvancedSession.tsx | 2 +-
.../SimpleSession/CanvasLaunchpadPanel.tsx | 36 +++++++++++++++++++
...alState.tsx => GenerateLaunchpadPanel.tsx} | 11 +++---
...nce.tsx => LaunchpadAddStyleReference.tsx} | 10 +++---
...ButtonGridItem.tsx => LaunchpadButton.tsx} | 4 +--
...eCard.tsx => LaunchpadEditImageButton.tsx} | 18 +++++-----
...sx => LaunchpadGenerateFromTextButton.tsx} | 10 +++---
...tsx => LaunchpadUseALayoutImageButton.tsx} | 19 +++++-----
.../SimpleSession/SimpleSession.tsx | 2 +-
.../Toolbar/CanvasToolbarResetViewButton.tsx | 4 +--
.../src/features/ui/layouts/AutoLayout.tsx | 5 +--
.../ui/layouts/canvas-tab-auto-layout.tsx | 2 +-
.../web/src/features/ui/layouts/components.ts | 2 +-
.../ui/layouts/generate-tab-auto-layout.tsx | 2 +-
14 files changed, 85 insertions(+), 42 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx
rename invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/{InitialState.tsx => GenerateLaunchpadPanel.tsx} (81%)
rename invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/{InitialStateAddAStyleReference.tsx => LaunchpadAddStyleReference.tsx} (82%)
rename invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/{InitialStateButtonGridItem.tsx => LaunchpadButton.tsx} (82%)
rename invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/{InitialStateEditImageCard.tsx => LaunchpadEditImageButton.tsx} (69%)
rename invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/{InitialStateGenerateFromText.tsx => LaunchpadGenerateFromTextButton.tsx} (66%)
rename invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/{InitialStateUseALayoutImageCard.tsx => LaunchpadUseALayoutImageButton.tsx} (70%)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
index fd21523c73..bed3ef2bb6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
@@ -26,7 +26,7 @@ import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
-import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx
new file mode 100644
index 0000000000..4e628e6f46
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx
@@ -0,0 +1,36 @@
+import { Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+
+import { InitialStateMainModelPicker } from './InitialStateMainModelPicker';
+import { LaunchpadAddStyleReference } from './LaunchpadAddStyleReference';
+import { LaunchpadEditImageButton } from './LaunchpadEditImageButton';
+import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton';
+import { LaunchpadUseALayoutImageButton } from './LaunchpadUseALayoutImageButton';
+
+export const CanvasLaunchpadPanel = memo(() => {
+ return (
+
+
+ Get started with Invoke.
+
+
+
+
+
+ Want to learn what prompts work best for each model?{' '}
+
+ Check our our Model Guide.
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+CanvasLaunchpadPanel.displayName = 'CanvasLaunchpadPanel';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx
similarity index 81%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx
index 16c6510417..3999f0580f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx
@@ -1,11 +1,12 @@
import { Alert, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
-import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
import { InitialStateMainModelPicker } from 'features/controlLayers/components/SimpleSession/InitialStateMainModelPicker';
+import { LaunchpadAddStyleReference } from 'features/controlLayers/components/SimpleSession/LaunchpadAddStyleReference';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
+import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton';
+
export const GenerateLaunchpadPanel = memo(() => {
const dispatch = useAppDispatch();
const newCanvasSession = useCallback(() => {
@@ -28,8 +29,8 @@ export const GenerateLaunchpadPanel = memo(() => {
-
-
+
+
Looking to get more control, edit, and iterate on your images?
@@ -43,4 +44,4 @@ export const GenerateLaunchpadPanel = memo(() => {
);
});
-GenerateLaunchpadPanel.displayName = 'InitialState';
+GenerateLaunchpadPanel.displayName = 'GenerateLaunchpad';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadAddStyleReference.tsx
similarity index 82%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadAddStyleReference.tsx
index 9a1c53a0a4..b4dfcff423 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadAddStyleReference.tsx
@@ -1,7 +1,7 @@
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
-import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
+import { LaunchpadButton } from 'features/controlLayers/components/SimpleSession/LaunchpadButton';
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
@@ -13,7 +13,7 @@ import type { ImageDTO } from 'services/api/types';
const dndTargetData = addGlobalReferenceImageDndTarget.getData();
-export const InitialStateAddAStyleReference = memo(() => {
+export const LaunchpadAddStyleReference = memo(() => {
const { dispatch, getState } = useAppStore();
const uploadOptions = useMemo(
@@ -32,7 +32,7 @@ export const InitialStateAddAStyleReference = memo(() => {
const uploadApi = useImageUploadButton(uploadOptions);
return (
-
+
Add a Style Reference
@@ -43,7 +43,7 @@ export const InitialStateAddAStyleReference = memo(() => {
-
+
);
});
-InitialStateAddAStyleReference.displayName = 'InitialStateAddAStyleReference';
+LaunchpadAddStyleReference.displayName = 'LaunchpadAddStyleReference';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadButton.tsx
similarity index 82%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadButton.tsx
index c7b85239d9..150fada4e6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateButtonGridItem.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadButton.tsx
@@ -2,7 +2,7 @@ import type { ButtonProps } from '@invoke-ai/ui-library';
import { Button, forwardRef } from '@invoke-ai/ui-library';
import { memo } from 'react';
-export const InitialStateButtonGridItem = memo(
+export const LaunchpadButton = memo(
forwardRef(({ children, ...rest }: ButtonProps, ref) => {
return (
{
+export const LaunchpadEditImageButton = memo(() => {
const { getState, dispatch } = useAppStore();
const onUpload = useCallback(
@@ -25,16 +25,18 @@ export const InitialStateEditImageCard = memo(() => {
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
return (
-
+
- Edit Image
- Add an image to refine.
-
+
+ Edit Image
+ Add an image to refine.
+
+
-
+
);
});
-InitialStateEditImageCard.displayName = 'InitialStateEditImageCard';
+LaunchpadEditImageButton.displayName = 'LaunchpadEditImageButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadGenerateFromTextButton.tsx
similarity index 66%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadGenerateFromTextButton.tsx
index b684ba60ec..bac9b2e750 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateGenerateFromText.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadGenerateFromTextButton.tsx
@@ -1,5 +1,5 @@
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
-import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
+import { LaunchpadButton } from 'features/controlLayers/components/SimpleSession/LaunchpadButton';
import { memo } from 'react';
import { PiCursorTextBold, PiTextAaBold } from 'react-icons/pi';
@@ -11,9 +11,9 @@ const focusOnPrompt = () => {
}
};
-export const InitialStateGenerateFromText = memo(() => {
+export const LaunchpadGenerateFromTextButton = memo(() => {
return (
-
+
Generate from Text
@@ -22,7 +22,7 @@ export const InitialStateGenerateFromText = memo(() => {
-
+
);
});
-InitialStateGenerateFromText.displayName = 'InitialStateGenerateFromText';
+LaunchpadGenerateFromTextButton.displayName = 'LaunchpadGenerateFromTextButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadUseALayoutImageButton.tsx
similarity index 70%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadUseALayoutImageButton.tsx
index 93e2e3b03d..204a684abb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/LaunchpadUseALayoutImageButton.tsx
@@ -1,7 +1,6 @@
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
-import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
@@ -9,11 +8,13 @@ import { memo, useCallback } from 'react';
import { PiRectangleDashedBold, PiUploadBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
+import { LaunchpadButton } from './LaunchpadButton';
+
const NEW_CANVAS_OPTIONS = { type: 'control_layer', withResize: true } as const;
const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
-export const InitialStateUseALayoutImageCard = memo(() => {
+export const LaunchpadUseALayoutImageButton = memo(() => {
const { getState, dispatch } = useAppStore();
const onUpload = useCallback(
@@ -25,16 +26,18 @@ export const InitialStateUseALayoutImageCard = memo(() => {
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
return (
-
+
- Use a Layout Image
- Add an image to control composition.
-
+
+ Use a Layout Image
+ Add an image to control composition.
+
+
-
+
);
});
-InitialStateUseALayoutImageCard.displayName = 'InitialStateUseALayoutImageCard';
+LaunchpadUseALayoutImageButton.displayName = 'LaunchpadUseALayoutImageButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
index 5d5dd3b79a..4ea346009a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
@@ -1,6 +1,6 @@
import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx
index 547575cff2..6c49342442 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx
@@ -14,14 +14,14 @@ export const CanvasToolbarResetViewButton = memo(() => {
useRegisteredHotkeys({
id: 'fitLayersToCanvas',
category: 'canvas',
- callback: canvasManager.stage.fitLayersToStage,
+ callback: () => canvasManager.stage.fitLayersToStage(),
options: { enabled: isCanvasFocused, preventDefault: true },
dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'fitBboxToCanvas',
category: 'canvas',
- callback: canvasManager.stage.fitBboxToStage,
+ callback: () => canvasManager.stage.fitBboxToStage(),
options: { enabled: isCanvasFocused, preventDefault: true },
dependencies: [isCanvasFocused],
});
diff --git a/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx b/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
index 9c205e8cd6..4422adac97 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
@@ -3,7 +3,8 @@ import { useAppSelector } from 'app/store/storeHooks';
import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent';
-import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { CanvasLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
@@ -30,7 +31,7 @@ export const dockviewComponents: IDockviewReactProps['components'] = {
// Workflows tab
WorkflowsLaunchpad: GenerateLaunchpadPanel,
// Canvas tab
- CanvasLaunchpad: GenerateLaunchpadPanel,
+ CanvasLaunchpad: CanvasLaunchpadPanel,
CanvasWorkspace: CanvasWorkspacePanel,
};
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 ed2cc14b8f..66c200db3f 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,7 +15,7 @@ import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
-import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/components.ts b/invokeai/frontend/web/src/features/ui/layouts/components.ts
index becb52fc3b..3182aebe91 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/components.ts
+++ b/invokeai/frontend/web/src/features/ui/layouts/components.ts
@@ -1,6 +1,6 @@
import type { IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent';
-import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
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 26ade5fe55..d24bf3707a 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
@@ -2,7 +2,7 @@ import { Box, Divider, Flex } from '@invoke-ai/ui-library';
import { $isLayoutLoading } from 'app/store/nanostores/globalIsLoading';
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
-import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
From e7e1142c77b27d2edddef26ac1381841ddc6f24e Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 19 Jun 2025 23:49:37 +1000
Subject: [PATCH 114/210] feat(ui): get layouts working
---
.../components/CanvasLayersPanelContent.tsx | 2 +-
.../SimpleSession/CanvasLaunchpadPanel.tsx | 2 +-
.../SimpleSession/GenerateLaunchpadPanel.tsx | 2 +-
.../SimpleSession/UpscalingLaunchpadPanel.tsx | 13 +
.../components/Toolbar/CanvasToolbar.tsx | 7 +-
.../src/features/system/store/configSlice.ts | 11 +
.../src/features/ui/components/AppContent.tsx | 29 +-
...tToImage.tsx => ParametersPanelCanvas.tsx} | 6 +-
.../ParametersPanelGenerate.tsx | 58 +++
.../ParametersPanelUpscale.tsx | 6 +-
.../src/features/ui/components/TabButton.tsx | 5 +-
.../ui/layouts/CanvasTabLeftPanel.tsx | 16 +
.../ui/layouts/CanvasWorkspacePanel.tsx | 1 +
.../ui/layouts/GenerateTabLeftPanel.tsx | 16 +
.../ui/layouts/UpscalingTabLeftPanel.tsx | 16 +
.../ui/layouts/canvas-tab-auto-layout.tsx | 352 ++++++------------
.../ui/layouts/generate-tab-auto-layout.tsx | 182 ++++-----
.../ui/layouts/upscaling-tab-auto-layout.tsx | 191 ++++++++++
.../features/ui/layouts/use-did-render-tab.ts | 25 ++
.../ui/layouts/use-on-first-visible.ts | 48 +++
.../web/src/features/ui/store/uiSelectors.ts | 4 +
.../web/src/features/ui/store/uiTypes.ts | 1 +
22 files changed, 641 insertions(+), 352 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx
rename invokeai/frontend/web/src/features/ui/components/ParametersPanels/{ParametersPanelTextToImage.tsx => ParametersPanelCanvas.tsx} (96%)
create mode 100644 invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/CanvasTabLeftPanel.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/GenerateTabLeftPanel.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/UpscalingTabLeftPanel.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/use-did-render-tab.ts
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx
index 3b28be2799..49ae65205b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx
@@ -14,7 +14,7 @@ export const CanvasLayersPanel = memo(() => {
return (
-
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx
index 4e628e6f46..4534cae881 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx
@@ -11,7 +11,7 @@ export const CanvasLaunchpadPanel = memo(() => {
return (
- Get started with Invoke.
+ Edit and refine on Canvas.
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx
index 3999f0580f..2452296fe8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx
@@ -16,7 +16,7 @@ export const GenerateLaunchpadPanel = memo(() => {
return (
- Get started with Invoke.
+ Generate images from text prompts.
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx
new file mode 100644
index 0000000000..6279cf5970
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx
@@ -0,0 +1,13 @@
+import { Flex, Heading } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+
+export const UpscalingLaunchpadPanel = memo(() => {
+ return (
+
+
+ Upscale and add detail.
+
+
+ );
+});
+UpscalingLaunchpadPanel.displayName = 'UpscalingLaunchpadPanel';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
index a233a14f13..077b9ff187 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
@@ -1,4 +1,4 @@
-import { Divider, Flex, Heading } from '@invoke-ai/ui-library';
+import { Divider, Flex } from '@invoke-ai/ui-library';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
@@ -29,10 +29,6 @@ export const CanvasToolbar = memo(() => {
return (
-
- Canvas
-
-
@@ -48,7 +44,6 @@ export const CanvasToolbar = memo(() => {
-
);
});
diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts
index bfe72e5df0..13dd2be08c 100644
--- a/invokeai/frontend/web/src/features/system/store/configSlice.ts
+++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts
@@ -2,6 +2,8 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { AppConfig, NumericalParameterConfig, PartialAppConfig } from 'app/types/invokeai';
+import type { TabName } from 'features/ui/store/uiTypes';
+import { ALL_TABS } from 'features/ui/store/uiTypes';
import { merge } from 'lodash-es';
const baseDimensionConfig: NumericalParameterConfig = {
@@ -225,3 +227,12 @@ export const selectIsClientSideUploadEnabled = createConfigSelector((config) =>
export const selectAllowPublishWorkflows = createConfigSelector((config) => config.allowPublishWorkflows);
export const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLocal);
export const selectShouldShowCredits = createConfigSelector((config) => config.shouldShowCredits);
+export const selectEnabledTabs = createConfigSelector((config) => {
+ const enabledTabs: TabName[] = [];
+ for (const tab of ALL_TABS) {
+ if (!config.disabledTabs.includes(tab)) {
+ enabledTabs.push(tab);
+ }
+ }
+ return enabledTabs;
+});
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index c66fdfb6c8..2e94ba0990 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -1,10 +1,14 @@
import 'dockview/dist/styles/dockview.css';
import 'features/ui/styles/dockview-theme-invoke.css';
-import { Flex } from '@invoke-ai/ui-library';
+import { TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
-import { AutoLayout } from 'features/ui/layouts/AutoLayout';
+import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
+import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout';
+import { UpscalingTabAutoLayout } from 'features/ui/layouts/upscaling-tab-auto-layout';
+import { selectActiveTabIndex } from 'features/ui/store/uiSelectors';
import { $isLeftPanelOpen, $isRightPanelOpen } from 'features/ui/store/uiSlice';
import type { CSSProperties } from 'react';
import { memo } from 'react';
@@ -16,6 +20,7 @@ const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!is
export const AppContent = memo(() => {
// const tab = useAppSelector(selectActiveTab);
+ const tabIndex = useAppSelector(selectActiveTabIndex);
// const imperativePanelGroupRef = useRef(null);
useDndMonitor();
@@ -93,10 +98,22 @@ export const AppContent = memo(() => {
// });
return (
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
});
AppContent.displayName = 'AppContent';
diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx
similarity index 96%
rename from invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx
rename to invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx
index 26ff557bf5..a51aa59a10 100644
--- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx
@@ -22,7 +22,7 @@ const overlayScrollbarsStyles: CSSProperties = {
width: '100%',
};
-const ParametersPanelTextToImage = () => {
+export const ParametersPanelCanvas = memo(() => {
const isSDXL = useAppSelector(selectIsSDXL);
const isCogview4 = useAppSelector(selectIsCogView4);
const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
@@ -55,6 +55,6 @@ const ParametersPanelTextToImage = () => {
);
-};
+});
-export default memo(ParametersPanelTextToImage);
+ParametersPanelCanvas.displayName = 'ParametersPanelCanvas';
diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx
new file mode 100644
index 0000000000..de15d4f706
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx
@@ -0,0 +1,58 @@
+import { Box, Flex } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppSelector } from 'app/store/storeHooks';
+import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
+import { selectIsCogView4, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
+import { Prompts } from 'features/parameters/components/Prompts/Prompts';
+import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
+import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
+import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
+import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
+import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
+import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
+import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
+import { $isStylePresetsMenuOpen } from 'features/stylePresets/store/stylePresetSlice';
+import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
+import type { CSSProperties } from 'react';
+import { memo } from 'react';
+
+const overlayScrollbarsStyles: CSSProperties = {
+ height: '100%',
+ width: '100%',
+};
+
+export const ParametersPanelGenerate = memo(() => {
+ const isSDXL = useAppSelector(selectIsSDXL);
+ const isCogview4 = useAppSelector(selectIsCogView4);
+ const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
+
+ const isApiModel = useIsApiModel();
+
+ return (
+
+
+
+
+ {isStylePresetsMenuOpen && (
+
+
+
+
+
+ )}
+
+
+
+
+
+ {isSDXL && }
+ {!isCogview4 && !isApiModel && }
+
+
+
+
+
+ );
+});
+
+ParametersPanelGenerate.displayName = 'ParametersPanelGenerate';
diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx
index 830bcd5709..2903d765b5 100644
--- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx
@@ -17,7 +17,7 @@ const overlayScrollbarsStyles: CSSProperties = {
width: '100%',
};
-const ParametersPanelUpscale = () => {
+export const ParametersPanelUpscale = memo(() => {
const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
return (
@@ -44,6 +44,6 @@ const ParametersPanelUpscale = () => {
);
-};
+});
-export default memo(ParametersPanelUpscale);
+ParametersPanelUpscale.displayName = 'ParametersPanelUpscale';
diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
index 8974a9c326..6bcc351618 100644
--- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { IconButton, Tooltip } from '@invoke-ai/ui-library';
+import { IconButton, Tab, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -26,13 +26,14 @@ export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: React
return (
{
+ return (
+
+
+
+
+
+
+ );
+});
+CanvasTabLeftPanel.displayName = 'CanvasTabLeftPanel';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx
index 1a2a592ddd..a2813fefa1 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx
@@ -66,6 +66,7 @@ export const CanvasWorkspacePanel = memo(() => {
alignItems="center"
justifyContent="center"
overflow="hidden"
+ p={2}
>
diff --git a/invokeai/frontend/web/src/features/ui/layouts/GenerateTabLeftPanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/GenerateTabLeftPanel.tsx
new file mode 100644
index 0000000000..598a51d142
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/GenerateTabLeftPanel.tsx
@@ -0,0 +1,16 @@
+import { Box, Flex } from '@invoke-ai/ui-library';
+import QueueControls from 'features/queue/components/QueueControls';
+import { ParametersPanelGenerate } from 'features/ui/components/ParametersPanels/ParametersPanelGenerate';
+import { memo } from 'react';
+
+export const GenerateTabLeftPanel = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+GenerateTabLeftPanel.displayName = 'GenerateTabLeftPanel';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/UpscalingTabLeftPanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/UpscalingTabLeftPanel.tsx
new file mode 100644
index 0000000000..ea5e006aba
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/UpscalingTabLeftPanel.tsx
@@ -0,0 +1,16 @@
+import { Box, Flex } from '@invoke-ai/ui-library';
+import QueueControls from 'features/queue/components/QueueControls';
+import { ParametersPanelUpscale } from 'features/ui/components/ParametersPanels/ParametersPanelUpscale';
+import { memo } from 'react';
+
+export const UpscalingTabLeftPanel = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+UpscalingTabLeftPanel.displayName = 'UpscalingTabLeftPanel';
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 66c200db3f..ab88198660 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
@@ -1,225 +1,71 @@
-import { Box, ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
-import { $isLayoutLoading } from 'app/store/nanostores/globalIsLoading';
-import { useAppSelector } from 'app/store/storeHooks';
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
-import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
-import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
-import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
-import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
-import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
-import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent';
-import { Filter } from 'features/controlLayers/components/Filters/Filter';
-import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
-import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
-import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
-import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
-import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
-import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
-import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
-import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
-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 { CanvasLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
-import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
-import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
-import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
-import QueueControls from 'features/queue/components/QueueControls';
-import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
+import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
+import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
-import { components } from 'features/ui/layouts/components';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import { dockviewTheme } from 'features/ui/styles/theme';
-import { memo, useCallback, useState } from 'react';
-import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
+import { atom } from 'nanostores';
+import { memo, useCallback, useRef, useState } from 'react';
-const MenuContent = memo(() => {
- return (
-
-
-
-
-
-
- );
-});
-MenuContent.displayName = 'MenuContent';
+import { CanvasTabLeftPanel } from './CanvasTabLeftPanel';
+import { CanvasWorkspacePanel } from './CanvasWorkspacePanel';
+import { useOnFirstVisible } from './use-on-first-visible';
-const canvasBgSx = {
- position: 'relative',
- w: 'full',
- h: 'full',
- borderRadius: 'base',
- overflow: 'hidden',
- bg: 'base.900',
- '&[data-dynamic-grid="true"]': {
- bg: 'base.850',
- },
-};
-
-export const CanvasWorkspacePanel = memo(() => {
- const dynamicGrid = useAppSelector(selectDynamicGrid);
- const showHUD = useAppSelector(selectShowHUD);
- const canvasId = useAppSelector(selectCanvasSessionId);
-
- const renderMenu = useCallback(() => {
- return ;
- }, []);
-
- return (
-
-
-
-
-
- renderMenu={renderMenu} withLongPress={false}>
- {(ref) => (
-
-
-
-
- {showHUD && }
-
-
-
-
-
-
- } colorScheme="base" />
-
-
-
-
-
- )}
-
- {canvasId !== null && (
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-CanvasWorkspacePanel.displayName = 'CanvasPanel';
-
-const LayersPanelContent = memo(() => (
-
-
-
-));
-LayersPanelContent.displayName = 'LayersPanelContent';
-
-const ViewerPanelContent = memo(() => (
-
-
-
-
-
-));
-ViewerPanelContent.displayName = 'ViewerPanelContent';
-
-const ProgressPanelContent = memo(() => (
-
-
-
-));
-ProgressPanelContent.displayName = 'ProgressPanelContent';
+const LAUNCHPAD_PANEL_ID = 'launchpad';
+const WORKSPACE_PANEL_ID = 'workspace';
+const VIEWER_PANEL_ID = 'viewer';
+const PROGRESS_PANEL_ID = 'progress';
const mainPanelComponents: IDockviewReactProps['components'] = {
- canvasLaunchpad: GenerateLaunchpadPanel,
- canvas: CanvasWorkspacePanel,
- viewer: ViewerPanelContent,
- progress: ProgressPanelContent,
+ [LAUNCHPAD_PANEL_ID]: CanvasLaunchpadPanel,
+ [WORKSPACE_PANEL_ID]: CanvasWorkspacePanel,
+ [VIEWER_PANEL_ID]: ImageViewerPanel,
+ [PROGRESS_PANEL_ID]: GenerationProgressPanel,
};
const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const { api } = event;
api.addPanel({
- id: 'canvasLaunchpad',
- component: 'canvasLaunchpad',
- title: 'canvasLaunchpad',
+ id: LAUNCHPAD_PANEL_ID,
+ component: LAUNCHPAD_PANEL_ID,
+ title: 'Launchpad',
});
api.addPanel({
- id: 'canvas',
- component: 'canvas',
+ id: WORKSPACE_PANEL_ID,
+ component: WORKSPACE_PANEL_ID,
title: 'Canvas',
position: {
direction: 'within',
- referencePanel: 'canvasLaunchpad',
+ referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.addPanel({
- id: 'viewer',
- component: 'viewer',
+ id: VIEWER_PANEL_ID,
+ component: VIEWER_PANEL_ID,
title: 'Image Viewer',
position: {
direction: 'within',
- referencePanel: 'canvasLaunchpad',
+ referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.addPanel({
- id: 'progress',
- component: 'progress',
+ id: PROGRESS_PANEL_ID,
+ component: PROGRESS_PANEL_ID,
title: 'Generation Progress',
position: {
direction: 'within',
- referencePanel: 'canvasLaunchpad',
+ referencePanel: LAUNCHPAD_PANEL_ID,
},
});
+ api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive();
+
const disposables = [
api.onWillShowOverlay((e) => {
if (e.kind === 'header_space' || e.kind === 'tab') {
@@ -238,102 +84,122 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const MainPanel = memo(() => {
return (
-
-
-
+
);
});
MainPanel.displayName = 'MainPanel';
-const Left = memo(() => {
- return (
-
-
-
-
-
-
- );
-});
-Left.displayName = 'Left';
-
-const Null = () => null;
+const LEFT_PANEL_ID = 'left';
+const MAIN_PANEL_ID = 'main';
+const BOARDS_PANEL_ID = 'boards';
+const GALLERY_PANEL_ID = 'gallery';
+const LAYERS_PANEL_ID = 'layers';
export const canvasTabComponents: IGridviewReactProps['components'] = {
- left: Left,
- main: MainPanel,
- boards: BoardsPanel,
- gallery: GalleryPanel,
- layers: LayersPanelContent,
+ [LEFT_PANEL_ID]: CanvasTabLeftPanel,
+ [MAIN_PANEL_ID]: MainPanel,
+ [BOARDS_PANEL_ID]: BoardsPanel,
+ [GALLERY_PANEL_ID]: GalleryPanel,
+ [LAYERS_PANEL_ID]: CanvasLayersPanel,
};
-export const initializeCanvasTabLayout = (api: GridviewApi) => {
- const main = api.addPanel({
- id: 'main',
- component: 'main',
- minimumWidth: 256,
+export const initializeLayout = (api: GridviewApi) => {
+ api.addPanel({
+ id: MAIN_PANEL_ID,
+ component: MAIN_PANEL_ID,
});
- const left = api.addPanel({
- id: 'left',
- component: 'left',
+ api.addPanel({
+ id: LEFT_PANEL_ID,
+ component: LEFT_PANEL_ID,
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
- referencePanel: 'main',
+ referencePanel: MAIN_PANEL_ID,
},
});
api.addPanel({
- id: 'gallery',
- component: 'gallery',
+ id: GALLERY_PANEL_ID,
+ component: GALLERY_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
position: {
direction: 'right',
- referencePanel: 'main',
+ referencePanel: MAIN_PANEL_ID,
},
});
api.addPanel({
- id: 'layers',
- component: 'layers',
+ id: LAYERS_PANEL_ID,
+ component: LAYERS_PANEL_ID,
minimumHeight: 256,
position: {
direction: 'below',
- referencePanel: 'gallery',
+ referencePanel: GALLERY_PANEL_ID,
},
});
- const boards = api.addPanel({
- id: 'boards',
- component: 'boards',
+ api.addPanel({
+ id: BOARDS_PANEL_ID,
+ component: BOARDS_PANEL_ID,
minimumHeight: 36,
position: {
direction: 'above',
- referencePanel: 'gallery',
+ referencePanel: GALLERY_PANEL_ID,
},
});
- left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
- boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+ api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+ api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
export const CanvasTabAutoLayout = memo(() => {
- const [api, setApi] = useState(null);
- const onReady = useCallback((event) => {
- $isLayoutLoading.set(true);
- setApi(event.api);
- initializeCanvasTabLayout(event.api);
- $isLayoutLoading.set(false);
- }, []);
+ const ref = useRef(null);
+ const $api = useState(() => atom(null))[0];
+ const onReady = useCallback(
+ (event) => {
+ $api.set(event.api);
+ initializeLayout(event.api);
+ },
+ [$api]
+ );
+ const resizeMainPanelOnFirstVisible = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const mainPanel = api.getPanel(MAIN_PANEL_ID);
+ if (!mainPanel) {
+ return;
+ }
+ if (mainPanel.width !== 0) {
+ return;
+ }
+ let count = 0;
+ const setSize = () => {
+ if (count++ > 50) {
+ return;
+ }
+ mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER });
+ if (mainPanel.width === 0) {
+ requestAnimationFrame(setSize);
+ return;
+ }
+ };
+ setSize();
+ }, [$api]);
+ useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
+
return (
-
+
(
-
-
-
-
-
-));
-ViewerPanelContent.displayName = 'ViewerPanelContent';
+import { GenerateTabLeftPanel } from './GenerateTabLeftPanel';
+import { useOnFirstVisible } from './use-on-first-visible';
-const ProgressPanelContent = memo(() => (
-
-
-
-));
-ProgressPanelContent.displayName = 'ProgressPanelContent';
+const LAUNCHPAD_PANEL_ID = 'launchpad';
+const VIEWER_PANEL_ID = 'viewer';
+const PROGRESS_PANEL_ID = 'progress';
const mainPanelComponents: IDockviewReactProps['components'] = {
- welcome: GenerateLaunchpadPanel,
- viewer: ViewerPanelContent,
- progress: ProgressPanelContent,
+ [LAUNCHPAD_PANEL_ID]: GenerateLaunchpadPanel,
+ [VIEWER_PANEL_ID]: ImageViewerPanel,
+ [PROGRESS_PANEL_ID]: GenerationProgressPanel,
};
const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const { api } = event;
api.addPanel({
- id: 'welcome',
- component: 'welcome',
+ id: LAUNCHPAD_PANEL_ID,
+ component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
});
api.addPanel({
- id: 'viewer',
- component: 'viewer',
+ id: VIEWER_PANEL_ID,
+ component: VIEWER_PANEL_ID,
title: 'Image Viewer',
position: {
direction: 'within',
- referencePanel: 'welcome',
+ referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.addPanel({
- id: 'progress',
- component: 'progress',
+ id: PROGRESS_PANEL_ID,
+ component: PROGRESS_PANEL_ID,
title: 'Generation Progress',
position: {
direction: 'within',
- referencePanel: 'welcome',
+ referencePanel: LAUNCHPAD_PANEL_ID,
},
});
+ api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive();
+
const disposables = [
api.onWillShowOverlay((e) => {
if (e.kind === 'header_space' || e.kind === 'tab') {
@@ -82,90 +71,111 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const MainPanel = memo(() => {
return (
-
-
-
+
);
});
MainPanel.displayName = 'MainPanel';
-export const GenerateLeftPanel = memo(() => {
- return (
-
-
-
-
-
-
- );
-});
-GenerateLeftPanel.displayName = 'GenerateLeftPanel';
+const LEFT_PANEL_ID = 'left';
+const MAIN_PANEL_ID = 'main';
+const BOARDS_PANEL_ID = 'boards';
+const GALLERY_PANEL_ID = 'gallery';
export const generateTabComponents: IGridviewReactProps['components'] = {
- left: GenerateLeftPanel,
- main: MainPanel,
- boards: BoardsPanel,
- gallery: GalleryPanel,
+ [LEFT_PANEL_ID]: GenerateTabLeftPanel,
+ [MAIN_PANEL_ID]: MainPanel,
+ [BOARDS_PANEL_ID]: BoardsPanel,
+ [GALLERY_PANEL_ID]: GalleryPanel,
};
-export const initializeGenerateTabLayout = (api: GridviewApi) => {
- const main = api.addPanel({
- id: 'main',
- component: 'main',
- minimumWidth: 256,
+export const initializeLayout = (api: GridviewApi) => {
+ api.addPanel({
+ id: MAIN_PANEL_ID,
+ component: MAIN_PANEL_ID,
});
- const left = api.addPanel({
- id: 'left',
- component: 'left',
+ api.addPanel({
+ id: LEFT_PANEL_ID,
+ component: LEFT_PANEL_ID,
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
- referencePanel: 'main',
+ referencePanel: MAIN_PANEL_ID,
},
});
api.addPanel({
- id: 'gallery',
- component: 'gallery',
+ id: GALLERY_PANEL_ID,
+ component: GALLERY_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
position: {
direction: 'right',
- referencePanel: 'main',
+ referencePanel: MAIN_PANEL_ID,
},
});
- const boards = api.addPanel({
- id: 'boards',
- component: 'boards',
+ api.addPanel({
+ id: BOARDS_PANEL_ID,
+ component: BOARDS_PANEL_ID,
minimumHeight: 36,
position: {
direction: 'above',
- referencePanel: 'gallery',
+ referencePanel: GALLERY_PANEL_ID,
},
});
- left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
- boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+ api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+ api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
export const GenerateTabAutoLayout = memo(() => {
- const [api, setApi] = useState(null);
- const onReady = useCallback((event) => {
- $isLayoutLoading.set(true);
- setApi(event.api);
- initializeGenerateTabLayout(event.api);
- $isLayoutLoading.set(false);
- }, []);
+ const ref = useRef(null);
+ const $api = useState(() => atom(null))[0];
+ const onReady = useCallback(
+ (event) => {
+ $api.set(event.api);
+ initializeLayout(event.api);
+ },
+ [$api]
+ );
+ const resizeMainPanelOnFirstVisible = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const mainPanel = api.getPanel(MAIN_PANEL_ID);
+ if (!mainPanel) {
+ return;
+ }
+ if (mainPanel.width !== 0) {
+ return;
+ }
+ let count = 0;
+ const setSize = () => {
+ if (count++ > 50) {
+ return;
+ }
+ mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER });
+ if (mainPanel.width === 0) {
+ requestAnimationFrame(setSize);
+ return;
+ }
+ };
+ setSize();
+ }, [$api]);
+ useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
+
return (
-
+
{
+ const { api } = event;
+ api.addPanel({
+ id: LAUNCHPAD_PANEL_ID,
+ component: LAUNCHPAD_PANEL_ID,
+ title: 'Launchpad',
+ });
+ api.addPanel({
+ id: VIEWER_PANEL_ID,
+ component: VIEWER_PANEL_ID,
+ title: 'Image Viewer',
+ position: {
+ direction: 'within',
+ referencePanel: LAUNCHPAD_PANEL_ID,
+ },
+ });
+ api.addPanel({
+ id: PROGRESS_PANEL_ID,
+ component: PROGRESS_PANEL_ID,
+ title: 'Generation Progress',
+ position: {
+ direction: 'within',
+ referencePanel: LAUNCHPAD_PANEL_ID,
+ },
+ });
+
+ api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive();
+
+ const disposables = [
+ api.onWillShowOverlay((e) => {
+ if (e.kind === 'header_space' || e.kind === 'tab') {
+ return;
+ }
+ e.preventDefault();
+ }),
+ ];
+
+ return () => {
+ disposables.forEach((disposable) => {
+ disposable.dispose();
+ });
+ };
+};
+
+const MainPanel = memo(() => {
+ return (
+
+ );
+});
+MainPanel.displayName = 'MainPanel';
+
+const LEFT_PANEL_ID = 'left';
+const MAIN_PANEL_ID = 'main';
+const BOARDS_PANEL_ID = 'boards';
+const GALLERY_PANEL_ID = 'gallery';
+
+export const gridviewComponents: IGridviewReactProps['components'] = {
+ [LEFT_PANEL_ID]: UpscalingTabLeftPanel,
+ [MAIN_PANEL_ID]: MainPanel,
+ [BOARDS_PANEL_ID]: BoardsPanel,
+ [GALLERY_PANEL_ID]: GalleryPanel,
+};
+
+export const initializeLayout = (api: GridviewApi) => {
+ api.addPanel({
+ id: MAIN_PANEL_ID,
+ component: MAIN_PANEL_ID,
+ // priority: LayoutPriority.High,
+ });
+ api.addPanel({
+ id: LEFT_PANEL_ID,
+ component: LEFT_PANEL_ID,
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: MAIN_PANEL_ID,
+ },
+ // priority: LayoutPriority.High,
+ });
+ api.addPanel({
+ id: GALLERY_PANEL_ID,
+ component: GALLERY_PANEL_ID,
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ position: {
+ direction: 'right',
+ referencePanel: MAIN_PANEL_ID,
+ },
+ // priority: LayoutPriority.High,
+ });
+ api.addPanel({
+ id: BOARDS_PANEL_ID,
+ component: BOARDS_PANEL_ID,
+ minimumHeight: 36,
+ position: {
+ direction: 'above',
+ referencePanel: GALLERY_PANEL_ID,
+ },
+ // priority: LayoutPriority.High,
+ });
+ api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+ api.getPanel(MAIN_PANEL_ID)?.api.setActive();
+};
+
+export const UpscalingTabAutoLayout = memo(() => {
+ const ref = useRef(null);
+ const $api = useState(() => atom(null))[0];
+ const onReady = useCallback(
+ (event) => {
+ $api.set(event.api);
+ initializeLayout(event.api);
+ },
+ [$api]
+ );
+ const resizeMainPanelOnFirstVisible = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const mainPanel = api.getPanel(MAIN_PANEL_ID);
+ if (!mainPanel) {
+ return;
+ }
+ if (mainPanel.width !== 0) {
+ return;
+ }
+ let count = 0;
+ const setSize = () => {
+ if (count++ > 50) {
+ return;
+ }
+ mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER });
+ if (mainPanel.width === 0) {
+ requestAnimationFrame(setSize);
+ return;
+ }
+ };
+ setSize();
+ }, [$api]);
+ useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
+
+ return (
+
+
+
+ );
+});
+UpscalingTabAutoLayout.displayName = 'UpscalingTabAutoLayout';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-did-render-tab.ts b/invokeai/frontend/web/src/features/ui/layouts/use-did-render-tab.ts
new file mode 100644
index 0000000000..3ad3bfb0f3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/use-did-render-tab.ts
@@ -0,0 +1,25 @@
+import { addAppListener } from 'app/store/middleware/listenerMiddleware';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { setActiveTab } from 'features/ui/store/uiSlice';
+import type { TabName } from 'features/ui/store/uiTypes';
+import { useEffect } from 'react';
+
+export const useOnFirstVisitToTab = (tab: TabName, cb: () => void) => {
+ const dispatch = useAppDispatch();
+ useEffect(() => {
+ dispatch(
+ addAppListener({
+ predicate: (action) => {
+ if (!setActiveTab.match(action)) {
+ return false;
+ }
+ return action.payload === tab;
+ },
+ effect: (_, api) => {
+ cb();
+ api.unsubscribe();
+ },
+ })
+ );
+ }, [cb, dispatch, tab]);
+};
diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts b/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts
new file mode 100644
index 0000000000..1122b44cac
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts
@@ -0,0 +1,48 @@
+import type { RefObject } from 'react';
+import { useEffect } from 'react';
+
+export const useOnFirstVisible = (elementRef: RefObject, callback: () => void): void => {
+ useEffect(() => {
+ const element = elementRef.current;
+ if (!element) {
+ return;
+ }
+
+ // Find the parent element that has display: none
+ const findParentWithDisplay = (el: HTMLElement): HTMLElement | null => {
+ let parent = el.parentElement;
+ while (parent) {
+ const computedStyle = window.getComputedStyle(parent);
+ if (computedStyle.display === 'none') {
+ return parent;
+ }
+ parent = parent.parentElement;
+ }
+ return null;
+ };
+
+ const targetParent = findParentWithDisplay(element);
+ if (!targetParent) {
+ return;
+ }
+
+ const observerCallback = () => {
+ if (window.getComputedStyle(targetParent).display === 'none') {
+ return;
+ }
+ observer.disconnect();
+ callback();
+ };
+ const observer = new MutationObserver(observerCallback);
+
+ observer.observe(targetParent, {
+ attributes: true,
+ attributeFilter: ['hidden', 'style', 'class'],
+ });
+
+ observerCallback();
+ return () => {
+ observer.disconnect();
+ };
+ }, [elementRef, callback]);
+};
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
index 4b11ca8d6a..381c046c36 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
@@ -1,7 +1,11 @@
import { createSelector } from '@reduxjs/toolkit';
+import { selectEnabledTabs } from 'features/system/store/configSlice';
import { selectUiSlice } from 'features/ui/store/uiSlice';
export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab);
+export const selectActiveTabIndex = createSelector(selectActiveTab, selectEnabledTabs, (activeTab, enabledTabs) => {
+ return enabledTabs.indexOf(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/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
index 6c0ab9023a..8ef49ac4c7 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
@@ -2,6 +2,7 @@ import { deepClone } from 'common/util/deepClone';
import { z } from 'zod';
const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']);
+export const ALL_TABS = zTabName.options;
export type TabName = z.infer;
const zCanvasRightPanelTabName = z.enum(['layers', 'gallery']);
export type CanvasRightPanelTabName = z.infer;
From e0ed56ff8ded78907bc999570f6b172f73f15d68 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 20 Jun 2025 11:36:26 +1000
Subject: [PATCH 115/210] fix(ui): inverted logic for resume queue button
---
.../frontend/web/src/features/queue/hooks/useResumeProcessor.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts
index baa02ece03..901bac39f8 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts
@@ -30,5 +30,5 @@ export const useResumeProcessor = () => {
}
}, [_trigger, t]);
- return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.processor.is_started };
+ return { trigger, isLoading, isDisabled: !isConnected || queueStatus?.processor.is_started };
};
From 7f222ffb9d5e0211b64ca7f2c54096866f31a11f Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 20 Jun 2025 11:48:48 +1000
Subject: [PATCH 116/210] fix(ui): unnecessary dependency on tab selection in
useCanvasDeleteLayerHotkey
---
.../hooks/useCanvasDeleteLayerHotkey.ts | 17 +++++------------
1 file changed, 5 insertions(+), 12 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts
index 11ac1b6897..324bd70300 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts
@@ -4,8 +4,8 @@ import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
-import { selectActiveTab, selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
-import { useCallback, useMemo } from 'react';
+import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
+import { useCallback } from 'react';
export function useCanvasDeleteLayerHotkey() {
useAssertSingleton(useCanvasDeleteLayerHotkey.name);
@@ -13,25 +13,18 @@ export function useCanvasDeleteLayerHotkey() {
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const isBusy = useCanvasIsBusy();
const canvasRightPanelTab = useAppSelector(selectActiveTabCanvasRightPanel);
- const appTab = useAppSelector(selectActiveTab);
const deleteSelectedLayer = useCallback(() => {
- if (selectedEntityIdentifier === null) {
+ if (selectedEntityIdentifier === null || isBusy || canvasRightPanelTab !== 'layers') {
return;
}
dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier }));
- }, [dispatch, selectedEntityIdentifier]);
-
- const isDeleteEnabled = useMemo(
- () => selectedEntityIdentifier !== null && !isBusy && canvasRightPanelTab === 'layers' && appTab === 'canvas',
- [selectedEntityIdentifier, isBusy, canvasRightPanelTab, appTab]
- );
+ }, [canvasRightPanelTab, dispatch, isBusy, selectedEntityIdentifier]);
useRegisteredHotkeys({
id: 'deleteSelected',
category: 'canvas',
callback: deleteSelectedLayer,
- options: { enabled: isDeleteEnabled },
- dependencies: [isDeleteEnabled, deleteSelectedLayer],
+ dependencies: [deleteSelectedLayer],
});
}
From 16993cd21686b83ba41af43f2393712101ef2a24 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 20 Jun 2025 11:49:06 +1000
Subject: [PATCH 117/210] feat(ui): get all tabs working w/ new layout
---
.../SimpleSession/WorkflowsLaunchpadPanel.tsx | 13 ++
.../features/nodes/components/NodeEditor.tsx | 33 +--
.../sidePanel/WorkflowsTabLeftPanel.tsx | 18 +-
.../queue/components/QueueTabContent.tsx | 13 +-
.../components/HotkeysModal/useHotkeyData.ts | 11 +-
.../src/features/ui/components/AppContent.tsx | 44 +++-
.../components/tabs/WorkflowsTabContent.tsx | 7 +-
.../ui/layouts/workflows-tab-auto-layout.tsx | 203 ++++++++++++++++++
8 files changed, 288 insertions(+), 54 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel.tsx
new file mode 100644
index 0000000000..e048d94d6c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel.tsx
@@ -0,0 +1,13 @@
+import { Flex, Heading } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+
+export const WorkflowsLaunchpadPanel = memo(() => {
+ return (
+
+
+ Go deep with Workflows.
+
+
+ );
+});
+WorkflowsLaunchpadPanel.displayName = 'WorkflowsLaunchpadPanel';
diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
index 168af9dff9..96a4c5cd0b 100644
--- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
@@ -1,4 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { ReactFlowProvider } from '@xyflow/react';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
@@ -29,21 +30,23 @@ const NodeEditor = () => {
const { t } = useTranslation();
return (
-
- {data && (
- <>
-
-
-
-
-
-
-
- >
- )}
-
- {isLoading && }
-
+
+
+ {data && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+
+ {isLoading && }
+
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowsTabLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowsTabLeftPanel.tsx
index dd114a22aa..5a20867115 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowsTabLeftPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowsTabLeftPanel.tsx
@@ -8,6 +8,7 @@ import { PublishWorkflowPanelContent } from 'features/nodes/components/sidePanel
import { ActiveWorkflowDescription } from 'features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowDescription';
import { ActiveWorkflowNameAndActions } from 'features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowNameAndActions';
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
+import QueueControls from 'features/queue/components/QueueControls';
import { memo } from 'react';
import { ViewModeLeftPanelContent } from './viewMode/ViewModeLeftPanelContent';
@@ -18,13 +19,16 @@ const WorkflowsTabLeftPanel = () => {
const isInPublishFlow = useStore($isInPublishFlow);
return (
-
- {isInPublishFlow && }
- {!isInPublishFlow && }
- {!isInPublishFlow && !isPublished && mode === 'view' && }
- {!isInPublishFlow && !isPublished && mode === 'view' && }
- {!isInPublishFlow && !isPublished && mode === 'edit' && }
- {isPublished && }
+
+
+
+ {isInPublishFlow && }
+ {!isInPublishFlow && }
+ {!isInPublishFlow && !isPublished && mode === 'view' && }
+ {!isInPublishFlow && !isPublished && mode === 'view' && }
+ {!isInPublishFlow && !isPublished && mode === 'edit' && }
+ {isPublished && }
+
);
};
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx
index 73a8dfc401..2dae5e6ebe 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx
@@ -1,7 +1,5 @@
import { Box, Flex } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import InvocationCacheStatus from './InvocationCacheStatus';
@@ -11,18 +9,9 @@ import QueueTabQueueControls from './QueueTabQueueControls';
const QueueTabContent = () => {
const isInvocationCacheEnabled = useFeatureStatus('invocationCache');
- const activeTabName = useAppSelector(selectActiveTab);
return (
-
+
diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts
index 9844fc81c3..781397e794 100644
--- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts
+++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts
@@ -78,11 +78,12 @@ export const useHotkeyData = (): HotkeysData => {
addHotkey('app', 'invokeFront', ['mod+shift+enter']);
addHotkey('app', 'cancelQueueItem', ['shift+x']);
addHotkey('app', 'clearQueue', ['mod+shift+x']);
- addHotkey('app', 'selectCanvasTab', ['1']);
- addHotkey('app', 'selectUpscalingTab', ['2']);
- addHotkey('app', 'selectWorkflowsTab', ['3']);
- addHotkey('app', 'selectModelsTab', ['4'], isModelManagerEnabled);
- addHotkey('app', 'selectQueueTab', isModelManagerEnabled ? ['5'] : ['4']);
+ addHotkey('app', 'selectGenerateTab', ['1']);
+ addHotkey('app', 'selectCanvasTab', ['2']);
+ addHotkey('app', 'selectUpscalingTab', ['3']);
+ addHotkey('app', 'selectWorkflowsTab', ['4']);
+ addHotkey('app', 'selectModelsTab', ['5'], isModelManagerEnabled);
+ addHotkey('app', 'selectQueueTab', isModelManagerEnabled ? ['6'] : ['5']);
addHotkey('app', 'focusPrompt', ['alt+a']);
addHotkey('app', 'toggleLeftPanel', ['t', 'o']);
addHotkey('app', 'toggleRightPanel', ['g']);
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 2e94ba0990..3cd6b73435 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -8,11 +8,16 @@ import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout';
import { UpscalingTabAutoLayout } from 'features/ui/layouts/upscaling-tab-auto-layout';
+import { WorkflowsTabAutoLayout } from 'features/ui/layouts/workflows-tab-auto-layout';
import { selectActiveTabIndex } from 'features/ui/store/uiSelectors';
import { $isLeftPanelOpen, $isRightPanelOpen } from 'features/ui/store/uiSlice';
import type { CSSProperties } from 'react';
import { memo } from 'react';
+import { TabMountGate } from './TabMountGate';
+import ModelManagerTab from './tabs/ModelManagerTab';
+import QueueTab from './tabs/QueueTab';
+
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCollapsed);
@@ -103,15 +108,36 @@ export const AppContent = memo(() => {
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/WorkflowsTabContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/WorkflowsTabContent.tsx
index c924855b5a..d1e506afff 100644
--- a/invokeai/frontend/web/src/features/ui/components/tabs/WorkflowsTabContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/tabs/WorkflowsTabContent.tsx
@@ -1,4 +1,3 @@
-import { ReactFlowProvider } from '@xyflow/react';
import { useAppSelector } from 'app/store/storeHooks';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import NodeEditor from 'features/nodes/components/NodeEditor';
@@ -9,11 +8,7 @@ export const WorkflowsMainPanel = memo(() => {
const mode = useAppSelector(selectWorkflowMode);
if (mode === 'edit') {
- return (
-
-
-
- );
+ return ;
}
return ;
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
new file mode 100644
index 0000000000..c94bd5f081
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx
@@ -0,0 +1,203 @@
+import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
+import { DockviewReact, GridviewReact, Orientation } from 'dockview';
+import { WorkflowsLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel';
+import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
+import { GalleryPanel } from 'features/gallery/components/Gallery';
+import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
+import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
+import NodeEditor from 'features/nodes/components/NodeEditor';
+import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
+import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
+import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
+import { dockviewTheme } from 'features/ui/styles/theme';
+import { atom } from 'nanostores';
+import { memo, useCallback, useRef, useState } from 'react';
+
+import { useOnFirstVisible } from './use-on-first-visible';
+
+const LAUNCHPAD_PANEL_ID = 'launchpad';
+const WORKSPACE_PANEL_ID = 'workspace';
+const VIEWER_PANEL_ID = 'viewer';
+const PROGRESS_PANEL_ID = 'progress';
+
+const dockviewComponents: IDockviewReactProps['components'] = {
+ [LAUNCHPAD_PANEL_ID]: WorkflowsLaunchpadPanel,
+ [WORKSPACE_PANEL_ID]: NodeEditor,
+ [VIEWER_PANEL_ID]: ImageViewerPanel,
+ [PROGRESS_PANEL_ID]: GenerationProgressPanel,
+};
+
+const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
+ const { api } = event;
+ api.addPanel({
+ id: LAUNCHPAD_PANEL_ID,
+ component: LAUNCHPAD_PANEL_ID,
+ title: 'Launchpad',
+ });
+ api.addPanel({
+ id: WORKSPACE_PANEL_ID,
+ component: WORKSPACE_PANEL_ID,
+ title: 'Workflow Editor',
+ position: {
+ direction: 'within',
+ referencePanel: LAUNCHPAD_PANEL_ID,
+ },
+ });
+ api.addPanel({
+ id: VIEWER_PANEL_ID,
+ component: VIEWER_PANEL_ID,
+ title: 'Image Viewer',
+ position: {
+ direction: 'within',
+ referencePanel: LAUNCHPAD_PANEL_ID,
+ },
+ });
+ api.addPanel({
+ id: PROGRESS_PANEL_ID,
+ component: PROGRESS_PANEL_ID,
+ title: 'Generation Progress',
+ position: {
+ direction: 'within',
+ referencePanel: LAUNCHPAD_PANEL_ID,
+ },
+ });
+
+ api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive();
+
+ const disposables = [
+ api.onWillShowOverlay((e) => {
+ if (e.kind === 'header_space' || e.kind === 'tab') {
+ return;
+ }
+ e.preventDefault();
+ }),
+ ];
+
+ return () => {
+ disposables.forEach((disposable) => {
+ disposable.dispose();
+ });
+ };
+};
+
+const MainPanel = memo(() => {
+ return (
+
+ );
+});
+MainPanel.displayName = 'MainPanel';
+
+const LEFT_PANEL_ID = 'left';
+const MAIN_PANEL_ID = 'main';
+const BOARDS_PANEL_ID = 'boards';
+const GALLERY_PANEL_ID = 'gallery';
+
+export const gridviewComponents: IGridviewReactProps['components'] = {
+ [LEFT_PANEL_ID]: WorkflowsTabLeftPanel,
+ [MAIN_PANEL_ID]: MainPanel,
+ [BOARDS_PANEL_ID]: BoardsPanel,
+ [GALLERY_PANEL_ID]: GalleryPanel,
+};
+
+export const initializeLayout = (api: GridviewApi) => {
+ api.addPanel({
+ id: MAIN_PANEL_ID,
+ component: MAIN_PANEL_ID,
+ // priority: LayoutPriority.High,
+ });
+ api.addPanel({
+ id: LEFT_PANEL_ID,
+ component: LEFT_PANEL_ID,
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: MAIN_PANEL_ID,
+ },
+ // priority: LayoutPriority.High,
+ });
+ api.addPanel({
+ id: GALLERY_PANEL_ID,
+ component: GALLERY_PANEL_ID,
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ position: {
+ direction: 'right',
+ referencePanel: MAIN_PANEL_ID,
+ },
+ // priority: LayoutPriority.High,
+ });
+ api.addPanel({
+ id: BOARDS_PANEL_ID,
+ component: BOARDS_PANEL_ID,
+ minimumHeight: 36,
+ position: {
+ direction: 'above',
+ referencePanel: GALLERY_PANEL_ID,
+ },
+ // priority: LayoutPriority.High,
+ });
+ api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+ api.getPanel(MAIN_PANEL_ID)?.api.setActive();
+};
+
+export const WorkflowsTabAutoLayout = memo(() => {
+ const ref = useRef(null);
+ const $api = useState(() => atom(null))[0];
+ const onReady = useCallback(
+ (event) => {
+ $api.set(event.api);
+ initializeLayout(event.api);
+ },
+ [$api]
+ );
+ const resizeMainPanelOnFirstVisible = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const mainPanel = api.getPanel(MAIN_PANEL_ID);
+ if (!mainPanel) {
+ return;
+ }
+ if (mainPanel.width !== 0) {
+ return;
+ }
+ let count = 0;
+ const setSize = () => {
+ if (count++ > 50) {
+ return;
+ }
+ mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER });
+ if (mainPanel.width === 0) {
+ requestAnimationFrame(setSize);
+ return;
+ }
+ };
+ setSize();
+ }, [$api]);
+ useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
+
+ return (
+
+
+
+ );
+});
+WorkflowsTabAutoLayout.displayName = 'WorkflowsTabAutoLayout';
From df87800d61ccee90ac280fb109b48f200bebbc89 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 20 Jun 2025 12:51:02 +1000
Subject: [PATCH 118/210] feat(ui): restore floating panel buttons
---
.../SimpleSession/CanvasLaunchpadPanel.tsx | 2 +-
.../SimpleSession/GenerateLaunchpadPanel.tsx | 2 +-
.../SimpleSession/UpscalingLaunchpadPanel.tsx | 2 +-
.../SimpleSession/WorkflowsLaunchpadPanel.tsx | 2 +-
.../gallery/components/GalleryTopBar.tsx | 2 +-
.../components/FloatingLeftPanelButtons.tsx | 43 +++--
.../components/FloatingRightPanelButtons.tsx | 17 +-
.../ui/layouts/auto-layout-context.tsx | 28 +++-
.../ui/layouts/canvas-tab-auto-layout.tsx | 150 ++++++++++++-----
.../ui/layouts/generate-tab-auto-layout.tsx | 141 ++++++++++++----
.../ui/layouts/upscaling-tab-auto-layout.tsx | 155 +++++++++++++-----
.../ui/layouts/workflows-tab-auto-layout.tsx | 154 ++++++++++++-----
12 files changed, 512 insertions(+), 186 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx
index 4534cae881..735f07d1af 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx
@@ -10,7 +10,7 @@ import { LaunchpadUseALayoutImageButton } from './LaunchpadUseALayoutImageButton
export const CanvasLaunchpadPanel = memo(() => {
return (
-
+
Edit and refine on Canvas.
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx
index 2452296fe8..969b2669dd 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx
@@ -15,7 +15,7 @@ export const GenerateLaunchpadPanel = memo(() => {
return (
-
+
Generate images from text prompts.
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx
index 6279cf5970..98db034c3a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx
@@ -4,7 +4,7 @@ import { memo } from 'react';
export const UpscalingLaunchpadPanel = memo(() => {
return (
-
+
Upscale and add detail.
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel.tsx
index e048d94d6c..0a2ee4cfb1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel.tsx
@@ -4,7 +4,7 @@ import { memo } from 'react';
export const WorkflowsLaunchpadPanel = memo(() => {
return (
-
+
Go deep with Workflows.
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
index 6a05236929..20634f7450 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
@@ -17,7 +17,7 @@ export const GalleryTopBar = memo(() => {
const dispatch = useAppDispatch();
const boardSearchText = useAppSelector(selectBoardSearchText);
const boardSearchDisclosure = useBoardSearchDisclosure();
- const $api = useAutoLayoutContext();
+ const { $api } = useAutoLayoutContext();
const api = useStore($api);
const boardsPanel = useCollapsibleGridviewPanel(api, 'Boards', 'vertical', 256);
const isBoardsPanelCollapsed = useStore(boardsPanel.$isCollapsed);
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
index 789dda66c2..60d4f7dd9f 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
@@ -1,12 +1,12 @@
import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftModifier } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
+import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
+import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -19,18 +19,11 @@ import {
} from 'react-icons/pi';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
-export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) => {
- const tab = useAppSelector(selectActiveTab);
-
+export const FloatingLeftPanelButtons = memo(() => {
return (
- {tab === 'canvas' && (
-
-
-
- )}
-
+
@@ -41,13 +34,37 @@ export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) =
FloatingLeftPanelButtons.displayName = 'FloatingLeftPanelButtons';
-const ToggleLeftPanelButton = memo((props: { onToggle: () => void }) => {
+export const FloatingCanvasLeftPanelButtons = memo(() => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+FloatingCanvasLeftPanelButtons.displayName = 'FloatingCanvasLeftPanelButtons';
+
+const ToggleLeftPanelButton = memo(() => {
+ const { toggleLeftPanel } = useAutoLayoutContext();
+ useRegisteredHotkeys({
+ category: 'app',
+ id: 'toggleLeftPanel',
+ callback: toggleLeftPanel,
+ });
const { t } = useTranslation();
return (
}
flexGrow={1}
/>
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx
index 3c33bca265..126d0a4195 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx
@@ -1,24 +1,33 @@
import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
+import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
+import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImagesSquareBold } from 'react-icons/pi';
-export const FloatingRightPanelButtons = memo((props: { onToggle: () => void }) => {
+export const FloatingRightPanelButtons = memo(() => {
return (
-
+
);
});
FloatingRightPanelButtons.displayName = 'FloatingRightPanelButtons';
-const ToggleRightPanelButton = memo((props: { onToggle: () => void }) => {
+const ToggleRightPanelButton = memo(() => {
const { t } = useTranslation();
+ const { toggleRightPanel } = useAutoLayoutContext();
+ useRegisteredHotkeys({
+ category: 'app',
+ id: 'toggleRightPanel',
+ callback: toggleRightPanel,
+ });
+
return (
}
h={48}
/>
diff --git a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
index 87ad53faf8..d518ddca73 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
@@ -1,18 +1,32 @@
import type { GridviewApi } from 'dockview';
import type { Atom } from 'nanostores';
import type { PropsWithChildren } from 'react';
-import { createContext, useContext } from 'react';
+import { createContext, useContext, useMemo } from 'react';
-const AutoLayoutContext = createContext | null>(null);
+type AutoLayoutContextValue = {
+ $api: Atom;
+ toggleLeftPanel: () => void;
+ toggleRightPanel: () => void;
+};
-export const AutoLayoutProvider = (props: PropsWithChildren<{ $api: Atom }>) => {
- return {props.children};
+const AutoLayoutContext = createContext(null);
+
+export const AutoLayoutProvider = (props: PropsWithChildren) => {
+ const value = useMemo(
+ () => ({
+ $api: props.$api,
+ toggleLeftPanel: props.toggleLeftPanel,
+ toggleRightPanel: props.toggleRightPanel,
+ }),
+ [props.$api, props.toggleLeftPanel, props.toggleRightPanel]
+ );
+ return {props.children};
};
export const useAutoLayoutContext = () => {
- const api = useContext(AutoLayoutContext);
- if (!api) {
+ const value = useContext(AutoLayoutContext);
+ if (!value) {
throw new Error('useAutoLayoutContext must be used within an AutoLayoutProvider');
}
- return api;
+ return value;
};
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 ab88198660..c8b8c4855c 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
@@ -1,11 +1,13 @@
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
-import { DockviewReact, GridviewReact, Orientation } from 'dockview';
+import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview';
import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
+import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
+import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
@@ -84,57 +86,40 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const MainPanel = memo(() => {
return (
-
+ <>
+
+
+
+ >
);
});
MainPanel.displayName = 'MainPanel';
-const LEFT_PANEL_ID = 'left';
-const MAIN_PANEL_ID = 'main';
const BOARDS_PANEL_ID = 'boards';
const GALLERY_PANEL_ID = 'gallery';
const LAYERS_PANEL_ID = 'layers';
-export const canvasTabComponents: IGridviewReactProps['components'] = {
- [LEFT_PANEL_ID]: CanvasTabLeftPanel,
- [MAIN_PANEL_ID]: MainPanel,
+const rightPanelComponents: IGridviewReactProps['components'] = {
[BOARDS_PANEL_ID]: BoardsPanel,
[GALLERY_PANEL_ID]: GalleryPanel,
[LAYERS_PANEL_ID]: CanvasLayersPanel,
};
-export const initializeLayout = (api: GridviewApi) => {
- api.addPanel({
- id: MAIN_PANEL_ID,
- component: MAIN_PANEL_ID,
- });
- api.addPanel({
- id: LEFT_PANEL_ID,
- component: LEFT_PANEL_ID,
- minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
- position: {
- direction: 'left',
- referencePanel: MAIN_PANEL_ID,
- },
- });
+export const initializeRightLayout = (api: GridviewApi) => {
api.addPanel({
id: GALLERY_PANEL_ID,
component: GALLERY_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
- position: {
- direction: 'right',
- referencePanel: MAIN_PANEL_ID,
- },
});
api.addPanel({
id: LAYERS_PANEL_ID,
@@ -154,8 +139,63 @@ export const initializeLayout = (api: GridviewApi) => {
referencePanel: GALLERY_PANEL_ID,
},
});
- api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+};
+
+const onReadyRightPanel: IGridviewReactProps['onReady'] = (event) => {
+ initializeRightLayout(event.api);
+};
+
+const RightPanel = memo(() => {
+ return (
+ <>
+
+ >
+ );
+});
+RightPanel.displayName = 'RightPanel';
+
+const LEFT_PANEL_ID = 'left';
+const MAIN_PANEL_ID = 'main';
+const RIGHT_PANEL_ID = 'right';
+
+export const rootComponents: IGridviewReactProps['components'] = {
+ [LEFT_PANEL_ID]: CanvasTabLeftPanel,
+ [MAIN_PANEL_ID]: MainPanel,
+ [RIGHT_PANEL_ID]: RightPanel,
+};
+
+export const initializeRootLayout = (api: GridviewApi) => {
+ api.addPanel({
+ id: MAIN_PANEL_ID,
+ component: MAIN_PANEL_ID,
+ priority: LayoutPriority.High,
+ });
+ api.addPanel({
+ id: LEFT_PANEL_ID,
+ component: LEFT_PANEL_ID,
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: MAIN_PANEL_ID,
+ },
+ });
+ api.addPanel({
+ id: RIGHT_PANEL_ID,
+ component: RIGHT_PANEL_ID,
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'right',
+ referencePanel: MAIN_PANEL_ID,
+ },
+ });
+ api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ api.getPanel(RIGHT_PANEL_ID)?.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
@@ -165,7 +205,7 @@ export const CanvasTabAutoLayout = memo(() => {
const onReady = useCallback(
(event) => {
$api.set(event.api);
- initializeLayout(event.api);
+ initializeRootLayout(event.api);
},
[$api]
);
@@ -195,13 +235,47 @@ export const CanvasTabAutoLayout = memo(() => {
setSize();
}, [$api]);
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
+ const toggleLeftPanel = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const left = api.getPanel(LEFT_PANEL_ID);
+ if (!left) {
+ return;
+ }
+ if (left.maximumWidth === 0) {
+ left.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: LEFT_PANEL_MIN_SIZE_PX });
+ left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ } else {
+ left.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
+ left.api.setSize({ width: 0 });
+ }
+ }, [$api]);
+ const toggleRightPanel = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const right = api.getPanel(RIGHT_PANEL_ID);
+ if (!right) {
+ return;
+ }
+ if (right.maximumWidth === 0) {
+ right.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX });
+ right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
+ } else {
+ right.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
+ right.api.setSize({ width: 0 });
+ }
+ }, [$api]);
return (
-
+
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 379330daf5..7e77ead21c 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
@@ -1,10 +1,12 @@
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
-import { DockviewReact, GridviewReact, Orientation } from 'dockview';
+import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview';
import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
+import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
+import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
@@ -71,36 +73,84 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const MainPanel = memo(() => {
return (
-
+ <>
+
+
+
+ >
);
});
MainPanel.displayName = 'MainPanel';
-const LEFT_PANEL_ID = 'left';
-const MAIN_PANEL_ID = 'main';
const BOARDS_PANEL_ID = 'boards';
const GALLERY_PANEL_ID = 'gallery';
-export const generateTabComponents: IGridviewReactProps['components'] = {
- [LEFT_PANEL_ID]: GenerateTabLeftPanel,
- [MAIN_PANEL_ID]: MainPanel,
+const rightPanelComponents: IGridviewReactProps['components'] = {
[BOARDS_PANEL_ID]: BoardsPanel,
[GALLERY_PANEL_ID]: GalleryPanel,
};
-export const initializeLayout = (api: GridviewApi) => {
+export const initializeRightLayout = (api: GridviewApi) => {
+ api.addPanel({
+ id: GALLERY_PANEL_ID,
+ component: GALLERY_PANEL_ID,
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ });
+ api.addPanel({
+ id: BOARDS_PANEL_ID,
+ component: BOARDS_PANEL_ID,
+ minimumHeight: 36,
+ position: {
+ direction: 'above',
+ referencePanel: GALLERY_PANEL_ID,
+ },
+ });
+ api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+};
+
+const onReadyRightPanel: IGridviewReactProps['onReady'] = (event) => {
+ initializeRightLayout(event.api);
+};
+
+const RightPanel = memo(() => {
+ return (
+ <>
+
+ >
+ );
+});
+RightPanel.displayName = 'RightPanel';
+
+const LEFT_PANEL_ID = 'left';
+const MAIN_PANEL_ID = 'main';
+const RIGHT_PANEL_ID = 'right';
+
+export const rootComponents: IGridviewReactProps['components'] = {
+ [LEFT_PANEL_ID]: GenerateTabLeftPanel,
+ [MAIN_PANEL_ID]: MainPanel,
+ [RIGHT_PANEL_ID]: RightPanel,
+};
+
+export const initializeRootLayout = (api: GridviewApi) => {
api.addPanel({
id: MAIN_PANEL_ID,
component: MAIN_PANEL_ID,
+ priority: LayoutPriority.High,
});
api.addPanel({
id: LEFT_PANEL_ID,
@@ -112,26 +162,16 @@ export const initializeLayout = (api: GridviewApi) => {
},
});
api.addPanel({
- id: GALLERY_PANEL_ID,
- component: GALLERY_PANEL_ID,
+ id: RIGHT_PANEL_ID,
+ component: RIGHT_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
- minimumHeight: 232,
position: {
direction: 'right',
referencePanel: MAIN_PANEL_ID,
},
});
- api.addPanel({
- id: BOARDS_PANEL_ID,
- component: BOARDS_PANEL_ID,
- minimumHeight: 36,
- position: {
- direction: 'above',
- referencePanel: GALLERY_PANEL_ID,
- },
- });
api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
- api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+ api.getPanel(RIGHT_PANEL_ID)?.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
@@ -141,7 +181,7 @@ export const GenerateTabAutoLayout = memo(() => {
const onReady = useCallback(
(event) => {
$api.set(event.api);
- initializeLayout(event.api);
+ initializeRootLayout(event.api);
},
[$api]
);
@@ -171,13 +211,46 @@ export const GenerateTabAutoLayout = memo(() => {
setSize();
}, [$api]);
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
-
+ const toggleLeftPanel = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const left = api.getPanel(LEFT_PANEL_ID);
+ if (!left) {
+ return;
+ }
+ if (left.maximumWidth === 0) {
+ left.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: LEFT_PANEL_MIN_SIZE_PX });
+ left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ } else {
+ left.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
+ left.api.setSize({ width: 0 });
+ }
+ }, [$api]);
+ const toggleRightPanel = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const right = api.getPanel(RIGHT_PANEL_ID);
+ if (!right) {
+ return;
+ }
+ if (right.maximumWidth === 0) {
+ right.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX });
+ right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
+ } else {
+ right.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
+ right.api.setSize({ width: 0 });
+ }
+ }, [$api]);
return (
-
+
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 36b2968147..175b77d67c 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
@@ -1,10 +1,12 @@
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
-import { DockviewReact, GridviewReact, Orientation } from 'dockview';
+import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview';
import { UpscalingLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
+import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
+import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
@@ -71,58 +73,38 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const MainPanel = memo(() => {
return (
-
+ <>
+
+
+
+ >
);
});
MainPanel.displayName = 'MainPanel';
-const LEFT_PANEL_ID = 'left';
-const MAIN_PANEL_ID = 'main';
const BOARDS_PANEL_ID = 'boards';
const GALLERY_PANEL_ID = 'gallery';
-export const gridviewComponents: IGridviewReactProps['components'] = {
- [LEFT_PANEL_ID]: UpscalingTabLeftPanel,
- [MAIN_PANEL_ID]: MainPanel,
+const rightPanelComponents: IGridviewReactProps['components'] = {
[BOARDS_PANEL_ID]: BoardsPanel,
[GALLERY_PANEL_ID]: GalleryPanel,
};
-export const initializeLayout = (api: GridviewApi) => {
- api.addPanel({
- id: MAIN_PANEL_ID,
- component: MAIN_PANEL_ID,
- // priority: LayoutPriority.High,
- });
- api.addPanel({
- id: LEFT_PANEL_ID,
- component: LEFT_PANEL_ID,
- minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
- position: {
- direction: 'left',
- referencePanel: MAIN_PANEL_ID,
- },
- // priority: LayoutPriority.High,
- });
+export const initializeRightLayout = (api: GridviewApi) => {
api.addPanel({
id: GALLERY_PANEL_ID,
component: GALLERY_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
- position: {
- direction: 'right',
- referencePanel: MAIN_PANEL_ID,
- },
- // priority: LayoutPriority.High,
});
api.addPanel({
id: BOARDS_PANEL_ID,
@@ -132,10 +114,64 @@ export const initializeLayout = (api: GridviewApi) => {
direction: 'above',
referencePanel: GALLERY_PANEL_ID,
},
- // priority: LayoutPriority.High,
+ });
+ api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+};
+
+const onReadyRightPanel: IGridviewReactProps['onReady'] = (event) => {
+ initializeRightLayout(event.api);
+};
+
+const RightPanel = memo(() => {
+ return (
+ <>
+
+ >
+ );
+});
+RightPanel.displayName = 'RightPanel';
+
+const LEFT_PANEL_ID = 'left';
+const MAIN_PANEL_ID = 'main';
+const RIGHT_PANEL_ID = 'right';
+
+export const rootComponents: IGridviewReactProps['components'] = {
+ [LEFT_PANEL_ID]: UpscalingTabLeftPanel,
+ [MAIN_PANEL_ID]: MainPanel,
+ [RIGHT_PANEL_ID]: RightPanel,
+};
+
+export const initializeRootLayout = (api: GridviewApi) => {
+ api.addPanel({
+ id: MAIN_PANEL_ID,
+ component: MAIN_PANEL_ID,
+ priority: LayoutPriority.High,
+ });
+ api.addPanel({
+ id: LEFT_PANEL_ID,
+ component: LEFT_PANEL_ID,
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: MAIN_PANEL_ID,
+ },
+ });
+ api.addPanel({
+ id: RIGHT_PANEL_ID,
+ component: RIGHT_PANEL_ID,
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'right',
+ referencePanel: MAIN_PANEL_ID,
+ },
});
api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
- api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+ api.getPanel(RIGHT_PANEL_ID)?.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
@@ -145,7 +181,7 @@ export const UpscalingTabAutoLayout = memo(() => {
const onReady = useCallback(
(event) => {
$api.set(event.api);
- initializeLayout(event.api);
+ initializeRootLayout(event.api);
},
[$api]
);
@@ -175,13 +211,46 @@ export const UpscalingTabAutoLayout = memo(() => {
setSize();
}, [$api]);
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
-
+ const toggleLeftPanel = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const left = api.getPanel(LEFT_PANEL_ID);
+ if (!left) {
+ return;
+ }
+ if (left.maximumWidth === 0) {
+ left.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: LEFT_PANEL_MIN_SIZE_PX });
+ left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ } else {
+ left.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
+ left.api.setSize({ width: 0 });
+ }
+ }, [$api]);
+ const toggleRightPanel = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const right = api.getPanel(RIGHT_PANEL_ID);
+ if (!right) {
+ return;
+ }
+ if (right.maximumWidth === 0) {
+ right.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX });
+ right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
+ } else {
+ right.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
+ right.api.setSize({ width: 0 });
+ }
+ }, [$api]);
return (
-
+
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 c94bd5f081..f8c5766c57 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
@@ -1,5 +1,5 @@
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
-import { DockviewReact, GridviewReact, Orientation } from 'dockview';
+import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview';
import { WorkflowsLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
@@ -7,6 +7,8 @@ import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer
import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
import NodeEditor from 'features/nodes/components/NodeEditor';
import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
+import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
+import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
@@ -83,58 +85,38 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const MainPanel = memo(() => {
return (
-
+ <>
+
+
+
+ >
);
});
MainPanel.displayName = 'MainPanel';
-const LEFT_PANEL_ID = 'left';
-const MAIN_PANEL_ID = 'main';
const BOARDS_PANEL_ID = 'boards';
const GALLERY_PANEL_ID = 'gallery';
-export const gridviewComponents: IGridviewReactProps['components'] = {
- [LEFT_PANEL_ID]: WorkflowsTabLeftPanel,
- [MAIN_PANEL_ID]: MainPanel,
+const rightPanelComponents: IGridviewReactProps['components'] = {
[BOARDS_PANEL_ID]: BoardsPanel,
[GALLERY_PANEL_ID]: GalleryPanel,
};
-export const initializeLayout = (api: GridviewApi) => {
- api.addPanel({
- id: MAIN_PANEL_ID,
- component: MAIN_PANEL_ID,
- // priority: LayoutPriority.High,
- });
- api.addPanel({
- id: LEFT_PANEL_ID,
- component: LEFT_PANEL_ID,
- minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
- position: {
- direction: 'left',
- referencePanel: MAIN_PANEL_ID,
- },
- // priority: LayoutPriority.High,
- });
+export const initializeRightLayout = (api: GridviewApi) => {
api.addPanel({
id: GALLERY_PANEL_ID,
component: GALLERY_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
- position: {
- direction: 'right',
- referencePanel: MAIN_PANEL_ID,
- },
- // priority: LayoutPriority.High,
});
api.addPanel({
id: BOARDS_PANEL_ID,
@@ -144,10 +126,64 @@ export const initializeLayout = (api: GridviewApi) => {
direction: 'above',
referencePanel: GALLERY_PANEL_ID,
},
- // priority: LayoutPriority.High,
+ });
+ api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+};
+
+const onReadyRightPanel: IGridviewReactProps['onReady'] = (event) => {
+ initializeRightLayout(event.api);
+};
+
+const RightPanel = memo(() => {
+ return (
+ <>
+
+ >
+ );
+});
+RightPanel.displayName = 'RightPanel';
+
+const LEFT_PANEL_ID = 'left';
+const MAIN_PANEL_ID = 'main';
+const RIGHT_PANEL_ID = 'right';
+
+export const rootComponents: IGridviewReactProps['components'] = {
+ [LEFT_PANEL_ID]: WorkflowsTabLeftPanel,
+ [MAIN_PANEL_ID]: MainPanel,
+ [RIGHT_PANEL_ID]: RightPanel,
+};
+
+export const initializeRootLayout = (api: GridviewApi) => {
+ api.addPanel({
+ id: MAIN_PANEL_ID,
+ component: MAIN_PANEL_ID,
+ priority: LayoutPriority.High,
+ });
+ api.addPanel({
+ id: LEFT_PANEL_ID,
+ component: LEFT_PANEL_ID,
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: MAIN_PANEL_ID,
+ },
+ });
+ api.addPanel({
+ id: RIGHT_PANEL_ID,
+ component: RIGHT_PANEL_ID,
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'right',
+ referencePanel: MAIN_PANEL_ID,
+ },
});
api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
- api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+ api.getPanel(RIGHT_PANEL_ID)?.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
@@ -157,7 +193,7 @@ export const WorkflowsTabAutoLayout = memo(() => {
const onReady = useCallback(
(event) => {
$api.set(event.api);
- initializeLayout(event.api);
+ initializeRootLayout(event.api);
},
[$api]
);
@@ -187,13 +223,47 @@ export const WorkflowsTabAutoLayout = memo(() => {
setSize();
}, [$api]);
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
+ const toggleLeftPanel = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const left = api.getPanel(LEFT_PANEL_ID);
+ if (!left) {
+ return;
+ }
+ if (left.maximumWidth === 0) {
+ left.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: LEFT_PANEL_MIN_SIZE_PX });
+ left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ } else {
+ left.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
+ left.api.setSize({ width: 0 });
+ }
+ }, [$api]);
+ const toggleRightPanel = useCallback(() => {
+ const api = $api.get();
+ if (!api) {
+ return;
+ }
+ const right = api.getPanel(RIGHT_PANEL_ID);
+ if (!right) {
+ return;
+ }
+ if (right.maximumWidth === 0) {
+ right.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX });
+ right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
+ } else {
+ right.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
+ right.api.setSize({ width: 0 });
+ }
+ }, [$api]);
return (
-
+
From f4794e409b21267d37f30331c5f265e6b4337042 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 20 Jun 2025 12:53:01 +1000
Subject: [PATCH 119/210] fix(ui): generate tab hotkey
---
.../frontend/web/src/common/hooks/useGlobalHotkeys.ts | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
index 88fc2bfdfb..9ff3141613 100644
--- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
+++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
@@ -61,6 +61,15 @@ export const useGlobalHotkeys = () => {
dependencies: [clearQueue],
});
+ useRegisteredHotkeys({
+ id: 'selectGenerateTab',
+ category: 'app',
+ callback: () => {
+ dispatch(setActiveTab('generate'));
+ },
+ dependencies: [dispatch],
+ });
+
useRegisteredHotkeys({
id: 'selectCanvasTab',
category: 'app',
From 553d1a6ac6cab1ea24071f1156dcf9b26b26752a Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 20 Jun 2025 13:33:09 +1000
Subject: [PATCH 120/210] feat(ui): restore all panel hotkeys
---
.../src/features/ui/components/AppContent.tsx | 44 -------
.../components/FloatingLeftPanelButtons.tsx | 6 -
.../components/FloatingRightPanelButtons.tsx | 6 -
.../ui/layouts/auto-layout-context.tsx | 119 +++++++++++++++++-
.../ui/layouts/canvas-tab-auto-layout.tsx | 84 +++----------
.../ui/layouts/generate-tab-auto-layout.tsx | 81 ++----------
.../web/src/features/ui/layouts/shared.ts | 6 +
.../ui/layouts/upscaling-tab-auto-layout.tsx | 81 ++----------
.../ui/layouts/use-on-first-visible.ts | 35 +++++-
.../ui/layouts/workflows-tab-auto-layout.tsx | 80 ++----------
10 files changed, 206 insertions(+), 336 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/shared.ts
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 3cd6b73435..5420511a6d 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -24,53 +24,9 @@ const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCo
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
export const AppContent = memo(() => {
- // const tab = useAppSelector(selectActiveTab);
const tabIndex = useAppSelector(selectActiveTabIndex);
- // const imperativePanelGroupRef = useRef(null);
useDndMonitor();
- // const withLeftPanel = useAppSelector(selectWithLeftPanel);
- // const leftPanelUsePanelOptions = useMemo(
- // () => ({
- // id: 'left-panel',
- // minSizePx: LEFT_PANEL_MIN_SIZE_PX,
- // defaultSizePx: LEFT_PANEL_MIN_SIZE_PX,
- // imperativePanelGroupRef,
- // panelGroupDirection: 'horizontal',
- // onCollapse: onLeftPanelCollapse,
- // }),
- // []
- // );
- // const leftPanel = usePanel(leftPanelUsePanelOptions);
- // useRegisteredHotkeys({
- // id: 'toggleLeftPanel',
- // category: 'app',
- // callback: leftPanel.toggle,
- // options: { enabled: withLeftPanel },
- // dependencies: [leftPanel.toggle, withLeftPanel],
- // });
-
- // const withRightPanel = useAppSelector(selectWithRightPanel);
- // const rightPanelUsePanelOptions = useMemo(
- // () => ({
- // id: 'right-panel',
- // minSizePx: RIGHT_PANEL_MIN_SIZE_PX,
- // defaultSizePx: RIGHT_PANEL_MIN_SIZE_PX,
- // imperativePanelGroupRef,
- // panelGroupDirection: 'horizontal',
- // onCollapse: onRightPanelCollapse,
- // }),
- // []
- // );
- // const rightPanel = usePanel(rightPanelUsePanelOptions);
- // useRegisteredHotkeys({
- // id: 'toggleRightPanel',
- // category: 'app',
- // callback: rightPanel.toggle,
- // options: { enabled: withRightPanel },
- // dependencies: [rightPanel.toggle, withRightPanel],
- // });
-
// useRegisteredHotkeys({
// id: 'resetPanelLayout',
// category: 'app',
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
index 60d4f7dd9f..d19f31798b 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx
@@ -5,7 +5,6 @@ import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/compone
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke';
-import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -54,11 +53,6 @@ FloatingCanvasLeftPanelButtons.displayName = 'FloatingCanvasLeftPanelButtons';
const ToggleLeftPanelButton = memo(() => {
const { toggleLeftPanel } = useAutoLayoutContext();
- useRegisteredHotkeys({
- category: 'app',
- id: 'toggleLeftPanel',
- callback: toggleLeftPanel,
- });
const { t } = useTranslation();
return (
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx
index 126d0a4195..247f28f351 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx
@@ -1,5 +1,4 @@
import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
-import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -17,11 +16,6 @@ FloatingRightPanelButtons.displayName = 'FloatingRightPanelButtons';
const ToggleRightPanelButton = memo(() => {
const { t } = useTranslation();
const { toggleRightPanel } = useAutoLayoutContext();
- useRegisteredHotkeys({
- category: 'app',
- id: 'toggleRightPanel',
- callback: toggleRightPanel,
- });
return (
diff --git a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
index d518ddca73..2d6f8426c2 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
@@ -1,24 +1,106 @@
import type { GridviewApi } from 'dockview';
+import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import type { Atom } from 'nanostores';
import type { PropsWithChildren } from 'react';
-import { createContext, useContext, useMemo } from 'react';
+import { createContext, memo, useCallback, useContext, useMemo } from 'react';
+
+import { LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX } from './shared';
type AutoLayoutContextValue = {
$api: Atom;
toggleLeftPanel: () => void;
toggleRightPanel: () => void;
+ toggleBothPanels: () => void;
+ resetPanels: () => void;
};
const AutoLayoutContext = createContext(null);
-export const AutoLayoutProvider = (props: PropsWithChildren) => {
+const expandPanel = (api: GridviewApi, panelId: string, width: number) => {
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+ panel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: width });
+ panel.api.setSize({ width: width });
+};
+
+const collapsePanel = (api: GridviewApi, panelId: string) => {
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+ panel.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
+ panel.api.setSize({ width: 0 });
+};
+
+const getIsCollapsed = (api: GridviewApi, panelId: string) => {
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return true; // ??
+ }
+ return panel.maximumWidth === 0;
+};
+
+export const AutoLayoutProvider = (props: PropsWithChildren<{ $api: Atom }>) => {
+ const toggleLeftPanel = useCallback(() => {
+ const api = props.$api.get();
+ if (!api) {
+ return;
+ }
+ if (getIsCollapsed(api, LEFT_PANEL_ID)) {
+ expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX);
+ } else {
+ collapsePanel(api, LEFT_PANEL_ID);
+ }
+ }, [props.$api]);
+
+ const toggleRightPanel = useCallback(() => {
+ const api = props.$api.get();
+ if (!api) {
+ return;
+ }
+ if (getIsCollapsed(api, RIGHT_PANEL_ID)) {
+ expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX);
+ } else {
+ collapsePanel(api, RIGHT_PANEL_ID);
+ }
+ }, [props.$api]);
+
+ const toggleBothPanels = useCallback(() => {
+ const api = props.$api.get();
+ if (!api) {
+ return;
+ }
+ requestAnimationFrame(() => {
+ if (getIsCollapsed(api, RIGHT_PANEL_ID) || getIsCollapsed(api, LEFT_PANEL_ID)) {
+ expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX);
+ expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX);
+ } else {
+ collapsePanel(api, LEFT_PANEL_ID);
+ collapsePanel(api, RIGHT_PANEL_ID);
+ }
+ });
+ }, [props.$api]);
+
+ const resetPanels = useCallback(() => {
+ const api = props.$api.get();
+ if (!api) {
+ return;
+ }
+ expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX);
+ expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX);
+ }, [props.$api]);
+
const value = useMemo(
() => ({
$api: props.$api,
- toggleLeftPanel: props.toggleLeftPanel,
- toggleRightPanel: props.toggleRightPanel,
+ toggleLeftPanel,
+ toggleRightPanel,
+ toggleBothPanels,
+ resetPanels,
}),
- [props.$api, props.toggleLeftPanel, props.toggleRightPanel]
+ [props.$api, resetPanels, toggleBothPanels, toggleLeftPanel, toggleRightPanel]
);
return