feat(ui): migrate to redux-remember

This commit is contained in:
psychedelicious 2023-05-06 00:28:00 +10:00
parent bcc21531fb
commit 09f166577e
47 changed files with 379 additions and 313 deletions

View File

@ -99,6 +99,7 @@
"redux-deep-persist": "^1.0.7",
"redux-dynamic-middlewares": "^2.2.0",
"redux-persist": "^6.0.0",
"redux-remember": "^3.2.1",
"roarr": "^7.15.0",
"serialize-error": "^11.0.0",
"socket.io-client": "^4.6.0",

View File

@ -1,8 +1,6 @@
import React, { lazy, memo, PropsWithChildren, useEffect } from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store } from 'app/store/store';
import { persistor } from '../store/persistor';
import { OpenAPI } from 'services/api';
import '@fontsource/inter/100.css';
import '@fontsource/inter/200.css';
@ -57,13 +55,11 @@ const InvokeAIUI = ({ apiUrl, token, config, children }: Props) => {
return (
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}>
<React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider>
<App config={config}>{children}</App>
</ThemeLocaleProvider>
</React.Suspense>
</PersistGate>
</Provider>
</React.StrictMode>
);

View File

@ -0,0 +1,4 @@
import { createAction } from '@reduxjs/toolkit';
import { InvokeTabName } from 'features/ui/store/tabMap';
export const userInvoked = createAction<InvokeTabName>('app/userInvoked');

View File

@ -0,0 +1,8 @@
export const LOCALSTORAGE_KEYS = [
'chakra-ui-color-mode',
'i18nextLng',
'ROARR_FILTER',
'ROARR_LOG',
];
export const LOCALSTORAGE_PREFIX = '@@invokeai-';

View File

@ -0,0 +1,36 @@
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
import { resultsPersistDenylist } from 'features/gallery/store/resultsPersistDenylist';
import { uploadsPersistDenylist } from 'features/gallery/store/uploadsPersistDenylist';
import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist';
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist';
import { modelsPersistDenylist } from 'features/system/store/modelsPersistDenylist';
import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist';
import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist';
import { omit } from 'lodash-es';
import { SerializeFunction } from 'redux-remember';
const serializationDenylist: {
[key: string]: string[];
} = {
canvas: canvasPersistDenylist,
gallery: galleryPersistDenylist,
generation: generationPersistDenylist,
lightbox: lightboxPersistDenylist,
models: modelsPersistDenylist,
nodes: nodesPersistDenylist,
postprocessing: postprocessingPersistDenylist,
results: resultsPersistDenylist,
system: systemPersistDenylist,
// config: configPersistDenyList,
ui: uiPersistDenylist,
uploads: uploadsPersistDenylist,
// hotkeys: hotkeysPersistDenylist,
};
export const serialize: SerializeFunction = (data, key) => {
const result = omit(data, serializationDenylist[key]);
return JSON.stringify(result);
};

View File

@ -0,0 +1,49 @@
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
import { resultsPersistDenylist } from 'features/gallery/store/resultsPersistDenylist';
import { initialResultsState } from 'features/gallery/store/resultsSlice';
import { uploadsPersistDenylist } from 'features/gallery/store/uploadsPersistDenylist';
import { initialUploadsState } from 'features/gallery/store/uploadsSlice';
import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist';
import { initialLightboxState } from 'features/lightbox/store/lightboxSlice';
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
import { initialNodesState } from 'features/nodes/store/nodesSlice';
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
import { initialGenerationState } from 'features/parameters/store/generationSlice';
import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist';
import { initialPostprocessingState } from 'features/parameters/store/postprocessingSlice';
import { initialConfigState } from 'features/system/store/configSlice';
import { initialModelsState } from 'features/system/store/modelSlice';
import { modelsPersistDenylist } from 'features/system/store/modelsPersistDenylist';
import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist';
import { initialSystemState } from 'features/system/store/systemSlice';
import { initialHotkeysState } from 'features/ui/store/hotkeysSlice';
import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist';
import { initialUIState } from 'features/ui/store/uiSlice';
import { defaultsDeep, merge, omit } from 'lodash-es';
import { UnserializeFunction } from 'redux-remember';
const initialStates: {
[key: string]: any;
} = {
canvas: initialCanvasState,
gallery: initialGalleryState,
generation: initialGenerationState,
lightbox: initialLightboxState,
models: initialModelsState,
nodes: initialNodesState,
postprocessing: initialPostprocessingState,
results: initialResultsState,
system: initialSystemState,
config: initialConfigState,
ui: initialUIState,
uploads: initialUploadsState,
hotkeys: initialHotkeysState,
};
export const unserialize: UnserializeFunction = (data, key) => {
const result = defaultsDeep(JSON.parse(data), initialStates[key]);
return result;
};

