From dbf6b1b68a240399f30d4397e3b0b817706b88ac Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 4 Apr 2023 00:31:17 +1000
Subject: [PATCH] feat(ui): clean up & comment results slice
---
.../web/src/app/nodesSocketio/listeners.ts | 34 +++-----
invokeai/frontend/web/src/app/store.ts | 2 +
.../components/ImageGalleryContent.tsx | 16 +++-
.../features/gallery/store/resultsSlice.ts | 74 +++++++++++++---
.../frontend/web/src/services/thunks/extra.ts | 86 +++----------------
.../web/src/services/util/buildImageUrls.ts | 17 ----
.../extractTimestampFromResultImageName.ts | 9 --
.../src/services/util/prepareResultImage.ts | 45 ++++++++++
8 files changed, 147 insertions(+), 136 deletions(-)
delete mode 100644 invokeai/frontend/web/src/services/util/buildImageUrls.ts
delete mode 100644 invokeai/frontend/web/src/services/util/extractTimestampFromResultImageName.ts
create mode 100644 invokeai/frontend/web/src/services/util/prepareResultImage.ts
diff --git a/invokeai/frontend/web/src/app/nodesSocketio/listeners.ts b/invokeai/frontend/web/src/app/nodesSocketio/listeners.ts
index febf1109cf..adc45f809b 100644
--- a/invokeai/frontend/web/src/app/nodesSocketio/listeners.ts
+++ b/invokeai/frontend/web/src/app/nodesSocketio/listeners.ts
@@ -33,10 +33,9 @@ import {
STATUS,
} from 'services/apiSlice';
import { emitUnsubscribe } from './actions';
-import { getGalleryImages } from 'services/thunks/extra';
import { resultAdded } from 'features/gallery/store/resultsSlice';
-import { buildImageUrls } from 'services/util/buildImageUrls';
-import { extractTimestampFromResultImageName } from 'services/util/extractTimestampFromResultImageName';
+import { getNextResultsPage } from 'services/thunks/extra';
+import { prepareResultImage } from 'services/util/prepareResultImage';
/**
* Returns an object containing listener callbacks
@@ -54,7 +53,12 @@ const makeSocketIOListeners = (
try {
dispatch(setIsConnected(true));
dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
- dispatch(getGalleryImages({ count: 20 }));
+
+ // fetch more results, but only if we don't already have results
+ // maybe we should have a different thunk for `onConnect` vs when you click 'Load More'?
+ if (!getState().results.ids.length) {
+ dispatch(getNextResultsPage());
+ }
} catch (e) {
console.error(e);
}
@@ -90,15 +94,9 @@ const makeSocketIOListeners = (
try {
const sessionId = data.graph_execution_state_id;
if (data.result.type === 'image') {
- const { image_name: imageName } = data.result.image;
-
- const { imageUrl, thumbnailUrl } = buildImageUrls(
- 'results',
- imageName
- );
-
- const timestamp = extractTimestampFromResultImageName(imageName);
+ const resultImage = prepareResultImage(data.result.image);
+ dispatch(resultAdded(resultImage));
// // need to update the type for this or figure out how to get these values
// dispatch(
// addImage({
@@ -116,20 +114,10 @@ const makeSocketIOListeners = (
// })
// );
- dispatch(
- resultAdded({
- name: imageName,
- url: imageUrl,
- thumbnail: thumbnailUrl,
- width: 512,
- height: 512,
- timestamp,
- })
- );
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
- message: `Generated: ${imageName}`,
+ message: `Generated: ${data.result.image.image_name}`,
})
);
dispatch(setIsProcessing(false));
diff --git a/invokeai/frontend/web/src/app/store.ts b/invokeai/frontend/web/src/app/store.ts
index 1d469d329a..346919120e 100644
--- a/invokeai/frontend/web/src/app/store.ts
+++ b/invokeai/frontend/web/src/app/store.ts
@@ -94,6 +94,8 @@ const rootPersistConfig = getPersistConfig({
...galleryBlacklist,
...lightboxBlacklist,
...apiBlacklist,
+ // for now, never persist the results slice
+ 'results',
],
debounce: 300,
});
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
index 7599ee8abf..8d8061e067 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
@@ -25,7 +25,8 @@ import HoverableImage from './HoverableImage';
import Scrollable from 'features/ui/components/common/Scrollable';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
-import { selectResultsAll } from '../store/resultsSlice';
+import { selectResultsAll, selectResultsTotal } from '../store/resultsSlice';
+import { getNextResultsPage } from 'services/thunks/extra';
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
@@ -49,9 +50,15 @@ const ImageGalleryContent = () => {
} = useAppSelector(imageGallerySelector);
const allResultImages = useAppSelector(selectResultsAll);
+ const currentResultsPage = useAppSelector((state) => state.results.page);
+ const totalResultsPages = useAppSelector((state) => state.results.pages);
+ const isLoadingResults = useAppSelector((state) => state.results.isLoading);
+ // const handleClickLoadMore = () => {
+ // dispatch(requestImages(currentCategory));
+ // };
const handleClickLoadMore = () => {
- dispatch(requestImages(currentCategory));
+ dispatch(getNextResultsPage());
};
const handleChangeGalleryImageMinimumWidth = (v: number) => {
@@ -222,10 +229,11 @@ const ImageGalleryContent = () => {
- {areMoreImagesAvailable
+ {currentResultsPage !== totalResultsPages - 1
? t('gallery.loadMore')
: t('gallery.allImagesLoaded')}
diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts
index 6abf2c38ba..2d0dea3805 100644
--- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts
@@ -1,27 +1,81 @@
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
+import { ResultImage } from 'app/invokeai';
-import * as InvokeAI from 'app/invokeai';
import { RootState } from 'app/store';
+import { map } from 'lodash';
+import { getNextResultsPage } from 'services/thunks/extra';
+import { isImageOutput } from 'services/types/guards';
+import { prepareResultImage } from 'services/util/prepareResultImage';
-const resultsAdapter = createEntityAdapter({
- // Image IDs are just their filename
+// use `createEntityAdapter` to create a slice for results images
+// https://redux-toolkit.js.org/api/createEntityAdapter#overview
+
+// the "Entity" is InvokeAI.ResultImage, while the "entities" are instances of that type
+const resultsAdapter = createEntityAdapter({
+ // Provide a callback to get a stable, unique identifier for each entity. This defaults to
+ // `(item) => item.id`, but for our result images, the `name` is the unique identifier.
selectId: (image) => image.name,
- // Keep the "all IDs" array sorted based on result timestamps
+ // Order all images by their time (in descending order)
sortComparer: (a, b) => b.timestamp - a.timestamp,
});
+// This type is intersected with the Entity type to create the shape of the state
+type AdditionalResultsState = {
+ // these are a bit misleading; they refer to sessions, not results, but we don't have a route
+ // to list all images directly at this time...
+ page: number; // current page we are on
+ pages: number; // the total number of pages available
+ isLoading: boolean; // whether we are loading more images or not, mostly a placeholder
+};
+
const resultsSlice = createSlice({
name: 'results',
- initialState: resultsAdapter.getInitialState(),
+ initialState: resultsAdapter.getInitialState({
+ // provide the additional initial state
+ page: 0,
+ pages: 0,
+ isLoading: false,
+ }),
reducers: {
- // Can pass adapter functions directly as case reducers. Because we're passing this
- // as a value, `createSlice` will auto-generate the action type / creator
+ // the adapter provides some helper reducers; see the docs for all of them
+ // can use them as helper functions within a reducer, or use the function itself as a reducer
+
+ // here we just use the function itself as the reducer. we'll call this on `invocation_complete`
+ // to add a single result
resultAdded: resultsAdapter.addOne,
- resultsReceived: resultsAdapter.setAll,
+ },
+ extraReducers: (builder) => {
+ // here we can respond to a fulfilled call of the `getNextResultsPage` thunk
+ // because we pass in the fulfilled thunk action creator, everything is typed
+ builder.addCase(getNextResultsPage.pending, (state, action) => {
+ state.isLoading = true;
+ });
+ builder.addCase(getNextResultsPage.fulfilled, (state, action) => {
+ const { items, page, pages } = action.payload;
+
+ // build flattened array of results ojects, use lodash `map()` to make results object an array
+ const allResults = items.flatMap((session) => map(session.results));
+
+ // filter out non-image-outputs (eg latents, prompts, etc)
+ const imageOutputResults = allResults.filter(isImageOutput);
+
+ // map results to ResultImage objects
+ const resultImages = imageOutputResults.map((result) =>
+ prepareResultImage(result.image)
+ );
+
+ // use the adapter reducer to add all the results to resultsSlice state
+ resultsAdapter.addMany(state, resultImages);
+
+ state.page = page;
+ state.pages = pages;
+ state.isLoading = false;
+ });
},
});
-// Can create a set of memoized selectors based on the location of this entity state
+// Create a set of memoized selectors based on the location of this entity state
+// to be used as selectors in a `useAppSelector()` call
export const {
selectAll: selectResultsAll,
selectById: selectResultsById,
@@ -30,6 +84,6 @@ export const {
selectTotal: selectResultsTotal,
} = resultsAdapter.getSelectors((state) => state.results);
-export const { resultAdded, resultsReceived } = resultsSlice.actions;
+export const { resultAdded } = resultsSlice.actions;
export default resultsSlice.reducer;
diff --git a/invokeai/frontend/web/src/services/thunks/extra.ts b/invokeai/frontend/web/src/services/thunks/extra.ts
index 5a3aaef0b4..d9a985e855 100644
--- a/invokeai/frontend/web/src/services/thunks/extra.ts
+++ b/invokeai/frontend/web/src/services/thunks/extra.ts
@@ -1,91 +1,31 @@
import { createAppAsyncThunk } from 'app/storeUtils';
-import { map } from 'lodash';
import { SessionsService } from 'services/api';
-import { isImageOutput } from 'services/types/guards';
-import { buildImageUrls } from 'services/util/buildImageUrls';
-import { extractTimestampFromResultImageName } from 'services/util/extractTimestampFromResultImageName';
-import { resultsReceived } from 'features/gallery/store/resultsSlice';
-
-type GetGalleryImagesArg = {
- count: number;
-};
/**
- * Get the last 20 sessions' worth of images.
+ * Get the last 10 sessions' worth of images.
*
- * This should be at most 20 images so long as we continue to make a new session for every
+ * This should be at most 10 images so long as we continue to make a new session for every
* generation.
*
- * If a session was created but no image generated, this will be < 20 images.
+ * If a session was created but no image generated, this will be < 10 images.
*
* When we allow more images per sesssion, this is kinda no longer a viable way to grab results,
* because a session could have many, many images. In that situation, barring a change to the api,
* we have to keep track of images we've grabbed and the session they came from, so that when we
* want to load more, we can "resume" fetching images from that session.
+ *
+ * The API should change.
*/
-export const getGalleryImages = createAppAsyncThunk(
- 'api/getGalleryImages',
- async (arg: GetGalleryImagesArg, { dispatch }) => {
+export const getNextResultsPage = createAppAsyncThunk(
+ 'results/getMoreResultsImages',
+ async (_arg, { getState }) => {
+ const { page } = getState().results;
+
const response = await SessionsService.listSessions({
- page: 0,
- perPage: 20,
+ page: page + 1,
+ perPage: 10,
});
- // build flattened array of results ojects, use lodash `map()` to make results object an array
- const allResults = response.items.flatMap((session) =>
- map(session.results)
- );
-
- // filter out non-image-outputs (eg latents, prompts, etc)
- const imageOutputResults = allResults.filter(isImageOutput);
-
- // build ResultImage objects
- const resultImages = imageOutputResults.map((result) => {
- const name = result.image.image_name;
-
- const { imageUrl, thumbnailUrl } = buildImageUrls('results', name);
- const timestamp = extractTimestampFromResultImageName(name);
-
- return {
- name,
- url: imageUrl,
- thumbnail: thumbnailUrl,
- timestamp,
- height: 512,
- width: 512,
- };
- });
-
- // update the results slice
- dispatch(resultsReceived(resultImages));
-
- // response.items.forEach((session) => {
- // forEach(session.results, (result) => {
- // if (isImageOutput(result)) {
- // const { imageUrl, thumbnailUrl } = buildImageUrls(
- // result.image.image_type!, // fix the generated types to avoid non-null assertion
- // result.image.image_name! // fix the generated types to avoid non-null assertion
- // );
-
- // dispatch
-
- // dispatch(
- // addImage({
- // category: 'result',
- // image: {
- // uuid: uuidv4(),
- // url: imageUrl,
- // thumbnail: ,
- // width: 512,
- // height: 512,
- // category: 'result',
- // name: result.image.image_name,
- // mtime: new Date().getTime(),
- // },
- // })
- // );
- // }
- // });
- // });
+ return response;
}
);
diff --git a/invokeai/frontend/web/src/services/util/buildImageUrls.ts b/invokeai/frontend/web/src/services/util/buildImageUrls.ts
deleted file mode 100644
index 3f2b1860f3..0000000000
--- a/invokeai/frontend/web/src/services/util/buildImageUrls.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { ImageType } from 'services/api';
-
-export const buildImageUrls = (
- imageType: ImageType,
- imageName: string
-): { imageUrl: string; thumbnailUrl: string } => {
- const imageUrl = `api/v1/images/${imageType}/${imageName}`;
-
- const thumbnailUrl = `api/v1/images/${imageType}/thumbnails/${
- imageName.split('.')[0]
- }.webp`;
-
- return {
- imageUrl,
- thumbnailUrl,
- };
-};
diff --git a/invokeai/frontend/web/src/services/util/extractTimestampFromResultImageName.ts b/invokeai/frontend/web/src/services/util/extractTimestampFromResultImageName.ts
deleted file mode 100644
index fe3dc65ec5..0000000000
--- a/invokeai/frontend/web/src/services/util/extractTimestampFromResultImageName.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export const extractTimestampFromResultImageName = (imageName: string) => {
- const timestamp = imageName.split('_')?.pop()?.split('.')[0];
-
- if (timestamp === undefined) {
- return 0;
- }
-
- return Number(timestamp);
-};
diff --git a/invokeai/frontend/web/src/services/util/prepareResultImage.ts b/invokeai/frontend/web/src/services/util/prepareResultImage.ts
new file mode 100644
index 0000000000..4793ff634c
--- /dev/null
+++ b/invokeai/frontend/web/src/services/util/prepareResultImage.ts
@@ -0,0 +1,45 @@
+import { ResultImage } from 'app/invokeai';
+import { ImageField, ImageType } from 'services/api';
+
+export const buildImageUrls = (
+ imageType: ImageType,
+ imageName: string
+): { imageUrl: string; thumbnailUrl: string } => {
+ const imageUrl = `api/v1/images/${imageType}/${imageName}`;
+
+ const thumbnailUrl = `api/v1/images/${imageType}/thumbnails/${
+ imageName.split('.')[0]
+ }.webp`;
+
+ return {
+ imageUrl,
+ thumbnailUrl,
+ };
+};
+
+export const extractTimestampFromResultImageName = (imageName: string) => {
+ const timestamp = imageName.split('_')?.pop()?.split('.')[0];
+
+ if (timestamp === undefined) {
+ return 0;
+ }
+
+ return Number(timestamp);
+};
+
+export const prepareResultImage = (image: ImageField): ResultImage => {
+ const name = image.image_name;
+
+ const { imageUrl, thumbnailUrl } = buildImageUrls('results', name);
+
+ const timestamp = extractTimestampFromResultImageName(name);
+
+ return {
+ name,
+ url: imageUrl,
+ thumbnail: thumbnailUrl,
+ timestamp,
+ height: 512,
+ width: 512,
+ };
+};