From 0221ca8f49db4a0f80635ced038f5bc33206f642 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Sat, 13 May 2023 16:24:04 +1000
Subject: [PATCH 1/9] fix(ui): use cloned canvas for retrieving dataURL/Blobs
---
.../src/features/canvas/util/getCanvasData.ts | 28 +++++++++++--------
1 file changed, 17 insertions(+), 11 deletions(-)
diff --git a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts b/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts
index 131b109f55..28900fcc44 100644
--- a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts
+++ b/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts
@@ -26,11 +26,11 @@ export const getCanvasData = async (state: RootState) => {
layerState: { objects },
boundingBoxCoordinates,
boundingBoxDimensions,
- stageScale,
isMaskEnabled,
shouldPreserveMaskedArea,
boundingBoxScaleMethod: boundingBoxScale,
scaledBoundingBoxDimensions,
+ stageCoordinates,
} = state.canvas;
const boundingBox = {
@@ -46,14 +46,14 @@ export const getCanvasData = async (state: RootState) => {
// generationParameters.bounding_box = boundingBox;
- const tempScale = canvasBaseLayer.scale();
+ // clone the base layer so we don't affect the actual canvas during scaling
+ const clonedBaseLayer = canvasBaseLayer.clone();
- canvasBaseLayer.scale({
- x: 1 / stageScale,
- y: 1 / stageScale,
- });
+ // scale to 1 so we get an uninterpolated image
+ clonedBaseLayer.scale({ x: 1, y: 1 });
- const absPos = canvasBaseLayer.getAbsolutePosition();
+ // absolute position is needed to get the bounding box coords relative to the base layer
+ const absPos = clonedBaseLayer.getAbsolutePosition();
const offsetBoundingBox = {
x: boundingBox.x + absPos.x,
@@ -62,35 +62,41 @@ export const getCanvasData = async (state: RootState) => {
height: boundingBox.height,
};
- const baseDataURL = canvasBaseLayer.toDataURL(offsetBoundingBox);
+ // get a dataURL of the bbox'd region (will convert this to an ImageData to check its transparency)
+ const baseDataURL = clonedBaseLayer.toDataURL(offsetBoundingBox);
+
+ // get a blob (will upload this as the canvas intermediate)
const baseBlob = await canvasToBlob(
- canvasBaseLayer.toCanvas(offsetBoundingBox)
+ clonedBaseLayer.toCanvas(offsetBoundingBox)
);
- canvasBaseLayer.scale(tempScale);
-
+ // build a new mask layer and get its dataURL and blob
const { maskDataURL, maskBlob } = await generateMask(
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
boundingBox
);
+ // convert to ImageData (via pure jank)
const baseImageData = await dataURLToImageData(
baseDataURL,
boundingBox.width,
boundingBox.height
);
+ // convert to ImageData (via pure jank)
const maskImageData = await dataURLToImageData(
maskDataURL,
boundingBox.width,
boundingBox.height
);
+ // check transparency
const {
isPartiallyTransparent: baseIsPartiallyTransparent,
isFullyTransparent: baseIsFullyTransparent,
} = getImageDataTransparency(baseImageData.data);
+ // check mask for black
const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data);
if (state.system.enableImageDebugging) {
From 5e4457445f77f2c52aa3bfc50db85f83f17308cf Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 15 May 2023 13:53:41 +1000
Subject: [PATCH 2/9] feat(ui): make toast/hotkey into logical components
---
.../frontend/web/src/app/components/App.tsx | 121 +++++++++---------
.../components/GlobalHotkeys.ts} | 11 +-
.../web/src/app/components/Toaster.ts | 65 ++++++++++
.../listeners/initialImageSelected.ts | 2 +-
.../src/common/components/ImageUploader.tsx | 15 ++-
.../components/CurrentImageButtons.tsx | 24 ++--
.../gallery/components/HoverableImage.tsx | 6 +-
.../features/nodes/components/AddNodeMenu.tsx | 10 +-
.../nodes/components/search/NodeSearch.tsx | 8 +-
.../parameters/hooks/useParameters.ts | 28 ++--
.../features/system/hooks/useToastWatcher.ts | 34 -----
.../src/features/system/store/systemSlice.ts | 2 +-
.../services/events/util/setEventListeners.ts | 2 +-
13 files changed, 181 insertions(+), 147 deletions(-)
rename invokeai/frontend/web/src/{common/hooks/useGlobalHotkeys.ts => app/components/GlobalHotkeys.ts} (89%)
create mode 100644 invokeai/frontend/web/src/app/components/Toaster.ts
delete mode 100644 invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index eb6496f43e..e819c04352 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -2,9 +2,6 @@ import ImageUploader from 'common/components/ImageUploader';
import SiteHeader from 'features/system/components/SiteHeader';
import ProgressBar from 'features/system/components/ProgressBar';
import InvokeTabs from 'features/ui/components/InvokeTabs';
-
-import useToastWatcher from 'features/system/hooks/useToastWatcher';
-
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
import { Box, Flex, Grid, Portal } from '@chakra-ui/react';
@@ -17,13 +14,14 @@ import { motion, AnimatePresence } from 'framer-motion';
import Loading from 'common/components/Loading/Loading';
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
import { PartialAppConfig } from 'app/types/invokeai';
-import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { configChanged } from 'features/system/store/configSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useLogger } from 'app/logging/useLogger';
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
import { languageSelector } from 'features/system/store/systemSelectors';
import i18n from 'i18n';
+import Toaster from './Toaster';
+import GlobalHotkeys from './GlobalHotkeys';
const DEFAULT_CONFIG = {};
@@ -38,9 +36,6 @@ const App = ({
headerComponent,
setIsReady,
}: Props) => {
- useToastWatcher();
- useGlobalHotkeys();
-
const language = useAppSelector(languageSelector);
const log = useLogger();
@@ -77,65 +72,69 @@ const App = ({
}, [isApplicationReady, setIsReady]);
return (
-
- {isLightboxEnabled && }
-
-
-
- {headerComponent || }
-
+
+ {isLightboxEnabled && }
+
+
+
-
-
-
-
+ {headerComponent || }
+
+
+
+
+
-
-
+
+
-
- {!isApplicationReady && !loadingOverridden && (
-
-
-
-
-
-
- )}
-
+
+ {!isApplicationReady && !loadingOverridden && (
+
+
+
+
+
+
+ )}
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/app/components/GlobalHotkeys.ts
similarity index 89%
rename from invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
rename to invokeai/frontend/web/src/app/components/GlobalHotkeys.ts
index 3935a390fb..c4660416bf 100644
--- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
+++ b/invokeai/frontend/web/src/app/components/GlobalHotkeys.ts
@@ -10,6 +10,7 @@ import {
togglePinParametersPanel,
} from 'features/ui/store/uiSlice';
import { isEqual } from 'lodash-es';
+import React, { memo } from 'react';
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
const globalHotkeysSelector = createSelector(
@@ -27,7 +28,11 @@ const globalHotkeysSelector = createSelector(
// TODO: Does not catch keypresses while focused in an input. Maybe there is a way?
-export const useGlobalHotkeys = () => {
+/**
+ * Logical component. Handles app-level global hotkeys.
+ * @returns null
+ */
+const GlobalHotkeys: React.FC = () => {
const dispatch = useAppDispatch();
const { shift } = useAppSelector(globalHotkeysSelector);
@@ -75,4 +80,8 @@ export const useGlobalHotkeys = () => {
useHotkeys('4', () => {
dispatch(setActiveTab('nodes'));
});
+
+ return null;
};
+
+export default memo(GlobalHotkeys);
diff --git a/invokeai/frontend/web/src/app/components/Toaster.ts b/invokeai/frontend/web/src/app/components/Toaster.ts
new file mode 100644
index 0000000000..66ba1d4925
--- /dev/null
+++ b/invokeai/frontend/web/src/app/components/Toaster.ts
@@ -0,0 +1,65 @@
+import { useToast, UseToastOptions } from '@chakra-ui/react';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { toastQueueSelector } from 'features/system/store/systemSelectors';
+import { addToast, clearToastQueue } from 'features/system/store/systemSlice';
+import { useCallback, useEffect } from 'react';
+
+export type MakeToastArg = string | UseToastOptions;
+
+/**
+ * Makes a toast from a string or a UseToastOptions object.
+ * If a string is passed, the toast will have the status 'info' and will be closable with a duration of 2500ms.
+ */
+export const makeToast = (arg: MakeToastArg): UseToastOptions => {
+ if (typeof arg === 'string') {
+ return {
+ title: arg,
+ status: 'info',
+ isClosable: true,
+ duration: 2500,
+ };
+ }
+
+ return { status: 'info', isClosable: true, duration: 2500, ...arg };
+};
+
+/**
+ * Logical component. Watches the toast queue and makes toasts when the queue is not empty.
+ * @returns null
+ */
+const Toaster = () => {
+ const dispatch = useAppDispatch();
+ const toastQueue = useAppSelector(toastQueueSelector);
+ const toast = useToast();
+ useEffect(() => {
+ toastQueue.forEach((t) => {
+ toast(t);
+ });
+ toastQueue.length > 0 && dispatch(clearToastQueue());
+ }, [dispatch, toast, toastQueue]);
+
+ return null;
+};
+
+/**
+ * Returns a function that can be used to make a toast.
+ * @example
+ * const toaster = useAppToaster();
+ * toaster('Hello world!');
+ * toaster({ title: 'Hello world!', status: 'success' });
+ * @returns A function that can be used to make a toast.
+ * @see makeToast
+ * @see MakeToastArg
+ * @see UseToastOptions
+ */
+export const useAppToaster = () => {
+ const dispatch = useAppDispatch();
+ const toaster = useCallback(
+ (arg: MakeToastArg) => dispatch(addToast(makeToast(arg))),
+ [dispatch]
+ );
+
+ return toaster;
+};
+
+export default Toaster;
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts
index 6bc2f9e9bc..ae3a35f537 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts
@@ -2,11 +2,11 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { Image, isInvokeAIImage } from 'app/types/invokeai';
import { selectResultsById } from 'features/gallery/store/resultsSlice';
import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
-import { makeToast } from 'features/system/hooks/useToastWatcher';
import { t } from 'i18next';
import { addToast } from 'features/system/store/systemSlice';
import { startAppListening } from '..';
import { initialImageSelected } from 'features/parameters/store/actions';
+import { makeToast } from 'app/components/Toaster';
export const addInitialImageSelectedListener = () => {
startAppListening({
diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx
index ee3b9d135e..c773fb85ed 100644
--- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx
+++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx
@@ -1,4 +1,4 @@
-import { Box, useToast } from '@chakra-ui/react';
+import { Box } from '@chakra-ui/react';
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import useImageUploader from 'common/hooks/useImageUploader';
@@ -16,6 +16,7 @@ import { FileRejection, useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { imageUploaded } from 'services/thunks/image';
import ImageUploadOverlay from './ImageUploadOverlay';
+import { useAppToaster } from 'app/components/Toaster';
type ImageUploaderProps = {
children: ReactNode;
@@ -25,7 +26,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props;
const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector);
- const toast = useToast({});
+ const toaster = useAppToaster();
const { t } = useTranslation();
const [isHandlingUpload, setIsHandlingUpload] = useState(false);
const { setOpenUploader } = useImageUploader();
@@ -37,14 +38,14 @@ const ImageUploader = (props: ImageUploaderProps) => {
(acc: string, cur: { message: string }) => `${acc}\n${cur.message}`,
''
);
- toast({
+ toaster({
title: t('toast.uploadFailed'),
description: msg,
status: 'error',
isClosable: true,
});
},
- [t, toast]
+ [t, toaster]
);
const fileAcceptedCallback = useCallback(
@@ -105,7 +106,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
e.stopImmediatePropagation();
if (imageItems.length > 1) {
- toast({
+ toaster({
description: t('toast.uploadFailedMultipleImagesDesc'),
status: 'error',
isClosable: true,
@@ -116,7 +117,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
const file = imageItems[0].getAsFile();
if (!file) {
- toast({
+ toaster({
description: t('toast.uploadFailedUnableToLoadDesc'),
status: 'error',
isClosable: true,
@@ -130,7 +131,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
return () => {
document.removeEventListener('paste', pasteImageListener);
};
- }, [t, dispatch, toast, activeTabName]);
+ }, [t, dispatch, toaster, activeTabName]);
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
activeTabName
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
index e76f3fa41e..980c317ac3 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
@@ -5,15 +5,8 @@ import {
ButtonGroup,
Flex,
FlexProps,
- IconButton,
Link,
- Menu,
- MenuButton,
- MenuItemOption,
- MenuList,
- MenuOptionGroup,
useDisclosure,
- useToast,
} from '@chakra-ui/react';
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@@ -70,6 +63,7 @@ import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceR
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
import { allParametersSet } from 'features/parameters/store/generationSlice';
import DeleteImageButton from './ImageActionButtons/DeleteImageButton';
+import { useAppToaster } from 'app/components/Toaster';
const currentImageButtonsSelector = createSelector(
[
@@ -164,7 +158,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
onClose: onDeleteDialogClose,
} = useDisclosure();
- const toast = useToast();
+ const toaster = useAppToaster();
const { t } = useTranslation();
const { recallPrompt, recallSeed, recallAllParameters } = useParameters();
@@ -213,7 +207,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const url = getImageUrl();
if (!url) {
- toast({
+ toaster({
title: t('toast.problemCopyingImageLink'),
status: 'error',
duration: 2500,
@@ -224,14 +218,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}
navigator.clipboard.writeText(url).then(() => {
- toast({
+ toaster({
title: t('toast.imageLinkCopied'),
status: 'success',
duration: 2500,
isClosable: true,
});
});
- }, [toast, shouldTransformUrls, getUrl, t, image]);
+ }, [toaster, shouldTransformUrls, getUrl, t, image]);
const handlePreviewVisibility = useCallback(() => {
dispatch(setShouldHidePreview(!shouldHidePreview));
@@ -346,13 +340,13 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
dispatch(setActiveTab('unifiedCanvas'));
}
- toast({
+ toaster({
title: t('toast.sentToUnifiedCanvas'),
status: 'success',
duration: 2500,
isClosable: true,
});
- }, [image, isLightboxOpen, dispatch, activeTabName, toast, t]);
+ }, [image, isLightboxOpen, dispatch, activeTabName, toaster, t]);
useHotkeys(
'i',
@@ -360,7 +354,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
if (image) {
handleClickShowImageDetails();
} else {
- toast({
+ toaster({
title: t('toast.metadataLoadFailed'),
status: 'error',
duration: 2500,
@@ -368,7 +362,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
});
}
},
- [image, shouldShowImageDetails]
+ [image, shouldShowImageDetails, toaster]
);
const handleDelete = useCallback(() => {
diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
index c3980a9ad4..6eb44de99c 100644
--- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
@@ -6,7 +6,6 @@ import {
MenuItem,
MenuList,
useDisclosure,
- useToast,
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { imageSelected } from 'features/gallery/store/gallerySlice';
@@ -35,6 +34,7 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useParameters } from 'features/parameters/hooks/useParameters';
import { initialImageSelected } from 'features/parameters/store/actions';
import { requestedImageDeletion } from '../store/actions';
+import { useAppToaster } from 'app/components/Toaster';
export const selector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
@@ -101,7 +101,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const [isHovered, setIsHovered] = useState(false);
- const toast = useToast();
+ const toaster = useAppToaster();
const { t } = useTranslation();
@@ -176,7 +176,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
dispatch(setActiveTab('unifiedCanvas'));
}
- toast({
+ toaster({
title: t('toast.sentToUnifiedCanvas'),
status: 'success',
duration: 2500,
diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx
index a4ce2f55f6..db390ed518 100644
--- a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx
@@ -13,10 +13,9 @@ import { nodeAdded } from '../store/nodesSlice';
import { map } from 'lodash-es';
import { RootState } from 'app/store/store';
import { useBuildInvocation } from '../hooks/useBuildInvocation';
-import { addToast } from 'features/system/store/systemSlice';
-import { makeToast } from 'features/system/hooks/useToastWatcher';
import { AnyInvocationType } from 'services/events/types';
import IAIIconButton from 'common/components/IAIIconButton';
+import { useAppToaster } from 'app/components/Toaster';
const AddNodeMenu = () => {
const dispatch = useAppDispatch();
@@ -27,22 +26,23 @@ const AddNodeMenu = () => {
const buildInvocation = useBuildInvocation();
+ const toaster = useAppToaster();
+
const addNode = useCallback(
(nodeType: AnyInvocationType) => {
const invocation = buildInvocation(nodeType);
if (!invocation) {
- const toast = makeToast({
+ toaster({
status: 'error',
title: `Unknown Invocation type ${nodeType}`,
});
- dispatch(addToast(toast));
return;
}
dispatch(nodeAdded(invocation));
},
- [dispatch, buildInvocation]
+ [dispatch, buildInvocation, toaster]
);
return (
diff --git a/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx b/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx
index b06619e76f..c441297fe8 100644
--- a/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx
@@ -16,11 +16,11 @@ import {
import { Tooltip } from '@chakra-ui/tooltip';
import { AnyInvocationType } from 'services/events/types';
import { useBuildInvocation } from 'features/nodes/hooks/useBuildInvocation';
-import { makeToast } from 'features/system/hooks/useToastWatcher';
import { addToast } from 'features/system/store/systemSlice';
import { nodeAdded } from '../../store/nodesSlice';
import Fuse from 'fuse.js';
import { InvocationTemplate } from 'features/nodes/types/types';
+import { useAppToaster } from 'app/components/Toaster';
interface NodeListItemProps {
title: string;
@@ -63,6 +63,7 @@ const NodeSearch = () => {
const buildInvocation = useBuildInvocation();
const dispatch = useAppDispatch();
+ const toaster = useAppToaster();
const [searchText, setSearchText] = useState('');
const [showNodeList, setShowNodeList] = useState(false);
@@ -89,17 +90,16 @@ const NodeSearch = () => {
const invocation = buildInvocation(nodeType);
if (!invocation) {
- const toast = makeToast({
+ toaster({
status: 'error',
title: `Unknown Invocation type ${nodeType}`,
});
- dispatch(addToast(toast));
return;
}
dispatch(nodeAdded(invocation));
},
- [dispatch, buildInvocation]
+ [dispatch, buildInvocation, toaster]
);
const renderNodeList = () => {
diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts
index a093010343..138d54402c 100644
--- a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts
+++ b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts
@@ -1,4 +1,3 @@
-import { useToast } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { isFinite, isString } from 'lodash-es';
import { useCallback } from 'react';
@@ -10,10 +9,11 @@ import { NUMPY_RAND_MAX } from 'app/constants';
import { initialImageSelected } from '../store/actions';
import { Image } from 'app/types/invokeai';
import { setActiveTab } from 'features/ui/store/uiSlice';
+import { useAppToaster } from 'app/components/Toaster';
export const useParameters = () => {
const dispatch = useAppDispatch();
- const toast = useToast();
+ const toaster = useAppToaster();
const { t } = useTranslation();
const setBothPrompts = useSetBothPrompts();
@@ -23,7 +23,7 @@ export const useParameters = () => {
const recallPrompt = useCallback(
(prompt: unknown) => {
if (!isString(prompt)) {
- toast({
+ toaster({
title: t('toast.promptNotSet'),
description: t('toast.promptNotSetDesc'),
status: 'warning',
@@ -34,14 +34,14 @@ export const useParameters = () => {
}
setBothPrompts(prompt);
- toast({
+ toaster({
title: t('toast.promptSet'),
status: 'info',
duration: 2500,
isClosable: true,
});
},
- [t, toast, setBothPrompts]
+ [t, toaster, setBothPrompts]
);
/**
@@ -51,7 +51,7 @@ export const useParameters = () => {
(seed: unknown) => {
const s = Number(seed);
if (!isFinite(s) || (isFinite(s) && !(s >= 0 && s <= NUMPY_RAND_MAX))) {
- toast({
+ toaster({
title: t('toast.seedNotSet'),
description: t('toast.seedNotSetDesc'),
status: 'warning',
@@ -62,14 +62,14 @@ export const useParameters = () => {
}
dispatch(setSeed(s));
- toast({
+ toaster({
title: t('toast.seedSet'),
status: 'info',
duration: 2500,
isClosable: true,
});
},
- [t, toast, dispatch]
+ [t, toaster, dispatch]
);
/**
@@ -78,7 +78,7 @@ export const useParameters = () => {
const recallInitialImage = useCallback(
async (image: unknown) => {
if (!isImageField(image)) {
- toast({
+ toaster({
title: t('toast.initialImageNotSet'),
description: t('toast.initialImageNotSetDesc'),
status: 'warning',
@@ -91,14 +91,14 @@ export const useParameters = () => {
dispatch(
initialImageSelected({ name: image.image_name, type: image.image_type })
);
- toast({
+ toaster({
title: t('toast.initialImageSet'),
status: 'info',
duration: 2500,
isClosable: true,
});
},
- [t, toast, dispatch]
+ [t, toaster, dispatch]
);
/**
@@ -123,14 +123,14 @@ export const useParameters = () => {
dispatch(setActiveTab('txt2img'));
}
- toast({
+ toaster({
title: t('toast.parametersSet'),
status: 'success',
duration: 2500,
isClosable: true,
});
} else {
- toast({
+ toaster({
title: t('toast.parametersNotSet'),
description: t('toast.parametersNotSetDesc'),
status: 'error',
@@ -139,7 +139,7 @@ export const useParameters = () => {
});
}
},
- [t, toast, dispatch]
+ [t, toaster, dispatch]
);
return {
diff --git a/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts b/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts
deleted file mode 100644
index b51bf48a36..0000000000
--- a/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useToast, UseToastOptions } from '@chakra-ui/react';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { toastQueueSelector } from 'features/system/store/systemSelectors';
-import { clearToastQueue } from 'features/system/store/systemSlice';
-import { useEffect } from 'react';
-
-export type MakeToastArg = string | UseToastOptions;
-
-export const makeToast = (arg: MakeToastArg): UseToastOptions => {
- if (typeof arg === 'string') {
- return {
- title: arg,
- status: 'info',
- isClosable: true,
- duration: 2500,
- };
- }
-
- return { status: 'info', isClosable: true, duration: 2500, ...arg };
-};
-
-const useToastWatcher = () => {
- const dispatch = useAppDispatch();
- const toastQueue = useAppSelector(toastQueueSelector);
- const toast = useToast();
- useEffect(() => {
- toastQueue.forEach((t) => {
- toast(t);
- });
- toastQueue.length > 0 && dispatch(clearToastQueue());
- }, [dispatch, toast, toastQueue]);
-};
-
-export default useToastWatcher;
diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts
index 5cc6ca3a43..bbe7ed4da6 100644
--- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts
+++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts
@@ -15,7 +15,7 @@ import {
} from 'services/events/actions';
import { ProgressImage } from 'services/events/types';
-import { makeToast } from '../hooks/useToastWatcher';
+import { makeToast } from '../../../app/components/Toaster';
import { sessionCanceled, sessionInvoked } from 'services/thunks/session';
import { receivedModels } from 'services/thunks/model';
import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice';
diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts
index 88bb11147c..e9356dd271 100644
--- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts
+++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts
@@ -22,7 +22,7 @@ import {
} from 'services/thunks/gallery';
import { receivedModels } from 'services/thunks/model';
import { receivedOpenAPISchema } from 'services/thunks/schema';
-import { makeToast } from '../../../features/system/hooks/useToastWatcher';
+import { makeToast } from '../../../app/components/Toaster';
import { addToast } from '../../../features/system/store/systemSlice';
type SetEventListenersArg = {
From e1e5266fc3133612273a41028ef4d07c7fba63b2 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 15 May 2023 17:45:05 +1000
Subject: [PATCH 3/9] feat(ui): refactor base image uploading logic
---
invokeai/frontend/web/public/locales/en.json | 2 +-
.../frontend/web/src/app/components/App.tsx | 3 +
.../components/AuxiliaryProgressIndicator.tsx | 44 +++++
.../listeners/imageUploaded.ts | 3 +
.../web/src/common/components/IAIInput.tsx | 3 +-
.../src/common/components/IAINumberInput.tsx | 2 +
.../web/src/common/components/IAITextarea.tsx | 9 ++
.../common/components/ImageToImageButtons.tsx | 8 +-
.../src/common/components/ImageUploader.tsx | 152 +++++++++---------
.../common/components/ImageUploaderButton.tsx | 11 +-
.../components/ImageUploaderIconButton.tsx | 7 +-
.../src/common/components/Loading/Loading.tsx | 1 -
.../web/src/common/hooks/useImageUploader.ts | 21 ++-
.../src/common/util/stopPastePropagation.ts | 5 +
.../canvas/components/IAICanvasResizer.tsx | 2 +-
.../components/GalleryProgressImage.tsx | 5 +-
.../Core/ParamNegativeConditioning.tsx | 5 +-
.../Core/ParamPositiveConditioning.tsx | 5 +-
.../system/store/systemPersistDenylist.ts | 23 +--
.../src/features/system/store/systemSlice.ts | 24 +++
.../src/features/ui/components/InvokeTabs.tsx | 4 +
21 files changed, 213 insertions(+), 126 deletions(-)
create mode 100644 invokeai/frontend/web/src/app/components/AuxiliaryProgressIndicator.tsx
create mode 100644 invokeai/frontend/web/src/common/components/IAITextarea.tsx
create mode 100644 invokeai/frontend/web/src/common/util/stopPastePropagation.ts
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index f82b3af677..319a920025 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -552,8 +552,8 @@
"canceled": "Processing Canceled",
"tempFoldersEmptied": "Temp Folder Emptied",
"uploadFailed": "Upload failed",
- "uploadFailedMultipleImagesDesc": "Multiple images pasted, may only upload one image at a time",
"uploadFailedUnableToLoadDesc": "Unable to load file",
+ "uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
"downloadImageStarted": "Image Download Started",
"imageCopied": "Image Copied",
"imageLinkCopied": "Image Link Copied",
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index e819c04352..929c64edbb 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -22,6 +22,7 @@ import { languageSelector } from 'features/system/store/systemSelectors';
import i18n from 'i18n';
import Toaster from './Toaster';
import GlobalHotkeys from './GlobalHotkeys';
+import AuxiliaryProgressIndicator from './AuxiliaryProgressIndicator';
const DEFAULT_CONFIG = {};
@@ -99,6 +100,8 @@ const App = ({
+ {/* */}
+
{!isApplicationReady && !loadingOverridden && (
{
+ const { isUploading } = system;
+
+ let tooltip = '';
+
+ if (isUploading) {
+ tooltip = 'Uploading...';
+ }
+
+ return {
+ tooltip,
+ shouldShow: isUploading,
+ };
+});
+
+export const AuxiliaryProgressIndicator = () => {
+ const { shouldShow, tooltip } = useAppSelector(selector);
+
+ if (!shouldShow) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default memo(AuxiliaryProgressIndicator);
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
index c32da2e710..5b67be418f 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
@@ -3,6 +3,7 @@ import { startAppListening } from '..';
import { uploadAdded } from 'features/gallery/store/uploadsSlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { imageUploaded } from 'services/thunks/image';
+import { addToast } from 'features/system/store/systemSlice';
export const addImageUploadedListener = () => {
startAppListening({
@@ -17,6 +18,8 @@ export const addImageUploadedListener = () => {
dispatch(uploadAdded(image));
+ dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
+
if (state.gallery.shouldAutoSwitchToNewImages) {
dispatch(imageSelected(image));
}
diff --git a/invokeai/frontend/web/src/common/components/IAIInput.tsx b/invokeai/frontend/web/src/common/components/IAIInput.tsx
index 3e90dca83a..3cba36d2c9 100644
--- a/invokeai/frontend/web/src/common/components/IAIInput.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIInput.tsx
@@ -5,6 +5,7 @@ import {
Input,
InputProps,
} from '@chakra-ui/react';
+import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { ChangeEvent, memo } from 'react';
interface IAIInputProps extends InputProps {
@@ -31,7 +32,7 @@ const IAIInput = (props: IAIInputProps) => {
{...formControlProps}
>
{label !== '' && {label}}
-
+
);
};
diff --git a/invokeai/frontend/web/src/common/components/IAINumberInput.tsx b/invokeai/frontend/web/src/common/components/IAINumberInput.tsx
index 762182eb47..bf598f3b12 100644
--- a/invokeai/frontend/web/src/common/components/IAINumberInput.tsx
+++ b/invokeai/frontend/web/src/common/components/IAINumberInput.tsx
@@ -14,6 +14,7 @@ import {
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
+import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { clamp } from 'lodash-es';
import { FocusEvent, memo, useEffect, useState } from 'react';
@@ -125,6 +126,7 @@ const IAINumberInput = (props: Props) => {
onChange={handleOnChange}
onBlur={handleBlur}
{...rest}
+ onPaste={stopPastePropagation}
>
{showStepper && (
diff --git a/invokeai/frontend/web/src/common/components/IAITextarea.tsx b/invokeai/frontend/web/src/common/components/IAITextarea.tsx
new file mode 100644
index 0000000000..b5247887bb
--- /dev/null
+++ b/invokeai/frontend/web/src/common/components/IAITextarea.tsx
@@ -0,0 +1,9 @@
+import { Textarea, TextareaProps, forwardRef } from '@chakra-ui/react';
+import { stopPastePropagation } from 'common/util/stopPastePropagation';
+import { memo } from 'react';
+
+const IAITextarea = forwardRef((props: TextareaProps, ref) => {
+ return ;
+});
+
+export default memo(IAITextarea);
diff --git a/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx b/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx
index 315571b6e7..469cd1695c 100644
--- a/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx
+++ b/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx
@@ -6,10 +6,12 @@ import { FaUndo, FaUpload } from 'react-icons/fa';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback } from 'react';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
+import useImageUploader from 'common/hooks/useImageUploader';
const InitialImageButtons = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
+ const { openUploader } = useImageUploader();
const handleResetInitialImage = useCallback(() => {
dispatch(clearInitialImage());
@@ -27,7 +29,11 @@ const InitialImageButtons = () => {
aria-label={t('accessibility.reset')}
onClick={handleResetInitialImage}
/>
- } aria-label={t('common.upload')} />
+ }
+ onClick={openUploader}
+ aria-label={t('common.upload')}
+ />
);
diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx
index c773fb85ed..a070b69bbc 100644
--- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx
+++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx
@@ -10,6 +10,8 @@ import {
ReactNode,
useCallback,
useEffect,
+ useMemo,
+ useRef,
useState,
} from 'react';
import { FileRejection, useDropzone } from 'react-dropzone';
@@ -17,6 +19,24 @@ import { useTranslation } from 'react-i18next';
import { imageUploaded } from 'services/thunks/image';
import ImageUploadOverlay from './ImageUploadOverlay';
import { useAppToaster } from 'app/components/Toaster';
+import { filter, map, some } from 'lodash-es';
+import { createSelector } from '@reduxjs/toolkit';
+import { systemSelector } from 'features/system/store/systemSelectors';
+import { ErrorCode } from 'react-dropzone';
+
+const selector = createSelector(
+ [systemSelector, activeTabNameSelector],
+ (system, activeTabName) => {
+ const { isConnected, isUploading } = system;
+
+ const isUploaderDisabled = !isConnected || isUploading;
+
+ return {
+ isUploaderDisabled,
+ activeTabName,
+ };
+ }
+);
type ImageUploaderProps = {
children: ReactNode;
@@ -25,24 +45,20 @@ type ImageUploaderProps = {
const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props;
const dispatch = useAppDispatch();
- const activeTabName = useAppSelector(activeTabNameSelector);
+ const { isUploaderDisabled, activeTabName } = useAppSelector(selector);
const toaster = useAppToaster();
const { t } = useTranslation();
const [isHandlingUpload, setIsHandlingUpload] = useState(false);
- const { setOpenUploader } = useImageUploader();
+ const { setOpenUploaderFunction } = useImageUploader();
const fileRejectionCallback = useCallback(
(rejection: FileRejection) => {
setIsHandlingUpload(true);
- const msg = rejection.errors.reduce(
- (acc: string, cur: { message: string }) => `${acc}\n${cur.message}`,
- ''
- );
+
toaster({
title: t('toast.uploadFailed'),
- description: msg,
+ description: rejection.errors.map((error) => error.message).join('\n'),
status: 'error',
- isClosable: true,
});
},
[t, toaster]
@@ -57,6 +73,15 @@ const ImageUploader = (props: ImageUploaderProps) => {
const onDrop = useCallback(
(acceptedFiles: Array, fileRejections: Array) => {
+ if (fileRejections.length > 1) {
+ toaster({
+ title: t('toast.uploadFailed'),
+ description: t('toast.uploadFailedInvalidUploadDesc'),
+ status: 'error',
+ });
+ return;
+ }
+
fileRejections.forEach((rejection: FileRejection) => {
fileRejectionCallback(rejection);
});
@@ -65,7 +90,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
fileAcceptedCallback(file);
});
},
- [fileAcceptedCallback, fileRejectionCallback]
+ [t, toaster, fileAcceptedCallback, fileRejectionCallback]
);
const {
@@ -74,92 +99,67 @@ const ImageUploader = (props: ImageUploaderProps) => {
isDragAccept,
isDragReject,
isDragActive,
+ inputRef,
open,
} = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
noClick: true,
onDrop,
onDragOver: () => setIsHandlingUpload(true),
- maxFiles: 1,
+ disabled: isUploaderDisabled,
+ multiple: false,
});
- setOpenUploader(open);
-
useEffect(() => {
- const pasteImageListener = (e: ClipboardEvent) => {
- const dataTransferItemList = e.clipboardData?.items;
- if (!dataTransferItemList) return;
-
- const imageItems: Array = [];
-
- for (const item of dataTransferItemList) {
- if (
- item.kind === 'file' &&
- ['image/png', 'image/jpg'].includes(item.type)
- ) {
- imageItems.push(item);
- }
- }
-
- if (!imageItems.length) return;
-
- e.stopImmediatePropagation();
-
- if (imageItems.length > 1) {
- toaster({
- description: t('toast.uploadFailedMultipleImagesDesc'),
- status: 'error',
- isClosable: true,
- });
+ const handlePaste = async (e: ClipboardEvent) => {
+ if (!inputRef.current) {
return;
}
- const file = imageItems[0].getAsFile();
-
- if (!file) {
- toaster({
- description: t('toast.uploadFailedUnableToLoadDesc'),
- status: 'error',
- isClosable: true,
- });
- return;
+ if (e.clipboardData?.files) {
+ inputRef.current.files = e.clipboardData.files;
+ inputRef.current?.dispatchEvent(new Event('change', { bubbles: true }));
}
-
- dispatch(imageUploaded({ imageType: 'uploads', formData: { file } }));
};
- document.addEventListener('paste', pasteImageListener);
+
+ setOpenUploaderFunction(open);
+ document.addEventListener('paste', handlePaste);
+
return () => {
- document.removeEventListener('paste', pasteImageListener);
+ document.removeEventListener('paste', handlePaste);
+ setOpenUploaderFunction(() => {
+ return;
+ });
};
- }, [t, dispatch, toaster, activeTabName]);
+ }, [inputRef, open, setOpenUploaderFunction]);
- const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
- activeTabName
- )
- ? ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`
- : ``;
+ const overlaySecondaryText = useMemo(() => {
+ if (['img2img', 'unifiedCanvas'].includes(activeTabName)) {
+ return ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`;
+ }
+
+ return '';
+ }, [t, activeTabName]);
return (
-
- {
- // Bail out if user hits spacebar - do not open the uploader
- if (e.key === ' ') return;
- }}
- >
-
- {children}
- {isDragActive && isHandlingUpload && (
-
- )}
-
-
+ {
+ // Bail out if user hits spacebar - do not open the uploader
+ if (e.key === ' ') return;
+ }}
+ >
+
+ {children}
+ {isDragActive && isHandlingUpload && (
+
+ )}
+
);
};
diff --git a/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx b/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx
index 6179efe5e6..bb24ce6e18 100644
--- a/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx
+++ b/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx
@@ -1,6 +1,5 @@
import { Flex, Heading, Icon } from '@chakra-ui/react';
-import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
-import { useContext } from 'react';
+import useImageUploader from 'common/hooks/useImageUploader';
import { FaUpload } from 'react-icons/fa';
type ImageUploaderButtonProps = {
@@ -9,11 +8,7 @@ type ImageUploaderButtonProps = {
const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
const { styleClass } = props;
- const open = useContext(ImageUploaderTriggerContext);
-
- const handleClickUpload = () => {
- open && open();
- };
+ const { openUploader } = useImageUploader();
return (
{
className={styleClass}
>
{
const { t } = useTranslation();
- const openImageUploader = useContext(ImageUploaderTriggerContext);
+ const { openUploader } = useImageUploader();
return (
}
- onClick={openImageUploader || undefined}
+ onClick={openUploader}
/>
);
};
diff --git a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
index 8625e5b49b..591668f0c2 100644
--- a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
+++ b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
@@ -24,7 +24,6 @@ const Loading = () => {
height="24px !important"
right="1.5rem"
bottom="1.5rem"
- speed="1.2s"
/>
);
diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploader.ts b/invokeai/frontend/web/src/common/hooks/useImageUploader.ts
index 10c81c4dfd..2b04ac9530 100644
--- a/invokeai/frontend/web/src/common/hooks/useImageUploader.ts
+++ b/invokeai/frontend/web/src/common/hooks/useImageUploader.ts
@@ -1,13 +1,22 @@
-let openFunction: () => void;
+import { useCallback } from 'react';
+
+let openUploader = () => {
+ return;
+};
const useImageUploader = () => {
- return {
- setOpenUploader: (open?: () => void) => {
- if (open) {
- openFunction = open;
+ const setOpenUploaderFunction = useCallback(
+ (openUploaderFunction?: () => void) => {
+ if (openUploaderFunction) {
+ openUploader = openUploaderFunction;
}
},
- openUploader: openFunction,
+ []
+ );
+
+ return {
+ setOpenUploaderFunction,
+ openUploader,
};
};
diff --git a/invokeai/frontend/web/src/common/util/stopPastePropagation.ts b/invokeai/frontend/web/src/common/util/stopPastePropagation.ts
new file mode 100644
index 0000000000..b6b237387d
--- /dev/null
+++ b/invokeai/frontend/web/src/common/util/stopPastePropagation.ts
@@ -0,0 +1,5 @@
+import { ClipboardEvent } from 'react';
+
+export const stopPastePropagation = (e: ClipboardEvent) => {
+ e.stopPropagation();
+};
diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx
index 013d689182..d16a5dab87 100644
--- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx
+++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx
@@ -81,7 +81,7 @@ const IAICanvasResizer = () => {
height: '100%',
}}
>
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx
index a2103eb8e2..98f7db2726 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx
@@ -62,7 +62,10 @@ const GalleryProgressImage = () => {
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
}}
/>
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx
index d3790d4c24..28ab50ff82 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx
@@ -1,6 +1,7 @@
-import { FormControl, Textarea } from '@chakra-ui/react';
+import { FormControl } from '@chakra-ui/react';
import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import IAITextarea from 'common/components/IAITextarea';
import { setNegativePrompt } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
@@ -14,7 +15,7 @@ const ParamNegativeConditioning = () => {
return (
-