View File

@ -0,0 +1,29 @@
import { AnyAction } from '@reduxjs/toolkit';
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
import { forEach } from 'lodash-es';
import { Graph } from 'services/api';
export const actionSanitizer = <A extends AnyAction>(action: A): A => {
if (isAnyGraphBuilt(action)) {
if (action.payload.nodes) {
const sanitizedNodes: Graph['nodes'] = {};
// Sanitize nodes as needed
forEach(action.payload.nodes, (node, key) => {
if (node.type === 'dataURL_image') {
const { dataURL, ...rest } = node;
sanitizedNodes[key] = { ...rest, dataURL: '<dataURL>' };
} else {
sanitizedNodes[key] = { ...node };
}
});
return {
...action,
payload: { ...action.payload, nodes: sanitizedNodes },
};
}
}
return action;
};

View File

@ -0,0 +1,11 @@
export const actionsDenylist = [
'canvas/setCursorPosition',
'canvas/setStageCoordinates',
'canvas/setStageScale',
'canvas/setIsDrawing',
'canvas/setBoundingBoxCoordinates',
'canvas/setBoundingBoxDimensions',
'canvas/setIsDrawing',
'canvas/addPointToCurrentLine',
'socket/generatorProgress',
];

View File

@ -0,0 +1,3 @@
export const stateSanitizer = <S>(state: S): S => {
return state;
};

View File

@ -11,12 +11,9 @@ import { addInitialImageSelectedListener } from './listeners/initialImageSelecte
import { addImageResultReceivedListener } from './listeners/invocationComplete';
import { addImageUploadedListener } from './listeners/imageUploaded';
import { addRequestedImageDeletionListener } from './listeners/imageDeleted';
import {
addUserInvokedCanvasListener,
addUserInvokedCreateListener,
addUserInvokedNodesListener,
} from './listeners/userInvoked';
import { addCanvasGraphBuiltListener } from './listeners/canvasGraphBuilt';
import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
import { addUserInvokedCreateListener } from './listeners/userInvokedCreate';
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
export const listenerMiddleware = createListenerMiddleware();
@ -40,7 +37,7 @@ addImageUploadedListener();
addInitialImageSelectedListener();
addImageResultReceivedListener();
addRequestedImageDeletionListener();
addUserInvokedCanvasListener();
addUserInvokedCreateListener();
addUserInvokedNodesListener();
// addCanvasGraphBuiltListener();

View File

@ -1,16 +1,8 @@
import { createAction } from '@reduxjs/toolkit';
import { startAppListening } from '..';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { buildLinearGraph } from 'features/nodes/util/buildLinearGraph';
import { sessionCreated, sessionInvoked } from 'services/thunks/session';
import { buildCanvasGraphAndBlobs } from 'features/nodes/util/buildCanvasGraph';
import { buildNodesGraph } from 'features/nodes/util/buildNodesGraph';
import { log } from 'app/logging/useLogger';
import {
canvasGraphBuilt,
createGraphBuilt,
nodesGraphBuilt,
} from 'features/nodes/store/actions';
import { canvasGraphBuilt } from 'features/nodes/store/actions';
import { imageUploaded } from 'services/thunks/image';
import { v4 as uuidv4 } from 'uuid';
import { Graph } from 'services/api';
@ -18,27 +10,10 @@ import {
canvasSessionIdChanged,
stagingAreaInitialized,
} from 'features/canvas/store/canvasSlice';
import { userInvoked } from 'app/store/actions';
const moduleLog = log.child({ namespace: 'invoke' });
export const userInvoked = createAction<InvokeTabName>('app/userInvoked');
export const addUserInvokedCreateListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof userInvoked> =>
userInvoked.match(action) && action.payload === 'generate',
effect: (action, { getState, dispatch }) => {
const state = getState();
const graph = buildLinearGraph(state);
dispatch(createGraphBuilt(graph));
moduleLog({ data: graph }, 'Create graph built');
dispatch(sessionCreated({ graph }));
},
});
};
export const addUserInvokedCanvasListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof userInvoked> =>
@ -149,19 +124,3 @@ export const addUserInvokedCanvasListener = () => {
},
});
};
export const addUserInvokedNodesListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof userInvoked> =>
userInvoked.match(action) && action.payload === 'nodes',
effect: (action, { getState, dispatch }) => {
const state = getState();
const graph = buildNodesGraph(state);
dispatch(nodesGraphBuilt(graph));
moduleLog({ data: graph }, 'Nodes graph built');
dispatch(sessionCreated({ graph }));
},
});
};

View File

