diff --git a/invokeai/frontend/web/src/app/App.tsx b/invokeai/frontend/web/src/app/App.tsx index 6a22610e46..d2287136c3 100644 --- a/invokeai/frontend/web/src/app/App.tsx +++ b/invokeai/frontend/web/src/app/App.tsx @@ -18,6 +18,7 @@ import { PropsWithChildren, useEffect } from 'react'; import { setDisabledPanels, setDisabledTabs } from 'features/ui/store/uiSlice'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { shouldTransformUrlsChanged } from 'features/system/store/systemSlice'; +import { setShouldFetchImages } from 'features/gallery/store/resultsSlice'; keepGUIAlive(); @@ -26,6 +27,7 @@ interface Props extends PropsWithChildren { disabledPanels: string[]; disabledTabs: InvokeTabName[]; shouldTransformUrls?: boolean; + shouldFetchImages: boolean; }; } @@ -50,6 +52,10 @@ const App = (props: Props) => { ); }, [dispatch, props.options.shouldTransformUrls]); + useEffect(() => { + dispatch(setShouldFetchImages(props.options.shouldFetchImages)); + }, [dispatch, props.options.shouldFetchImages]); + useEffect(() => { setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark'); }, [setColorMode, currentTheme]); diff --git a/invokeai/frontend/web/src/component.tsx b/invokeai/frontend/web/src/component.tsx index 01c3513a78..c4cd876fbd 100644 --- a/invokeai/frontend/web/src/component.tsx +++ b/invokeai/frontend/web/src/component.tsx @@ -30,6 +30,7 @@ interface Props extends PropsWithChildren { disabledTabs?: InvokeTabName[]; token?: string; shouldTransformUrls?: boolean; + shouldFetchImages?: boolean; } export default function Component({ @@ -39,6 +40,7 @@ export default function Component({ token, children, shouldTransformUrls, + shouldFetchImages = false, }: Props) { useEffect(() => { // configure API client token @@ -70,7 +72,12 @@ export default function Component({ }> {children} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 31a2c4b055..327326a403 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -254,7 +254,7 @@ const ImageGalleryContent = () => { const isSelected = currentImageUuid === name; return ( diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts index bb789a4a5f..e0165eab8a 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -1,4 +1,8 @@ -import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { + PayloadAction, + createEntityAdapter, + createSlice, +} from '@reduxjs/toolkit'; import { Image } from 'app/invokeai'; import { invocationComplete } from 'services/events/actions'; @@ -13,6 +17,7 @@ import { extractTimestampFromImageName, } from 'services/util/deserializeImageField'; import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; +import { imageReceived, thumbnailReceived } from 'services/thunks/image'; // use `createEntityAdapter` to create a slice for results images // https://redux-toolkit.js.org/api/createEntityAdapter#overview @@ -34,6 +39,7 @@ type AdditionalResultsState = { pages: number; // the total number of pages available isLoading: boolean; // whether we are loading more images or not, mostly a placeholder nextPage: number; // the next page to request + shouldFetchImages: boolean; // whether we need to re-fetch images or not }; export const initialResultsState = @@ -43,6 +49,7 @@ export const initialResultsState = pages: 0, isLoading: false, nextPage: 0, + shouldFetchImages: false, }); export type ResultsState = typeof initialResultsState; @@ -57,6 +64,10 @@ const resultsSlice = createSlice({ // here we just use the function itself as the reducer. we'll call this on `invocation_complete` // to add a single result resultAdded: resultsAdapter.upsertOne, + + setShouldFetchImages: (state, action: PayloadAction) => { + state.shouldFetchImages = action.payload; + }, }, extraReducers: (builder) => { // here we can respond to a fulfilled call of the `getNextResultsPage` thunk @@ -98,7 +109,10 @@ const resultsSlice = createSlice({ if (isImageOutput(result)) { const name = result.image.image_name; const type = result.image.image_type; - const { url, thumbnail } = buildImageUrls(type, name); + // if we need to refetch, set URLs to placeholder for now + const { url, thumbnail } = state.shouldFetchImages + ? { url: '', thumbnail: '' } + : buildImageUrls(type, name); const timestamp = extractTimestampFromImageName(name); @@ -121,6 +135,30 @@ const resultsSlice = createSlice({ resultsAdapter.addOne(state, image); } }); + + builder.addCase(imageReceived.fulfilled, (state, action) => { + const { imagePath } = action.payload; + const { imageName } = action.meta.arg; + + resultsAdapter.updateOne(state, { + id: imageName, + changes: { + url: imagePath, + }, + }); + }); + + builder.addCase(thumbnailReceived.fulfilled, (state, action) => { + const { thumbnailPath } = action.payload; + const { imageName } = action.meta.arg; + + resultsAdapter.updateOne(state, { + id: imageName, + changes: { + thumbnail: thumbnailPath, + }, + }); + }); }, }); @@ -134,6 +172,6 @@ export const { selectTotal: selectResultsTotal, } = resultsAdapter.getSelectors((state) => state.results); -export const { resultAdded } = resultsSlice.actions; +export const { resultAdded, setShouldFetchImages } = resultsSlice.actions; export default resultsSlice.reducer; diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts index 9a462a1f85..33e198d797 100644 --- a/invokeai/frontend/web/src/services/events/middleware.ts +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -30,6 +30,8 @@ import { import { OpenAPI } from 'services/api'; import { receivedModels } from 'services/thunks/model'; import { receivedOpenAPISchema } from 'services/thunks/schema'; +import { isImageOutput } from 'services/types/guards'; +import { imageReceived, thumbnailReceived } from 'services/thunks/image'; export const socketMiddleware = () => { let areListenersSet = false; @@ -213,6 +215,21 @@ export const socketMiddleware = () => { dispatch(sessionInvoked({ sessionId })); } + if (invocationComplete.match(action)) { + const { results } = getState(); + + if (results.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({ imageName, imageType })); + } + } + } + // Always pass the action on so other middleware and reducers can handle it next(action); }; diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index 7014925d87..d7318f318c 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -16,6 +16,21 @@ export const imageReceived = createAppAsyncThunk( } ); +type ThumbnailReceivedArg = Parameters< + (typeof ImagesService)['getThumbnail'] +>[0]; + +/** + * `ImagesService.getThumbnail()` thunk + */ +export const thumbnailReceived = createAppAsyncThunk( + 'api/thumbnailReceived', + async (arg: ThumbnailReceivedArg, _thunkApi) => { + const response = await ImagesService.getThumbnail(arg); + return response; + } +); + type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0]; /**