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;
}