@ -0,0 +1,24 @@
import { startAppListening } from '..';
import { buildLinearGraph } from 'features/nodes/util/buildLinearGraph';
import { sessionCreated } from 'services/thunks/session';
import { log } from 'app/logging/useLogger';
import { createGraphBuilt } from 'features/nodes/store/actions';
import { userInvoked } from 'app/store/actions';
const moduleLog = log.child({ namespace: 'invoke' });
export const addUserInvokedCreateListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof userInvoked> =>
userInvoked.match(action) && action.payload === 'generate',
effect: (action, { getState, dispatch }) => {
const state = getState();
const graph = buildLinearGraph(state);
dispatch(createGraphBuilt(graph));
moduleLog({ data: graph }, 'Create graph built');
dispatch(sessionCreated({ graph }));
},
});
};

View File

@ -0,0 +1,24 @@
import { startAppListening } from '..';
import { sessionCreated } from 'services/thunks/session';
import { buildNodesGraph } from 'features/nodes/util/buildNodesGraph';
import { log } from 'app/logging/useLogger';
import { nodesGraphBuilt } from 'features/nodes/store/actions';
import { userInvoked } from 'app/store/actions';
const moduleLog = log.child({ namespace: 'invoke' });
export const addUserInvokedNodesListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof userInvoked> =>
userInvoked.match(action) && action.payload === 'nodes',
effect: (action, { getState, dispatch }) => {
const state = getState();
const graph = buildNodesGraph(state);
dispatch(nodesGraphBuilt(graph));
moduleLog({ data: graph }, 'Nodes graph built');
dispatch(sessionCreated({ graph }));
},
});
};

View File

@ -1,4 +0,0 @@
import { store } from 'app/store/store';
import { persistStore } from 'redux-persist';
export const persistor = persistStore(store);

View File

