diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index ef61f65cf4..da5e58c9c9 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -118,6 +118,7 @@ "@types/node": "^18.16.2", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.1", + "@types/react-redux": "^7.1.25", "@types/react-transition-group": "^4.4.5", "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.59.1", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 876cd96b39..18f506d5bf 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -550,7 +550,7 @@ "imageCopied": "Image Copied", "imageLinkCopied": "Image Link Copied", "imageNotLoaded": "No Image Loaded", - "imageNotLoadedDesc": "No image found to send to image to image module", + "imageNotLoadedDesc": "Could not find image", "imageSavedToGallery": "Image Saved to Gallery", "canvasMerged": "Canvas Merged", "sentToImageToImage": "Sent To Image To Image", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts new file mode 100644 index 0000000000..563f12ba02 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -0,0 +1,59 @@ +import { + createListenerMiddleware, + addListener, + ListenerEffect, + AnyAction, +} from '@reduxjs/toolkit'; +import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit'; + +import type { RootState, AppDispatch } from '../../store'; +import { initialImageSelected } from 'features/parameters/store/actions'; +import { initialImageListener } from './listeners/initialImageListener'; +import { + imageResultReceivedListener, + imageResultReceivedPrediate, +} from './listeners/invocationCompleteListener'; +import { imageUploaded } from 'services/thunks/image'; +import { imageUploadedListener } from './listeners/imageUploadedListener'; + +export const listenerMiddleware = createListenerMiddleware(); + +export type AppStartListening = TypedStartListening; + +export const startAppListening = + listenerMiddleware.startListening as AppStartListening; + +export const addAppListener = addListener as TypedAddListener< + RootState, + AppDispatch +>; + +export type AppListenerEffect = ListenerEffect< + AnyAction, + RootState, + AppDispatch +>; + +/** + * Initial image selected + */ +startAppListening({ + actionCreator: initialImageSelected, + effect: initialImageListener, +}); + +/** + * Image Result received + */ +startAppListening({ + predicate: imageResultReceivedPrediate, + effect: imageResultReceivedListener, +}); + +/** + * Image Uploaded + */ +startAppListening({ + actionCreator: imageUploaded.fulfilled, + effect: imageUploadedListener, +}); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploadedListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploadedListener.ts new file mode 100644 index 0000000000..5ae24f315b --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploadedListener.ts @@ -0,0 +1,19 @@ +import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; +import { AppListenerEffect } from '..'; +import { uploadAdded } from 'features/gallery/store/uploadsSlice'; +import { imageSelected } from 'features/gallery/store/gallerySlice'; + +export const imageUploadedListener: AppListenerEffect = ( + action, + { dispatch, getState } +) => { + const { response } = action.payload; + const state = getState(); + const image = deserializeImageResponse(response); + + dispatch(uploadAdded(image)); + + if (state.gallery.shouldAutoSwitchToNewImages) { + dispatch(imageSelected(image)); + } +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageListener.ts new file mode 100644 index 0000000000..231e1153ed --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageListener.ts @@ -0,0 +1,53 @@ +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 { AnyAction, ListenerEffect } from '@reduxjs/toolkit'; +import { AppDispatch, RootState } from 'app/store/store'; + +export const initialImageListener: ListenerEffect< + AnyAction, + RootState, + AppDispatch +> = (action, { getState, dispatch }) => { + if (!action.payload) { + dispatch( + addToast( + makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' }) + ) + ); + return; + } + + if (isInvokeAIImage(action.payload)) { + dispatch(initialImageChanged(action.payload)); + dispatch(addToast(makeToast(t('toast.sentToImageToImage')))); + return; + } + + const { name, type } = action.payload; + + let image: Image | undefined; + const state = getState(); + + if (type === 'results') { + image = selectResultsById(state, name); + } else if (type === 'uploads') { + image = selectUploadsById(state, name); + } + + if (!image) { + dispatch( + addToast( + makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' }) + ) + ); + return; + } + + dispatch(initialImageChanged(image)); + dispatch(addToast(makeToast(t('toast.sentToImageToImage')))); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationCompleteListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationCompleteListener.ts new file mode 100644 index 0000000000..4b6c2e060b --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationCompleteListener.ts @@ -0,0 +1,75 @@ +import { AnyListenerPredicate } from '@reduxjs/toolkit'; +import { invocationComplete } from 'services/events/actions'; +import { isImageOutput } from 'services/types/guards'; +import { RootState } from 'app/store/store'; +import { + buildImageUrls, + extractTimestampFromImageName, +} from 'services/util/deserializeImageField'; +import { Image } from 'app/types/invokeai'; +import { resultAdded } from 'features/gallery/store/resultsSlice'; +import { imageReceived, thumbnailReceived } from 'services/thunks/image'; +import { AppListenerEffect } from '..'; + +export const imageResultReceivedPrediate: AnyListenerPredicate = ( + action, + _currentState, + _originalState +) => { + if ( + invocationComplete.match(action) && + isImageOutput(action.payload.data.result) + ) { + return true; + } + return false; +}; + +export const imageResultReceivedListener: AppListenerEffect = ( + action, + { getState, dispatch } +) => { + const { data, shouldFetchImages } = action.payload; + const { result, node, graph_execution_state_id } = data; + + if (isImageOutput(result)) { + const name = result.image.image_name; + const type = result.image.image_type; + const state = getState(); + + // if we need to refetch, set URLs to placeholder for now + const { url, thumbnail } = shouldFetchImages + ? { url: '', thumbnail: '' } + : buildImageUrls(type, name); + + const timestamp = extractTimestampFromImageName(name); + + const image: Image = { + name, + type, + url, + thumbnail, + metadata: { + created: timestamp, + width: result.width, + height: result.height, + invokeai: { + session_id: graph_execution_state_id, + ...(node ? { node } : {}), + }, + }, + }; + + dispatch(resultAdded(image)); + + if (state.config.shouldFetchImages) { + dispatch(imageReceived({ imageName: name, imageType: type })); + dispatch( + thumbnailReceived({ + thumbnailName: name, + thumbnailType: type, + }) + ); + } + } +}; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index b0f73a759e..2663adfb6b 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -1,4 +1,9 @@ -import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import { + AnyAction, + ThunkDispatch, + combineReducers, + configureStore, +} from '@reduxjs/toolkit'; import { persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web @@ -30,6 +35,7 @@ import { systemDenylist } from 'features/system/store/systemPersistDenylist'; import { uiDenylist } from 'features/ui/store/uiPersistDenylist'; import { resultsDenylist } from 'features/gallery/store/resultsPersistDenylist'; import { uploadsDenylist } from 'features/gallery/store/uploadsPersistDenylist'; +import { listenerMiddleware } from './middleware/listenerMiddleware'; /** * redux-persist provides an easy and reliable way to persist state across reloads. @@ -101,7 +107,9 @@ export const store = configureStore({ getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, - }).concat(dynamicMiddlewares), + }) + .concat(dynamicMiddlewares) + .prepend(listenerMiddleware.middleware), devTools: { // Uncommenting these very rapidly called actions makes the redux dev tools output much more readable actionsDenylist: [ @@ -120,4 +128,5 @@ export const store = configureStore({ export type AppGetState = typeof store.getState; export type RootState = ReturnType; +export type AppThunkDispatch = ThunkDispatch; export type AppDispatch = typeof store.dispatch; diff --git a/invokeai/frontend/web/src/app/store/storeHooks.ts b/invokeai/frontend/web/src/app/store/storeHooks.ts index 387bc7ea68..2d2d632caa 100644 --- a/invokeai/frontend/web/src/app/store/storeHooks.ts +++ b/invokeai/frontend/web/src/app/store/storeHooks.ts @@ -1,6 +1,6 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; -import { AppDispatch, RootState } from 'app/store/store'; +import { AppDispatch, AppThunkDispatch, RootState } from 'app/store/store'; // Use throughout your app instead of plain `useDispatch` and `useSelector` -export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 05e6e088d6..f2be887c93 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -13,6 +13,7 @@ */ import { GalleryCategory } from 'features/gallery/store/gallerySlice'; +import { SelectedImage } from 'features/parameters/store/actions'; import { FacetoolType } from 'features/parameters/store/postprocessingSlice'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { IRect } from 'konva/lib/types'; @@ -126,6 +127,14 @@ export type Image = { metadata: ImageResponseMetadata; }; +export const isInvokeAIImage = (obj: Image | SelectedImage): obj is Image => { + if ('url' in obj && 'thumbnail' in obj) { + return true; + } + + return false; +}; + /** * Types related to the system status. */ diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index 22d4270e18..5eaca8274f 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -29,7 +29,6 @@ import { isCanvasBaseImage, isCanvasMaskLine, } from './canvasTypes'; -import { invocationComplete } from 'services/events/actions'; export const initialLayerState: CanvasLayerState = { objects: [], @@ -816,11 +815,6 @@ export const canvasSlice = createSlice({ state.isTransformingBoundingBox = false; }, }, - extraReducers(builder) { - builder.addCase(invocationComplete, (state, action) => { - // - }); - }, }); export const { diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 4fef811d46..94f54d1f3e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -22,7 +22,7 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; import { - initialImageSelected, + initialImageChanged, setAllParameters, // setInitialImage, setSeed, @@ -68,6 +68,7 @@ import { useGetUrl } from 'common/util/getUrl'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { imageDeleted } from 'services/thunks/image'; import { useParameters } from 'features/parameters/hooks/useParameters'; +import { initialImageSelected } from 'features/parameters/store/actions'; const currentImageButtonsSelector = createSelector( [ @@ -264,8 +265,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { useHotkeys('p', handleUsePrompt, [image]); const handleSendToImageToImage = useCallback(() => { - sendToImageToImage(image); - }, [image, sendToImageToImage]); + dispatch(initialImageSelected(image)); + }, [dispatch, image]); useHotkeys('shift+i', handleSendToImageToImage, [image]); diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index f6cfa99237..61a6bd9279 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -11,7 +11,7 @@ import CurrentImageFallback from './CurrentImageFallback'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import NextPrevImageButtons from './NextPrevImageButtons'; import CurrentImageHidden from './CurrentImageHidden'; -import { memo } from 'react'; +import { DragEvent, memo, useCallback } from 'react'; export const imagesSelector = createSelector( [uiSelector, selectedImageSelector, systemSelector], @@ -36,6 +36,18 @@ const CurrentImagePreview = () => { useAppSelector(imagesSelector); const { getUrl } = useGetUrl(); + const handleDragStart = useCallback( + (e: DragEvent) => { + if (!image) { + return; + } + e.dataTransfer.setData('invokeai/imageName', image.name); + e.dataTransfer.setData('invokeai/imageType', image.type); + e.dataTransfer.effectAllowed = 'move'; + }, + [image] + ); + return ( { > {image && ( { const handleDragStart = useCallback( (e: DragEvent) => { - console.log('dragging'); e.dataTransfer.setData('invokeai/imageName', image.name); e.dataTransfer.setData('invokeai/imageType', image.type); e.dataTransfer.effectAllowed = 'move'; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 47c2c4e0fd..4a56eb68d7 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -4,7 +4,7 @@ import { invocationComplete } from 'services/events/actions'; import { isImageOutput } from 'services/types/guards'; import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; import { imageUploaded } from 'services/thunks/image'; -import { SelectedImage } from 'features/parameters/store/generationSlice'; +import { Image } from 'app/types/invokeai'; type GalleryImageObjectFitType = 'contain' | 'cover'; @@ -12,7 +12,7 @@ export interface GalleryState { /** * The selected image */ - selectedImage?: SelectedImage; + selectedImage?: Image; galleryImageMinimumWidth: number; galleryImageObjectFit: GalleryImageObjectFitType; shouldAutoSwitchToNewImages: boolean; @@ -22,7 +22,6 @@ export interface GalleryState { } const initialState: GalleryState = { - selectedImage: undefined, galleryImageMinimumWidth: 64, galleryImageObjectFit: 'cover', shouldAutoSwitchToNewImages: true, @@ -35,10 +34,7 @@ export const gallerySlice = createSlice({ name: 'gallery', initialState, reducers: { - imageSelected: ( - state, - action: PayloadAction - ) => { + imageSelected: (state, action: PayloadAction) => { state.selectedImage = action.payload; // TODO: if the user selects an image, disable the auto switch? // state.shouldAutoSwitchToNewImages = false; @@ -84,16 +80,6 @@ export const gallerySlice = createSlice({ }; } }); - - /** - * Upload Image - FULFILLED - */ - builder.addCase(imageUploaded.fulfilled, (state, action) => { - const { response } = action.payload; - - const uploadedImage = deserializeImageResponse(response); - state.selectedImage = { name: uploadedImage.name, type: 'uploads' }; - }); }, }); diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts index 26af366e03..056b92887d 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -73,43 +73,43 @@ const resultsSlice = createSlice({ state.isLoading = false; }); - /** - * Invocation Complete - */ - builder.addCase(invocationComplete, (state, action) => { - const { data, shouldFetchImages } = action.payload; - const { result, node, graph_execution_state_id } = data; + // /** + // * Invocation Complete + // */ + // builder.addCase(invocationComplete, (state, action) => { + // const { data, shouldFetchImages } = action.payload; + // const { result, node, graph_execution_state_id } = data; - if (isImageOutput(result)) { - const name = result.image.image_name; - const type = result.image.image_type; + // if (isImageOutput(result)) { + // const name = result.image.image_name; + // const type = result.image.image_type; - // if we need to refetch, set URLs to placeholder for now - const { url, thumbnail } = shouldFetchImages - ? { url: '', thumbnail: '' } - : buildImageUrls(type, name); + // // if we need to refetch, set URLs to placeholder for now + // const { url, thumbnail } = shouldFetchImages + // ? { url: '', thumbnail: '' } + // : buildImageUrls(type, name); - const timestamp = extractTimestampFromImageName(name); + // const timestamp = extractTimestampFromImageName(name); - const image: Image = { - name, - type, - url, - thumbnail, - metadata: { - created: timestamp, - width: result.width, - height: result.height, - invokeai: { - session_id: graph_execution_state_id, - ...(node ? { node } : {}), - }, - }, - }; + // const image: Image = { + // name, + // type, + // url, + // thumbnail, + // metadata: { + // created: timestamp, + // width: result.width, + // height: result.height, + // invokeai: { + // session_id: graph_execution_state_id, + // ...(node ? { node } : {}), + // }, + // }, + // }; - resultsAdapter.setOne(state, image); - } - }); + // resultsAdapter.setOne(state, image); + // } + // }); /** * Image Received - FULFILLED diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index bb77844f42..e1d8e5ea8f 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -35,7 +35,7 @@ const uploadsSlice = createSlice({ name: 'uploads', initialState: initialUploadsState, reducers: { - uploadAdded: uploadsAdapter.addOne, + uploadAdded: uploadsAdapter.upsertOne, }, extraReducers: (builder) => { /** @@ -61,17 +61,6 @@ const uploadsSlice = createSlice({ state.isLoading = false; }); - /** - * Upload Image - FULFILLED - */ - builder.addCase(imageUploaded.fulfilled, (state, action) => { - const { location, response } = action.payload; - - const uploadedImage = deserializeImageResponse(response); - - uploadsAdapter.setOne(state, uploadedImage); - }); - /** * Delete Image - FULFILLED */ diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx index 9682d2eb0b..ddfdb622ef 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx @@ -5,9 +5,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; import { useGetUrl } from 'common/util/getUrl'; import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName'; -import { +import generationSlice, { clearInitialImage, - initialImageSelected, + initialImageChanged, } from 'features/parameters/store/generationSlice'; import { addToast } from 'features/system/store/systemSlice'; import { isEqual } from 'lodash-es'; @@ -15,23 +15,26 @@ import { DragEvent, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ImageType } from 'services/api'; import ImageToImageOverlay from 'common/components/ImageToImageOverlay'; -import { initialImageSelector } from 'features/parameters/store/generationSelectors'; +import { + generationSelector, + initialImageSelector, +} from 'features/parameters/store/generationSelectors'; +import { initialImageSelected } from 'features/parameters/store/actions'; const selector = createSelector( - [initialImageSelector], - (initialImage) => { + [generationSelector], + (generation) => { + const { initialImage, isImageToImageEnabled } = generation; return { initialImage, + isImageToImageEnabled, }; }, { memoizeOptions: { resultEqualityCheck: isEqual } } ); const InitialImagePreview = () => { - const isImageToImageEnabled = useAppSelector( - (state: RootState) => state.generation.isImageToImageEnabled - ); - const { initialImage } = useAppSelector(selector); + const { initialImage, isImageToImageEnabled } = useAppSelector(selector); const { getUrl } = useGetUrl(); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -55,22 +58,13 @@ const InitialImagePreview = () => { const handleDrop = useCallback( (e: DragEvent) => { setIsLoaded(false); + const name = e.dataTransfer.getData('invokeai/imageName'); const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; - if (!name || !type) { - return; - } - - const image = getImageByNameAndType(name, type); - - if (!image) { - return; - } - dispatch(initialImageSelected({ name, type })); }, - [getImageByNameAndType, dispatch] + [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts index 7c45f159b2..66f9287c1f 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts @@ -4,9 +4,11 @@ import { isFinite, isString } from 'lodash-es'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import useSetBothPrompts from './usePrompt'; -import { initialImageSelected, setSeed } from '../store/generationSlice'; +import { initialImageChanged, setSeed } from '../store/generationSlice'; import { isImage, isImageField } from 'services/types/guards'; import { NUMPY_RAND_MAX } from 'app/constants'; +import { initialImageSelected } from '../store/actions'; +import { Image } from 'app/types/invokeai'; export const useParameters = () => { const dispatch = useAppDispatch(); @@ -86,7 +88,7 @@ export const useParameters = () => { } dispatch( - initialImageSelected({ name: image.image_name, type: image.image_type }) + initialImageChanged({ name: image.image_name, type: image.image_type }) ); toast({ title: t('toast.initialImageSet'), @@ -102,27 +104,10 @@ export const useParameters = () => { * Sets image as initial image with toast */ const sendToImageToImage = useCallback( - (image: unknown) => { - if (!isImage(image)) { - toast({ - title: t('toast.imageNotLoaded'), - description: t('toast.imageNotLoadedDesc'), - status: 'warning', - duration: 2500, - isClosable: true, - }); - return; - } - + (image: Image) => { dispatch(initialImageSelected({ name: image.name, type: image.type })); - toast({ - title: t('toast.sentToImageToImage'), - status: 'info', - duration: 2500, - isClosable: true, - }); }, - [t, toast, dispatch] + [dispatch] ); return { recallPrompt, recallSeed, recallInitialImage, sendToImageToImage }; diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts new file mode 100644 index 0000000000..4b261d7783 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts @@ -0,0 +1,12 @@ +import { createAction } from '@reduxjs/toolkit'; +import { Image } from 'app/types/invokeai'; +import { ImageType } from 'services/api'; + +export type SelectedImage = { + name: string; + type: ImageType; +}; + +export const initialImageSelected = createAction< + Image | SelectedImage | undefined +>('generation/initialImageSelected'); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 9d9d689cb0..d889dd88e1 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -7,17 +7,12 @@ import { seedWeightsToString } from 'common/util/seedWeightPairs'; import { clamp } from 'lodash-es'; import { ImageField, ImageType } from 'services/api'; -export type SelectedImage = { - name: string; - type: ImageType; -}; - export interface GenerationState { cfgScale: number; height: number; img2imgStrength: number; infillMethod: string; - initialImage?: SelectedImage; // can be an Image or url + initialImage?: InvokeAI.Image; // can be an Image or url iterations: number; maskPath: string; perlin: number; @@ -351,7 +346,7 @@ export const generationSlice = createSlice({ setVerticalSymmetrySteps: (state, action: PayloadAction) => { state.verticalSymmetrySteps = action.payload; }, - initialImageSelected: (state, action: PayloadAction) => { + initialImageChanged: (state, action: PayloadAction) => { state.initialImage = action.payload; state.isImageToImageEnabled = true; }, @@ -399,7 +394,7 @@ export const { setShouldUseSymmetry, setHorizontalSymmetrySteps, setVerticalSymmetrySteps, - initialImageSelected, + initialImageChanged, isImageToImageEnabledChanged, } = generationSlice.actions; diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 7d8d4978a0..75aa758198 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 { initialImageSelected } from 'features/parameters/store/generationSlice'; +import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { makeToast } from '../hooks/useToastWatcher'; import { sessionCanceled, sessionInvoked } from 'services/thunks/session'; import { receivedModels } from 'services/thunks/model'; @@ -434,13 +434,6 @@ export const systemSlice = createSlice({ state.statusTranslationKey = 'common.statusConnected'; }); - /** - * Initial Image Selected - */ - builder.addCase(initialImageSelected, (state) => { - state.toastQueue.push(makeToast(t('toast.sentToImageToImage'))); - }); - /** * Received available models from the backend */ diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx index f59028c8ca..a9701bfee7 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx @@ -1,7 +1,7 @@ import { Box, BoxProps, Grid, GridItem } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { initialImageSelected } from 'features/parameters/store/generationSlice'; +import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { activeTabNameSelector, uiSelector, diff --git a/invokeai/frontend/web/src/services/api/models/DataURLToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/DataURLToImageInvocation.ts index 86b49e6cab..b1e35d9e0c 100644 --- a/invokeai/frontend/web/src/services/api/models/DataURLToImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/DataURLToImageInvocation.ts @@ -3,7 +3,7 @@ /* eslint-disable */ /** - * Outputs an image from a base 64 data URL. + * Outputs an image from a data URL. */ export type DataURLToImageInvocation = { /** diff --git a/invokeai/frontend/web/src/services/api/schemas/$DataURLToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$DataURLToImageInvocation.ts index 34a9fa23c0..f875cd0a11 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$DataURLToImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$DataURLToImageInvocation.ts @@ -2,7 +2,7 @@ /* tslint:disable */ /* eslint-disable */ export const $DataURLToImageInvocation = { - description: `Outputs an image from a base 64 data URL.`, + description: `Outputs an image from a data URL.`, properties: { id: { type: 'string', diff --git a/invokeai/frontend/web/src/services/events/actions.ts b/invokeai/frontend/web/src/services/events/actions.ts index 84268773a9..192061aa73 100644 --- a/invokeai/frontend/web/src/services/events/actions.ts +++ b/invokeai/frontend/web/src/services/events/actions.ts @@ -1,4 +1,4 @@ -import { createAction } from '@reduxjs/toolkit'; +import { AnyAction, createAction } from '@reduxjs/toolkit'; import { GeneratorProgressEvent, GraphExecutionStateCompleteEvent, diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts index 65652ae7ee..bc2e180da8 100644 --- a/invokeai/frontend/web/src/services/events/middleware.ts +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -5,20 +5,14 @@ import { ClientToServerEvents, ServerToClientEvents, } from 'services/events/types'; -import { - invocationComplete, - socketSubscribed, - socketUnsubscribed, -} from './actions'; -import { AppDispatch, RootState } from 'app/store/store'; +import { socketSubscribed, socketUnsubscribed } from './actions'; +import { AppThunkDispatch, RootState } from 'app/store/store'; import { getTimestamp } from 'common/util/getTimestamp'; import { sessionInvoked, isFulfilledSessionCreatedAction, } from 'services/thunks/session'; import { OpenAPI } from 'services/api'; -import { isImageOutput } from 'services/types/guards'; -import { imageReceived, thumbnailReceived } from 'services/thunks/image'; import { setEventListeners } from 'services/events/util/setEventListeners'; import { log } from 'app/logging/useLogger'; @@ -56,13 +50,15 @@ export const socketMiddleware = () => { ); const middleware: Middleware = - (store: MiddlewareAPI) => (next) => (action) => { - const { dispatch, getState } = store; + (storeApi: MiddlewareAPI) => + (next) => + (action) => { + const { dispatch, getState } = storeApi; // Set listeners for `connect` and `disconnect` events once // Must happen in middleware to get access to `dispatch` if (!areListenersSet) { - setEventListeners({ store, socket, log: socketioLog }); + setEventListeners({ storeApi, socket, log: socketioLog }); areListenersSet = true; @@ -107,26 +103,6 @@ export const socketMiddleware = () => { dispatch(sessionInvoked({ sessionId })); } - if (invocationComplete.match(action)) { - const { config } = getState(); - - if (config.shouldFetchImages) { - const { result } = action.payload.data; - if (isImageOutput(result)) { - const imageName = result.image.image_name; - const imageType = result.image.image_type; - - dispatch(imageReceived({ imageName, imageType })); - dispatch( - thumbnailReceived({ - thumbnailName: imageName, - thumbnailType: imageType, - }) - ); - } - } - } - next(action); }; diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts index 581363e446..1cd06c51ff 100644 --- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts +++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts @@ -27,13 +27,13 @@ import { addToast } from '../../../features/system/store/systemSlice'; type SetEventListenersArg = { socket: Socket; - store: MiddlewareAPI; + storeApi: MiddlewareAPI; log: Logger; }; export const setEventListeners = (arg: SetEventListenersArg) => { - const { socket, store, log } = arg; - const { dispatch, getState } = store; + const { socket, storeApi, log } = arg; + const { dispatch, getState } = storeApi; /** * Connect diff --git a/invokeai/frontend/web/tsconfig.json b/invokeai/frontend/web/tsconfig.json index 8276f461eb..fa8f1fd262 100644 --- a/invokeai/frontend/web/tsconfig.json +++ b/invokeai/frontend/web/tsconfig.json @@ -20,7 +20,12 @@ "*": ["./src/*"] } }, - "include": ["src/**/*.ts", "src/**/*.tsx", "*.d.ts"], + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "*.d.ts", + "src/app/store/middleware/listenerMiddleware" + ], "exclude": ["src/services/fixtures/*", "node_modules", "dist"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 59a2607db7..3901903bd4 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -1836,7 +1836,7 @@ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== -"@types/hoist-non-react-statics@^3.3.1": +"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== @@ -1907,6 +1907,16 @@ dependencies: "@types/react" "*" +"@types/react-redux@^7.1.25": + version "7.1.25" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88" + integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-transition-group@^4.4.5": version "4.4.5" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" @@ -5687,7 +5697,7 @@ redux-thunk@^2.4.2: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@^4.2.1: +redux@^4.0.0, redux@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==