@ -1,16 +1,13 @@
import {
Action,
AnyAction,
Store,
ThunkDispatch,
combineReducers,
configureStore,
isAnyOf,
} from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
import { rememberReducer, rememberEnhancer } from 'redux-remember';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import { getPersistConfig } from 'redux-deep-persist';
import canvasReducer from 'features/canvas/store/canvasSlice';
import galleryReducer from 'features/gallery/store/gallerySlice';
@ -26,35 +23,17 @@ import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import modelsReducer from 'features/system/store/modelSlice';
import nodesReducer from 'features/nodes/store/nodesSlice';
import { canvasDenylist } from 'features/canvas/store/canvasPersistDenylist';
import { galleryDenylist } from 'features/gallery/store/galleryPersistDenylist';
import { generationDenylist } from 'features/parameters/store/generationPersistDenylist';
import { lightboxDenylist } from 'features/lightbox/store/lightboxPersistDenylist';
import { modelsDenylist } from 'features/system/store/modelsPersistDenylist';
import { nodesDenylist } from 'features/nodes/store/nodesPersistDenylist';
import { postprocessingDenylist } from 'features/parameters/store/postprocessingPersistDenylist';
import { systemDenylist } from 'features/system/store/systemPersistDenylist';
import { uiDenylist } from 'features/ui/store/uiPersistDenylist';
import { listenerMiddleware } from './middleware/listenerMiddleware';
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
import { forEach } from 'lodash-es';
import { Graph } from 'services/api';
/**
* redux-persist provides an easy and reliable way to persist state across reloads.
*
* While we definitely want generation parameters to be persisted, there are a number
* of things we do *not* want to be persisted across reloads:
* - Gallery/selected image (user may add/delete images from disk between page loads)
* - Connection/processing status
* - Availability of external libraries like ESRGAN/GFPGAN
*
* These can be denylisted in redux-persist.
*
* The necesssary nested persistors with denylists are configured below.
*/
import { actionSanitizer } from './middleware/devtools/actionSanitizer';
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { actionsDenylist } from './middleware/devtools/actionsDenylist';
const rootReducer = combineReducers({
import { serialize } from './enhancers/reduxRemember/serialize';
import { unserialize } from './enhancers/reduxRemember/unserialize';
import { LOCALSTORAGE_PREFIX } from './constants';
const allReducers = {
canvas: canvasReducer,
gallery: galleryReducer,
generation: generationReducer,
@ -68,65 +47,38 @@ const rootReducer = combineReducers({
ui: uiReducer,
uploads: uploadsReducer,
hotkeys: hotkeysReducer,
});
};
const rootPersistConfig = getPersistConfig({
key: 'root',
storage,
rootReducer,
blacklist: [
...canvasDenylist,
...galleryDenylist,
...generationDenylist,
...lightboxDenylist,
...modelsDenylist,
...nodesDenylist,
...postprocessingDenylist,
// ...resultsDenylist,
'results',
...systemDenylist,
...uiDenylist,
// ...uploadsDenylist,
'uploads',
'hotkeys',
'config',
const rootReducer = combineReducers(allReducers);
const rememberedRootReducer = rememberReducer(rootReducer);
const rememberedKeys: (keyof typeof allReducers)[] = [
'canvas',
'gallery',
'generation',
'lightbox',
// 'models',
'nodes',
'postprocessing',
'system',
'ui',
// 'hotkeys',
// 'results',
// 'uploads',
// 'config',
];
export const store: Store = configureStore({
reducer: rememberedRootReducer,
enhancers: [
rememberEnhancer(window.localStorage, rememberedKeys, {
persistDebounce: 300,
serialize,
unserialize,
prefix: LOCALSTORAGE_PREFIX,
}),
],
});
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
// TODO: rip the old middleware out when nodes is complete
// export function buildMiddleware() {
// if (import.meta.env.MODE === 'nodes' || import.meta.env.MODE === 'package') {
// return socketMiddleware();
// } else {
// return socketioMiddleware();
// }
// }
// const actionSanitizer = (action: AnyAction): AnyAction => {
// if (isAnyGraphBuilt(action)) {
// if (action.payload.nodes) {
// const sanitizedNodes: Graph['nodes'] = {};
// forEach(action.payload.nodes, (node, key) => {
// if (node.type === 'dataURL_image') {
// const { dataURL, ...rest } = node;
// sanitizedNodes[key] = { ...rest, dataURL: '<<dataURL>>' };
// }
// });
// const sanitizedAction: AnyAction = {
// ...action,
// payload: { ...action.payload, nodes: sanitizedNodes },
// };
// return sanitizedAction;
// }
// }
// return action;
// };
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
immutableCheck: false,
@ -135,43 +87,10 @@ export const store = configureStore({
.concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware),
devTools: {
// Uncommenting these very rapidly called actions makes the redux dev tools output much more readable
actionsDenylist: [
'canvas/setCursorPosition',
'canvas/setStageCoordinates',
'canvas/setStageScale',
'canvas/setIsDrawing',
'canvas/setBoundingBoxCoordinates',
'canvas/setBoundingBoxDimensions',
'canvas/setIsDrawing',
'canvas/addPointToCurrentLine',
'socket/generatorProgress',
],
actionSanitizer: (action) => {
if (isAnyGraphBuilt(action)) {
if (action.payload.nodes) {
const sanitizedNodes: Graph['nodes'] = {};
forEach(action.payload.nodes, (node, key) => {
if (node.type === 'dataURL_image') {
const { dataURL, ...rest } = node;
sanitizedNodes[key] = { ...rest, dataURL: '<<dataURL>>' };
} else {
sanitizedNodes[key] = { ...node };
}
});
return {
...action,
payload: { ...action.payload, nodes: sanitizedNodes },
};
}
}
return action;
},
// stateSanitizer: (state) =>
// state.data ? { ...state, data: '<<LONG_BLOB>>' } : state,
actionsDenylist,
actionSanitizer,
stateSanitizer,
trace: true,
},
});

View File

@ -9,6 +9,12 @@ const itemsToDenylist: (keyof CanvasState)[] = [
'doesCanvasNeedScaling',
];
export const canvasPersistDenylist: (keyof CanvasState)[] = [
'cursorPosition',
'isCanvasInitialized',
'doesCanvasNeedScaling',
];
export const canvasDenylist = itemsToDenylist.map(
(denylistItem) => `canvas.${denylistItem}`
);

View File

@ -40,7 +40,7 @@ export const initialLayerState: CanvasLayerState = {
},
};
const initialCanvasState: CanvasState = {
export const initialCanvasState: CanvasState = {
boundingBoxCoordinates: { x: 0, y: 0 },
boundingBoxDimensions: { width: 512, height: 512 },
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 },

View File

@ -8,6 +8,11 @@ const itemsToDenylist: (keyof GalleryState)[] = [
'shouldAutoSwitchToNewImages',
];
export const galleryPersistDenylist: (keyof GalleryState)[] = [
'currentCategory',
'shouldAutoSwitchToNewImages',
];
export const galleryDenylist = itemsToDenylist.map(
(denylistItem) => `gallery.${denylistItem}`
);

View File

@ -44,6 +44,11 @@ export const imageGallerySelector = createSelector(
const { isLightboxOpen } = lightbox;
const images =
currentCategory === 'results'
? selectResultsEntities(state)
: selectUploadsAll(state);
return {
shouldPinGallery,
galleryImageMinimumWidth,
@ -53,7 +58,7 @@ export const imageGallerySelector = createSelector(
: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
shouldAutoSwitchToNewImages,
currentCategory,
images: state[currentCategory].entities,
images,
galleryWidth,
shouldEnableResize:
isLightboxOpen ||

View File

@ -17,7 +17,7 @@ export interface GalleryState {
currentCategory: 'results' | 'uploads';
}
const initialState: GalleryState = {
export const initialGalleryState: GalleryState = {
galleryImageMinimumWidth: 64,
galleryImageObjectFit: 'cover',
shouldAutoSwitchToNewImages: true,
@ -28,7 +28,7 @@ const initialState: GalleryState = {
export const gallerySlice = createSlice({
name: 'gallery',
initialState,
initialState: initialGalleryState,
reducers: {
imageSelected: (state, action: PayloadAction<Image | undefined>) => {
state.selectedImage = action.payload;

View File

@ -7,6 +7,8 @@ import { ResultsState } from './resultsSlice';
*/
const itemsToDenylist: (keyof ResultsState)[] = [];
export const resultsPersistDenylist: (keyof ResultsState)[] = [];
export const resultsDenylist = itemsToDenylist.map(
(denylistItem) => `results.${denylistItem}`
);

View File

@ -6,6 +6,7 @@ import { UploadsState } from './uploadsSlice';
* Currently denylisting uploads slice entirely, see persist config in store.ts
*/
const itemsToDenylist: (keyof UploadsState)[] = [];
export const uploadsPersistDenylist: (keyof UploadsState)[] = [];
export const uploadsDenylist = itemsToDenylist.map(
(denylistItem) => `uploads.${denylistItem}`

View File

@ -21,7 +21,7 @@ type AdditionalUploadsState = {
nextPage: number;
};
const initialUploadsState =
export const initialUploadsState =
uploadsAdapter.getInitialState<AdditionalUploadsState>({
page: 0,
pages: 0,

View File

@ -4,6 +4,9 @@ import { LightboxState } from './lightboxSlice';
* Lightbox slice persist denylist
*/
const itemsToDenylist: (keyof LightboxState)[] = ['isLightboxOpen'];
export const lightboxPersistDenylist: (keyof LightboxState)[] = [
'isLightboxOpen',
];
export const lightboxDenylist = itemsToDenylist.map(
(denylistItem) => `lightbox.${denylistItem}`

View File

@ -5,7 +5,7 @@ export interface LightboxState {
isLightboxOpen: boolean;
}
const initialLightboxState: LightboxState = {
export const initialLightboxState: LightboxState = {
isLightboxOpen: false,
};

View File

@ -1,5 +1,5 @@
import { HStack } from '@chakra-ui/react';
import { userInvoked } from 'app/store/middleware/listenerMiddleware/listeners/userInvoked';
import { userInvoked } from 'app/store/actions';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import { memo, useCallback } from 'react';

View File

@ -4,6 +4,10 @@ import { NodesState } from './nodesSlice';
* Nodes slice persist denylist
*/
const itemsToDenylist: (keyof NodesState)[] = ['schema', 'invocationTemplates'];
export const nodesPersistDenylist: (keyof NodesState)[] = [
'schema',
'invocationTemplates',
];
export const nodesDenylist = itemsToDenylist.map(
(denylistItem) => `nodes.${denylistItem}`

View File

@ -1,6 +1,6 @@
import { Box } from '@chakra-ui/react';
import { readinessSelector } from 'app/selectors/readinessSelector';
import { userInvoked } from 'app/store/middleware/listenerMiddleware/listeners/userInvoked';
import { userInvoked } from 'app/store/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton, { IAIButtonProps } from 'common/components/IAIButton';
import IAIIconButton, {

View File

@ -15,7 +15,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash-es';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { userInvoked } from 'app/store/middleware/listenerMiddleware/listeners/userInvoked';
import { userInvoked } from 'app/store/actions';
const promptInputSelector = createSelector(
[(state: RootState) => state.generation, activeTabNameSelector],

View File

@ -4,6 +4,7 @@ import { GenerationState } from './generationSlice';
* Generation slice persist denylist
*/
const itemsToDenylist: (keyof GenerationState)[] = [];
export const generationPersistDenylist: (keyof GenerationState)[] = [];
export const generationDenylist = itemsToDenylist.map(
(denylistItem) => `generation.${denylistItem}`

View File

@ -38,9 +38,10 @@ export interface GenerationState {
horizontalSymmetrySteps: number;
verticalSymmetrySteps: number;
isImageToImageEnabled: boolean;
model: string;
}
const initialGenerationState: GenerationState = {
export const initialGenerationState: GenerationState = {
cfgScale: 7.5,
height: 512,
img2imgStrength: 0.75,
@ -70,6 +71,7 @@ const initialGenerationState: GenerationState = {
horizontalSymmetrySteps: 0,
verticalSymmetrySteps: 0,
isImageToImageEnabled: false,
model: '',
};
const initialState: GenerationState = initialGenerationState;
@ -353,6 +355,9 @@ export const generationSlice = createSlice({
isImageToImageEnabledChanged: (state, action: PayloadAction<boolean>) => {
state.isImageToImageEnabled = action.payload;
},
modelSelected: (state, action: PayloadAction<string>) => {
state.model = action.payload;
},
},
});
@ -396,6 +401,7 @@ export const {
setVerticalSymmetrySteps,
initialImageChanged,
isImageToImageEnabledChanged,
modelSelected,
} = generationSlice.actions;
export default generationSlice.reducer;

View File

@ -4,6 +4,7 @@ import { PostprocessingState } from './postprocessingSlice';
* Postprocessing slice persist denylist
*/
const itemsToDenylist: (keyof PostprocessingState)[] = [];
export const postprocessingPersistDenylist: (keyof PostprocessingState)[] = [];
export const postprocessingDenylist = itemsToDenylist.map(
(denylistItem) => `postprocessing.${denylistItem}`

View File

@ -20,7 +20,7 @@ export interface PostprocessingState {
upscalingStrength: number;
}
const initialPostprocessingState: PostprocessingState = {
export const initialPostprocessingState: PostprocessingState = {
codeformerFidelity: 0.75,
facetoolStrength: 0.75,
facetoolType: 'gfpgan',
@ -34,11 +34,9 @@ const initialPostprocessingState: PostprocessingState = {
upscalingStrength: 0.75,
};
const initialState: PostprocessingState = initialPostprocessingState;
export const postprocessingSlice = createSlice({
name: 'postprocessing',
initialState,
initialState: initialPostprocessingState,
reducers: {
setFacetoolStrength: (state, action: PayloadAction<number>) => {
state.facetoolStrength = action.payload;

View File

@ -6,16 +6,22 @@ import { useTranslation } from 'react-i18next';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAISelect from 'common/components/IAISelect';
import {
modelSelected,
// modelSelected,
selectedModelSelector,
selectModelsById,
selectModelsIds,
} from '../store/modelSlice';
import { RootState } from 'app/store/store';
import generationSlice, {
modelSelected,
} from 'features/parameters/store/generationSlice';
import { generationSelector } from 'features/parameters/store/generationSelectors';
const selector = createSelector(
[(state: RootState) => state],
(state) => {
const selectedModel = selectedModelSelector(state);
[(state: RootState) => state, generationSelector],
(state, generation) => {
// const selectedModel = selectedModelSelector(state);
const selectedModel = selectModelsById(state, generation.model);
const allModelNames = selectModelsIds(state);
return {
allModelNames,

View File

@ -34,11 +34,11 @@ import {
} from 'features/ui/store/uiSlice';
import { UIState } from 'features/ui/store/uiTypes';
import { isEqual } from 'lodash-es';
import { persistor } from 'app/store/persistor';
import { ChangeEvent, cloneElement, ReactElement, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { VALID_LOG_LEVELS } from 'app/logging/useLogger';
import { LogLevelName } from 'roarr';
import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants';
const selector = createSelector(
[systemSelector, uiSelector],
@ -119,15 +119,18 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
shouldLogToConsole,
} = useAppSelector(selector);
/**
* Resets localstorage, then opens a secondary modal informing user to
* refresh their browser.
* */
const handleClickResetWebUI = useCallback(() => {
persistor.purge().then(() => {
// Only remove our keys
Object.keys(window.localStorage).forEach((key) => {
if (
LOCALSTORAGE_KEYS.includes(key) ||
key.startsWith(LOCALSTORAGE_PREFIX)
) {
localStorage.removeItem(key);
}
});
onSettingsModalClose();
onRefreshModalOpen();
});
}, [onSettingsModalClose, onRefreshModalOpen]);
const handleLogLevelChanged = useCallback(

View File

@ -3,7 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
import { AppConfig, PartialAppConfig } from 'app/types/invokeai';
import { merge } from 'lodash-es';
const initialConfigState: AppConfig = {
export const initialConfigState: AppConfig = {
shouldTransformUrls: false,
shouldFetchImages: false,
disabledTabs: [],

View File

@ -27,12 +27,13 @@ export type ModelsState = typeof initialModelsState;
export const modelsSlice = createSlice({
name: 'models',
initialState: initialModelsState,
initialState: modelsAdapter.getInitialState(),
// initialState: initialModelsState,
reducers: {
modelAdded: modelsAdapter.upsertOne,
modelSelected: (state, action: PayloadAction<string>) => {
state.selectedModelName = action.payload;
},
// modelSelected: (state, action: PayloadAction<string>) => {
// state.selectedModelName = action.payload;
// },
},
extraReducers(builder) {
/**
@ -44,18 +45,18 @@ export const modelsSlice = createSlice({
// If the current selected model is `''` or isn't actually in the list of models,
// choose a random model
if (
!state.selectedModelName ||
!keys(models).includes(state.selectedModelName)
) {
const randomModel = sample(models);
// if (
// !state.selectedModelName ||
// !keys(models).includes(state.selectedModelName)
// ) {
// const randomModel = sample(models);
if (randomModel) {
state.selectedModelName = randomModel.name;
} else {
state.selectedModelName = '';
}
}
// if (randomModel) {
// state.selectedModelName = randomModel.name;
// } else {
// state.selectedModelName = '';
// }
// }
});
},
});
@ -75,6 +76,9 @@ export const {
selectTotal: selectModelsTotal,
} = modelsAdapter.getSelectors<RootState>((state) => state.models);
export const { modelAdded, modelSelected } = modelsSlice.actions;
export const {
modelAdded,
// modelSelected
} = modelsSlice.actions;
export default modelsSlice.reducer;

View File

@ -4,6 +4,7 @@ import { ModelsState } from './modelSlice';
* Models slice persist denylist
*/
const itemsToDenylist: (keyof ModelsState)[] = ['entities', 'ids'];
export const modelsPersistDenylist: (keyof ModelsState)[] = ['entities', 'ids'];
export const modelsDenylist = itemsToDenylist.map(
(denylistItem) => `models.${denylistItem}`

View File

@ -21,6 +21,25 @@ const itemsToDenylist: (keyof SystemState)[] = [
'wereModelsReceived',
'wasSchemaParsed',
];
export const systemPersistDenylist: (keyof SystemState)[] = [
'currentIteration',
'currentStatus',
'currentStep',
'isCancelable',
'isConnected',
'isESRGANAvailable',
'isGFPGANAvailable',
'isProcessing',
'socketId',
'totalIterations',
'totalSteps',
'openModel',
'isCancelScheduled',
'progressImage',
'wereModelsReceived',
'wasSchemaParsed',
'isPersisted',
];
export const systemDenylist = itemsToDenylist.map(
(denylistItem) => `system.${denylistItem}`

View File

@ -89,9 +89,10 @@ export interface SystemState {
* TODO: get this from backend
*/
infillMethods: InfillMethod[];
isPersisted: boolean;
}
const initialSystemState: SystemState = {
export const initialSystemState: SystemState = {
isConnected: false,
isProcessing: false,
shouldDisplayGuides: true,
@ -121,6 +122,7 @@ const initialSystemState: SystemState = {
statusTranslationKey: 'common.statusDisconnected',
canceledSession: '',
infillMethods: ['tile', 'patchmatch'],
isPersisted: false,
};
export const systemSlice = createSlice({
@ -259,6 +261,9 @@ export const systemSlice = createSlice({
shouldLogToConsoleChanged: (state, action: PayloadAction<boolean>) => {
state.shouldLogToConsole = action.payload;
},
isPersistedChanged: (state, action: PayloadAction<boolean>) => {
state.isPersisted = action.payload;
},
},
extraReducers(builder) {
/**
@ -476,6 +481,7 @@ export const {
subscribedNodeIdsSet,
consoleLogLevelChanged,
shouldLogToConsoleChanged,
isPersistedChanged,
} = systemSlice.actions;
export default systemSlice.reducer;

View File

@ -6,15 +6,13 @@ type HotkeysState = {
shift: boolean;
};
const initialHotkeysState: HotkeysState = {
export const initialHotkeysState: HotkeysState = {
shift: false,
};
const initialState: HotkeysState = initialHotkeysState;
export const hotkeysSlice = createSlice({
name: 'hotkeys',
initialState,
initialState: initialHotkeysState,
reducers: {
shiftKeyPressed: (state, action: PayloadAction<boolean>) => {
state.shift = action.payload;

View File

@ -4,6 +4,9 @@ import { UIState } from './uiTypes';
* UI slice persist denylist
*/
const itemsToDenylist: (keyof UIState)[] = ['floatingProgressImageRect'];
export const uiPersistDenylist: (keyof UIState)[] = [
'floatingProgressImageRect',
];
export const uiDenylist = itemsToDenylist.map(
(denylistItem) => `ui.${denylistItem}`

View File

@ -4,7 +4,7 @@ import { setActiveTabReducer } from './extraReducers';
import { InvokeTabName, tabMap } from './tabMap';
import { AddNewModelType, Coordinates, Rect, UIState } from './uiTypes';
const initialUIState: UIState = {
export const initialUIState: UIState = {
activeTab: 0,
currentTheme: 'dark',
parametersPanelScrollPosition: 0,
@ -26,11 +26,9 @@ const initialUIState: UIState = {
shouldAutoShowProgressImages: false,
};
const initialState: UIState = initialUIState;
export const uiSlice = createSlice({
name: 'ui',
initialState,
initialState: initialUIState,
reducers: {
setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => {
setActiveTabReducer(state, action.payload);

View File

@ -3,7 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
import translationEN from '../public/locales/en.json';
import translationEN from '../dist/locales/en.json';
import { LOCALSTORAGE_PREFIX } from 'app/store/constants';
if (import.meta.env.MODE === 'package') {
i18n.use(initReactI18next).init({
@ -20,7 +21,11 @@ if (import.meta.env.MODE === 'package') {
} else {
i18n
.use(Backend)
.use(LanguageDetector)
.use(
new LanguageDetector(null, {
lookupLocalStorage: `${LOCALSTORAGE_PREFIX}lng`,
})
)
.use(initReactI18next)
.init({
fallbackLng: 'en',

View File

@ -8,11 +8,7 @@ import {
import { socketSubscribed, socketUnsubscribed } from './actions';
import { AppThunkDispatch, RootState } from 'app/store/store';
import { getTimestamp } from 'common/util/getTimestamp';
import {
sessionInvoked,
isFulfilledSessionCreatedAction,
sessionCreated,
} from 'services/thunks/session';
import { sessionInvoked, sessionCreated } from 'services/thunks/session';
import { OpenAPI } from 'services/api';
import { setEventListeners } from 'services/events/util/setEventListeners';
import { log } from 'app/logging/useLogger';

View File

@ -1,71 +1,10 @@
import { createAppAsyncThunk } from 'app/store/storeUtils';
import { SessionsService } from 'services/api';
import { buildLinearGraph as buildGenerateGraph } from 'features/nodes/util/buildLinearGraph';
import { buildCanvasGraphAndBlobs } from 'features/nodes/util/buildCanvasGraph';
import { isAnyOf, isFulfilled } from '@reduxjs/toolkit';
import { buildNodesGraph } from 'features/nodes/util/buildNodesGraph';
import { log } from 'app/logging/useLogger';
import { serializeError } from 'serialize-error';
const sessionLog = log.child({ namespace: 'session' });
// export const generateGraphBuilt = createAppAsyncThunk(
// 'api/generateGraphBuilt',
// async (_, { dispatch, getState, rejectWithValue }) => {
// try {
// const graph = buildGenerateGraph(getState());
// dispatch(sessionCreated({ graph }));
// return graph;
// } catch (err: any) {
// sessionLog.error(
// { error: serializeError(err) },
// 'Problem building graph'
// );
// return rejectWithValue(err.message);
// }
// }
// );
// export const nodesGraphBuilt = createAppAsyncThunk(
// 'api/nodesGraphBuilt',
// async (_, { dispatch, getState, rejectWithValue }) => {
// try {
// const graph = buildNodesGraph(getState());
// dispatch(sessionCreated({ graph }));
// return graph;
// } catch (err: any) {
// sessionLog.error(
// { error: serializeError(err) },
// 'Problem building graph'
// );
// return rejectWithValue(err.message);
// }
// }
// );
// export const canvasGraphBuilt = createAppAsyncThunk(
// 'api/canvasGraphBuilt',
// async (_, { dispatch, getState, rejectWithValue }) => {
// try {
// const graph = await buildCanvasGraph(getState());
// dispatch(sessionCreated({ graph }));
// return graph;
// } catch (err: any) {
// sessionLog.error(
// { error: serializeError(err) },
// 'Problem building graph'
// );
// return rejectWithValue(err.message);
// }
// }
// );
// export const isFulfilledAnyGraphBuilt = isAnyOf(
// generateGraphBuilt.fulfilled,
// nodesGraphBuilt.fulfilled,
// canvasGraphBuilt.fulfilled
// );
type SessionCreatedArg = {
graph: Parameters<
(typeof SessionsService)['createSession']
@ -96,11 +35,6 @@ export const sessionCreated = createAppAsyncThunk(
}
);
/**
* Function to check if an action is a fulfilled `SessionsService.createSession()` thunk
*/
export const isFulfilledSessionCreatedAction = isFulfilled(sessionCreated);
type NodeAddedArg = Parameters<(typeof SessionsService)['addNode']>[0];
/**

View File

@ -5692,6 +5692,11 @@ redux-persist@^6.0.0:
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
redux-remember@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/redux-remember/-/redux-remember-3.2.1.tgz#e58600336ac7341c56dfe9d69d95db234a7c404e"
integrity sha512-ep2E5KOJDGmrvbAuHVfmVpnuftqhJ2um6VpHw/iWa7WvAIFcPq/B678n51NBd/g8BWnNdQ5131cDRKWrRd041Q==
redux-thunk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"